Conversation
Bumps [pathval](https://github.com/chaijs/pathval) from 1.1.0 to 1.1.1. - [Release notes](https://github.com/chaijs/pathval/releases) - [Changelog](https://github.com/chaijs/pathval/blob/master/CHANGELOG.md) - [Commits](chaijs/pathval@v1.1.0...v1.1.1) --- updated-dependencies: - dependency-name: pathval dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com>
…tion-notice Add note on deprecation
- Pick up latest `typescript-template-language-service-decorator` with fix for TS 5.0 - Mark package as deprecated in favor of https://github.com/styled-components/typescript-styled-plugin.
…rn/pathval-1.1.1 Bump pathval from 1.1.0 to 1.1.1
0.18.3 — Pick up new typescript-template-language-service-decorator
Bumps [ansi-regex](https://github.com/chalk/ansi-regex) from 3.0.0 to 3.0.1. - [Release notes](https://github.com/chalk/ansi-regex/releases) - [Commits](chalk/ansi-regex@v3.0.0...v3.0.1) --- updated-dependencies: - dependency-name: ansi-regex dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com>
…rn/ansi-regex-3.0.1 Bump ansi-regex from 3.0.0 to 3.0.1
Bumps [nanoid](https://github.com/ai/nanoid) to 3.3.3 and updates ancestor dependency [mocha](https://github.com/mochajs/mocha). These dependencies need to be updated together. Updates `nanoid` from 3.1.20 to 3.3.3 - [Release notes](https://github.com/ai/nanoid/releases) - [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md) - [Commits](ai/nanoid@3.1.20...3.3.3) Updates `mocha` from 8.3.0 to 10.2.0 - [Release notes](https://github.com/mochajs/mocha/releases) - [Changelog](https://github.com/mochajs/mocha/blob/master/CHANGELOG.md) - [Commits](mochajs/mocha@v8.3.0...v10.2.0) --- updated-dependencies: - dependency-name: nanoid dependency-type: indirect - dependency-name: mocha dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com>
…rn/nanoid-and-mocha-3.3.3 Bump nanoid and mocha
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.0.4 to 3.1.2. - [Release notes](https://github.com/isaacs/minimatch/releases) - [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md) - [Commits](isaacs/minimatch@v3.0.4...v3.1.2) --- updated-dependencies: - dependency-name: minimatch dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com>
…rn/minimatch-3.1.2 Bump minimatch from 3.0.4 to 3.1.2
🦋 Changeset detectedLatest commit: 8570f92 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
The diagnostic filter must distinguish code interpolations from comment-only interpolations.
Review effort: Lite
Findings: None
What changed in this PR
This PR makes emptyRules diagnostics interpolation-aware, adds coverage, and documents Helix setup and host requirements.
Changes:
- Suppresses eligible diagnostics for interpolated rule bodies.
- Adds unit, end-to-end, and scaling tests.
- Documents global and project-local Helix integration.
- Adds a patch changeset.
Review finding: comment-only interpolations can incorrectly suppress emptyRules; CSS-aware filtering and regression coverage are needed.
| File | Description |
|---|---|
test/unit/template-language-service.test.ts |
Adds diagnostic edge-case tests. |
test/performance/template-language-service-fixture.ts |
Supports configured performance fixtures. |
test/performance/scaling-check.ts |
Adds interpolation scaling coverage. |
test/e2e/scenarios/plugin-lifecycle.test.ts |
Verifies real tsserver behavior. |
src/features/diagnostics.ts |
Filters interpolated empty-rule diagnostics. |
src/features/css-diagnostic-code.ts |
Defines the empty-rule diagnostic code. |
README.md |
Documents Helix setup. |
docs/usage.md |
References Helix integration. |
docs/tsserver-host.md |
Documents language-server host requirements. |
docs/architecture.md |
Documents diagnostic filtering behavior. |
.changeset/quiet-rules-interpolate.md |
Records the patch release change. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Important
The emptyRules fix and its tests look right — I reverted src/features/ to the pre-fix commit and exactly the three new unit cases fail, and the new scaling check is linear at 3.35. The two README problems below are in the new Helix section: following the snippet as written silently turns off Helix's TypeScript inlay hints, and the location rule is stated backwards.
Reviewed changes
- Empty-rule filter —
DiagnosticsFeature.validatedrops anemptyRulesdiagnostic when the rule's own body holds a template interpolation, finding rule bodies with the existing boundary scanner and deriving the interpolation spans from the template node; a genuinely empty body, an interpolation that appears only in a selector, and a third-partyVirtualDocumentProvider's diagnostics are all left as they were. - Lint-rule id —
EMPTY_RULESET_DIAGNOSTIC_CODEinsrc/features/css-diagnostic-code.ts, internal only, not on the./apisurface. - Spec — a new "Empty rules" bullet in
docs/architecture.mdrecords the exemption, the body-scan rules (comments, strings, unquotedurl(...), unclosed bodies), and the custom-provider carve-out. - Tests — four unit cases (bodies with and without interpolations, CRLF and U+2028/U+2029 expressions, nested and unclosed rules, a custom provider), one tsserver e2e scenario with a genuinely empty rule as its positive control, and a
test:scalingcheck for a template of many interpolated and many empty rules. - Helix documentation — a README section for editor-global registration through
typescript-language-server, a pinned Node floor for that server indocs/tsserver-host.md, and Helix added to the editor list indocs/usage.md. - Changeset —
.changeset/quiet-rules-interpolate.md, apatchwhose sentence says what changed and for whom.
I also drove the real service over 29 hand-built shapes (keyframes frames, @media and @layer preludes, parent-selector blocks, &-joined and escaped selectors, interpolations in comments and strings, \} and \7b, url(x{y), unclosed bodies). Every outcome is either correct or errs silent, which is the direction AGENTS.md asks for.
ℹ️ The user-facing lint table does not mention the exemption
docs/usage.md's lint table still describes emptyRules as plain "Empty rulesets." A user who turns that rule on will now find a rule they deliberately emptied with a mixin is not reported, and nothing in the user-facing guide says so. The fix belongs in that table's cell or in one sentence under it; docs/architecture.md is the home to link, not to restate.
ℹ️ Three kinds of change, and a merge commit that only changes the DAG
The behavior fix, the Helix documentation, and 6a0049d are three independent things, and AGENTS.md asks for one kind per PR. 6a0049d is an empty merge whose only effect is making twelve microsoft/vscode-css-languageservice commits (dependabot bumps, 0.18.3 tag, a deprecation note) ancestors of main. Nothing in the repo's tooling breaks — scripts/changelog.cjs scopes attribution to the changeset file's path — but git log on main, the contributors graph, and any future git bisect all traverse that unrelated history from here on. Worth confirming that is the trade you want in main, and splitting the fix and the docs either way.
ℹ️ Nitpicks
- README:195 says "Version 6 of that language server";
docs/tsserver-host.md:29pins6.0.1, which the rest of the section's style would match, and a bare major will rot. - The snippet needs
[language-server.<name>].config, which arrived around Helix 23.05 and settled in 23.10; on 22.03 through 22.12 the equivalent lives inconfig.tomlunder[editor.lsp.typescript-language-server]. Every other editor section in this file names a host floor, and this one names none. - Every other editor section ends by saying the path "requires validation against the actual editor's tsserver host and Node runtime". The Helix section makes verified-looking claims (the Node floor,
hx --health) without that sentence, while making no claim about what it was actually run against.
Verified locally: test:unit (932 pass), test:e2e (160 pass, 12 files), test:scaling (all checks ok, new check 3.35), format:check, lint, typecheck. Reverting src/features/diagnostics.ts and src/features/css-diagnostic-code.ts to 6a0049d fails exactly the three new unit cases that name the behavior; the custom-provider case is a guard that cannot fail before the fix.
Space Bunny (free) | 𝕏
There was a problem hiding this comment.
Important
The diagnosis in the new comment is right, and I reproduced both halves of it: the override does fix the case it names, and the class it names is still live on this commit. corepack yarn test:scaling fails at ef69124 with FAIL substitution (statements each opening an unterminated comment): N=6000 0.000ms, ratio=Infinity from the same coarse-tick cause. A per-case n bump is the wrong layer for that.
Reviewed changes
- Raised the N sample of the
placeholders inside one block commentsubstitution check to 48,000, adding an optional per-casentoTextCheckCaseand threading it throughdefineTextCheckfor the substitution and escape case lists, to stopMath.minpicking a zero-length N sample and reporting a phantom superlinear regression.
Technical details
# The zero N sample is a class, not one case
## Affected sites
- `test/performance/scaling-check.ts:441` — the per-case `n: 48_000` override this commit adds
- `test/performance/scaling-check.ts:1058` — `timing.bestMs = Math.min(timing.bestMs, measurement.elapsedMs)` in `timeInterleaved`; one zero sample sets a size's minimum to 0
- `test/performance/scaling-check.ts:1096` — `const ratio = at4N.bestMs / atN.bestMs`; a zero N side makes the ratio `Infinity`, reported through `isSuperlinear` as "grew superlinearly"
- `test/performance/scaling-check.ts:609` — `n = SUBSTITUTION_CHECK_N`, the shared default this commit now lets one case override
- `test/performance/scaling-check.ts:878` — the `escapeCases` map now destructures and forwards `n`, which no escape case sets
## What the evidence shows
`elapsedCpuMs` (line 167) divides `process.threadCpuUsage()` microseconds by 1000, so every reading is quantized to the kernel's `CLOCK_THREAD_CPUTIME_ID` tick. Sampling the two at-risk fixtures directly, 25 runs each, using the same `repeatUnit` build the checks use:
| case | N | min | median | samples reading 0.0000 ms |
|---|---|---|---|---|
| `placeholders inside one block comment` | 6,000 | 0.0000 | 1.0000 | 3 / 25 |
| `placeholders inside one block comment` | 48,000 | 7.9720 | 8.0450 | 0 / 25 |
| `statements each opening an unterminated comment` | 6,000 | 0.0000 | 1.0000 | 2 / 25 |
| `statements each opening an unterminated comment` | 24,000 | 3.9770 | 4.9990 | 0 / 25 |
A `0.0000 ms` reading is a one-tick sample whose accounted CPU time had not ticked yet, not a free run: the same fixture's 4N sample reads 3.98 to 4.00 ms. The override is the right answer for the case it names, and the `20d2ca2` CI log confirms the failure it removes: `FAIL substitution (placeholders inside one block comment): N=6000 0.000ms, 4N=24000 3.999ms, ratio=Infinity`.
The class is unfixed. On this head commit `corepack yarn test:scaling` fails:
```
FAIL substitution (statements each opening an unterminated comment): N=6000 0.000ms, 4N=24000 4.277ms,
ratio=Infinity (threshold 7, floor 1.5), stopped after 4 of 5 runs per size above 14, outside the range on all 3 attempts
```
A filtered rerun of that one check failed once in three runs. `JavaScript escape replacement (escape runs inside one name)` reads 0.998 ms at N=6,000, one tick from the same cliff, and `adjacent placeholders joined into one property name` plus the two padded-escape cases read two. This is pre-existing rather than introduced here, but it is the same defect this commit sets out to stabilize, so AGENTS.md's "search for every instance and fix all of them in the same change, or list the rest" applies.
## Required outcome
- No size reaches `timeInterleaved`'s minimum as 0 while a nonzero sample of that size exists, so a coarse tick can never be reported as a superlinear regression.
## Suggested approach
- In `timeInterleaved`, exclude a zero reading from a size's running minimum once that size has a nonzero reading. If every sample of a size reads 0, keep the 0, so a genuinely dead probe behaves exactly as it does today rather than gaining new handling here.
- That removes the need for `n: 48_000`, for `TextCheckCase.n`, for the `n` parameter on `defineTextCheck`, and for the `n` destructuring in the `escapeCases` map, and returns `substitution (placeholders inside one block comment)` to `SUBSTITUTION_CHECK_N`'s 0.5 s instead of 0.9 s.
- If the per-case override is kept instead, its comment should record the measurement that picked 48,000, namely the N sample's tick count and the margin over the coarsest tick worth supporting. `SUBSTITUTION_CHECK_N` at line 594 documents its own derivation the same way.Space Bunny (free) | 𝕏
There was a problem hiding this comment.
ℹ️ No blocking issues in the docs commit — one filing nit inline.
Reviewed changes
Everything since the last pullfrog review (ef69124) is documentation across four files.
- Helix snippet — rewrote the
languages.tomlblock under[language-server.typescript-language-server.config], dropped the redundantcommand/args, and addedhostInfoplus the seven TypeScript and seven JavaScript inlay-hint keys, so copying it no longer wipes Helix's built-in initialization options. locationguidance — replaced the backwards "folder containing the scoped@styledpackage" rule with a pointer at the exactnpm root -goutput, and named the versions the setup was verified against (Helix 25.07.1,typescript-language-server6.0.1).- Escape clause — narrowed the new "Empty rules" bullet in
docs/architecture.mdfrom "escapes" to "CSS escapes" and recorded that a JavaScript escape is cooked into a real brace before the body scan. - Host notes — added a Helix bullet to
docs/tsserver-host.md, replaced the literal U+2028/U+2029 in thecomputeLineStartsexample with\u2028/\u2029escapes, refreshed the file's date, and pinned the language server's Node floor to the release it came from. - User-facing lint table —
docs/usage.mdnow describesemptyRulesas provably empty rulesets with body interpolations exempt, and lists Helix among the setup paths.
I checked the load-bearing claims against primary sources rather than taking them on faith: Helix 25.07.1's languages.toml carries byte-identically to master and holds exactly the 15 leaf keys the snippet now repeats; merge_toml_values(a, b, 3) does hand back the right-hand config table whole at depth 0, so command and args genuinely survive and the "keep the complete table" instruction is right; typescript-language-server 6.0.1 declares engines.node >= 22.22.2 and does push each plugin location through --pluginProbeLocations; and docs/configuration.md for that release states both accepted location forms. corepack yarn format:check and corepack yarn test:scaling are clean at this head.
The docs/architecture.md thread stays open for the sibling clause at line 108, which the commit did not narrow, and the scaling-guardrail thread from the last review is untouched by a docs-only commit.
Space Bunny (free) | 𝕏
There was a problem hiding this comment.
ℹ️ All three open threads are closed by this commit. Two leftovers, one inline and one here.
Reviewed changes
Everything since the prior pullfrog review (51e7e47) is one commit, 40fc6f6, across five files, and it lands the three open threads.
- Zero-CPU-sample fold — new
selectMinimumPositiveSampleintest/performance/scaling-check-cli.tsreplaces theMath.minintimeInterleaved(test/performance/scaling-check.ts:1052), so a size whose samples include a0.0000 msreading keeps its smallest positive reading instead of collapsing to zero and reporting anInfinityratio as a superlinear regression. A size where every sample is zero stays zero, andhasUnmeasurableTiming(test/performance/scaling-check.ts:1094) turns that into a distinctprobe-brokenoutcome with its own retry message rather thanover-limit. - Override removed —
TextCheckCase.n, the per-casen: 48_000, and thendestructuring in theescapeCasesmap all go, so the fix lands once in the fold rather than once per case. I grepped the repo for the leftovers: nothing still names48_000or the removed field.corepack yarn test:scalingat this head is 36 checksokand 0FAIL, withplaceholders inside one block commentback atN=6000 0.999ms, ratio=5.00in 0.5s wall andstatements each opening an unterminated commentatN=6000 0.997ms, ratio=5.70, the two cases that were one tick from the cliff. That is a coarse-clock Linux run, which is exactly the class the fix targets. - Early-exit guard —
timeInterleavednow requiresatN.bestMs > 0before comparing againstEARLY_EXIT_RATIO(test/performance/scaling-check.ts:1055), so an unmeasured N side runs allRUNS_PER_SIZEsamples and reports why, rather than stopping afterEARLY_EXIT_MIN_RUNSand appending a meaninglessstopped after 4 of 5 runsnote to a reading that was never going to be meaningful. - Spec sibling bullet —
docs/architecture.md:108now reads "read after JavaScript escapes are cooked, so a JavaScript escape can contribute a structural brace" and narrows the scanner clause to CSS escapes, matching the "Empty rules" bullet. Verified againstfindStrayClosingBraceOffset(getTemplateCssText(context))atsrc/features/diagnostics.ts:122andgetTemplateCssTextreturningreplaceEscapes(...)output atsrc/virtual-document/javascript-escapes.ts:53, so the claim is true. - Helix host note relocated — the
docs/tsserver-host.md:13bullet moved up into How tsserver loads a plugin and now closes with "its acceptedlocationforms resolve under the classic Node10 rules above" instead of restating the--pluginProbeLocationsresolution that line 8 owns. - Unit coverage — three
describecases intest/unit/scaling-check-cli.test.tspinselectMinimumPositiveSamplewith exact values across the not-yet-measured, smallest-positive, and all-unusable states. These can fail: with the oldMath.min, the fold returns0for a(0, 4)pair and the first case fails. Extracting the fold into a pure exported helper is what makes that assertable at all, sincetimeInterleavedreads real CPU time.
One note on the thread I just closed on this commit: the scaling-guardrail fix is complete and verified, so I resolved it, but the one item from its suggested approach this commit left behind is now a dead parameter at test/performance/scaling-check.ts:607 and is filed inline rather than left implicit.
ℹ️ The command's reference doc still describes the old sampling rule
docs/maintenance.md is the home for what yarn test:scaling does and how it fails, and this commit changed both without touching it. Line 147 still says the check takes "the minimum of several runs per size" where it now takes the smallest positive one, and the failure-mode paragraph that follows still lists only a ratio above the threshold or below the floor, where a size with no positive sample is now a third way a timed check fails. The same stale enumeration survives in the CheckOutcome JSDoc at test/performance/scaling-check.ts:142, which still defines probe-broken as only the two pre-existing causes. Only main()'s runtime message was updated for the new case.
Technical details
# The positive-sample fold and its failure mode are undocumented
## Affected sites
- `docs/maintenance.md:147` — "taking the minimum of several runs per size"
- `docs/maintenance.md:153-163` — the failure-mode paragraph: only the ratio above the threshold and the ratio below the floor
- `docs/maintenance.md:258` — "(`--filter` parsing, per-check progress tracking, both covered by `test/unit/scaling-check-cli.test.ts`)"
- `test/performance/scaling-check.ts:142-147` — the `CheckOutcome` JSDoc, which enumerates `probe-broken`'s causes and does not include the new one
- `test/performance/scaling-check.ts:53-59` — the JSDoc this doc paraphrases, which now adds "coarse-clock noise" to the reason for taking the minimum
- `test/performance/scaling-check.ts:1094-1097` — `hasUnmeasurableTiming`, the third failure mode
- `test/performance/scaling-check.ts:1440-1448` — `main()`'s `probe-broken` message, already updated for it
## Required outcome
- The Scaling guardrail section and the `CheckOutcome` JSDoc state the current sampling rule and all three ways a timed check can fail, so a contributor reading them does not have to re-derive the coarse-clock cause from a `0.000ms` reading in a CI log.
## Suggested approach
- `docs/maintenance.md:147`: the smallest *positive* sample of several runs per size, and why, namely that a coarse `CLOCK_THREAD_CPUTIME_ID` clock can report zero for a sub-tick sample. The same paragraph already explains the neighbouring `process.threadCpuUsage` noise source, so the sentence has company.
- Failure modes: a size whose every sample reads zero, reported as a broken probe rather than as a ratio, retried like the other two. `probe-broken` is already the name `main()` prints, so the doc can use it directly.
- `test/performance/scaling-check.ts:144`: add the unmeasured timed size to the `probe-broken` enumeration, or point at `hasUnmeasurableTiming` as the single place that decides it.
- `docs/maintenance.md:258`: three exports and three `describe` blocks now, so "both" is one word stale.
## Open questions for the human
- `docs/architecture.md` needs no change here: its only mention of the check (`Caching`, the validation-cache byte budget) does not describe sampling.Also clean locally at this head: corepack yarn format:check (136 files), lint (0 warnings, 0 errors), typecheck, test:unit (935 pass, 16 files).
Space Bunny (free) | 𝕏
|
@usercao after merge a prerelease will be automatically cut for you to test :) thanks for your help! |
|
@quantizor Thanks! Just to make sure I understand correctly, would you prefer that I |
|
You are free to merge when ready. Looks like there's some potential extra cleanup you can do if you want. |
|
Thanks for confirming! I’ll go through the additional cleanup and address it before merging. Once everything is ready and all the checks are passing, I’ll squash and merge it. Thanks again! |
There was a problem hiding this comment.
✅ No new issues found. This delta closes the last open thread, and the reworded JSDoc now matches what the filter actually does.
Reviewed changes
Everything since the prior pullfrog review is one commit, 8570f92, across two files.
- JSDoc corrected to match the filter.
filterEmptyRulesWithInterpolations(src/features/diagnostics.ts:195) now says "a template interpolation" rather than "a block-position interpolation." The code never tested block position:containsInterpolationasks only whether an interpolation span overlaps the rule body, so a block-position test would be false fora { /* ${mixin} */ }. This is also the wordingdocs/architecture.md:103and thedocs/usage.mdlint table already used, so it closes a comment-versus-spec divergence rather than opening one. - Dead
nparameter removed.defineTextCheck(test/performance/scaling-check.ts:603) no longer takes annargument;n: SUBSTITUTION_CHECK_Nsits directly in thedefineCheckcall. Both call sites (lines 868 and 873) already passed no fourth argument, so every substitution and escape check keeps the size it had. The restored shape is byte-identical to the pre-ef69124version at20d2ca2.
I drove the real service over 37 hand-built shapes to confirm the filter the comment now describes: an interpolation in a selector (${sel} {}), in a comment (a { /* ${mixin} */ }), and in a string (a { content: "${mixin}"; }) each suppress the diagnostic, while a {}, a { } ${mixin}, a { b {} ${mixin} }, and /* ${mixin} */ a {} keep it. Every outcome is either correct or errs silent, the direction AGENTS.md asks for.
Clean locally at this head: corepack yarn format:check (136 files), lint (0 warnings, 0 errors), typecheck, test:unit (935 pass, 16 files), and test:scaling (36 checks ok, 0 FAIL, with placeholders inside one block comment back at N=6000 0.977ms, ratio=4.09 on a coarse-clock host).
Space Bunny (free) | 𝕏

Hi @quantizor, when you have some time, could you please review the changes below? Thanks!
Summary
emptyRulesdiagnostics when a styled rule body contains a template interpolation that may produce declarations at runtime.typescript-language-server, including the required plugin location and host requirements.Motivation
Interpolated mixins can populate a styled rule at runtime, so reporting those rules as empty is a false positive. The plugin should remain silent when it cannot prove that a rule is empty while continuing to report rules that are demonstrably empty.
Helix users also need a complete setup path that does not require adding the plugin to every project. The new documentation explains both global registration and the existing project-local alternative.
Testing
corepack yarn verify, including formatting, lint, type checking, unit and end-to-end tests, scaling checks, package API checks, and packed-content validation.Closes #4.
Closes #12.