tfrere HF Staff Cursor commited on
Commit
cf69ce3
·
1 Parent(s): 3364be7

feat(categories): switch to single-label classification

Browse files

Each app now gets EXACTLY ONE category - the dominant slug that
best captures its primary identity - instead of up to 3.

Rationale
Multi-label was producing the same app in multiple mobile-shell
swipers (a music+dance+games app would surface in three category
rows), which made the browse surface feel noisy and duplicated.
Single-label gives every app exactly one home, which is what the
mobile category chips and per-category swipers need.

Changes
- MAX_CATEGORIES_PER_APP: 3 -> 1.
- Prompt OUTPUT FORMAT: "Pick EXACTLY ONE slug" instead of "1 to 3".
- STEP 2 of the DECISION ALGORITHM: explicit tie-break guidance
(which slug a user would name FIRST in one word), with concrete
examples (music+dance, storytelling+kids, vision+games).
- Few-shot: Music Quiz example moved from ["music","games","dance"]
to ["music"] to model the tie-break.
- TAXONOMY_VERSION: 2 -> 3 (forces a reclassify on the next boot).

Side effects (positive)
Single-label naturally resolves the two pitfalls that v2 still had:
- `surf-report`: kids hallucination disappears (only voice remains).
- `marionette-js`: dev-tools-combined-with-dance disappears (only
dance remains, since dev-tools is now the SOLE category when used).

Output shape stays `categories: string[]` (always 0 or 1 entry) for
forward compatibility - reverting to multi-label later would be a
prompt change only, no API break.

Co-authored-by: Cursor <cursoragent@cursor.com>

Files changed (2) hide show
  1. server/categories.js +4 -1
  2. server/categorize.js +27 -11
server/categories.js CHANGED
@@ -33,8 +33,11 @@
33
  * - v1: initial 8-slug taxonomy.
34
  * - v2: added `games`, tightened `kids` + `dev-tools` descriptions,
35
  * switched the prompt to a DECISION ALGORITHM with few-shot.
 
 
 
36
  */
37
- export const TAXONOMY_VERSION = 2;
38
 
39
  /**
40
  * Canonical category list. Keep slugs short, kebab-case, and
 
33
  * - v1: initial 8-slug taxonomy.
34
  * - v2: added `games`, tightened `kids` + `dev-tools` descriptions,
35
  * switched the prompt to a DECISION ALGORITHM with few-shot.
36
+ * - v3: switched from multi-label (up to 3 slugs) to single-label
37
+ * (exactly 1 slug). Each app surfaces in exactly one category
38
+ * section on the mobile shell - no duplicates across swipers.
39
  */
40
+ export const TAXONOMY_VERSION = 3;
41
 
