Write down the rule for UI changes to Studio, and put it on every pull request (#9463)

* Write down the rule for UI changes to Studio, and put it on every pull request

Studio has taken a run of performance changes recently, and the question that
decided each of them was not whether it was faster but whether the interface
still behaved the same way. That rule has been applied consistently and written
down nowhere, so it is stated here.

Two exemptions, both of which have come up for real: a dramatic performance win
can justify a deliberate UI difference if it is called out with its cost, and a
difference that exists only off screen is fine, because rendering only what is
visible is a technique rather than a violation.

The off-screen exemption is the one that needs the guard rails, so those are
spelled out. A partly visible element is visible. Selection, clipboard,
find-in-page and printing are whole-document operations, so a copied thread that
stops at the edge of the viewport is data loss rather than an off-screen
difference. Scroll geometry is visible even when the thing that shrank is not.
Each of those is a mistake that was made and measured rather than imagined.

Adds a pull request template so the question is asked once per PR instead of
being remembered.

* Require the parity verdict, and its null, on the pull request

Asking whether the UI changed is not the same as checking. Every Studio change
in the recent run that turned out to alter the interface was caught by the
parity suite rather than by reading the diff, and the two that were nearly
missed were missed because the verdict was quoted without its control.

So the template now asks for the verdict, the number of action pairs compared,
and the null control's score on the same run. A verdict with no null has no
floor, which means a suite that compared nothing reports the same clean line as
a suite that compared everything and agreed. That happened: a run once scored
the treatment better than two copies of the same build.

It also asks which claim the verdict supports. The structural digest covers
thread structure and is blind to the sidebar and to computed layout, so a change
confined to either passes while being invisible to it. Visible-region parity is
the check that backs the off-screen exemption, since a whole-document digest
reports differences by design on a windowed or deferred change and proves
nothing.

* Link the contribution guidelines by a URL that survives a pull request body

* Run the template link guard in the lane with no paths filter

* Catch a relative reference-style link in the template too

* Accept root-relative targets in the PR template link guard

A leading slash discards the current path and resolves against the host, so
/owner/repo/blob/main/CONTRIBUTING.md reaches the same page the absolute URL does
from any PR at any number. The guard classified it alongside ../CONTRIBUTING.md and
would have failed CI on a valid link.

The guard's own message gave the reason it does not apply: the template is rendered
at /owner/repo/pull/N, so a DOCUMENT-relative target resolves off the repository
tree. A root-relative target throws that base away, which is the point of the form.

* Unwrap angle-bracketed inline link destinations in the PR template guard

* Tighten the comments in the PR template link guard

---------

Co-authored-by: danielhanchen <unslothshared@gmail.com>
Co-authored-by: danielhanchen <unslothai@gmail.com>
This commit is contained in:
Daniel Han 2026-08-22 21:35:35 -07:00 • committed by GitHub
parent d3acce846b
commit 963639df6a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 218 additions and 0 deletions

52
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,52 @@
## What this changes
<!-- One or two sentences. What moves, and why. -->
## Motivation
<!-- Link the issue if there is one. If this is a performance change, say what the cost was
and where you measured it. -->
## Testing
<!-- What you ran, and what it said. Numbers rather than adjectives where you have them. -->
---
### If this touches the Studio UI
Studio changes are expected to leave the interface behaving the same way it did before.
Tick whichever applies, and delete the rest.
- [ ] No user-visible change. The UI behaves identically.
- [ ] A deliberate UI difference, justified by a dramatic performance improvement. The
difference and its cost to the user are described above, with the measurement.
- [ ] The difference exists only off screen. Everything inside the viewport is unchanged.
If you ticked the third box, confirm that selection, clipboard, native find-in-page,
printing and scroll geometry all still cover the whole conversation rather than the visible
window. Those are whole-document operations and the off-screen exemption does not reach
them.
**Whichever box you ticked, run the parity suite and paste its verdict**, including the
number of action pairs compared and the concurrent null control's score on the same run. A
parity result without its null beside it is not evidence, because it gives no floor to judge
a difference against.
```
parity verdict:
pairs compared:
null control:
```
Two things the verdict does not tell you on its own, so say which one you are claiming:
- The structural digest covers thread structure. It is blind to the sidebar and to computed
layout, and it never reads geometry or custom properties. A change confined to either will
pass it while being invisible to it.
- Visible-region parity is the check that supports the off-screen exemption. It is the one to
use for a windowed or deferred change, since a whole-document digest will report
differences by design and prove nothing.
See [CONTRIBUTING.md](https://github.com/unslothai/unsloth/blob/main/CONTRIBUTING.md) for the
full rule.

View file

@ -154,6 +154,7 @@ jobs:
tests/studio/test_main_runs_survive_merge_bursts.py \
tests/studio/test_pester_bootstrap_hardening.py \
tests/studio/test_playwright_suites_run_in_ci.py \
tests/studio/test_pull_request_template_links.py \
tests/studio/test_sdk_installs_are_major_bounded.py \
tests/studio/test_short_job_absorption.py \
tests/studio/test_smoke_workflows_share_one_script.py \

View file

@ -33,3 +33,43 @@ Thank you so much for reading and we hope you have lots of fun using Unsloth!
- Keep PRs focused on a single change
- Include a concise description and motivation
- Link related issues when applicable
### Changes to Studio's UI
A pull request that touches the Studio frontend must leave the interface behaving the same
way it did before. Reviewers should be able to assume that a change described as a
performance fix is a performance fix and nothing else.
There are two exemptions.
1. **A dramatic performance improvement can justify a deliberate UI difference.** State the
difference plainly in the PR body, say what it costs the user, and attach the measurement
that justifies it. A difference that is not called out is a regression, however fast the
PR is.
2. **A difference that exists only off screen is fine.** Rendering only what is visible is an
accepted technique, not a parity violation. Windowing, deferring off-screen work and
unmounting rows the user cannot see are all allowed, provided everything inside the
viewport is identical to what it was before.
The second exemption is narrower than it sounds, and the boundary is where these changes go
wrong in practice:
- An element that is partly on screen is visible, and so is one that scrolls into view during
an interaction.
- Selection, clipboard, native find-in-page and printing are whole-document operations. A
user pressing Ctrl+A, Ctrl+C or Ctrl+F is asking about the conversation, not about the
viewport, so off-screen content still has to participate. Truncating a copied thread is a
data-loss bug, not an off-screen rendering difference.
- Scroll geometry is visible. A scrollbar that no longer describes the length of the thread
is a UI change even though the thing that shrank was off screen.
If you are changing what the thread renders, measure at the 100K rung or larger against a
concurrent control, and say which of the two exemptions you are relying on, or that you are
relying on neither.
Run the parity suite either way and put its verdict in the PR, with the number of action
pairs compared and the concurrent null control's score on the same run. Without the null
there is no floor, so a clean-looking verdict cannot be distinguished from a suite that
compared nothing. Say which claim the verdict supports: the structural digest covers thread
structure and is blind to the sidebar and to computed layout, while visible-region parity is
the check that supports the off-screen exemption.

View file

@ -0,0 +1,125 @@
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
"""Every link in the PR template has to survive being pasted into a pull request body.
GitHub copies the template verbatim into the PR description, rendered at
`/<owner>/<repo>/pull/<number>`, so a DOCUMENT-relative target resolves from THAT path:
`../CONTRIBUTING.md` becomes `/<owner>/<repo>/CONTRIBUTING.md`, which 404s. A ROOT-relative
target is safe, since the leading slash discards the PR path and resolves against the host.
"""
from __future__ import annotations
import re
from pathlib import Path
import pytest
TEMPLATE = Path(__file__).resolve().parents[2] / ".github" / "PULL_REQUEST_TEMPLATE.md"
# [text](target), ignoring images. The <> wrapper is not part of the URI (CommonMark 0.31.2
# section 6.3), so keeping the brackets would read an absolute `<https://...>` as relative.
LINK = re.compile(r"(?<!!)\[[^\]]*\]\(<?([^)<>\s]+)>?")
# [label]: target -- reference definitions resolve against the same wrong base, so a guard reading
# only the inline form would pass a template whose links all 404. The optional <> wrapper and the
# trailing title must not end up in the target.
REF_DEF = re.compile(r"^[ ]{0,3}\[[^\]]+\]:[ \t]*<?([^>\s]+)>?", re.MULTILINE)
def _targets(text: str) -> list[str]:
return LINK.findall(text) + REF_DEF.findall(text)
def _resolves_off_the_pr_path(target: str) -> bool:
"""True when GitHub would resolve `target` from `/<owner>/<repo>/pull/<number>`.
A leading slash is NOT such a target: it resolves against the host, landing on the same
page as the absolute URL.
"""
if target.startswith(("http://", "https://", "mailto:", "#", "/")):
return False
return True
def test_template_exists() -> None:
assert TEMPLATE.is_file(), f"{TEMPLATE} is missing"
def test_no_relative_link_targets_in_the_pr_template() -> None:
relative = [
target
for target in _targets(TEMPLATE.read_text(encoding = "utf-8"))
if _resolves_off_the_pr_path(target)
]
assert not relative, (
f"{len(relative)} relative link target(s) in .github/PULL_REQUEST_TEMPLATE.md: "
f"{relative}. The template is rendered inside a PR body at /owner/repo/pull/N, not "
"from .github/, so a document-relative target resolves off the repository tree and "
"404s. Use the absolute https://github.com/... blob URL, or a root-relative "
"/owner/repo/blob/... target, either of which ignores the PR path."
)
@pytest.mark.parametrize(
"target,relative",
[
("../CONTRIBUTING.md", True),
("CONTRIBUTING.md", True),
("./docs/CONTRIBUTING.md", True),
# Root-relative: the leading slash drops the PR path, so rejecting it would fail CI
# on a valid link.
("/unslothai/unsloth/blob/main/CONTRIBUTING.md", False),
("https://github.com/unslothai/unsloth/blob/main/CONTRIBUTING.md", False),
("#testing", False),
],
)
def test_the_guard_classifies_targets_the_way_github_resolves_them(
target: str, relative: bool
) -> None:
"""The guard itself, against the shapes it has to separate.
Without this the test above also passes for a template with no links at all.
"""
found = _targets(f"See [CONTRIBUTING.md]({target}) for the full rule.")
assert found == [target]
assert _resolves_off_the_pr_path(target) is relative
@pytest.mark.parametrize(
"target",
[
"https://github.com/unslothai/unsloth/blob/main/CONTRIBUTING.md",
"/unslothai/unsloth/blob/main/CONTRIBUTING.md",
"../CONTRIBUTING.md",
],
)
def test_the_scan_unwraps_angle_bracketed_inline_destinations(target: str) -> None:
"""`[text](<target>)` is the same link as `[text](target)`: the brackets wrap the
destination and are not part of the URI.
"""
found = _targets(f"See [CONTRIBUTING.md](<{target}>) for the full rule.")
assert found == [target]
assert _resolves_off_the_pr_path(target) is _resolves_off_the_pr_path(found[0])
@pytest.mark.parametrize(
"body, caught",
[
("See [guidelines][contrib].\n\n[contrib]: ../CONTRIBUTING.md\n", True),
("See [guidelines][contrib].\n\n[contrib]: <../CONTRIBUTING.md>\n", True),
('See [g][c].\n\n[c]: ../CONTRIBUTING.md "Contributing"\n', True),
("[c]: https://github.com/unslothai/unsloth/blob/main/CONTRIBUTING.md\n", False),
("See [inline](../CONTRIBUTING.md).\n", True),
("![shot](../docs/a.png)\n", False),
],
)
def test_the_scan_reads_reference_definitions_as_well_as_inline_links(body, caught) -> None:
"""A reference-style link resolves against the same wrong base as an inline one, so a
template that moved to `[guidelines][contrib]` would have passed while every rendered
link 404'd.
"""
relative = [
t for t in _targets(body) if not t.startswith(("http://", "https://", "mailto:", "#"))
]
assert bool(relative) is caught, f"targets found: {_targets(body)}"