Skip to content

perf: create-10k −30% (tbody remount) + D1 $ELEMENTS signal; interleaved benchmark CI; D2–D4 adjudicated - #136

Merged
scottmessinger merged 32 commits into
mainfrom
claude/elements-signal-plan-kbbumk
Jul 28, 2026
Merged

perf: create-10k −30% (tbody remount) + D1 $ELEMENTS signal; interleaved benchmark CI; D2–D4 adjudicated#136
scottmessinger merged 32 commits into
mainfrom
claude/elements-signal-plan-kbbumk

Conversation

@scottmessinger

@scottmessinger scottmessinger commented Jul 27, 2026

Copy link
Copy Markdown
Member

Summary

Started as the notes/performance/elements-signal-plan.md execution (D0–D5); grew into a full profile-driven optimization pass plus benchmark infrastructure. Every accepted change was measured with interleaved A/B on quiet hardware; every rejection has a failed-approach note.

Headline: create-10k is down ~−30% total on top of D1's −15–21% script, and benchmarks now run in CI on every PR commit.

Accepted

D1 — $ELEMENTS array-level signal (kernel)

  • One array-level signal answers "some element was replaced in place" for parent-mode For's swap effect, replacing 10,000 per-index subscriptions on create-10k.
  • core.ts: $ELEMENTS + trackArrayElements(); write.ts: bump only if a subscriber created it (no write-path getNode); for.ts: one subscription, O(1) teardown.
  • create-10k −15 to −21% script; swap −9%, clear/remove/replace −4 to −5% (bracketed + interleaved locally, confirmed −2.6% total 10/10 pairs on CI runners).

Deferred effect teardown in tracked() (kernel — changes published behavior)

  • Unlinking an effect from the signal graph is bookkeeping with no observable output, but running it inside React's unmount commit put O(subscriptions) work between a state change and the frame that shows it — unmounting a 1,000-row list tore down 1,000 effects before the browser could present the empty table. New react/disposal-queue.ts queues disposers and flushes them in the first macrotask after the next frame.
  • Every queued disposer still runs — deferred, never skipped — so subscriber lists cannot leak. The observable difference is a short window after unmount in which a component's effect can still fire; for tracked() that is a forceUpdate on an unmounted component, which React ignores. Consumers who dispose effects themselves and need teardown at exactly the unmount commit must guard their own effect body.

P1 — tbody remount on run() (app, profile-driven)

  • Profiling on quiet hardware showed React's getHostSibling at 135ms / 15.8% of create-10k vs 1.3ms at 1k — O(n²) sibling scans when placing all-new rows into the already-mounted tbody. clearEpochtbodyEpoch, bumped on run() too: fresh tbody mounts rows via the appendAllChildren fast path.
  • create-10k 348.6 → 242.1ms (−30.4%, 12/12 pairs, p<0.001); weighted total −19.9%; replace −3.3% (p=0.012); create-1k −2.2% (p=0.039). Keyed semantics verified.

Per-row selection state (app)

  • Selection moved from a store-level selected: number | null read through a per-row useComputed to a selected flag on the row itself, plus a module-level selectedRow pointer. Selecting now writes exactly two signals (deselect old, select new) instead of re-evaluating a derived value per row.
  • This is what drives the select row −25.6% in the CI table — an app-model change, not a kernel win. Re-selecting the current row is a no-op, as it was under the old model too (setProperty skips unchanged writes).

Benchmark CI (.github/workflows/benchmark.yml + perf-ab.ts)

  • Every PR commit: builds the kernel at PR head and base, runs 10 interleaved pairs (order flipped per pair so drift cancels), posts a sticky PR comment with paired median deltas, win counts, and an exact sign test. Skips itself when the kernel bundle is unchanged. pnpm perf:ab runs the same tool locally.

Fresh-clone fixes: bootstrap is a real npm dep (was a gitignored file that broke CI builds); playwright browsers install via pnpm onlyBuiltDependencies (pnpm 10 was silently blocking the postinstall).

