Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions bin/css-winners
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

# css-winners OLD.css NEW.css [reverse-sed-name reverse-sed-id]...
#
# Compares two compiled CSS bundles at the per-selector last-wins winner
# level (media-aware, paren-aware selector-group splitting). This is the
# gate that catches cascade-order regressions that declaration-SET
# comparison cannot: two rules with equal specificity whose relative
# order decides the computed value (caught the ud8jroeig5h2 mobile shift
# and the single-career apply-form flip in Phase C/D).
#
# Optional trailing name/id pairs reverse-substitute renamed classes in
# NEW before comparing (boundary-guarded), for gating re-key commits.
#
# Exit 0 + "WINNERS IDENTICAL" when equal; exit 1 with the flips listed.

old_path, new_path, *subs = ARGV
abort "usage: css-winners OLD.css NEW.css [name id]..." unless old_path && new_path

new_css = File.read(new_path)
subs.each_slice(2) do |name, id|
next unless name && id
new_css.gsub!(/(?<![-a-z0-9_])#{Regexp.escape(name)}(?![-a-z0-9_])/, "fl-node-#{id}")
end

def split_selectors(str)
out = []
depth = 0
cur = +""
str.each_char do |c|
depth += 1 if c == "("
depth -= 1 if c == ")"
if c == "," && depth.zero?
out << cur
cur = +""
else
cur << c
end
end
out << cur
out.map(&:strip)
end

def winners(css)
win = Hash.new { |h, k| h[k] = {} }
media = nil
css.tr("\n", " ").scan(/@media[^{]*\{|[^{}]*\{[^}]*\}|\}/).each do |tok|
if tok.start_with?("@media")
media = tok[0..-2].strip
elsif tok == "}"
media = nil
else
b = tok.index("{")
next unless b
sels = split_selectors(tok[0...b])
tok[(b + 1)..-2].split(";").each do |d|
p, v = d.split(":", 2)
next unless p && v
sels.each { |s| win[[media, s]][p.strip] = v.strip }
end
end
end
win
end

a = winners(File.read(old_path))
b = winners(new_css)
diffs = []
(a.keys | b.keys).each do |k|
(a[k].keys | b[k].keys).each do |p|
next if a[k][p] == b[k][p]
diffs << "#{k[0] ? "#{k[0]} " : ""}#{k[1]} | #{p}: #{a[k][p].inspect} -> #{b[k][p].inspect}"
end
end

if diffs.empty?
puts "WINNERS IDENTICAL"
else
puts diffs
exit 1
end
30 changes: 30 additions & 0 deletions docs/projects/2509-css-migration/TASK-TRACKER.md
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,36 @@ non_goals: NO wrapper-div collapse, NO visual changes, NO rule edits —

---

## ✅ PHASE D COMPLETE (2026-07-19) — PROJECT COMPLETE; ROADMAP REVISED

D1 fold-then-dedup: all 4 position-sensitive twin sets resolved
(single-career folded to live winners + 18 twins; clients 45 twins -
the C5 "flip" was a winner-parser comma artifact; single-service 49 +
single-use-cases 53 twins with the TRIPLY position-load-bearing
.clearfix pair kept and documented in-file). D2 dead-rule sweep: 760
purged-everywhere rules deleted (~89KB source) from the 5 legacy files
via agent survival audit + byte-gate (one false positive caught by the
gate and restored). bin/css-winners promoted: the per-selector
last-wins gate that caught both real regressions this phase.

**ROADMAP REVISION (Claude, per Paul delegation 2026-07-19)**:
- Project goal ACHIEVED by its own five criteria; this tracker moves to
maintenance mode.
- Wrapper collapse + critical/fl-* trio retirement + deeper legacy
strangler: PARKED as trigger-conditioned work (do only when a page
redesign touches that DOM anyway). Rationale: position-load-bearing
accidents make per-line archaeology cost grow while the evidence rule
shows ~zero shipped-byte win (PurgeCSS already strips unused legacy
rules per bundle).
- PROPOSED Phase E (awaiting Paul): "new-page paved path" - a page
starter template/archetype, 2-3 more extracted section components
(hero, card-grid, stat-row) via the proven C1 protocol, and
docs/workflows/new-page.md covering the traps (bundle-slice order,
purge greedy prefix for new class prefixes, qtest page map, ownership
map entry). Compounding value for every future page.
- Standing user-visible item: jt-reviews-box swiper design restoration
(Paul design call; recipe in the C2 note).

## ✅ PHASE C CLOSURE (2026-07-19) + Phase D backlog

Final scorecard: (1) FL files 0 ✅ · (2) no obfuscated artifacts ✅ - zero
Expand Down
Loading
Loading