UI Primitive Drift Audit — 2026-07-19
Status: Archived
- Cards (batches 1+2): commit
cd51677a, OTA groupa6348dd6. - Batch 3 (primitive hygiene): commit
0391ec60, OTA groupedc93de1. - Deferred (batch 4 + SessionCompactRow bundle): SessionCompactRow dead dot-status branch, 44px row min, footer start-time, rail-ring size · 4 low-priority doc/consistency fixes · the member-dues obligations row (needs a per-member dues source). Each carries an exact file:line spec below.
- Ops note: batch 3 ran 3 parallel Agent-tool agents on a shared tree; one ran
git stashand clobbered the others' in-flight edits. All recovered; tree reconciled + gate-green before commit. Lesson: parallel agents must never run git stash/reset in a shared tree — use worktree isolation or single-thread.Method: Adversarial, source-grounded Workflow — 8 audit slices × parallel find → per-finding adversarial verify against real source → synthesis. 63 agents, 52 confirmed findings, 3.15M subagent tokens. Trigger: The owner reported the recomposed club cards "look nothing like the wireframes." Root cause: the earlier card slice reordered Slice-1 components but never built the wireframe's five NEW canonical primitives (marked "NEW (Phase 2)" on the surface-system spec's Primitives page). This audit generalizes that finding to the whole primitive set.
Ground truth = the surface-system spec + wireframes (artifacts 0c120711, 8068762b), fetched and read directly — not the spec's prose restated.
Confirmed findings by slice
| Slice | Confirmed |
|---|---|
| card-composition | 13 |
| tokens-consistency | 9 |
| well-and-surface | 7 |
| section-vocabulary | 5 |
| date-session-rows | 5 |
| text-roles | 5 |
| badge-family | 4 |
| capacity-people-family | 4 |
Correction applied to the synthesized plan
The synthesis put TierRangeBadge in packages/ui, but it needs domain tier colors from packages/app (TIER_DEFINITIONS), and packages/ui imports zero outer layers (ArchUnitTS-enforced). → TierRangeBadge lives in packages/app/src/presentation/components/status/ alongside EloTierBadge. Same principle keeps MeetAgendaRow in packages/features/clubs (consumes a club regular-meet) and the pure primitives (SectionEyebrow, GlyphFactRow, Well, BannerCapacityCluster) in packages/ui.
Execution batches (card-critical first)
Batch 1 — Card-critical NEW primitives (Wave 1)
- SectionEyebrow (
packages/ui) — the one titled-section vocabulary; extract from format-chooser-sheet's privateGroupLabel.role="micro"uppercase$textTertiary+$borderSubtlehairline. Rhythm contract:gap="$3"between zones,gap="$2"label→first row. - TierRangeBadge (
packages/app— corrected) — dot-pair pill (●min ~ ●max), 22px bordered, dots/labels from existingTIER_DEFINITIONS. ReplacesformatTierRange()+ neutral Badge at ~6 sites (card sites this wave; other 5 deferred to Wave B). - BannerCapacityCluster (
packages/ui) — fuses faces + ring +N/Mcount into one banner overlay chip (today the cards render bareParticipationCluster, no ring/count). - MeetAgendaRow (
packages/features/clubs) — per regular-meet row, WHEN-left (mini-week 110 + time 42) / WHERE-right (venue + 18px court glyph), reusing ClubScheduleBlock's day-cell values. Deliberately reintroduces per-meet detail (ClubScheduleBlock stays for the aggregate week-glance). - ClubNextSessionWell v2 (
packages/features/clubs, Wave 2) — 5 bundled fixes: body → canonicalSessionCompactRow; real venue photo (venuePhotoUrlexists on the entity, currently hardcoded[]);RecruitmentStatusBadge+recruitmentFillColor(replace hand-rolledisFullBadge); whole-well tappable viasessionId; extract sharedWellprimitive.
Batch 2 — High-priority recompose-supporting drift
- Banner badge placement:
ClubRecruitBadgeowns the banner corner (its own docstring says so);ClubJoinabilityBadgedata moves into the new 가입 안내 strip. - GlyphFactRow (
packages/ui) — 📍/👥 glyph-anchored facts column (replaces plain meta lines). Distance out of scope (no lat/lng+user-location data). - 가입 안내 strip — two chips (join policy +
duesLabel); addduesLabelto the card model. - Card frame padding
$4→$3(uniform per ruler);BANNER_OVERHANG24→$3. - 클럽 소개: chips-first then tagline; scope chip strip to
chipGroups.people(format/daytime detail moves to MeetAgendaRow). - Obligations warn-strip: non-admin member 회비-due row in the admin-row slot, self-hiding.
Batch 3 — Broader token/behavior drift (Wave B, med)
tabular-nums foundation (Text tabularNums prop + ESLint rule) · WinResultBadge 8px dot mode · AttendanceStatusBadge (COMP-2 gap) · SessionCompactRow bundle (dead dot-status branch, 44px min, footer time, rail ring size) · Badge/Text line-height reconciliation · ParticipationCluster true self-hide · avatar a11y labels · token-grid size cleanup (SectionHeader/Button/ProgressBar/SelectionChip) · Card default size $4-vs-$3 (document as two-tier, don't flip blind).
Batch 4 — Low-priority docs/consistency (Wave B, low)
Text role-count docstring (10→11) · SpecList/Callout emoji lineHeight · badge muted-gray vs brand-gray doc cross-ref · activity-heatmap 9/12 escape hatch (named constant if it recurs).
Card recompose (the acute deliverable)
DiscoverClubCard (A1): banner (ClubRecruitBadge corner · logo · fused BannerCapacityCluster) → identity (name + TierRangeBadge inline · GlyphFactRow 📍/👥) → §클럽 소개 (chips then tagline) → §정기 모임 (MeetAgendaRow ×N) → §가입 안내 (2-chip strip) → §다가오는 일정 (ClubNextSessionWell v2). Every § under a SectionEyebrow; gap="$3" between zones.
MyClubCard (B1): banner (role badge · fused cluster) → identity (name + TierRangeBadge) → §다가오는 일정 (ClubNextSessionWell v2, primary zone) → §클럽 소개 → self-hiding obligations strip.
Corrections to the synthesis (applied at execution review)
The synthesis got two card details wrong vs the real wireframe (A1/B1); execution uses the corrected version:
- DiscoverClubCard 다가오는 일정 =
SessionCompactRow dateStyle="tile", NOT the Well. Spec beat ⑥ + wireframe A1 use the compact line on the discover card; the fullClubNextSessionWellis the public-profile / member payoff only. The current card already rendersSessionCompactRow— the fix isdateStyle"dot"→"tile" + aSectionEyebrowtitle (this also sidesteps the dead status-dot finding, since tile mode needs no status). ClubNextSessionWellv2 keeps its{ nextSession }signature —nextSessionalready carriesid(tap target) andvenuePhotoUrl(real photo); no extraclub/clubIdprops. Only MyClubCard renders it.- Member-dues obligations row deferred to Wave B — it needs a real per-member club-dues data source (not
sessionPaymentGateState, which is session-scoped); don't fabricate a query. MyClubCard keeps the existing staff-onlyClubAdminRow, moved up to the obligations slot (right after identity per B1). The about/schedule zone is retained under a §클럽 소개 title (kept, not deleted — content-removal is the risky default; a trivial follow-up if the owner wants B1's minimal member card).
Full source-grounded item specs (file:line + real values): workflow wf_b255c91d-662 journal + task output.