42
  /**
43
  * Canonical category list. Keep slugs short, kebab-case, and
server/categorize.js CHANGED
@@ -39,7 +39,13 @@ const DEFAULT_MODEL = 'meta-llama/Llama-3.1-8B-Instruct';
39
 
40
  // README budget
41
  const README_MAX_CHARS = 3000;
42
- const MAX_CATEGORIES_PER_APP = 3;
 
 
 
 
 
 
43
 
44
  // LLM call budget
45
  const LLM_TIMEOUT_MS = 30_000;
@@ -175,8 +181,9 @@ const FEW_SHOT_EXAMPLES = [
175
  [
176
  'Music Quiz',
177
  'Play a blind test music game with a dancing Reachy.',
178
- ['music', 'games', 'dance'],
179
- '(multi-label: three slugs truly co-apply, ordered by relevance)',
 
180
  ],
181
  [
182
  'Mime Bot',
@@ -217,9 +224,11 @@ function buildMessages({ name, description, readme }) {
217
  const system = `You classify a Reachy Mini robot app into a CLOSED list of categories.
218
 
219
  OUTPUT FORMAT
220
- Return ONLY a single JSON object: {"categories": ["slug1", "slug2"]}.
221
- Pick 1 to ${MAX_CATEGORIES_PER_APP} slugs, ordered from most to least relevant.
222
- Use the EXACT slug. No prose, no code fences, no commentary outside the JSON.
 
 
223
 
224
  DECISION ALGORITHM (apply in order)
225
 
@@ -231,13 +240,20 @@ raw remote-control interface, dev-only test space.
231
  Examples that DO NOT pass the veto (they are user-facing apps):
232
  TTS players, voice chat, music apps, storytelling, companions -
233
  even when the README is dev-heavy.
234
- - YES -> return {"categories": ["dev-tools"]} and STOP. Never combine.
235
  - NO -> continue to STEP 2.
236
 
237
- STEP 2 - Pick 1 to ${MAX_CATEGORIES_PER_APP} user-facing slugs from the
238
- list below. Choose the MOST SPECIFIC categories. Order from most to
239
- least relevant. Multi-label is encouraged when two categories truly
240
- co-apply (e.g. music-and-dance, kids storytelling, vision game).
 
 
 
 
 
 
 
241
  If the README is empty or very sparse, USE THE NAME AND DESCRIPTION
242
  as the primary signal - do not bail to an empty list just because the
243
  README is thin.
 
39
 
40
  // README budget
41
  const README_MAX_CHARS = 3000;
42
+
43
+ // Single-label classification: each app gets EXACTLY ONE slug -
44
+ // the dominant one. The shape stays `string[]` for forward
45
+ // compatibility (if we ever revert to multi-label, no API break),
46
+ // but the array always contains 0 or 1 entry. Mobile chips and
47
+ // "swipers per category" thus surface each app once and only once.
48
+ const MAX_CATEGORIES_PER_APP = 1;
49
 
50
  // LLM call budget
51
  const LLM_TIMEOUT_MS = 30_000;
 
181
  [
182
  'Music Quiz',
183
  'Play a blind test music game with a dancing Reachy.',
184
+ ['music'],
185
+ '(single dominant slug - music wins over games/dance because ' +
186
+ "the app's primary identity is a music blind-test)",
187
  ],
188
  [
189
  'Mime Bot',
 
224
  const system = `You classify a Reachy Mini robot app into a CLOSED list of categories.
225
 
226
  OUTPUT FORMAT
227
+ Return ONLY a single JSON object: {"categories": ["slug"]}.
228
+ Pick EXACTLY ONE slug - the single dominant category that best
229
+ captures the app's primary identity. Use the EXACT slug. The list
230
+ always contains 0 or 1 entry.
231
+ No prose, no code fences, no commentary outside the JSON.
232
 
233
  DECISION ALGORITHM (apply in order)
234
 
 
240
  Examples that DO NOT pass the veto (they are user-facing apps):
241
  TTS players, voice chat, music apps, storytelling, companions -
242
  even when the README is dev-heavy.
243
+ - YES -> return {"categories": ["dev-tools"]} and STOP.
244
  - NO -> continue to STEP 2.
245
 
246
+ STEP 2 - Pick the SINGLE most dominant user-facing slug from the list
247
+ below. Choose the slug that captures the app's primary identity, not
248
+ every aspect it touches. When two slugs feel equally fitting, pick the
249
+ one that a user would name FIRST when describing the app in one word.
250
+ Examples of tie-breaks:
251
+ - music + dance app -> prefer \`music\` if the user picks the song,
252
+ \`dance\` if the choreography is the point.
253
+ - storytelling + kids app -> prefer \`kids\` if it explicitly targets
254
+ children, \`storytelling\` otherwise.
255
+ - vision + games app -> prefer \`games\` if there is a play loop,
256
+ \`vision\` if it is mostly a perception demo.
257
  If the README is empty or very sparse, USE THE NAME AND DESCRIPTION
258
  as the primary signal - do not bail to an empty list just because the
259
  README is thin.