diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml
index 3b092dd5..ed40f498 100644
--- a/.github/workflows/conformance.yml
+++ b/.github/workflows/conformance.yml
@@ -9,8 +9,8 @@ on:
- reopened
- ready_for_review
schedule:
- - cron: '43 7 * * *'
- cron: '13 9 * * 1'
+ - cron: '43 7 1 * *'
workflow_dispatch:
inputs:
shard:
@@ -28,6 +28,17 @@ on:
- '6'
- '7'
- '8'
+ suite:
+ description: First-party correctness or exhaustive competitor comparison
+ type: choice
+ default: first-party
+ options:
+ - first-party
+ - comparison
+ cases:
+ description: Optional comma-separated case IDs for a targeted run
+ type: string
+ default: ''
permissions:
contents: read
@@ -39,13 +50,15 @@ jobs:
github.event_name == 'schedule' ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request' &&
- contains(github.event.pull_request.labels.*.name, 'full-conformance') &&
- (github.event.action != 'labeled' || github.event.label.name == 'full-conformance'))
+ (contains(github.event.pull_request.labels.*.name, 'full-conformance') ||
+ contains(github.event.pull_request.labels.*.name, 'browser-correctness')) &&
+ (github.event.action != 'labeled' || github.event.label.name == 'full-conformance' || github.event.label.name == 'browser-correctness'))
runs-on: ubuntu-24.04
timeout-minutes: 2
outputs:
mode: ${{ steps.selection.outputs.mode }}
shards: ${{ steps.selection.outputs.shards }}
+ suite: ${{ steps.selection.outputs.suite }}
steps:
- id: selection
@@ -54,17 +67,22 @@ jobs:
EVENT_NAME: ${{ github.event_name }}
EVENT_SCHEDULE: ${{ github.event.schedule }}
REQUESTED_SHARD: ${{ inputs.shard }}
+ REQUESTED_SUITE: ${{ inputs.suite }}
+ REQUESTED_CASES: ${{ inputs.cases }}
+ FULL_LABEL: ${{ contains(github.event.pull_request.labels.*.name, 'full-conformance') }}
run: |
- if [ "$EVENT_NAME" = schedule ] && [ "$EVENT_SCHEDULE" = '43 7 * * *' ]; then
- epoch_day=$(( $(date -u +%s) / 86400 ))
- shard=$(( epoch_day % 8 + 1 ))
- mode=nightly
- shards="[$shard]"
+ suite=first-party
+ if [ "$EVENT_SCHEDULE" = '43 7 1 * *' ] || [ "$FULL_LABEL" = true ] || [ "$REQUESTED_SUITE" = comparison ]; then
+ suite=comparison
+ fi
+ if [ -n "$REQUESTED_CASES" ]; then
+ mode=targeted
+ shards='[1]'
elif [ "$EVENT_NAME" = workflow_dispatch ] && [ "$REQUESTED_SHARD" != full ]; then
mode=manual
shards="[$REQUESTED_SHARD]"
elif [ "$EVENT_NAME" = schedule ]; then
- mode=weekly
+ mode=scheduled
shards='[1,2,3,4,5,6,7,8]'
elif [ "$EVENT_NAME" = pull_request ]; then
mode=label
@@ -76,10 +94,11 @@ jobs:
echo "mode=$mode" >> "$GITHUB_OUTPUT"
echo "shards=$shards" >> "$GITHUB_OUTPUT"
- echo "Mode: $mode; shards: $shards; revision: $GITHUB_SHA" >> "$GITHUB_STEP_SUMMARY"
+ echo "suite=$suite" >> "$GITHUB_OUTPUT"
+ echo "Suite: $suite; mode: $mode; shards: $shards; revision: $GITHUB_SHA" >> "$GITHUB_STEP_SUMMARY"
conformance:
- name: Conformance (${{ matrix.shard }}/8, ${{ needs.select.outputs.mode }})
+ name: Conformance (${{ matrix.shard }}/8, ${{ needs.select.outputs.suite }}, ${{ needs.select.outputs.mode }})
needs: select
runs-on: ubuntu-24.04
timeout-minutes: 25
@@ -103,7 +122,16 @@ jobs:
playwright: 'true'
- name: Run standard conformance shard
- run: pnpm conformance -- --shard=${{ matrix.shard }}/8
+ env:
+ SUITE: ${{ needs.select.outputs.suite }}
+ CASES: ${{ inputs.cases }}
+ SHARD: ${{ matrix.shard }}
+ run: |
+ args=()
+ if [ -z "$CASES" ]; then args+=(--shard="$SHARD/8"); fi
+ if [ "$SUITE" = first-party ]; then args+=(--first-party); fi
+ if [ -n "$CASES" ]; then args+=(--case="$CASES"); fi
+ pnpm conformance -- "${args[@]}"
- name: Publish conformance summary
if: success()
diff --git a/API-FRICTION.md b/API-FRICTION.md
index d1ab54b8..7bfc1098 100644
--- a/API-FRICTION.md
+++ b/API-FRICTION.md
@@ -8318,6 +8318,19 @@ Each entry records:
complete standard shard 4/8 passes all 24 cases, including the nine former
failures, with the original geometry, paint, and accessibility gates intact.
+- Follow-up from scheduled run 34865024647: active and mixed bars render five
+ browser data rows, and stacked radial renders two series, but generated
+ expectations used generic family counts. The interactive line selects one
+ series at a time, not two simultaneous lines. Correct the source generator and
+ pinned reference data. The stacked radial example also used 1260 as the
+ cumulative end of a 570 + 1260 stack; its cumulative end and domain are 1830.
+ First-party browser checks exposed outside radar labels with no reserved
+ space, a negative-bar label below the viewport, and dashboard media queries
+ tied to the host window instead of the embedded panel. Reserve label space
+ and use dashboard container queries. Its actual scroll viewport is now
+ identified for the existing offscreen-label rule, not treated as a clipped
+ static chart. Retain these cases in routine first-party browser coverage.
+
### F-275 — Preview transparency validation rejected semantic IDs
- Status: resolved
diff --git a/benchmarks/conformance/README.md b/benchmarks/conformance/README.md
index 2f239e59..8081919e 100644
--- a/benchmarks/conformance/README.md
+++ b/benchmarks/conformance/README.md
@@ -47,6 +47,12 @@ pnpm conformance:quick
# Standard 320/640/960, light/dark matrix
pnpm conformance
+# First-party browser correctness, no competitor builds or comparative audits
+pnpm conformance -- --first-party
+
+# Target an example while keeping its full browser matrix
+pnpm conformance -- --first-party --case=150-shadcn-bar-negative
+
# Isolated bundle and type audit only
pnpm conformance:size
@@ -84,6 +90,19 @@ and tooling compatibility even when a run includes Recharts references.
the complete catalog evidence. Long selections use a bounded digest; the JSON
always records the resolved case filter.
+First-party reports add `--first-party` to the artifact name. They retain all
+case geometry counts, guide assertions, containment, accessible names, and
+native interaction scenarios, before and after updates at every profile size
+and theme. They do not measure type safety, bundle size, timing, competitor
+paint parity, or relative geometry similarity. The ordinary cached checks
+still validate types, examples, bundles, and generated assets.
+
+CI runs first-party correctness weekly and the paired comparison monthly.
+Ordinary PRs do not run this workflow. Use the `browser-correctness` label for
+first-party coverage or `full-conformance` for comparison coverage. Manual
+runs accept a suite, shard, or comma-separated case IDs; targeted case runs
+use one job rather than launching empty shards.
+
The [interaction UX audit](./INTERACTION-UX-AUDIT.md) preserves the before-state
review of cases 80–92 and records the implementation follow-through for
discoverability, rendered feedback, keyboard and touch operation, cancellation,
diff --git a/benchmarks/conformance/cases/127-shadcn-dashboard/dashboard.tsx b/benchmarks/conformance/cases/127-shadcn-dashboard/dashboard.tsx
index fc34d630..17a60647 100644
--- a/benchmarks/conformance/cases/127-shadcn-dashboard/dashboard.tsx
+++ b/benchmarks/conformance/cases/127-shadcn-dashboard/dashboard.tsx
@@ -156,7 +156,7 @@ export function ShadcnDashboard({ ChartRenderer, input }: DashboardProps) {
-
+
diff --git a/benchmarks/conformance/cases/127-shadcn-dashboard/styles.ts b/benchmarks/conformance/cases/127-shadcn-dashboard/styles.ts
index 8f396b08..0bb3aa32 100644
--- a/benchmarks/conformance/cases/127-shadcn-dashboard/styles.ts
+++ b/benchmarks/conformance/cases/127-shadcn-dashboard/styles.ts
@@ -1,5 +1,6 @@
export const shadcnDashboardStyles = `
.shadcn-dashboard {
+ container: dashboard / inline-size;
--sd-background: oklch(1 0 0);
--sd-foreground: oklch(0.145 0 0);
--sd-card: oklch(1 0 0);
@@ -752,7 +753,7 @@ export const shadcnDashboardStyles = `
}
}
- @media (max-width: 767px) {
+ @container dashboard (max-width: 767px) {
.sd-sidebar {
display: none;
}
@@ -781,7 +782,7 @@ export const shadcnDashboardStyles = `
}
}
- @media (max-width: 480px) {
+ @container dashboard (max-width: 480px) {
.sd-chart-card {
height: 414px;
}
diff --git a/benchmarks/conformance/cases/143-shadcn-bar-active/case.json b/benchmarks/conformance/cases/143-shadcn-bar-active/case.json
index 83b28ee8..effa32ea 100644
--- a/benchmarks/conformance/cases/143-shadcn-bar-active/case.json
+++ b/benchmarks/conformance/cases/143-shadcn-bar-active/case.json
@@ -13,7 +13,8 @@
"geometry": [
{
"role": "bar",
- "count": 6
+ "count": 5,
+ "maxCount": 5
}
],
"source": {
diff --git a/benchmarks/conformance/cases/149-shadcn-bar-mixed/case.json b/benchmarks/conformance/cases/149-shadcn-bar-mixed/case.json
index 09816de3..3998307c 100644
--- a/benchmarks/conformance/cases/149-shadcn-bar-mixed/case.json
+++ b/benchmarks/conformance/cases/149-shadcn-bar-mixed/case.json
@@ -13,7 +13,8 @@
"geometry": [
{
"role": "bar",
- "count": 6
+ "count": 5,
+ "maxCount": 5
}
],
"source": {
diff --git a/benchmarks/conformance/cases/150-shadcn-bar-negative/example.tsx b/benchmarks/conformance/cases/150-shadcn-bar-negative/example.tsx
index 3d447a98..47478127 100644
--- a/benchmarks/conformance/cases/150-shadcn-bar-negative/example.tsx
+++ b/benchmarks/conformance/cases/150-shadcn-bar-negative/example.tsx
@@ -50,7 +50,7 @@ export function createExampleChart() {
y: { scale: scaleLinear, grid: true, axis: false },
},
- margin: { top: 24, right: 5, bottom: 24, left: 5 },
+ margin: { top: 24, right: 5, bottom: 36, left: 5 },
theme: shadcnTheme(),
},
{
diff --git a/benchmarks/conformance/cases/156-shadcn-line-interactive/case.json b/benchmarks/conformance/cases/156-shadcn-line-interactive/case.json
index 2ad25717..40eec23d 100644
--- a/benchmarks/conformance/cases/156-shadcn-line-interactive/case.json
+++ b/benchmarks/conformance/cases/156-shadcn-line-interactive/case.json
@@ -13,7 +13,8 @@
"geometry": [
{
"role": "line",
- "count": 2
+ "count": 1,
+ "maxCount": 1
}
],
"source": {
diff --git a/benchmarks/conformance/cases/181-shadcn-radar-label-custom/example.tsx b/benchmarks/conformance/cases/181-shadcn-radar-label-custom/example.tsx
index 95591749..f0276dd8 100644
--- a/benchmarks/conformance/cases/181-shadcn-radar-label-custom/example.tsx
+++ b/benchmarks/conformance/cases/181-shadcn-radar-label-custom/example.tsx
@@ -41,10 +41,14 @@ export function createExampleChart() {
}),
]
return defineChart(
- ({ height }) => ({
+ ({ width, height }) => ({
marks: [
polar({
- radiusRatio: height < 220 ? 0.64 : 0.76,
+ // Reserve space for the two outside label rows at every chart size.
+ radiusRatio: Math.min(
+ 0.76,
+ Math.max(0.1, 1 - 120 / Math.min(width, height)),
+ ),
scales: {
angle: { scale: scalePoint().domain(months), wrap: true },
radius: { scale: scaleLinear().domain([0, radiusMax]) },
diff --git a/benchmarks/conformance/cases/189-shadcn-radial-stacked/case.json b/benchmarks/conformance/cases/189-shadcn-radial-stacked/case.json
index 9ebebe00..c9a83b62 100644
--- a/benchmarks/conformance/cases/189-shadcn-radial-stacked/case.json
+++ b/benchmarks/conformance/cases/189-shadcn-radial-stacked/case.json
@@ -13,7 +13,8 @@
"geometry": [
{
"role": "bar",
- "count": 5,
+ "count": 2,
+ "maxCount": 2,
"rendererRoles": {
"recharts": "arc",
"tanstack": "arc"
diff --git a/benchmarks/conformance/cases/189-shadcn-radial-stacked/example.tsx b/benchmarks/conformance/cases/189-shadcn-radial-stacked/example.tsx
index 1b220d38..7810f4e0 100644
--- a/benchmarks/conformance/cases/189-shadcn-radial-stacked/example.tsx
+++ b/benchmarks/conformance/cases/189-shadcn-radial-stacked/example.tsx
@@ -25,7 +25,7 @@ export function createExampleChart() {
id: 'desktop',
ring: 'visitors',
start: 570,
- end: 1260,
+ end: 1830,
fill: shadcnColors[0],
},
]
@@ -36,7 +36,7 @@ export function createExampleChart() {
startAngle: rechartsPolarAngle(0),
endAngle: rechartsPolarAngle(180),
scales: {
- angle: { scale: scaleLinear().domain([0, 1260]) },
+ angle: { scale: scaleLinear().domain([0, 1830]) },
radius: {
scale: scaleBand().domain(['visitors']),
range: [80, 110],
diff --git a/benchmarks/conformance/catalog-index.json b/benchmarks/conformance/catalog-index.json
index bad37c69..721ec1ca 100644
--- a/benchmarks/conformance/catalog-index.json
+++ b/benchmarks/conformance/catalog-index.json
@@ -10694,7 +10694,8 @@
"geometry": [
{
"role": "bar",
- "count": 6
+ "count": 5,
+ "maxCount": 5
}
],
"source": {
@@ -10880,7 +10881,8 @@
"geometry": [
{
"role": "bar",
- "count": 6
+ "count": 5,
+ "maxCount": 5
}
],
"source": {
@@ -11097,7 +11099,8 @@
"geometry": [
{
"role": "line",
- "count": 2
+ "count": 1,
+ "maxCount": 1
}
],
"source": {
@@ -12136,7 +12139,8 @@
"geometry": [
{
"role": "bar",
- "count": 5,
+ "count": 2,
+ "maxCount": 2,
"rendererRoles": {
"recharts": "arc",
"tanstack": "arc"
diff --git a/benchmarks/conformance/previews/150-shadcn-bar-negative.svg b/benchmarks/conformance/previews/150-shadcn-bar-negative.svg
index 35cf1e4e..1ce78378 100644
--- a/benchmarks/conformance/previews/150-shadcn-bar-negative.svg
+++ b/benchmarks/conformance/previews/150-shadcn-bar-negative.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/181-shadcn-radar-label-custom.svg b/benchmarks/conformance/previews/181-shadcn-radar-label-custom.svg
index 1b372e88..9e4a108a 100644
--- a/benchmarks/conformance/previews/181-shadcn-radar-label-custom.svg
+++ b/benchmarks/conformance/previews/181-shadcn-radar-label-custom.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/189-shadcn-radial-stacked.svg b/benchmarks/conformance/previews/189-shadcn-radial-stacked.svg
index ed2802ba..86bd8f79 100644
--- a/benchmarks/conformance/previews/189-shadcn-radial-stacked.svg
+++ b/benchmarks/conformance/previews/189-shadcn-radial-stacked.svg
@@ -1 +1 @@
-
+
diff --git a/benchmarks/conformance/previews/manifest.json b/benchmarks/conformance/previews/manifest.json
index f18c50d3..5e6ec216 100644
--- a/benchmarks/conformance/previews/manifest.json
+++ b/benchmarks/conformance/previews/manifest.json
@@ -2,7 +2,7 @@
"schemaVersion": 1,
"width": 288,
"height": 192,
- "sourceHash": "c5e393081d8428d13d2efdeca32ad1e7fc56062b610b5c014d29c28af02bbde9",
+ "sourceHash": "33ff2cb340686f5c9fedbeb0f14be908c1f28a6bb5e55483a6fe38fc2a213434",
"assets": [
{
"id": "01-line-gaps",
@@ -706,8 +706,8 @@
},
{
"id": "150-shadcn-bar-negative",
- "sha256": "117285f70baf2b44b96a56023a8b016b4c34ef0b0e44e0f12a23e3311cfbc0d9",
- "bytes": 4403
+ "sha256": "1d5424d59119d1c1ac939f38581389e0b494ec7aa34927998be1e3a9ceb16e11",
+ "bytes": 4245
},
{
"id": "151-shadcn-bar-stacked",
@@ -861,8 +861,8 @@
},
{
"id": "181-shadcn-radar-label-custom",
- "sha256": "a01a6cb688b465ba300b0b176bce46e2423c76d648358294f6caae6ebb5a199a",
- "bytes": 6909
+ "sha256": "7f4db6bb2a10fbf96037affa7f88f9558b43231aa166f3e60ec1d810d4547350",
+ "bytes": 6807
},
{
"id": "182-shadcn-radar-legend",
@@ -901,8 +901,8 @@
},
{
"id": "189-shadcn-radial-stacked",
- "sha256": "b280f74e86ffef5e6865462a9be2d51b4e35479223f49df6fc73c64b063b91dd",
- "bytes": 2971
+ "sha256": "15396b305fbec445a0de4ca90898d18657823e7152c58e4f5160bbec352c5766",
+ "bytes": 2964
},
{
"id": "190-shadcn-tooltip-default",
diff --git a/benchmarks/conformance/shared/shadcn-catalog-recharts.tsx b/benchmarks/conformance/shared/shadcn-catalog-recharts.tsx
index 3b0ffb45..942dffb6 100644
--- a/benchmarks/conformance/shared/shadcn-catalog-recharts.tsx
+++ b/benchmarks/conformance/shared/shadcn-catalog-recharts.tsx
@@ -21,6 +21,7 @@ import {
XAxis,
YAxis,
} from 'recharts'
+import interactiveLineData from '@tanstack/charts-data/shadcn-area-interactive-data'
import {
getShadcnCatalogSpec,
shadcnActivities,
@@ -172,6 +173,54 @@ function areaChart(spec: ShadcnCatalogSpec, width: number, height: number) {
}
function barChart(spec: ShadcnCatalogSpec, width: number, height: number) {
+ if (spec.variant === 'active' || spec.variant === 'mixed') {
+ const active = spec.variant === 'active'
+ const data = shadcnBrowsers.map((row, index) => ({
+ ...row,
+ visitors:
+ active && row.browser === 'chrome'
+ ? 187
+ : active && row.browser === 'firefox'
+ ? 275
+ : row.visitors,
+ fill:
+ active && row.browser === 'firefox'
+ ? `color-mix(in srgb, ${shadcnColors[2]} 80%, transparent)`
+ : shadcnColors[index],
+ }))
+ return (
+
+ {active ? : null}
+
+
+
+
+
+ )
+ }
const horizontal =
spec.variant === 'horizontal' ||
spec.variant === 'label-custom' ||
@@ -286,20 +335,22 @@ function lineChart(spec: ShadcnCatalogSpec, width: number, height: number) {
: 'natural'
const dots = spec.variant.includes('dots')
const labels = spec.variant.includes('label')
- const multiple = spec.variant === 'multiple' || spec.variant === 'interactive'
+ const multiple = spec.variant === 'multiple'
return (
-
aria-label={spec.title}
width={width}
height={height}
- data={shadcnMonths}
+ data={spec.variant === 'interactive' ? interactiveLineData : shadcnMonths}
>
+
+
+
+ 1,830
+
+
+ Visitors
+
+
+ )
+ }
const shape = spec.variant === 'shape'
const centeredValue = shape || spec.variant === 'text'
const data = centeredValue
diff --git a/benchmarks/conformance/shared/shadcn-data-contracts.test.ts b/benchmarks/conformance/shared/shadcn-data-contracts.test.ts
new file mode 100644
index 00000000..be31d7f5
--- /dev/null
+++ b/benchmarks/conformance/shared/shadcn-data-contracts.test.ts
@@ -0,0 +1,27 @@
+import { createChartRuntime } from '@tanstack/charts'
+import { describe, expect, it } from 'vitest'
+import { createExampleChart as stackedRadial } from '../cases/189-shadcn-radial-stacked/example'
+
+describe('pinned shadcn example data contracts', () => {
+ it('stacks 570 mobile and 1260 desktop visitors without losing desktop values', () => {
+ const scene = createChartRuntime().render(stackedRadial(), {
+ width: 250,
+ height: 250,
+ })
+ const rows = scene.points
+ .map((point) => point.datum)
+ .filter(
+ (datum): datum is { id: string; start: number; end: number } =>
+ typeof datum === 'object' &&
+ datum !== null &&
+ 'start' in datum &&
+ 'end' in datum,
+ )
+ expect(rows).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({ id: 'mobile', start: 0, end: 570 }),
+ expect.objectContaining({ id: 'desktop', start: 570, end: 1830 }),
+ ]),
+ )
+ })
+})
diff --git a/benchmarks/conformance/shared/shadcn-reference-accessibility.test.ts b/benchmarks/conformance/shared/shadcn-reference-accessibility.test.ts
index 594a783c..b18c54ae 100644
--- a/benchmarks/conformance/shared/shadcn-reference-accessibility.test.ts
+++ b/benchmarks/conformance/shared/shadcn-reference-accessibility.test.ts
@@ -4,6 +4,39 @@ import { createShadcnRechartsExample } from './shadcn-catalog-recharts'
import { getShadcnCatalogSpec, shadcnColors } from './shadcn-catalog-data'
describe('shadcn reference accessibility', () => {
+ it.each([
+ ['chart-bar-active', '.recharts-bar-rectangle path', 5],
+ ['chart-bar-mixed', '.recharts-bar-rectangle path', 5],
+ ['chart-line-interactive', '.recharts-line-curve', 1],
+ [
+ 'chart-radial-stacked',
+ 'path.recharts-radial-bar-sector, .recharts-radial-bar-sector path',
+ 2,
+ ],
+ ])(
+ 'preserves the pinned datum count through resize and revision: %s',
+ async (name, selector, count) => {
+ const root = document.createElement('div')
+ document.body.append(root)
+ const handle = await act(async () =>
+ createShadcnRechartsExample(name).mount(root, {
+ width: 640,
+ height: 600,
+ revision: 0,
+ }),
+ )
+ try {
+ expect(root.querySelectorAll(selector)).toHaveLength(count)
+ await act(async () =>
+ handle.update({ width: 320, height: 600, revision: 1 }),
+ )
+ expect(root.querySelectorAll(selector)).toHaveLength(count)
+ } finally {
+ await act(async () => handle.destroy())
+ root.remove()
+ }
+ },
+ )
it.each([
['chart-radial-text', '200'],
['chart-radial-shape', '1,260'],
diff --git a/scripts/benchmark/stress-phases.mjs b/scripts/benchmark/stress-phases.mjs
new file mode 100644
index 00000000..4cbc83cd
--- /dev/null
+++ b/scripts/benchmark/stress-phases.mjs
@@ -0,0 +1,92 @@
+// Raw scatter has independent mount and update trials, not a continuous stream.
+// Give each trial its own context and deadline, identically for every renderer.
+export async function runStressTimingPhases(workload, run, expectedSamples) {
+ if (workload.id !== 'raw-scatter') return run(undefined)
+ const phases = [
+ { name: 'mount', mount: true, updates: [] },
+ ...workload.updates.map((kind) => ({
+ name: `update:${kind}`,
+ mount: false,
+ updates: [kind],
+ })),
+ ]
+ const results = []
+ for (const phase of phases) {
+ const startedAt = Date.now()
+ let result = await run(phase)
+ if (
+ result.status === 'ok' &&
+ (result.updates.map((update) => update.kind).join(',') !==
+ phase.updates.join(',') ||
+ (results.length && result.digest !== results[0].result.digest) ||
+ (expectedSamples !== undefined &&
+ (phase.mount
+ ? result.mount.rawSamples.length !== expectedSamples
+ : result.updates.some(
+ (update) => update.timing.rawSamples.length !== expectedSamples,
+ ))))
+ ) {
+ result = {
+ ...result,
+ status: 'error',
+ error:
+ 'Incomplete timing samples, update kinds, or inconsistent source digest.',
+ }
+ }
+ results.push({
+ phase: phase.name,
+ elapsedMs: Date.now() - startedAt,
+ result,
+ })
+ if (result.status !== 'ok') {
+ return {
+ ...result,
+ error: `${phase.name}: ${result.error}`,
+ timingPhases: results,
+ }
+ }
+ }
+ const recoveries = results.flatMap(({ result }) =>
+ result.recovery ? [result.recovery] : [],
+ )
+ return {
+ ...results[0].result,
+ updates: results.flatMap(({ result }) => result.updates),
+ timingPhases: results.map(({ phase, elapsedMs, result }) => ({
+ phase,
+ elapsedMs,
+ recovery: result.recovery,
+ })),
+ ...(recoveries.length
+ ? {
+ recovery: {
+ phase: 'timing',
+ attempts:
+ 1 +
+ recoveries.reduce(
+ (total, recovery) => total + recovery.attempts - 1,
+ 0,
+ ),
+ recovered: true,
+ errors: recoveries.flatMap((recovery) => recovery.errors),
+ },
+ }
+ : {}),
+ longTasks: {
+ count: results.reduce(
+ (sum, { result }) => sum + result.longTasks.count,
+ 0,
+ ),
+ totalMs: results.reduce(
+ (sum, { result }) => sum + result.longTasks.totalMs,
+ 0,
+ ),
+ maximumMs: Math.max(
+ ...results.map(({ result }) => result.longTasks.maximumMs),
+ ),
+ entries: results.flatMap(({ phase, result }) =>
+ result.longTasks.entries.map((entry) => ({ ...entry, phase })),
+ ),
+ },
+ }
+}
diff --git a/scripts/benchmark/stress-phases.test.mjs b/scripts/benchmark/stress-phases.test.mjs
new file mode 100644
index 00000000..e2e05dc3
--- /dev/null
+++ b/scripts/benchmark/stress-phases.test.mjs
@@ -0,0 +1,83 @@
+import { describe, expect, it, vi } from 'vitest'
+import { runStressTimingPhases } from './stress-phases.mjs'
+
+const workload = {
+ id: 'raw-scatter',
+ updates: ['noop', 'same', 'append', 'replace', 'reorder', 'resize'],
+}
+function result(phase) {
+ return {
+ status: 'ok',
+ mount: { samples: [1, 2] },
+ output: { items: 10000 },
+ updates: phase.updates.map((kind) => ({ kind, samples: [3, 4] })),
+ longTasks: {
+ count: 1,
+ totalMs: 60,
+ maximumMs: 60,
+ entries: [{ duration: 60 }],
+ },
+ }
+}
+describe('stress timing phase isolation', () => {
+ it('rejects omitted trials and shortened sample sets', async () => {
+ const omitted = await runStressTimingPhases(workload, async (phase) => ({
+ ...result(phase),
+ updates: [],
+ }))
+ expect(omitted.status).toBe('error')
+ const short = await runStressTimingPhases(
+ workload,
+ async (phase) => ({ ...result(phase), mount: { rawSamples: [1] } }),
+ 20,
+ )
+ expect(short.status).toBe('error')
+ })
+ it('runs mount and every update once, retaining samples, output, and long tasks', async () => {
+ const run = vi.fn(async (phase) => result(phase))
+ const combined = await runStressTimingPhases(workload, run)
+ expect(run.mock.calls.map(([phase]) => phase.name)).toEqual([
+ 'mount',
+ ...workload.updates.map((kind) => `update:${kind}`),
+ ])
+ expect(combined.updates).toEqual(
+ workload.updates.map((kind) => ({ kind, samples: [3, 4] })),
+ )
+ expect(combined.mount.samples).toEqual([1, 2])
+ expect(combined.output.items).toBe(10000)
+ expect(combined.longTasks.count).toBe(7)
+ expect(combined.recovery).toBeUndefined()
+ })
+ it('does not mask a failed phase or continue after it', async () => {
+ const run = vi.fn(async (phase) =>
+ phase.name === 'update:same'
+ ? { status: 'error', error: 'wrong item count' }
+ : result(phase),
+ )
+ const combined = await runStressTimingPhases(workload, run)
+ expect(combined.status).toBe('error')
+ expect(combined.error).toBe('update:same: wrong item count')
+ expect(run).toHaveBeenCalledTimes(3)
+ })
+ it('retains recovered retries from any phase', async () => {
+ const combined = await runStressTimingPhases(workload, async (phase) => ({
+ ...result(phase),
+ ...(phase.name === 'update:append'
+ ? { recovery: { attempts: 2, errors: ['timeout'], recovered: true } }
+ : {}),
+ }))
+ expect(combined.recovery).toEqual({
+ phase: 'timing',
+ attempts: 2,
+ errors: ['timeout'],
+ recovered: true,
+ })
+ })
+ it('leaves continuous and other workloads intact', async () => {
+ const run = vi.fn(async () => ({ status: 'ok' }))
+ expect(
+ await runStressTimingPhases({ id: 'rolling-keyed-window' }, run),
+ ).toEqual({ status: 'ok' })
+ expect(run).toHaveBeenCalledExactlyOnceWith(undefined)
+ })
+})
diff --git a/scripts/compare-plot-catalog-helpers.mjs b/scripts/compare-plot-catalog-helpers.mjs
index 14c3be4f..eef936c0 100644
--- a/scripts/compare-plot-catalog-helpers.mjs
+++ b/scripts/compare-plot-catalog-helpers.mjs
@@ -1,5 +1,17 @@
import { selectWeightedShard } from './benchmark/filters.mjs'
+export function conformanceInspectionPasses(inspection) {
+ return Boolean(
+ inspection &&
+ inspection.guidesContained &&
+ inspection.accessibleName &&
+ inspection.guideAssertions.every((assertion) => assertion.pass) &&
+ Object.values(inspection.geometry).every(
+ (geometry) => geometry.present && geometry.withinMaximum,
+ ),
+ )
+}
+
export function conformanceCaseHeight(entry) {
const height = entry.height === undefined ? 360 : entry.height
if (!Number.isFinite(height) || height <= 0) {
diff --git a/scripts/compare-plot-catalog-helpers.test.mjs b/scripts/compare-plot-catalog-helpers.test.mjs
index 2afc4189..5a9c9a9a 100644
--- a/scripts/compare-plot-catalog-helpers.test.mjs
+++ b/scripts/compare-plot-catalog-helpers.test.mjs
@@ -1,6 +1,7 @@
import assert from 'node:assert/strict'
import {
conformanceCaseHeight,
+ conformanceInspectionPasses,
normalizeTypeDiagnosticPath,
selectCatalogCases,
} from './compare-plot-catalog-helpers.mjs'
@@ -10,6 +11,25 @@ const { describe, test } = process.env.VITEST
: await import('node:test')
describe('catalog comparison helpers', () => {
+ test('first-party inspection retains containment, names, guides, and both count bounds', () => {
+ const good = {
+ guidesContained: true,
+ accessibleName: true,
+ guideAssertions: [{ pass: true }],
+ geometry: { bar: { present: true, withinMaximum: true } },
+ }
+ assert.equal(conformanceInspectionPasses(good), true)
+ for (const bad of [
+ undefined,
+ { ...good, guidesContained: false },
+ { ...good, accessibleName: false },
+ { ...good, guideAssertions: [{ pass: false }] },
+ { ...good, geometry: { bar: { present: false, withinMaximum: true } } },
+ { ...good, geometry: { bar: { present: true, withinMaximum: false } } },
+ ]) {
+ assert.equal(conformanceInspectionPasses(bad), false)
+ }
+ })
test('uses authored case heights without changing the legacy default', () => {
assert.equal(conformanceCaseHeight({}), 360)
assert.equal(conformanceCaseHeight({ height: 600 }), 600)
diff --git a/scripts/compare-plot-catalog.mjs b/scripts/compare-plot-catalog.mjs
index 591919be..81bb82ce 100644
--- a/scripts/compare-plot-catalog.mjs
+++ b/scripts/compare-plot-catalog.mjs
@@ -20,6 +20,7 @@ import { estimateConformanceCaseWeight } from './benchmark/conformance-sharding.
import { assertKnownFilterValues, parseShard } from './benchmark/filters.mjs'
import {
conformanceCaseHeight,
+ conformanceInspectionPasses,
normalizeTypeDiagnosticPath,
selectCatalogCases,
} from './compare-plot-catalog-helpers.mjs'
@@ -117,6 +118,9 @@ if (!profile) {
)
}
const sizeOnly = process.argv.includes('--size-only')
+const firstParty = process.argv.includes('--first-party')
+if (firstParty && sizeOnly)
+ throw new Error('--first-party cannot use --size-only.')
const caseFilter = csvOption('--case')
const shard = parseShard(optionValue('--shard'))
@@ -138,9 +142,13 @@ const selectedCases = selectCatalogCases(allCases, caseFilter, shard, (entry) =>
estimateConformanceCaseWeight(entry, profile),
)
-const typeDiagnostics = await createTypeDiagnostics()
-const typeAudit = await auditTypes(selectedCases, typeDiagnostics.byFile)
-const typeProtection = auditTypeProtection(typeDiagnostics)
+const typeDiagnostics = firstParty ? undefined : await createTypeDiagnostics()
+const typeAudit = firstParty
+ ? new Map()
+ : await auditTypes(selectedCases, typeDiagnostics.byFile)
+const typeProtection = firstParty
+ ? undefined
+ : auditTypeProtection(typeDiagnostics)
const bundles = await buildImplementations(selectedCases, typeAudit)
let measurements = []
let visualChecks = []
@@ -156,7 +164,7 @@ if (!sizeOnly) {
const implementations = bundles.filter(
(bundle) => bundle.caseId === entry.id,
)
- for (const implementation of implementations) {
+ for (const implementation of firstParty ? [] : implementations) {
measurements.push(
await measureImplementation(
browser,
@@ -198,6 +206,7 @@ if (!sizeOnly) {
const result = {
schemaVersion: 1,
+ mode: firstParty ? 'first-party' : 'comparison',
createdAt: new Date().toISOString(),
profile: profileName,
filters: {
@@ -220,8 +229,8 @@ const result = {
typescript: ts.version,
},
protocol: {
- sameTypedRows: true,
- sameSemanticDomains: true,
+ sameTypedRows: !firstParty,
+ sameSemanticDomains: !firstParty,
isolatedBundles: true,
width: 640,
height: 360,
@@ -231,8 +240,8 @@ const result = {
variants: profile.widths.flatMap((width) =>
profile.themes.map((theme) => ({ width, theme })),
),
- warmup: profile.warmup,
- samples: profile.samples,
+ warmup: firstParty ? 0 : profile.warmup,
+ samples: firstParty ? 0 : profile.samples,
mount:
'Synchronous mount plus forced layout after module loading; animations disabled.',
update:
@@ -245,6 +254,17 @@ const result = {
'Geometry, guide containment, and accessibility run before and after a data revision at every viewport/theme variant. A 640px light-mode side-by-side screenshot is retained per case.',
interaction:
'Ordered semantic scenarios run from fresh mounts with native Playwright mouse, keyboard, CDP touch, drag, pixel-wheel streams, bounded waits, and in-place revision updates; pointer cancellation and line/page delta modes use explicit DOM events. Driver-state assertions remain renderer-independent; rendered assertions inspect root-scoped DOM text, attributes, focus, visibility, scroll metrics, and bounds directly. Scenarios may retain named checkpoint screenshots. Uncaught page errors fail the active step.',
+ ...(firstParty
+ ? {
+ mount: 'Not measured.',
+ update: 'Not measured.',
+ typeSafety: 'Not audited; covered by separate cached validation.',
+ geometry:
+ 'First-party logical geometry counts and guide assertions; no relative geometry or paint comparison.',
+ visual:
+ 'First-party geometry, containment, and accessible naming before and after revision at every viewport/theme. One first-party screenshot per case.',
+ }
+ : {}),
},
cases: selectedCases,
bundles,
@@ -252,28 +272,36 @@ const result = {
measurements,
visualChecks,
behaviorChecks,
- summaries: createSummaries(
- selectedCases,
- bundles,
- measurements,
- visualChecks,
- behaviorChecks,
- typeProtection,
- ),
+ summaries: firstParty
+ ? undefined
+ : createSummaries(
+ selectedCases,
+ bundles,
+ measurements,
+ visualChecks,
+ behaviorChecks,
+ typeProtection,
+ ),
}
const json = `${JSON.stringify(result, null, 2)}\n`
-const markdown = renderMarkdown(result)
-const artifactStem = conformanceArtifactStem(result.filters.cases)
+const markdown = firstParty
+ ? `# First-party browser correctness\n\n${visualChecks.filter((check) => check.status === 'pass').length}/${visualChecks.length} layout cases passed; ${behaviorChecks.filter((check) => check.status === 'pass').length}/${behaviorChecks.length} interaction cases passed.\n\nNo comparative type, size, or timing measurements were run.\n`
+ : renderMarkdown(result)
+const artifactStem =
+ conformanceArtifactStem(result.filters.cases) +
+ (firstParty ? '--first-party' : '')
await Promise.all([
writeFile(resolve(resultDirectory, `${artifactStem}.json`), json),
writeFile(resolve(resultDirectory, `${artifactStem}.md`), markdown),
])
console.log(markdown)
-const failedVisuals = visualChecks.filter((check) => check.status === 'fail')
-const failedBehaviors = behaviorChecks.filter(
- (check) => check.status === 'fail',
+const failedVisuals = visualChecks.filter((check) =>
+ firstParty ? check.status !== 'pass' : check.status === 'fail',
+)
+const failedBehaviors = behaviorChecks.filter((check) =>
+ firstParty ? check.status !== 'pass' : check.status === 'fail',
)
if (failedVisuals.length || failedBehaviors.length) {
throw new Error(
@@ -1156,7 +1184,9 @@ function typeProtectionProbes() {
async function buildImplementations(cases, typeAudit) {
const candidates = cases.flatMap((entry) =>
- pairedRenderers(entry).map((renderer) => ({ entry, renderer })),
+ (firstParty ? [targetRenderer] : pairedRenderers(entry)).map(
+ (renderer) => ({ entry, renderer }),
+ ),
)
const bundles = new Array(candidates.length)
@@ -1189,6 +1219,10 @@ async function buildImplementations(cases, typeAudit) {
legalComments: 'none',
logLevel: 'silent',
})
+ if (firstParty) {
+ bundles[index] = { id, caseId: entry.id, renderer }
+ return
+ }
const measurementResult = await build({
entryPoints: [sourcePath],
outfile: outputPath,
@@ -1408,7 +1442,7 @@ async function compareVisuals(
const tanstack = implementations.find(
(implementation) => implementation.renderer === targetRenderer,
)
- if (!reference || !tanstack) {
+ if ((!firstParty && !reference) || !tanstack) {
return {
caseId: entry.id,
referenceRenderer,
@@ -1440,7 +1474,10 @@ async function compareVisuals(
height,
}) => {
const [{ mount: mountReference }, { mount: mountTanstack }] =
- await Promise.all([import(referenceUrl), import(tanstackUrl)])
+ await Promise.all([
+ referenceUrl ? import(referenceUrl) : {},
+ import(tanstackUrl),
+ ])
await document.fonts?.ready
const root = document.createElement('main')
root.style.display = 'grid'
@@ -1450,7 +1487,8 @@ async function compareVisuals(
document.body.append(root)
const referenceContainer = document.createElement('div')
const tanstackContainer = document.createElement('div')
- root.append(referenceContainer, tanstackContainer)
+ if (mountReference) root.append(referenceContainer)
+ root.append(tanstackContainer)
let referenceHandle
let tanstackHandle
const results = []
@@ -1467,29 +1505,31 @@ async function compareVisuals(
theme === 'dark' ? '#151a24' : '#ffffff'
}
- if (!referenceHandle) {
- referenceHandle = mountReference(referenceContainer, input)
+ if (!tanstackHandle) {
+ referenceHandle = mountReference?.(referenceContainer, input)
tanstackHandle = mountTanstack(tanstackContainer, input)
} else {
- referenceHandle.update(input)
+ referenceHandle?.update(input)
tanstackHandle.update(input)
}
forceLayout(referenceContainer)
forceLayout(tanstackContainer)
await Promise.all([
- referenceHandle.driver?.settle?.(),
+ referenceHandle?.driver?.settle?.(),
tanstackHandle.driver?.settle?.(),
])
forceLayout(referenceContainer)
forceLayout(tanstackContainer)
- const referenceInspection = inspect(
- referenceContainer,
- referenceHandle,
- referenceRenderer,
- geometry,
- guideAssertions,
- )
+ const referenceInspection = referenceHandle
+ ? inspect(
+ referenceContainer,
+ referenceHandle,
+ referenceRenderer,
+ geometry,
+ guideAssertions,
+ )
+ : undefined
const tanstackInspection = inspect(
tanstackContainer,
tanstackHandle,
@@ -1498,23 +1538,25 @@ async function compareVisuals(
guideAssertions,
)
const updatedInput = { ...input, revision: 1 }
- referenceHandle.update(updatedInput)
+ referenceHandle?.update(updatedInput)
tanstackHandle.update(updatedInput)
forceLayout(referenceContainer)
forceLayout(tanstackContainer)
await Promise.all([
- referenceHandle.driver?.settle?.(),
+ referenceHandle?.driver?.settle?.(),
tanstackHandle.driver?.settle?.(),
])
forceLayout(referenceContainer)
forceLayout(tanstackContainer)
- const updatedReferenceInspection = inspect(
- referenceContainer,
- referenceHandle,
- referenceRenderer,
- geometry,
- guideAssertions,
- )
+ const updatedReferenceInspection = referenceHandle
+ ? inspect(
+ referenceContainer,
+ referenceHandle,
+ referenceRenderer,
+ geometry,
+ guideAssertions,
+ )
+ : undefined
const updatedTanstackInspection = inspect(
tanstackContainer,
tanstackHandle,
@@ -1528,26 +1570,34 @@ async function compareVisuals(
referenceRenderer,
[referenceResultKey]: referenceInspection,
tanstack: tanstackInspection,
- geometrySimilarity: compareGeometry(
- referenceInspection.geometry,
- tanstackInspection.geometry,
- ),
- paintParity: comparePaints(
- referenceInspection.geometry,
- tanstackInspection.geometry,
- ),
+ geometrySimilarity:
+ referenceInspection &&
+ compareGeometry(
+ referenceInspection.geometry,
+ tanstackInspection.geometry,
+ ),
+ paintParity:
+ referenceInspection &&
+ comparePaints(
+ referenceInspection.geometry,
+ tanstackInspection.geometry,
+ ),
updated: {
referenceRenderer,
[referenceResultKey]: updatedReferenceInspection,
tanstack: updatedTanstackInspection,
- geometrySimilarity: compareGeometry(
- updatedReferenceInspection.geometry,
- updatedTanstackInspection.geometry,
- ),
- paintParity: comparePaints(
- updatedReferenceInspection.geometry,
- updatedTanstackInspection.geometry,
- ),
+ geometrySimilarity:
+ updatedReferenceInspection &&
+ compareGeometry(
+ updatedReferenceInspection.geometry,
+ updatedTanstackInspection.geometry,
+ ),
+ paintParity:
+ updatedReferenceInspection &&
+ comparePaints(
+ updatedReferenceInspection.geometry,
+ updatedTanstackInspection.geometry,
+ ),
},
})
}
@@ -2345,7 +2395,9 @@ async function compareVisuals(
}
},
{
- referenceUrl: `${serverUrl}bundles/${reference.id}.js`,
+ referenceUrl: reference
+ ? `${serverUrl}bundles/${reference.id}.js`
+ : null,
referenceRenderer,
referenceResultKey,
tanstackUrl: `${serverUrl}bundles/${tanstack.id}.js`,
@@ -2372,8 +2424,11 @@ async function compareVisuals(
document.body.style.color = '#172033'
document.body.style.background = '#f4f6fa'
const [{ mount: mountReference }, { mount: mountTanstack }] =
- await Promise.all([import(referenceUrl), import(tanstackUrl)])
- for (const mount of [mountReference, mountTanstack]) {
+ await Promise.all([
+ referenceUrl ? import(referenceUrl) : {},
+ import(tanstackUrl),
+ ])
+ for (const mount of [mountReference, mountTanstack].filter(Boolean)) {
const panel = document.createElement('div')
panel.style.width = '640px'
panel.style.minHeight = `${height}px`
@@ -2384,7 +2439,9 @@ async function compareVisuals(
await document.fonts?.ready
},
{
- referenceUrl: `${serverUrl}bundles/${reference.id}.js`,
+ referenceUrl: reference
+ ? `${serverUrl}bundles/${reference.id}.js`
+ : null,
tanstackUrl: `${serverUrl}bundles/${tanstack.id}.js`,
height: conformanceCaseHeight(entry),
},
@@ -2397,18 +2454,20 @@ async function compareVisuals(
return {
caseId: entry.id,
referenceRenderer,
- minimumGeometrySimilarity: entry.minimumGeometrySimilarity,
+ minimumGeometrySimilarity: firstParty
+ ? undefined
+ : entry.minimumGeometrySimilarity,
status: variants.every(
(variant) =>
visualPairPasses(
variant,
referenceResultKey,
- entry.minimumGeometrySimilarity,
+ firstParty ? undefined : entry.minimumGeometrySimilarity,
) &&
visualPairPasses(
variant.updated,
referenceResultKey,
- entry.minimumGeometrySimilarity,
+ firstParty ? undefined : entry.minimumGeometrySimilarity,
),
)
? 'pass'
@@ -2439,7 +2498,7 @@ async function compareBehaviors(
const tanstack = implementations.find(
(implementation) => implementation.renderer === targetRenderer,
)
- if (!reference || !tanstack) {
+ if ((!firstParty && !reference) || !tanstack) {
return {
caseId: entry.id,
referenceRenderer,
@@ -2457,13 +2516,15 @@ async function compareBehaviors(
theme,
revision,
referenceRenderer,
- [referenceResultKey]: await runBehaviorImplementation(
- browser,
- serverUrl,
- reference,
- entry.interactionScenarios,
- { width, height: conformanceCaseHeight(entry), theme, revision },
- ),
+ [referenceResultKey]:
+ reference &&
+ (await runBehaviorImplementation(
+ browser,
+ serverUrl,
+ reference,
+ entry.interactionScenarios,
+ { width, height: conformanceCaseHeight(entry), theme, revision },
+ )),
tanstack: await runBehaviorImplementation(
browser,
serverUrl,
@@ -2481,7 +2542,9 @@ async function compareBehaviors(
caseId: entry.id,
referenceRenderer,
status: variants.every(
- (variant) => variant[referenceResultKey].pass && variant.tanstack.pass,
+ (variant) =>
+ (firstParty || variant[referenceResultKey].pass) &&
+ variant.tanstack.pass,
)
? 'pass'
: 'fail',
@@ -3354,19 +3417,14 @@ function jsonValuesEqual(left, right) {
function visualPairPasses(pair, referenceResultKey, minimumGeometrySimilarity) {
return (
- pair.paintParity &&
+ (firstParty || pair.paintParity) &&
(minimumGeometrySimilarity === undefined ||
(pair.geometrySimilarity !== undefined &&
pair.geometrySimilarity >= minimumGeometrySimilarity)) &&
- [pair[referenceResultKey], pair.tanstack].every(
- (inspection) =>
- inspection.guidesContained &&
- inspection.accessibleName &&
- inspection.guideAssertions.every((assertion) => assertion.pass) &&
- Object.values(inspection.geometry).every(
- (geometry) => geometry.present && geometry.withinMaximum,
- ),
- )
+ (firstParty
+ ? [pair.tanstack]
+ : [pair[referenceResultKey], pair.tanstack]
+ ).every(conformanceInspectionPasses)
)
}
diff --git a/scripts/conformance-workflow.test.mjs b/scripts/conformance-workflow.test.mjs
index 763c33f8..bf7f8fbb 100644
--- a/scripts/conformance-workflow.test.mjs
+++ b/scripts/conformance-workflow.test.mjs
@@ -30,19 +30,20 @@ describe('conformance monitoring workflow contract', () => {
)
})
- test('rotates one nightly shard and runs all shards weekly', () => {
+ test('runs first-party correctness weekly and comparisons monthly, with no nightly audit', () => {
assert.deepEqual(
[...workflow.matchAll(/^\s+- cron:\s*'([^']+)'\s*$/gm)].map(
(match) => match[1],
),
- ['43 7 * * *', '13 9 * * 1'],
+ ['13 9 * * 1', '43 7 1 * *'],
)
const select = job('select')
- assert.match(select, /epoch_day=\$\(\( \$\(date -u \+%s\) \/ 86400 \)\)/)
- assert.match(select, /shard=\$\(\( epoch_day % 8 \+ 1 \)\)/)
- assert.match(select, /mode=nightly\s*\n\s+shards="\[\$shard\]"/)
- assert.match(select, /mode=weekly\s*\n\s+shards='\[1,2,3,4,5,6,7,8\]'/)
+ assert.doesNotMatch(select, /epoch_day|mode=nightly/)
+ assert.match(select, /suite=first-party/)
+ assert.match(select, /EVENT_SCHEDULE" = '43 7 1 \* \*'/)
+ assert.match(select, /suite=comparison/)
+ assert.match(select, /mode=scheduled\s*\n\s+shards='\[1,2,3,4,5,6,7,8\]'/)
assert.match(select, /mode:\s*\${{ steps\.selection\.outputs\.mode }}/)
assert.match(select, /shards:\s*\${{ steps\.selection\.outputs\.shards }}/)
})
@@ -82,10 +83,10 @@ describe('conformance monitoring workflow contract', () => {
/shard:\s*\${{ fromJSON\(needs\.select\.outputs\.shards\) }}/,
)
assert.match(conformance, /playwright:\s*['"]true['"]/)
- assert.match(
- conformance,
- /pnpm conformance -- --shard=\${{ matrix\.shard }}\/8/,
- )
+ assert.match(conformance, /pnpm conformance -- "\$\{args\[@\]\}"/)
+ assert.match(conformance, /args\+=\(--first-party\)/)
+ assert.match(conformance, /args\+=\(--case="\$CASES"\)/)
+ assert.match(job('select'), /mode=targeted\s*\n\s+shards='\[1\]'/)
assert.doesNotMatch(conformance, /conformance:quick|--profile=full/)
assert.match(
conformance,
diff --git a/scripts/generate-shadcn-cases.mjs b/scripts/generate-shadcn-cases.mjs
index 13a04e12..f5cf7707 100644
--- a/scripts/generate-shadcn-cases.mjs
+++ b/scripts/generate-shadcn-cases.mjs
@@ -310,13 +310,20 @@ function geometry(name, family) {
if (family === 'bar') {
return {
role: 'bar',
- count: variant === 'multiple' || variant === 'stacked' ? 12 : 6,
+ ...(['active', 'mixed'].includes(variant) ? { maxCount: 5 } : {}),
+ count:
+ variant === 'multiple' || variant === 'stacked'
+ ? 12
+ : ['active', 'mixed'].includes(variant)
+ ? 5
+ : 6,
}
}
if (family === 'line') {
return {
role: 'line',
- count: variant === 'multiple' || variant === 'interactive' ? 2 : 1,
+ ...(variant === 'interactive' ? { maxCount: 1 } : {}),
+ count: variant === 'multiple' ? 2 : 1,
}
}
if (family === 'pie') return { role: 'arc', count: 5 }
@@ -329,7 +336,12 @@ function geometry(name, family) {
if (family === 'radial') {
return {
role: 'bar',
- count: ['simple', 'text', 'shape'].includes(variant) ? 1 : 5,
+ ...(variant === 'stacked' ? { maxCount: 2 } : {}),
+ count: ['simple', 'text', 'shape'].includes(variant)
+ ? 1
+ : variant === 'stacked'
+ ? 2
+ : 5,
rendererRoles: { recharts: 'arc', tanstack: 'arc' },
}
}
diff --git a/scripts/stress-chart-libraries.mjs b/scripts/stress-chart-libraries.mjs
index 0901b04d..35be0ab4 100644
--- a/scripts/stress-chart-libraries.mjs
+++ b/scripts/stress-chart-libraries.mjs
@@ -9,6 +9,7 @@ import {
startBenchmarkServer,
} from './benchmark/browser.mjs'
import { CellTimeoutError } from './benchmark/cell-timeout.mjs'
+import { runStressTimingPhases } from './benchmark/stress-phases.mjs'
import { createStressDiagnostics } from './benchmark/stress-diagnostics.mjs'
import {
installStressPointerTiming,
@@ -140,20 +141,26 @@ try {
)
if (!benchmarkCase) throw new Error(`Missing case for ${cell.id}.`)
- const timing = await runIsolatedWithRetry(
- browser,
- 120_000,
- (context, diagnostics) =>
- runTimingCell(
- context,
- server.url,
- benchmarkCase,
- cell.sourceCount,
- profile,
- diagnostics,
+ const timing = await runStressTimingPhases(
+ cell.workload,
+ (timingPhase) =>
+ runIsolatedWithRetry(
+ browser,
+ 120_000,
+ (context, diagnostics) =>
+ runTimingCell(
+ context,
+ server.url,
+ benchmarkCase,
+ cell.sourceCount,
+ profile,
+ diagnostics,
+ timingPhase,
+ ),
+ cell,
+ timingPhase?.name ?? 'timing',
),
- cell,
- 'timing',
+ profile.samples,
)
let memory
if (
@@ -234,6 +241,8 @@ const result = {
retry: diagnosticMode
? 'No automatic retries in diagnostic mode.'
: 'An outer timeout or browser-context infrastructure failure receives one immediate fresh-context retry. Renderer, page, protocol, and correctness failures are not retried; every attempted error remains explicit in the result and report.',
+ timingIsolation:
+ 'Raw scatter runs mount and each independent update trial in fresh contexts, each with the original 120-second deadline and unchanged samples. This applies to every renderer. Other workloads retain whole-cell isolation. Long-task entry timestamps are local to their recorded phase.',
output:
'Adapter probes gate rendered dimensions, data items or path vertices, numeric endpoint visibility, and multi-series path, identity, and per-series vertex accounting.',
ranking:
@@ -407,6 +416,7 @@ async function runTimingCell(
sourceCount,
benchmarkProfile,
diagnostics,
+ timingPhase,
) {
const page = await context.newPage()
if (diagnosticMode)
@@ -429,6 +439,7 @@ async function runTimingCell(
profile: currentProfile,
profileName: selectedProfile,
diagnosticsEnabled,
+ timingPhase,
}) => {
const phase = diagnosticsEnabled
? (name) => console.info(`__chartsStressPhase__:${name}`)
@@ -513,7 +524,10 @@ async function runTimingCell(
let output
for (
let sampleIndex = 0;
- sampleIndex < currentProfile.warmup + currentProfile.samples;
+ sampleIndex <
+ (timingPhase && !timingPhase.mount
+ ? 0
+ : currentProfile.warmup + currentProfile.samples);
sampleIndex++
) {
const root = createRoot(initial.input, instances)
@@ -546,7 +560,7 @@ async function runTimingCell(
const updates = []
const pointerStateInputs = new Map([['initial', initial.input]])
- for (const kind of workload.updates) {
+ for (const kind of timingPhase?.updates ?? workload.updates) {
phase(`update:${kind}`)
const target =
kind === 'roll'
@@ -1824,6 +1838,7 @@ async function runTimingCell(
profile: benchmarkProfile,
profileName,
diagnosticsEnabled: diagnosticMode,
+ timingPhase,
},
)
@@ -2549,6 +2564,7 @@ function renderMarkdown(result) {
'- Grouped pointer probes gate exact focused x and per-series values before and after reorder, append, and visibility updates.',
'- Rolling streams await every monotonic revision. Bursts enqueue synchronously, drain every returned operation, and reject stale final output.',
'- Memory covers JavaScript heap and DOM counters only; GPU and native canvas allocations are outside this protocol.',
+ '- Raw scatter isolates mount and each independent update trial in fresh contexts for every renderer, retaining all samples and the 120-second deadline per phase. Other workloads keep whole-cell isolation.',
'- Compare timing only within this run and browser build.',
'',
)