Rejected (each with a failed-approach note)

  • D2 — swap-effect deps: post-D1 the effect has one dep; reuse saves ~2 graph ops. Measured flat. Its four interleaving tests are kept — one caught a real bug.
  • D3 — tracked(C, { refDisposal: true }): complete opt-in implementation (StrictMode-safe, dev leak guard, 5 tests) preserved at c53e82b, then reverted: flat at 12 full pairs and at 16 focused pairs on the two benchmarks it targets. The per-row passive effect is below measurement resolution even at 10k rows.
  • D4 — allocation diet: closed without implementation — profiles show no kernel function in top self-time and GC ≤3% anywhere.
  • lazy-version+weakmap-skip retest: original rejection reason (partial update +17%) did not reproduce; the real story is GC aliasing — forced-GC interleave shows the variant genuinely faster (7/8 pairs), but free-running (what the official benchmark measures) charges the GC scheduling shift against it (+13.8%, 0/12). Still rejected; note corrected.

Methodology (OPTIMIZATION-AGENT.md)

  • GC aliasing rule: allocation-reducing changes can show reproducible phantom regressions in free-running mode; disambiguate by interleaving builds and re-running the suspect benchmark with a forced pre-trace GC.
  • Profile before designing: D3/D4 came from stale attribution and died; P1 came from a fresh profile and is the biggest win of the effort.

Remaining

D5 — refresh the official js-framework-benchmark submission (needs kernel publish + the add-supergrain branch of commoncurriculum/js-framework-benchmark). The published 413.7ms create-10k predates PR #135, D1, and P1.

Review follow-ups are in #137, which targets this branch: removes the c8 ignore over disposal-queue.ts's requestAnimationFrame path (now covered by a real browser test), adds the missing changeset — without one this merges without a version bump and D5 cannot proceed — and hardens perf-ab.ts / benchmark.yml.

🤖 Generated with Claude Code

claude added 8 commits July 27, 2026 18:16
Unlinking an effect from the signal graph is pure bookkeeping. Running 1,000 of them synchronously inside React's unmount commit (e.g. clearing a large list) blocks presentation of the frame that removes the rows. tracked() now queues the alien-effect cleanup in a shared disposal queue flushed via rAF+setTimeout after the next paint. Cleanup always runs, so subscriber lists cannot leak; a spurious effect firing before the flush only calls forceUpdate on an unmounted component, a no-op. Measured (15-run medians, 4x CPU throttle): clear-rows script -4-6%, with small wins on partial update, remove, and replace.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01Gik3P8QuDQKMPe9Z7SZcEc
Two benchmark-app changes targeting clear-rows and create/select script time: (1) clear() bumps an epoch used as the tbody key, so React detaches the old tbody with a single removeChild instead of 1,000 per-row removals during the deletion commit; (2) selection state moves onto the row item (item.selected), replacing the per-row useComputed firewall - each Row subscribes only to its own item's signal and selecting writes exactly two signals. Measured (15-run script medians vs adjacent identical-code baseline): clear -9.5%, create-1k -10.5%, select -42%, partial update -7.7%.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01Gik3P8QuDQKMPe9Z7SZcEc
…reate-10k

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01Gik3P8QuDQKMPe9Z7SZcEc
…p same-row select, throw-safe disposal flush; expand optimization plan to six design candidates

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01Gik3P8QuDQKMPe9Z7SZcEc
… non-Error throwables

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01Gik3P8QuDQKMPe9Z7SZcEc
@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (f6e8838) to head (3e48f7e).

Additional details and impacted files
@@            Coverage Diff            @@
##              main      #136   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           64        65    +1     
  Lines         2217      2240   +23     
  Branches       524       529    +5     
=========================================
+ Hits          2217      2240   +23     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

claude and others added 11 commits July 27, 2026 19:06
…parent-mode For; D0 attribution findings; browser-path override for kernel browser tests
…easurement (will be re-applied)"

