Dead CSS deleted, and the instrument that found it sharpened (#2962, #2970) #129

Merged
bvandeusen merged 5 commits from dev into main 2026-08-23 21:34:18 -04:00
Owner

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 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.
bvandeusen added 5 commits 2026-08-23 21:34:09 -04:00
refactor(frontend): delete the dead CSS the consumer map surfaced — 102 classes, 588 lines (#2962)
CI & Build / Python tests (push) Successful in 1m1s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / TypeScript typecheck (push) Successful in 10s
CI & Build / integration (push) Successful in 26s
CI & Build / Build & push image (push) Successful in 36s
c5faaf38fb
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>
refactor(frontend): the second sweep — scoped rules that no longer match their own template (#2962)
CI & Build / Python lint (push) Successful in 3s
CI & Build / integration (push) Successful in 27s
CI & Build / Python tests (push) Successful in 1m4s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / TypeScript typecheck (push) Successful in 10s
CI & Build / Build & push image (push) Successful in 34s
4179f3e560
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>
feat(ledger): the consumer map reads transition names and concatenated prefixes (#2970)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Failing after 10s
CI & Build / integration (push) Successful in 26s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 1m6s
CI & Build / Build & push image (push) Successful in 23s
df18e897af
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>
test(ledger): the transition/prefix references clear the flag end to end; plugin 0.1.45 (#2970)
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 25s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 1m3s
CI & Build / Python lint (push) Successful in 4s
CI & Build / Build & push image (push) Successful in 14s
57f6982f56
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>
docs(frontend): the task-body comment no longer points at a class that was deleted (#2962)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / TypeScript typecheck (push) Successful in 10s
CI & Build / integration (push) Successful in 24s
CI & Build / Python tests (push) Successful in 1m2s
CI & Build / Build & push image (push) Successful in 35s
6871c25445
`.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>
bvandeusen merged commit 136dbc16a6 into main 2026-08-23 21:34:18 -04:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bvandeusen/FabledScribe#129