The Signals list is the narrowest content in the app — a name plus a right-aligned badge, in a panel that must stay thin because Bristlenose competes for the screen with the product under test, the report and the client mail. Width alone can't fix it: the row label spans a wide range. Measured across the corpus: a section name runs 12 characters at the median, an elaborated signal name 23, and a theme label 21 at the median but 51 at its longest. The ikea, uxfriends and theme-label sets are real, read from the projects’ own databases; only "torture" is synthetic.
We have more depth than width. These variants spend the depth differently.
Every panel is the real design system — the V0 rules are copied verbatim from
sidebar.css.
Each V0 panel prints how many characters fit on one line at the current width, and what share of the 347 real labels in the local corpus that covers — 27 project databases, section names, theme labels and elaborated signal names together. Drag the slider and watch it:
| width | fits | corpus covered |
|---|---|---|
| 200px min | 12 chars | 23% |
| 240px now | 18 chars | 51% |
| 280px | 24 chars | 75% |
| 320px | 30 chars | 86% |
| 360px | 36 chars | 93% |
| 480px max | 54 chars | 100% |
Covering the corpus on one line costs the entire drag range. Not the default — the maximum, 480px, the widest a user can make the panel at all, on every lens, permanently. 360px still drops 7%. That is the whole argument for spending depth instead: there is no width worth paying that is also a width that works.
The tail has a specific cause, and it is not the LLM signal names. Those are
capped — signal-elaboration.md says “Signal name MUST be 2–4 words”
and the schema repeats it, and the corpus obeys: median 23, max 31. Theme labels
have no number, only the adjective “concise” in
thematic-grouping.md and ThemeGroupItem — and they run
median 21, p90 41, max 51. The field with a number keeps to it;
the field with an adjective does not. Bounding it is a one-line prompt change,
but it would not settle the layout: theme labels are user-editable, so the row
has to survive whatever someone types regardless.
Two facts about the shipped list change the arithmetic of everything above, and neither is visible from the sidebar itself.
It is capped at six per section.
MAX_SIGNALS = 6 in AnalysisPage.tsx, applied as
slice(0, MAX_SIGNALS) to the sentiment and tag lists — and the
sidebar reads those same capped arrays, because setAnalysisSignals
is handed the capped ones. So the panel is never more than twelve
rows. Depth is not an unbounded scroll here; it is a bounded, computable
worst case, which is a much weaker argument against wrapping than it first looks.
It is sorted strongest first. Both analysis modules sort
composite_signal descending, and the client sorts again. Signal
weakens top to bottom. The card shows its rank explicitly
(.signal-rank, “#2”); the sidebar shows no number, so
position is the only rank cue it has.
That is a real strike against the regrouping variants, and it lands hardest on the ones I liked most. V4 buckets rows by badge value and V6/V7 pair them by location — both reorder a list whose order is its ranking. Pairing puts a location at the rank of its strongest signal and silently drags the weaker one up with it; grouping abandons rank entirely. On the ikea sentiment set the four rows happen to pair adjacently so nothing moves, which is exactly the kind of luck that hides the problem. V9 inherits it, since it pairs the sentiment half. Any of these needs an answer to “what happened to the ranking?” before it ships — showing the rank number, or accepting that the sidebar is a directory rather than a ranked list.
And there are two caps, both of them the same kind. The
meaningful one — “don't surface a card if the signal is too weak” — does not
exist. MIN_QUOTES_PER_CELL = 2 is a volume floor, not a strength
one, and confidence (strong / moderate / emerging) computes exactly
that judgement, is passed to the client, and gates nothing. Measured across both
axes of every real project, ranked by composite_signal, of the cards
the sidebar surfaces: 2% strong, 5% moderate, 93% emerging —
“emerging” meaning it failed both bars. In ten of eleven projects the navigation
cap never binds at all, because fewer than six signals exist to cut.
And the binding constraint is participants, not quotes. Taking
the one analysed project with real interview-length transcripts (20 sessions,
136k words, 9 participants, 484 quotes): 334 quotes carry a sentiment, landing in
58 populated cells at a median of 4 quotes each. 38 cells clear the
“moderate” quote bar of ≥4. Only 10 clear its ≥3-participant bar, and exactly 1
clears “strong”'s ≥5. Quotes are not scarce; breadth of agreement is —
which is what simpsons_neff exists to measure, and it is the right
thing to be scarce.
That reprices the whole exercise. Scaled to a five-interview study, the same funnel yields something like two to four signals worth surfacing — not twenty, not even six. So a panel built to rank twelve is, on realistic data, showing three rows. Which makes depth very cheap and truncation very expensive: each row is a large share of everything the researcher gets. (Caveats: that project is oral history rather than usability, its quotes are overwhelmingly themed rather than sectioned, and one project is not a corpus. Also: an earlier version of this paragraph counted the section axis only and read 96% / 4% / 0%. The method was wrong; the conclusion was not.)
rockclimbing · shipped is not a stress test. It is a screenshot: the twelve signals that project actually renders, 8 participants, 6 per section, with its real theme labels and its real elaborated signal names. At the 240px the panel now opens to, V0 ellipses 11 of those 12 rows. At 320px — well up the drag range — it still ellipses 11. V6 and V8 do the same, because they keep the badge on the name's line.
rockclimbing · long tail is the same project's longest real theme
labels, the ones ranking happened not to surface: “Inclusion, Representation, and
Gym Infrastructure” (49), “Training Structures and Progression Strategies” (46),
“Footwork, Technique, and Movement Fundamentals” (46). Nothing invented, nothing
padded. V0 ellipses 12 of 12. Its codebook rows carry that project's real groups
and codes — Control and freedom with undo available,
emergency exit, destructive action — so V9 is showing
real open-vocabulary tags rather than sentiment words wearing a different hat.
These labels also carry punctuation the synthetic sets did not: an en dash in “The Physical–Mental Duality of Climbing”, a slash in “Unrelated / Out-of-Scope Content”, hyphenated compounds in “Long-Term” and “Out-of-Scope”. Each is a different break opportunity, and they are the reason a wrapping variant behaves better on this set than the character counts alone would predict.
Two sets push both axes at once, at measured percentiles rather than at made-up extremes:
The tag names are real, lifted from the corpus:
subcommand-hierarchy-confusion, multi-terminal-context-switching,
Jakob: convention expectation, destructive-action-protection.
Measured over 620 tag names: median 14 characters, p90 23, max 32, and up to 11
per group. That is roughly double a sentiment word, in an open vocabulary, where
sentiment is a closed set of seven running 5–12.
At stress · max, 240px: V0 and V8 clip 6 rows, V6 clips 5, and every variant that gives the name its own line clips nothing. V9 clips nothing and carries 9 distinct badges against V0's 3 — and note these sets deliberately model a project with three codebook groups, the case where the group name is at its most informative. It still loses three to one.
Uncapped, the largest real group puts eleven pills under one name. Measured at stress · max, 240px:
2 588px 6 of 28 codes shown
3 655px 9 of 28
5 770px 15 of 28
all 1036px 28 of 28
Uncapped nearly doubles the panel. Three per row costs 67px over two and shows half again as many codes, which is where I would start — but this is a call to make by looking, which is what the control is for.
The overflow indicator is not a new control. The string is the
shipped analysis.more (“+{{count}} more”, already translated in all
21 locales) and the treatment is .cell-tooltip-footer from
analysis.css, where it already means “there are N more than shown”
— in this same lens, on the heatmap cell tooltip. The only invention here is
placement: the shipped use is a footer line beneath a list, and a footer
line per row is a line per row in a sidebar, so it rides inline at the end of the
pill flow instead. If V9 ships, that treatment should be promoted to a shared
atom rather than copied a third time — .cell-tooltip-footer is named
for its only use site. The 2 / 3 / 5 / all buttons are a playground instrument,
not a proposed affordance; the product would carry one cap, not a picker.
The pill is the shipped quote-card tag pill, and that is an argument
rather than a style choice. The sentiment badges above are a
partition: Quote.sentiment is “a single dominant sentiment,
or None”, so those badges genuinely decompose the location and their counts sum.
Codebook tags are a relation — quote_tags is many-to-many,
and analysis.py's own trade-off note says quotes tagged from several
groups “count in each group column”. So the codebook badges are a cover, not a
decomposition. Borrowing the quote card's pill — where badges have always meant
“codes present on this thing” — lets the two sections claim different things
without inventing an element to say so.
V0–V7 argue about where the badge sits. V8 asks what it says, and keeps V0's geometry exactly. The commentary strip counts distinct badge values in the Codebooks section — the measure of whether that column carries information or merely repeats the heading above it:
V0 … V7 1 distinct badge
V8 3 distinct badges
One is not a column, it is a caption printed once per row. The codebook
badge shows group_name — and measured across every real project
in the corpus, 11 of 11 have exactly one substantive codebook group
once Uncategorised is excluded. So that badge prints the same word
on every row of every real project. pattern does not: 30 tension,
12 gap, 12 success, 8 recovery across 62 elaborated signals, and it varies
inside a single project (project-ikea alone runs 5 tension, 1 gap,
1 success, 1 recovery).
V8 is strictly better than V0 on identical geometry — same height, one fewer truncation on the elaborated set, because “GAP” and “SUCCESS” are shorter than the group name they replace. And on the un-elaborated set it is shorter than V0 (−12px) with zero badges: no pattern exists yet, so nothing is drawn and the name takes the full width exactly when the name is a short location. Absence carries the information.
So V8 composes with the others rather than competing. The honest recommendation is V8's badge inside V1's wrapping — the badge stops lying, and the name stops being clipped. Nothing here measures that pair yet; it is the next panel to draw.
Numbers below are from ikea · elaborated at 240px — the real content that ships once elaboration has run, at the width the panel now opens to. They are reproduced live in each panel's commentary strip, so a change to the CSS re-measures rather than re-asserting.
V0 is the only variant that loses words — 7 of 11 rows clipped at 240px, 11 of 11 at the 200px drag minimum. Its height never changes, because that is exactly what it is buying. Every other variant trades depth for the words back, and the question is only what the depth costs.
On the ikea set, just letting it wrap looks like the cheapest fix — and the Rockclimbing set says it is not enough. V1 costs +138px there and clips nothing, but on real Rockclimbing labels it clips 3 rows at 240px and 8 at 200px: the badge holds the first line, so the name gets roughly half the width for as long as it is sharing that line. Only the variants that give the name a line of its own — V2, V7, V9 — hold at zero across every set. That is a reversal of what this file said an hour earlier, and the real data is what reversed it. I also expected V4's regrouping to win, and it does not: at +159px it is taller than simply wrapping. The reason is visible in the panel — the Codebook-tags group has only one distinct badge value (“Sentiment”), so promoting it to a sub-heading removes six identical pills and adds a heading row, netting nothing. Grouping only pays where the column actually varies, which here is the Sentiment group and not the tag group. That is an argument for applying it per-group, not for the list as a whole.
V3 is nearly free on depth — −3px, because dropping the pill buys back most of what wrapping costs. It also reads best. It should not ship alone: colour as the sole carrier of sentiment fails a colour-vision read and needs a legend. It is here because rail + grouped sentiment is a real combination, where the sub-heading spells out what the rail merely echoes.
V2 and V5 both cost more than V1 for no additional words. V2 (+235px) is the most legible two-line row and the most expensive. V5's inline tail (+176px) forces a wrap wherever the badge doesn't fit after the last word, which on this content is most rows — it flatters short names and punishes long ones, which is the wrong way round for the set that has the problem.
Set the content picker to ikea · plain to see the other half of the bind: at 240px nothing truncates in any variant, so every one of these layouts is pure cost. The row is bimodal, and any layout chosen here is being chosen for the elaborated case while the plain case pays for it.
Each variant shares one markup contract, so the shipped component only has to grow a class:
SignalEntry already exists as a discrete component in
AnalysisSidebar.tsx — extract it to its own file and give it a
variant prop.--rail custom property from the sentiment name, which
getGroupBg() already does for the tag colour sets.signalsBySourceType() gains a
sibling that buckets by columnLabel, and the badge stops rendering
per row — worth doing only for groups where that value varies.Whatever wins, turn “break long words” on and check the torture set.
The shipped .signal-entry-name sets no overflow-wrap, which
costs nothing while the name is ellipsed on one line and starts mattering the moment
it is allowed to wrap. An unbreakable token — a compound identifier, a URL, a German
noun — overflows the panel silently in every wrapping variant until that property is
set.
The width slider stays useful after a variant wins — it's the check that the chosen layout still holds at the 200px drag minimum, which is where a user who wants their content back will put it.