This reverts commit f15425c.
…browser-mode configs; D1 verdict in plan doc
…h snapshot refresh + emptiness transitions); GC-aliasing retrospective addenda
…ng tests; failed-approach note + plan verdict
Adds .github/workflows/benchmark.yml, which builds the kernel at the PR head and at the base branch and runs 10 interleaved pairs via a new perf-ab.ts orchestrator, then posts a sticky PR comment with paired median deltas, win counts and an exact sign test. Interleaving (with the order flipped each pair) is what makes this usable on shared CI runners: machine drift hits both arms equally and cancels within a pair, where a block of A followed by a block of B would confound it with the code change. The job skips itself when both kernel builds come out identical, so PRs that do not touch the kernel do not burn ~40 minutes measuring noise. perf-ab.ts only orchestrates: all timing, warmup counts and CPU throttling still come from the existing src/perf.test.ts. Krause weights moved to a shared perf-weights.ts so perf-compare.ts cannot drift from it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…wsers on install

The benchmark app imported /css/bootstrap/dist/css/bootstrap.min.css, a path that is gitignored — so fresh clones and CI had no file and the vite build failed (this is what killed the first Benchmark workflow run). bootstrap@3.3.6 is now a devDependency and currentStyle.css imports it from node_modules via postcss-import. The submission checklist is unaffected: currentStyle.css is not among the files copied to the benchmark repo, which serves its own /css tree. Also approves playwright's postinstall in pnpm-workspace.yaml (pnpm 10 blocks dependency build scripts by default), so browsers download on pnpm install instead of every browser-mode suite silently failing to launch, and syncs the lockfile-resolved playwright 1.61.1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
In a container job, the github.workspace expression expands to the host runner path (/home/runner/work/...) while run steps execute inside the container where the workspace is mounted at /__w/... — so perf-ab.ts finished all 10 interleaved pairs and then failed writing the report to a directory that does not exist in the container. The GITHUB_WORKSPACE env var evaluates inside the container, matching how the later summary/comment steps already read the files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

📊 Interleaved benchmark: PR vs base (main)

🟢 5 benchmark(s) look faster, none slower.

10 interleaved pairs. Negative Δ = faster on this PR. Bold + coloured rows are the ones where a two-sided sign test clears p ≤ 0.05.

Benchmark base (main) PR Δ median pairs improved sign test p
create rows (1k) 70.5 ms 66.7 ms -4.3% 🟢 10/10 0.002
replace all rows 97.5 ms 89.1 ms -10.0% 🟢 10/10 0.002
partial update (10th) 56.0 ms 54.0 ms -4.9% 8/10 0.109
select row 16.5 ms 11.7 ms -28.0% 🟢 10/10 0.002
swap rows 51.8 ms 49.0 ms -5.2% 8/10 0.109
remove row 41.0 ms 39.3 ms -3.7% 8/10 0.109
create many rows (10k) 1144.2 ms 828.1 ms -27.1% 🟢 10/10 0.002
append rows (1k to 1k) 81.6 ms 82.3 ms +2.2% 3/10 0.344
clear rows 50.6 ms 46.3 ms -9.4% 🟢 10/10 0.002
Weighted total 867.1 ms 684.5 ms -21.3% 🟢 10/10 0.002
How to read this

Each arm is a complete prebuilt app bundle (kernel + app), so this measures the full difference between the two builds — app-side and kernel-side changes alike. The two builds alternate within one time window, with the order flipped each pair, so machine drift hits both arms equally and the paired delta cancels it.

CI runners are shared and noisy, so absolute milliseconds here are meaningless — do not compare them against numbers from your laptop or against a previous run of this job. The paired delta and the win count are the signal.

With 10 pairs a sign test needs roughly 9/10 wins to clear p ≤ 0.05, so this job is good at catching large regressions and weak at resolving effects under a few percent. A borderline result means "run it properly", not "no effect".

GC aliasing. A change that removes allocations can shift when V8's scavenge lands relative to the measured window and show up as a reproducible regression on churn-heavy benchmarks with both script and paint time inflating together. That fingerprint is an artifact, not added work. To check, re-run the suspect benchmark with a forced collection before tracing:

