Skip to content

UI Primitive Drift Audit — 2026-07-19

Status: Archived

  • Cards (batches 1+2): commit cd51677a, OTA group a6348dd6.
  • Batch 3 (primitive hygiene): commit 0391ec60, OTA group edc93de1.
  • 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 stash and 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

SliceConfirmed
card-composition13
tokens-consistency9
well-and-surface7
section-vocabulary5
date-session-rows5
text-roles5
badge-family4
capacity-people-family4

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 private GroupLabel. role="micro" uppercase $textTertiary + $borderSubtle hairline. 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 existing TIER_DEFINITIONS. Replaces formatTierRange() + neutral Badge at ~6 sites (card sites this wave; other 5 deferred to Wave B).
  • BannerCapacityCluster (packages/ui) — fuses faces + ring + N/M count into one banner overlay chip (today the cards render bare ParticipationCluster, 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 → canonical SessionCompactRow; real venue photo (venuePhotoUrl exists on the entity, currently hardcoded []); RecruitmentStatusBadge + recruitmentFillColor (replace hand-rolled isFull Badge); whole-well tappable via sessionId; extract shared Well primitive.

Batch 2 — High-priority recompose-supporting drift

  • Banner badge placement: ClubRecruitBadge owns the banner corner (its own docstring says so); ClubJoinabilityBadge data 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); add duesLabel to the card model.
  • Card frame padding $4$3 (uniform per ruler); BANNER_OVERHANG 24→$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:

  1. DiscoverClubCard 다가오는 일정 = SessionCompactRow dateStyle="tile", NOT the Well. Spec beat ⑥ + wireframe A1 use the compact line on the discover card; the full ClubNextSessionWell is the public-profile / member payoff only. The current card already renders SessionCompactRow — the fix is dateStyle "dot"→"tile" + a SectionEyebrow title (this also sidesteps the dead status-dot finding, since tile mode needs no status).
  2. ClubNextSessionWell v2 keeps its { nextSession } signaturenextSession already carries id (tap target) and venuePhotoUrl (real photo); no extra club/clubId props. Only MyClubCard renders it.
  3. 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-only ClubAdminRow, 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.

Markdown remains the source of truth. Run yarn docs:check before handoff.