My Family Said, 'Too Much Data, Don't Know How to Choose'—I Tore Down the Weight Slider and Put Up Eight Category Tags


After my family and friends actually used my coupon website, their feedback converged on a single sentence:

Too much data, don’t know how to choose.

They also added, “Like hamburgers, fried chicken, fries, egg tarts.”

And the website’s answer at the time to “help me choose” was a weight slider. It asked, “What fraction of the price do you think the side dish is worth?”—you slid it, the website recalculated the deal score for each coupon, and then re-ranked them. As for the number of coupons on the screen? Not a single one fewer.

Where That Slider Came From

During the previous round of building the CP (cost-performance) ranking engine, Claude inferred from the regression formula that “weight adjustments are needed,” and built this slider, tucking it inside the ▸ Advanced accordion. Claude didn’t explicitly mention it to me beforehand; I only found out when I saw the webpage.

I didn’t hold it against Claude. There was always going to be a real-world testing phase once the feature was built, which I would personally review—and I did review it, even speaking the sentence that later became the starting point for the pivot. I simply didn’t escalate “I can’t see the difference” into “this needs to be torn down” at the time. It took my family’s feedback, articulating that same feeling into a specific complaint, to turn it into an instruction to rebuild.

What I Said

Changing the weights makes no visible difference, might as well make it an inclusion option. For example, if you select hamburgers and fries, the coupons that appear must contain both of these items.

The starting point of the pivot was precisely this sentence. Looking back, “if you select hamburgers and fries, they must contain both”—the AND semantics of the filtering were already established in my sentence. The entire subsequent design round was merely about implementing it correctly.

I had also offered a compromise condition at the time: if preserving the weights was truly desired, they could be calculated secretly in the background, using the options as toggles. Claude said doing so would be meaningless. I was indifferent to it and agreed not to build it. The reason it was meaningless will be covered later when discussing the design decisions.

Weights Could Never Cure This Feedback By Design

Claude expanded my intuition into a mechanism-level diagnosis: getImpliedTotal() in score.ts was only called by the value and ratio ranking dimensions—weights wouldn’t filter out a single coupon. After sliding the slider, there were still 915 rows on the screen. The feedback complained about “too much data,” which is a filtering problem. Weights are a ranking tool; they were never on the same path to begin with.

There were three collateral problems, each pushing the slider further away from the user: it asked an appraiser’s question (“what fraction of the price do you think the side dish is worth?”), not a customer’s question (“what do you want to eat?”); it was hidden in the ▸ Advanced accordion, which the people complaining likely never expanded; and what was actually reducing the data volume at the time was only the search function (which requires knowing what to type beforehand) and the portion size tags (solo/duo/family/dessert—that’s portion size, not item category).

There was no control element allowing the user to say “I want fries.”

Both sides share the blame for this. The slider was built by Claude guessing from a regression formula, answering a question nobody asked it. But during the testing phase, I personally reviewed it, even felt that “changing the weights makes no visible difference,” yet didn’t take it down. The one who built something unrequested was Claude; the one who kept it was me.

Granularity: The Only Design Decision

My family said “hamburgers,” but there were no “hamburgers” in menu.json at all—burgers were split into 8 item names (kara chicken thigh burgers, crispy chicken burgers, peanut lava chicken thigh burgers…), and fries were split into small, medium, and large. Which level should the tags target? Claude didn’t just pick by feel; instead, Claude measured three levels using the corpus of 917 real coupons:

LevelTag CountPairwise AND Empty Sets
Original item names36391 out of 630 pairs empty (62%)
Item families (adopted)80 out of 28 pairs
Existing item_types5Old wine in new bottles, side mixed fries and cod rings together

Why did the original item names fail so badly? A coupon rarely contains two types of burgers simultaneously—only 25 out of 917 did—and the number of coupons containing two types of fries simultaneously was 0. Someone who wanted a hamburger facing the original item name tags would have to click 8 times, and they couldn’t even click them together.

Therefore, the load-bearing wall of the entire design was this sentence: the premise that “default AND” holds up is that each tag itself is already an OR. 8 family tags, cross-family AND, intra-family OR—these aren’t incidental details; they are the entire prerequisite for this filtering to work. Should anyone in the future want to “make the tags a bit more granular,” that would be precisely the change to knock it all down.