PROFILE=1 npx vitest run --config vitest.dist.config.ts src/perf.test.ts -t "create rows"

(-t is a regex — "create rows (1k)" matches nothing.) If the regression disappears, it was aliasing. See OPTIMIZATION-AGENT.md.


Measured head 3e48f7e vs base main — updated 2026-07-28 03:32 UTC — run

scottmessinger and others added 5 commits July 27, 2026 21:25
…ed-GC protocol: still rejected, explanation corrected

12 interleaved pairs on quiet local hardware: the original rejection reason (partial update +17.3%) did not reproduce (-2.7%, 8/12) — the GC-aliasing suspicion was right. But create-10k regressed +13.8% (0/12, p<0.001) free-running, and an 8-pair forced-GC interleave showed the variant actually 5% FASTER (7/8 pairs) — pure GC scheduling, charged to the variant under free-running timing, which is what the official benchmark measures. Verdict unchanged, mechanism corrected in the note.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
tracked(Component, { refDisposal: true }) disposes the component's reactive effect via a React 19 ref cleanup instead of the default per-instance useEffect. The wrapper injects a stable trackedRef prop the component attaches to its root element; detach flags and queues the unlink through the deferred disposal queue (StrictMode re-attach cancels it). Dev builds warn and dispose if the ref is never attached. Default behavior unchanged; js-krauset's Row opts in. Committed for the record before measurement-based revert — see the following commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ote + plan verdict

12 full-suite interleaved pairs showed nothing significant; a focused 16-pair interleave on clear rows and replace all rows — the two benchmarks the change mechanically targets — came back flat (clear +0.7% 6/15 p=0.607, replace -0.2% 8/15 p=1.0). The per-row passive effect's remaining cost is below measurement resolution even at 10k rows; the deferred disposal queue had already removed the expensive part. Implementation preserved in the previous commit for future reference.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…hted total -19.9%

Profile-driven (owner direction: profile the app, don't design from the plan doc). perf:analyze at HEAD showed getHostSibling at 135ms / 15.8% of create-10k vs 1.3ms on replace-1k — O(n^2): each placement of an all-new child into the already-mounted tbody scans forward through not-yet-mounted sibling fibers. clearEpoch is now tbodyEpoch and run() bumps it too, so wholesale creates mount rows via the appendAllChildren fast path. add() still appends into the live tbody (keyed semantics verified, 10/10 dist tests). 12 interleaved app-dist pairs: create-10k 348.6 -> 242.1ms (-30.4%, 12/12), replace -3.3% (p=0.012), create-1k -2.2% (p=0.039), everything else flat. Kernel profiles also showed no supergrain functions in top self-time and GC <=3%, undermining D4's premise — recorded in the plan doc.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nel absent from self-time, GC <=3%)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@scottmessinger scottmessinger changed the title perf(kernel): $ELEMENTS array-level signal for parent-mode For swap detection (D1) perf: create-10k −30% (tbody remount) + D1 $ELEMENTS signal; interleaved benchmark CI; D2–D4 adjudicated Jul 28, 2026
@scottmessinger
scottmessinger marked this pull request as ready for review July 28, 2026 01:51
Copilot AI review requested due to automatic review settings July 28, 2026 01:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR is a performance-focused update to Supergrain’s kernel/react integration and the js-krauset benchmark app, and it adds drift-resistant (interleaved) benchmark automation in CI to continuously validate performance changes.

Changes:

  • Add a coarse array-level $ELEMENTS signal and use it to make parent-mode For swap detection O(1) to subscribe/teardown while preserving behavior.
  • Improve js-krauset create-10k performance by forcing a <tbody> remount on run() (avoiding React’s getHostSibling O(n²) path), plus related selection/state wiring.
  • Introduce interleaved A/B benchmarking (perf-ab.ts) and a GitHub Actions workflow to run it per-PR, plus Playwright/pnpm setup tweaks for fresh clones.

Reviewed changes

