First sweep (c5faaf3, 102): re-derived the list locally with a stricter test than the map's — dead only if the token appears nowhere outside a <style> block, in markup or script, and is not the static half of a concatenated name. git log -S traces almost all of it to one commit, 18eb1e7 refactor(ui): Phase 7 — strip chat/voice/journal/workspace/home surfaces: that change removed the markup and the backends (no ollama, no webpush, no voice_blend anywhere in src/scribe) and left every scoped rule behind. Ollama model management, push notifications, the voice library and blend builder, geo status, MCP package rows, retention, the learned summary — plus editor-shared.css's standalone assist panel, superseded by the sidebar assist section.
Second sweep (4179f3e, 19): the first pass was too generous for a scoped rule, which can only match its own template, the component's root element, or what :deep() reaches — so a scoped rule whose name lives only in some other file is dead regardless. The map had this right and my local pass did not. Of 79 scoped classes absent from their own file, 48 are names their own file builds and 12 are transition classes; the other 19 matched nothing, including AppHeader's whole connection-status indicator and both its @keyframes.
Deletions were done by a pruner, not by hand: a selector dies when any class in its descendant chain is dead, a rule only when its entire comma list is dead — mixed lists keep their live half and its leading comment. Two defects were caught in dry-run before applying (a dropped rationale comment; blank-line normalisation reaching into script regions). Every template and script region is byte-identical.
Auditing the flag showed it had a permanent false-positive floor: ~60 classes it calls unused are alive and always would be. class_references now reads the two forms templates never spell out — a transition name= (every tag spelling, Vue 3 / Vue 2 / React suffix union) and a concatenated name's static head, emitted as the prefix reference head-* and resolved against every row sharing that head. * cannot occur in a class token, so it rides the existing dict[str, int] with no schema change.
The prefix is deliberately generous — status-* credits .status-badge alongside .status-done, because the template does not say which it built. That direction is chosen on purpose: a false "unused" makes someone delete live UI; a false "used" only leaves a rule in place.
Still out of scope, and now said so in the skill and the docstrings: a name assembled in a script (classList.add).
Verification
New unit cover for each transition spelling, the prefix forms and their guards, and prefix resolution; new integration test carrying both forms through the real extractor into flag="unused-css". check_dangling_styles.py and check_design_tokens.py pass with only the six pre-existing reports. CI green on every commit — runs 4320, 4322, 4324, 4325.
Plugin 0.1.45: df18e89 edited the shape-accounting skill without a manifest bump and CI caught it exactly as #2209 intends.
Milestone 302 left one standing fact — `195 unused classes`. This acts on it, and then fixes the reason the number could not be trusted.
## The deletions — 121 classes, 671 lines (#2962)
**First sweep** (`c5faaf3`, 102): re-derived the list locally with a stricter test than the map's — dead only if the token appears nowhere outside a `<style>` block, in markup or script, and is not the static half of a concatenated name. `git log -S` traces almost all of it to one commit, `18eb1e7 refactor(ui): Phase 7 — strip chat/voice/journal/workspace/home surfaces`: that change removed the markup *and* the backends (no `ollama`, no `webpush`, no `voice_blend` anywhere in `src/scribe`) and left every scoped rule behind. Ollama model management, push notifications, the voice library and blend builder, geo status, MCP package rows, retention, the learned summary — plus `editor-shared.css`'s standalone assist panel, superseded by the sidebar assist section.
**Second sweep** (`4179f3e`, 19): the first pass was too generous for a `scoped` rule, which can only match its own template, the component's root element, or what `:deep()` reaches — so a scoped rule whose name lives only in some *other* file is dead regardless. The map had this right and my local pass did not. Of 79 scoped classes absent from their own file, 48 are names their own file builds and 12 are transition classes; the other 19 matched nothing, including AppHeader's whole connection-status indicator and both its `@keyframes`.
Deletions were done by a pruner, not by hand: a selector dies when any class in its descendant chain is dead, a rule only when its entire comma list is dead — mixed lists keep their live half and its leading comment. Two defects were caught in dry-run before applying (a dropped rationale comment; blank-line normalisation reaching into script regions). Every template and script region is byte-identical.
## The instrument (#2970)
Auditing the flag showed it had a permanent false-positive floor: ~60 classes it calls unused are alive and always would be. `class_references` now reads the two forms templates never spell out — a transition `name=` (every tag spelling, Vue 3 / Vue 2 / React suffix union) and a concatenated name's static head, emitted as the prefix reference `head-*` and resolved against every row sharing that head. `*` cannot occur in a class token, so it rides the existing `dict[str, int]` with no schema change.
The prefix is deliberately generous — `status-*` credits `.status-badge` alongside `.status-done`, because the template does not say which it built. That direction is chosen on purpose: a false "unused" makes someone delete live UI; a false "used" only leaves a rule in place.
Still out of scope, and now said so in the skill and the docstrings: a name assembled in a script (`classList.add`).
## Verification
New unit cover for each transition spelling, the prefix forms and their guards, and prefix resolution; new integration test carrying both forms through the real extractor into `flag="unused-css"`. `check_dangling_styles.py` and `check_design_tokens.py` pass with only the six pre-existing reports. CI green on every commit — runs 4320, 4322, 4324, 4325.
Plugin 0.1.45: `df18e89` edited the shape-accounting skill without a manifest bump and CI caught it exactly as #2209 intends.
Milestone 302's map flagged 195 css rows no template names. Re-derived the
list locally with a stricter test than the map's — a class is dead only if
its token appears NOWHERE outside a <style> block, in markup or script, and
is not the static half of a concatenated name (`priority-${p}`) — which
takes the map's known blind spot off the table. 123 survived that; 21 of
those are Vue <Transition>/<transition-group> classes generated at runtime
from a name= attribute (detail-fade, peek-slide, shortcuts-fade, tab-fade,
toast) and one is ProseMirror's vendor class. Those stay. The other 102 go.
- SettingsView.vue (80, -417 lines): whole features whose UI was removed —
Ollama model management (model-*, pull-bar-*, suggestion-chip), push
notifications (push-*), the voice library and voice blending (voice-*,
blend-*), geo status (geo-*), MCP package rows, retention, the learned
summary, and the form furniture that served them.
- editor-shared.css (16, -151): the standalone assist panel, superseded by
the sidebar assist section — streaming now renders as .stream-preview in
the main area, so .assist-panel*, .assist-sections*, .assist-preview-box
and .typing-indicator have no markup left. @keyframes blink went with the
last rule that animated it. .editor-body/.editor-main are dead too:
TaskEditorView replaced them with .task-body/.task-main.
- theme.css: .btn-new-conv/.btn-send dropped from the touch-target list
(names from another app), and the .hide-desktop utility no one used. The
generated --fs-* token block is untouched.
- ShareDialog .user-result-email, KnowledgeView .today-link,
ProjectView .edit-input (the remainder rule keeps its rationale comment
and its two live selectors).
A selector dies only when every class in its descendant chain is checked —
`.live .dead` matches nothing either — and a rule only when its whole comma
list is dead, so mixed lists keep their live half. Template and script
regions are byte-identical; check_dangling_styles.py and
check_design_tokens.py both pass, with only the six pre-existing reports.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first sweep asked "does this class token appear anywhere outside a
<style> block?". That is too generous for a `<style scoped>` rule, which can
only ever match its own template, the component's root element, or whatever
`:deep()` reaches — so a scoped rule whose name lives only in some OTHER file
is dead regardless. The server's map had this right and my local pass did
not; this closes the gap.
79 scoped classes are absent from their own file. 48 are names their own file
BUILDS — `status-${task.status}`, `pri-${p}`, `toast--${type}`,
`perm-${permission}`, `diff-${op}`, `is-${kind}` — and 12 more are Vue
transition classes. Those are the map's documented blind spot and they stay.
The remaining 19 match nothing:
- AppHeader (9, -39 lines): the whole connection-status indicator —
.status-indicator/.status-dot/.status-text and the five colour states,
plus @keyframes pulse-dot and status-pulse, which had no other user. Same
Phase 7 residue as the last commit. `.btn-icon.active` goes too: `active`
is never applied in this component (its nav uses router-link-active).
- SettingsView (6): .status-badge/.status-on/.status-off,
.perm-granted/.perm-denied, .location-row. This view renders no child
components at all, so nothing can inherit its scope.
- WorkspaceNoteEditor: .note-row:hover .btn-delete and .btn-suggest-tags —
those buttons are .btn-danger-outline/.btn-ghost now.
- ProjectListView .loading-msg; SnippetEditorView .field-row.three, whose
media-query companion keeps its live .field-row half.
Checked against child-component roots before deleting, since a child's root
element does inherit the parent's scope id — none of the 19 is one. Template
and script regions byte-identical; both style checks still pass with only
the six pre-existing reports.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paying down #2962 measured `flag="unused-css"` against a hand audit and
found it had a permanent false-positive floor: roughly sixty classes it
called unused are alive and always would be, because two ordinary authoring
forms produce names no reader of `class=` attributes can see. A flag whose
list you cannot act on line by line is worse than no flag — act on it and
you delete live UI.
- A transition `name=` IS a class reference. `<Transition name="toast">`
makes Vue apply `.toast-enter-active` and its siblings at runtime, and
React's `<CSSTransition classNames="fade">` does the same with a different
suffix set. Every spelling of the tag is read (`Transition`,
`TransitionGroup`, `transition-group`), and the emitted suffix set is the
union of Vue 3, Vue 2 and React: naming a class no rule defines costs
nothing, since it resolves to no row. A bound `:name` stays unknowable.
- A concatenated name contributes its static head as a PREFIX reference.
`` `status-${s}` ``, `'pri-' + p` and `class="card-{{ v }}"` all leave a
head behind once the hole is blanked — and `_CLASS_TOKEN_RE` accepts a
trailing hyphen, so until now the extractor emitted a junk token
`"status-"` that matched nothing. It is now `status-*`, and
resolve_consumers credits every row whose symbol starts with that head,
each under the same own-file-else-fan-out rule as an exact token. `*`
cannot occur in a class token, so the marker rides the existing
dict[str, int] with no schema change. A head shorter than two characters
says nothing and is dropped.
Crediting every candidate row is the honest reading: the template genuinely
does not say which one it built, and the alternative is reporting live rules
as dead. What the map still cannot see is a name assembled in a script —
`classList.add` — which stays deliberately out of scope; the skill, the
`flag=` docstring and the refresh payload docs all say so now.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Integration cover for the seam the unit tests only meet at either end: a
template that names no class at all (`<transition-group name="toast">`) and
one that builds its class (`` `status-${s}` ``) run through the real
extractor, through resolve_consumers, into `flag="unused-css"` — and the
rules they reach stop being reported, while a rule nothing can reach stays
listed. The prefix credits BOTH status rows, which is the documented
reading: the template does not say which one it built.
The manifest bump is what the last commit owed — it edited the
shape-accounting skill, and the installer compares versions to decide
whether to refresh the cache that actually executes (#2209), so plugin
edits that ship without a bump reach the repo and stop there. CI caught it
on 4323; 0.1.44 -> 0.1.45.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`.editor-body` was removed from editor-shared.css in c5faaf3 — nothing used
it once TaskEditorView replaced it — so "Replace .editor-body for task
editor" now names something a reader cannot find. Say what .task-body is and
record that its predecessor is gone.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Milestone 302 left one standing fact —
195 unused classes. This acts on it, and then fixes the reason the number could not be trusted.The deletions — 121 classes, 671 lines (#2962)
First sweep (
c5faaf3, 102): re-derived the list locally with a stricter test than the map's — dead only if the token appears nowhere outside a<style>block, in markup or script, and is not the static half of a concatenated name.git log -Straces almost all of it to one commit,18eb1e7 refactor(ui): Phase 7 — strip chat/voice/journal/workspace/home surfaces: that change removed the markup and the backends (noollama, nowebpush, novoice_blendanywhere insrc/scribe) and left every scoped rule behind. Ollama model management, push notifications, the voice library and blend builder, geo status, MCP package rows, retention, the learned summary — pluseditor-shared.css's standalone assist panel, superseded by the sidebar assist section.Second sweep (
4179f3e, 19): the first pass was too generous for ascopedrule, which can only match its own template, the component's root element, or what:deep()reaches — so a scoped rule whose name lives only in some other file is dead regardless. The map had this right and my local pass did not. Of 79 scoped classes absent from their own file, 48 are names their own file builds and 12 are transition classes; the other 19 matched nothing, including AppHeader's whole connection-status indicator and both its@keyframes.Deletions were done by a pruner, not by hand: a selector dies when any class in its descendant chain is dead, a rule only when its entire comma list is dead — mixed lists keep their live half and its leading comment. Two defects were caught in dry-run before applying (a dropped rationale comment; blank-line normalisation reaching into script regions). Every template and script region is byte-identical.
The instrument (#2970)
Auditing the flag showed it had a permanent false-positive floor: ~60 classes it calls unused are alive and always would be.
class_referencesnow reads the two forms templates never spell out — a transitionname=(every tag spelling, Vue 3 / Vue 2 / React suffix union) and a concatenated name's static head, emitted as the prefix referencehead-*and resolved against every row sharing that head.*cannot occur in a class token, so it rides the existingdict[str, int]with no schema change.The prefix is deliberately generous —
status-*credits.status-badgealongside.status-done, because the template does not say which it built. That direction is chosen on purpose: a false "unused" makes someone delete live UI; a false "used" only leaves a rule in place.Still out of scope, and now said so in the skill and the docstrings: a name assembled in a script (
classList.add).Verification
New unit cover for each transition spelling, the prefix forms and their guards, and prefix resolution; new integration test carrying both forms through the real extractor into
flag="unused-css".check_dangling_styles.pyandcheck_design_tokens.pypass with only the six pre-existing reports. CI green on every commit — runs 4320, 4322, 4324, 4325.Plugin 0.1.45:
df18e89edited the shape-accounting skill without a manifest bump and CI caught it exactly as #2209 intends.Milestone 302's map flagged 195 css rows no template names. Re-derived the list locally with a stricter test than the map's — a class is dead only if its token appears NOWHERE outside a <style> block, in markup or script, and is not the static half of a concatenated name (`priority-${p}`) — which takes the map's known blind spot off the table. 123 survived that; 21 of those are Vue <Transition>/<transition-group> classes generated at runtime from a name= attribute (detail-fade, peek-slide, shortcuts-fade, tab-fade, toast) and one is ProseMirror's vendor class. Those stay. The other 102 go. - SettingsView.vue (80, -417 lines): whole features whose UI was removed — Ollama model management (model-*, pull-bar-*, suggestion-chip), push notifications (push-*), the voice library and voice blending (voice-*, blend-*), geo status (geo-*), MCP package rows, retention, the learned summary, and the form furniture that served them. - editor-shared.css (16, -151): the standalone assist panel, superseded by the sidebar assist section — streaming now renders as .stream-preview in the main area, so .assist-panel*, .assist-sections*, .assist-preview-box and .typing-indicator have no markup left. @keyframes blink went with the last rule that animated it. .editor-body/.editor-main are dead too: TaskEditorView replaced them with .task-body/.task-main. - theme.css: .btn-new-conv/.btn-send dropped from the touch-target list (names from another app), and the .hide-desktop utility no one used. The generated --fs-* token block is untouched. - ShareDialog .user-result-email, KnowledgeView .today-link, ProjectView .edit-input (the remainder rule keeps its rationale comment and its two live selectors). A selector dies only when every class in its descendant chain is checked — `.live .dead` matches nothing either — and a rule only when its whole comma list is dead, so mixed lists keep their live half. Template and script regions are byte-identical; check_dangling_styles.py and check_design_tokens.py both pass, with only the six pre-existing reports. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>The first sweep asked "does this class token appear anywhere outside a <style> block?". That is too generous for a `<style scoped>` rule, which can only ever match its own template, the component's root element, or whatever `:deep()` reaches — so a scoped rule whose name lives only in some OTHER file is dead regardless. The server's map had this right and my local pass did not; this closes the gap. 79 scoped classes are absent from their own file. 48 are names their own file BUILDS — `status-${task.status}`, `pri-${p}`, `toast--${type}`, `perm-${permission}`, `diff-${op}`, `is-${kind}` — and 12 more are Vue transition classes. Those are the map's documented blind spot and they stay. The remaining 19 match nothing: - AppHeader (9, -39 lines): the whole connection-status indicator — .status-indicator/.status-dot/.status-text and the five colour states, plus @keyframes pulse-dot and status-pulse, which had no other user. Same Phase 7 residue as the last commit. `.btn-icon.active` goes too: `active` is never applied in this component (its nav uses router-link-active). - SettingsView (6): .status-badge/.status-on/.status-off, .perm-granted/.perm-denied, .location-row. This view renders no child components at all, so nothing can inherit its scope. - WorkspaceNoteEditor: .note-row:hover .btn-delete and .btn-suggest-tags — those buttons are .btn-danger-outline/.btn-ghost now. - ProjectListView .loading-msg; SnippetEditorView .field-row.three, whose media-query companion keeps its live .field-row half. Checked against child-component roots before deleting, since a child's root element does inherit the parent's scope id — none of the 19 is one. Template and script regions byte-identical; both style checks still pass with only the six pre-existing reports. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>Integration cover for the seam the unit tests only meet at either end: a template that names no class at all (`<transition-group name="toast">`) and one that builds its class (`` `status-${s}` ``) run through the real extractor, through resolve_consumers, into `flag="unused-css"` — and the rules they reach stop being reported, while a rule nothing can reach stays listed. The prefix credits BOTH status rows, which is the documented reading: the template does not say which one it built. The manifest bump is what the last commit owed — it edited the shape-accounting skill, and the installer compares versions to decide whether to refresh the cache that actually executes (#2209), so plugin edits that ship without a bump reach the repo and stop there. CI caught it on 4323; 0.1.44 -> 0.1.45. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>