The number of coupons hit by the 8 generated family tags: fried chicken 604, egg tarts 495, hamburgers 290, fries 271, nuggets 194, desserts 138, other fried foods 118, rice dishes 46. Two families had the family tag but generated no visual tags: drink hit 842/917 (92%), and when a filter tag can only filter out 8%, it purely wastes a slot on the mobile layout; sauce had only 2 coupons. Both were marked chip: false in brand.json—the classification was kept (so new items wouldn’t be silently missed), but the tags were not rendered.

Four Design Decisions, Each Vetoing an Alternative

  1. Putting the family list in the data-families attribute of each row, instead of the cp-data JSON. The JSON only exists when the CP engine is enabled; routing through the JSON would hard-couple the item filtering with the CP ranking. The attribute version allows list brands that don’t do ranking to use the same filtering system—testing by temporarily switching the brand to a cp_ranking: false build showed the 8 tags rendering normally and the filtering functioning normally.
  2. The numbers on the tags are dynamic facet counts—indicating how many valid coupons will remain after clicking, graying out those with 0 to prevent clicking. Vetoed static build-time counts: a stale number on a tag saying 271 while the screen only has 94 remaining would be misleading. The dynamic version makes “clicking it turns the screen blank” impossible by design.
  3. Filtering state is not written to localStorage. Filtering is an intent for the current session; a returning visitor seeing an already-filtered page would mistakenly assume that’s all there is. Ranking preferences are still persisted as usual.
  4. No secret weight calculation in the background. This was the compromise condition I offered, and the reason Claude vetoed it was: the existing ranking results were already evaluated as “unsatisfactory,” and changing an untrusted formula from visible to invisible is just hiding the problem. Furthermore, once filtered down to a few dozen entries, the importance of ranking naturally drops—wait for the feedback to change from “don’t know how to choose” to “the selected order is wrong”; only then is it a ranking problem.

Additionally, the per_item (average price per item) ranking was removed along with the item dropdown. Losing sorts like “lowest average price per egg tart” was a known and approved tradeoff.

After the Review Ruled PASS, the Documentation Still Described the Slider

Implementation was handed over to Gemini 3.1 Pro. The in-run review was run by qwen (grok was unavailable that day), and ruled FAIL on the first round—render() was not called once on startup, so the numbers on the tags had to wait for the user’s first interaction to appear. The implementer self-corrected for one round before passing; that fix was the only functional defect in this round. qwen verified very solidly this time: headless Chrome testing, CDP measuring the mobile sticky height, and an APFS clone running the cp_ranking: false build.

After the PASS, Claude performed an additional verification (without citing the self-reported numbers from the report): corpus checks hit all 14 items; mutation testing changed families() into two bad implementations (throwing if mismatched, not deduplicating), both turned the tests red, and after restoring, they were 55/55 green. The test count dropping from 56 to 55 was correct: 2 cases testing the “removed mechanism” were deleted, and 1 was added.

Then came the final discovery of this round: Claude read the diff one more time and caught three pieces of stale text left by the implementer, all orphans caused by this change, and all outside the acceptance criteria. The item_types comment in content.config.ts still said “driving the weight sliders…weight fixed at 1.0”—the slider no longer existed; the cp_ranking comment still said “no weights”; and the heaviest one, the privacy policy in /about/ still read:

The sorting or weight preference settings you adjusted on the website

A public page describing a feature that no longer existed. All three were fixed in the same commit.

Question the Classification First, Then Question the Tool

Looking back, the biggest cost of this feedback was that it was misclassified from the very beginning. “Too much data, don’t know how to choose” is a filtering problem, yet the website’s closest tool for “helping you choose” was ranking—answering a filtering problem with a ranking tool scores a 0 no matter how well it’s done, because after pulling the slider, not a single one of those 915 rows will be gone.

So now the criteria I leave for myself are three rules. When feedback comes in, ask if the classification is right first, then ask if the tool is good. To make AND semantics like “selected A and selected B” hold up, confirm first that each tag itself is already an OR; otherwise, 62% of the combinations will turn up blank when clicked. When removing a feature, grep its name—including the Chinese, not just the identifiers—because preventing rot is something no test can do for you.