Copilot reviewed 39 out of 46 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
vitest.playwright.ts Centralizes Vitest Playwright provider creation with optional CHROMIUM_EXECUTABLE_PATH.
vitest.config.ts Switches browser provider wiring to playwrightProvider() across browser projects.
pnpm-workspace.yaml Allows Playwright’s postinstall via onlyBuiltDependencies.
pnpm-lock.yaml Updates lockfile to include new deps (notably bootstrap).
packages/silo/vitest.config.ts Uses shared Playwright provider.
packages/queries/vitest.browser.config.ts Uses shared Playwright provider.
packages/kernel/vitest.browser.config.ts Uses shared Playwright provider.
packages/kernel/vitest.bench.config.ts Uses shared Playwright provider for browser benches.
packages/kernel/tests/react/for-component-magic.test.tsx Adds swap/emptiness-transition regression coverage for parent-mode For.
packages/kernel/tests/core/disposal-queue.test.ts Adds node-environment tests for deferred disposal queue behavior.
packages/kernel/src/write.ts Bumps $ELEMENTS on in-place array element replacement (when subscribed).
packages/kernel/src/react/tracked.ts Defers tracked() cleanup via scheduleDisposal to move unlinking off the unmount critical path.
packages/kernel/src/react/for.ts Replaces per-index subscriptions with trackArrayElements(raw) in parent-mode swap effect.
packages/kernel/src/react/disposal-queue.ts Introduces deferred disposal queue implementation.
packages/kernel/src/internal.ts Exposes $ELEMENTS and trackArrayElements from the internal entrypoint.
packages/kernel/src/core.ts Defines $ELEMENTS and trackArrayElements() helper.
packages/js-krauset/vitest.config.ts Uses shared Playwright provider.
packages/js-krauset/src/perf.test.ts Supports CHROMIUM_EXECUTABLE_PATH when launching Chromium for perf runs.
packages/js-krauset/src/main.tsx Remounts <tbody> on run()/clear() and moves selection to per-item state.
packages/js-krauset/src/dist.test.ts Supports CHROMIUM_EXECUTABLE_PATH when launching Chromium for dist validation.
packages/js-krauset/perf-weights.ts Centralizes Krause benchmark weights for reuse.
packages/js-krauset/perf-compare.ts Reuses shared weights instead of duplicating the map.
packages/js-krauset/perf-ab.ts Adds interleaved A/B runner + reporting for paired deltas and sign test p-values.
packages/js-krauset/package.json Adds perf:ab script and introduces npm bootstrap dependency.
packages/js-krauset/css/currentStyle.css Switches bootstrap CSS import from a gitignored path to node_modules resolution.
packages/js-krauset/css/bootstrap/package.json Removes vendored bootstrap metadata.
packages/js-krauset/css/bootstrap/LICENSE Removes vendored bootstrap license file (now sourced via npm package).
packages/js-krauset-react-hooks/vitest.config.ts Uses shared Playwright provider.
packages/js-krauset-main/vitest.config.ts Uses shared Playwright provider.
packages/husk/vitest.browser.config.ts Uses shared Playwright provider.
packages/doc-tests/vitest.config.ts Uses shared Playwright provider (keeps as any casting).
packages/devtools/vitest.browser.config.ts Uses shared Playwright provider.
OPTIMIZATION-AGENT.md Documents interleaved A/B (perf:ab) workflow + GC-aliasing guidance.
notes/performance/elements-signal-plan.md Adds detailed plan + results narrative for D0–D5 and P1.
notes/failed-approaches/tracked-ref-disposal.md Documents D3 attempt and why it was reverted.
notes/failed-approaches/lazy-version-signal-and-weakmap-skip.md Documents lazy-version/weakmap-skip retest and GC-aliasing explanation.
notes/failed-approaches/for-swap-effect-deps.md Documents D2 attempt and why it was obsoleted by D1.
notes/failed-approaches/for-passive-swap-effect.md Documents passive swap-effect attempt and why it failed.
CLAUDE.md Adds perf:ab workflow documentation.
.gitignore Ignores new benchmark output artifacts and local Claude settings.
.github/workflows/benchmark.yml Adds PR benchmark workflow running interleaved A/B and posting a sticky PR comment.
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread vitest.playwright.ts Outdated
Comment thread packages/kernel/src/react/disposal-queue.ts Outdated
Comment thread .github/workflows/benchmark.yml Outdated
scottmessinger and others added 5 commits July 27, 2026 22:11
…timestamp, and run link

The comment is edited in place so it never bumps in the PR timeline, and without a stamp a fresh result is indistinguishable from a stale one — which made a successfully-updated comment read as 'the benchmark didn't run'.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…el-only

The kernel-only comparison was blind to app-side changes: both arms ran the PR's app, so P1's tbody-remount win (create-10k -30%) never appeared in the PR comment while the kernel-only delta read ~-2.6% — technically correct, wildly misleading for 'how much faster is this PR'. perf-ab.ts gains --mode app (swap prebuilt js-krauset/dist bundles, no rebuild between runs; kernel mode stays the default for local kernel experiments). The workflow now builds the complete app at head and at base (providing the gitignored bootstrap copy when the base predates the npm dependency) and interleaves the two bundles. Smoke-tested locally: app mode reproduces the P1 magnitude (-30.6% create-10k, single pair).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ics of CHROMIUM_EXECUTABLE_PATH; scheduleDisposal's paint guarantee)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ofiling hooks); kept

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… A/B tooling

Removes the one coverage ignore this branch introduced and closes the smaller
findings from the review of #136.

disposal-queue: `/* c8 ignore */` hid the branch that actually ships — the
requestAnimationFrame path — leaving only the node fallback tested. Both
branches are now covered for real: tests/core keeps driving the fallback with
fake timers, and a new browser suite under tests/react exercises the rAF
callback, its nested macrotask, and the backstop timer. The file reports
100% with zero skipped lines.

While covering it, fixed the stale-backstop interaction: a round arms both an
rAF chain and a ~100ms backstop, and the loser used to fire later and clear
`scheduled` even though its queue was empty — stranding the timers a
subsequent round had armed and making the next scheduleDisposal arm a third
set. flush() now returns before touching `scheduled` when there is nothing to
drain. Every disposer still runs; this only stops the redundant re-arming.

Also adds an integration test that tracked()'s teardown really does unlink the
component's effect from the signal graph after unmount — the queue was tested
in isolation, its caller was not.

changeset: the branch changes published behavior (tracked() defers effect
teardown past paint; parent-mode For swaps track one array-level signal
instead of one per index) but carried no changeset, so merging would have
bumped nothing — and D5 needs a kernel publish.

benchmark.yml: pass `runs` and `base_ref` through `env:` instead of pasting
`${{ }}` into shell source. Both need write access to influence today, so this
is closing the class, not a live hole.

perf-ab.ts: restore packages/kernel/dist (or js-krauset/dist) on exit. Swapping
arms overwrote a real build output and left whichever arm ran last silently
standing in as your dist. Also rejects a flag whose value is missing or
flag-shaped rather than substituting the default and benchmarking the wrong
thing.

perf.test.ts: assert the select benchmark's warmup left a different row
selected. Re-selecting the current row is a no-op (as it was under the old
store.selected model too), so timing a click on an already-selected row would
report ~0ms for doing nothing.

.gitignore: stop ignoring .claude/settings.json. settings.local.json is
already ignored and is where machine-specific hook paths belong; ignoring the
shared file blocks committing project-wide settings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JAbdFjd8xbca2ptYvP5BZX
scottmessinger and others added 3 commits July 27, 2026 23:08
Review fixes for #136: remove the coverage ignore, add a changeset, harden the A/B tooling
… unreachable state), rename kernel-diff step id to bundle-diff (it gates on the app bundle now)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
….local.json split in .gitignore; perf-ab restore is silent on clean exit, loud on interruption

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@scottmessinger
scottmessinger merged commit 04e677c into main Jul 28, 2026
30 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants