vibe-roast 0.98.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/README.md +418 -0
  2. package/assests/README.md +43 -0
  3. package/assests/badges/.gitkeep +0 -0
  4. package/assests/badges/01-builder/builder-badge.png +0 -0
  5. package/assests/badges/01-builder/builder-badge.svg +4 -0
  6. package/assests/badges/02-debugger/debugger-badge.png +0 -0
  7. package/assests/badges/02-debugger/debugger-badge.svg +4 -0
  8. package/assests/badges/03-architect/architect-badge.png +0 -0
  9. package/assests/badges/03-architect/architect-badge.svg +4 -0
  10. package/assests/badges/04-codex-believer/codex-believer-badge.png +0 -0
  11. package/assests/badges/04-codex-believer/codex-believer-badge.svg +4 -0
  12. package/assests/badges/05-prompt-priest/prompt-priest-badge.png +0 -0
  13. package/assests/badges/05-prompt-priest/prompt-priest-badge.svg +4 -0
  14. package/assests/badges/06-tab-hoarder/tab-hoarder-badge.png +0 -0
  15. package/assests/badges/06-tab-hoarder/tab-hoarder-badge.svg +4 -0
  16. package/assests/badges/07-context-maxxer/context-maxxer-badge.png +0 -0
  17. package/assests/badges/07-context-maxxer/context-maxxer-badge.svg +4 -0
  18. package/assests/badges/08-yolo-shipper/yolo-shipper-badge.png +0 -0
  19. package/assests/badges/08-yolo-shipper/yolo-shipper-badge.svg +4 -0
  20. package/assests/banners/.gitkeep +0 -0
  21. package/assests/banners/home-hero.png +0 -0
  22. package/assests/banners/home-hero.svg +3 -0
  23. package/assests/banners/result-header.png +0 -0
  24. package/assests/banners/result-header.svg +3 -0
  25. package/assests/banners/share-strip.png +0 -0
  26. package/assests/banners/share-strip.svg +3 -0
  27. package/assests/characters/01-builder/builder-card.sbti-archive.svg +5 -0
  28. package/assests/characters/01-builder/builder-card.svg +3 -0
  29. package/assests/characters/01-builder/builder-character.sbti-archive.svg +3 -0
  30. package/assests/characters/01-builder/builder-character.svg +3 -0
  31. package/assests/characters/01-builder/builder-figure.png +0 -0
  32. package/assests/characters/01-builder/builder-mascot.png +0 -0
  33. package/assests/characters/02-debugger/debugger-card.sbti-archive.svg +5 -0
  34. package/assests/characters/02-debugger/debugger-card.svg +3 -0
  35. package/assests/characters/02-debugger/debugger-character.sbti-archive.svg +3 -0
  36. package/assests/characters/02-debugger/debugger-character.svg +3 -0
  37. package/assests/characters/02-debugger/debugger-figure.png +0 -0
  38. package/assests/characters/02-debugger/debugger-mascot.png +0 -0
  39. package/assests/characters/03-architect/architect-card.sbti-archive.svg +5 -0
  40. package/assests/characters/03-architect/architect-card.svg +3 -0
  41. package/assests/characters/03-architect/architect-character.sbti-archive.svg +3 -0
  42. package/assests/characters/03-architect/architect-character.svg +3 -0
  43. package/assests/characters/03-architect/architect-figure.png +0 -0
  44. package/assests/characters/03-architect/architect-mascot.png +0 -0
  45. package/assests/characters/04-codex-believer/codex-believer-card.sbti-archive.svg +5 -0
  46. package/assests/characters/04-codex-believer/codex-believer-card.svg +3 -0
  47. package/assests/characters/04-codex-believer/codex-believer-character.sbti-archive.svg +3 -0
  48. package/assests/characters/04-codex-believer/codex-believer-character.svg +3 -0
  49. package/assests/characters/04-codex-believer/codex-believer-figure.png +0 -0
  50. package/assests/characters/04-codex-believer/codex-believer-mascot.png +0 -0
  51. package/assests/characters/05-prompt-priest/prompt-priest-card.sbti-archive.svg +5 -0
  52. package/assests/characters/05-prompt-priest/prompt-priest-card.svg +3 -0
  53. package/assests/characters/05-prompt-priest/prompt-priest-character.sbti-archive.svg +3 -0
  54. package/assests/characters/05-prompt-priest/prompt-priest-character.svg +3 -0
  55. package/assests/characters/05-prompt-priest/prompt-priest-figure.png +0 -0
  56. package/assests/characters/05-prompt-priest/prompt-priest-mascot.png +0 -0
  57. package/assests/characters/06-tab-hoarder/tab-hoarder-card.sbti-archive.svg +5 -0
  58. package/assests/characters/06-tab-hoarder/tab-hoarder-card.svg +3 -0
  59. package/assests/characters/06-tab-hoarder/tab-hoarder-character.sbti-archive.svg +3 -0
  60. package/assests/characters/06-tab-hoarder/tab-hoarder-character.svg +3 -0
  61. package/assests/characters/06-tab-hoarder/tab-hoarder-figure.png +0 -0
  62. package/assests/characters/06-tab-hoarder/tab-hoarder-mascot.png +0 -0
  63. package/assests/characters/07-context-maxxer/context-maxxer-card.sbti-archive.svg +5 -0
  64. package/assests/characters/07-context-maxxer/context-maxxer-card.svg +3 -0
  65. package/assests/characters/07-context-maxxer/context-maxxer-character.sbti-archive.svg +3 -0
  66. package/assests/characters/07-context-maxxer/context-maxxer-character.svg +3 -0
  67. package/assests/characters/07-context-maxxer/context-maxxer-figure.png +0 -0
  68. package/assests/characters/07-context-maxxer/context-maxxer-mascot.png +0 -0
  69. package/assests/characters/08-yolo-shipper/yolo-shipper-card.sbti-archive.svg +5 -0
  70. package/assests/characters/08-yolo-shipper/yolo-shipper-card.svg +3 -0
  71. package/assests/characters/08-yolo-shipper/yolo-shipper-character.sbti-archive.svg +3 -0
  72. package/assests/characters/08-yolo-shipper/yolo-shipper-character.svg +3 -0
  73. package/assests/characters/08-yolo-shipper/yolo-shipper-figure.png +0 -0
  74. package/assests/characters/08-yolo-shipper/yolo-shipper-mascot.png +0 -0
  75. package/assests/characters-vibe-types/aosf-strategy-shipper/strategy-shipper-figure.png +0 -0
  76. package/assests/characters-vibe-types/aosx-agent-believer/agent-believer-figure.png +0 -0
  77. package/assests/characters-vibe-types/aovf-systems-architect/systems-architect-figure.png +0 -0
  78. package/assests/characters-vibe-types/aovx-context-cartographer/context-cartographer-figure.png +0 -0
  79. package/assests/characters-vibe-types/apsf-diagram-sprinter/diagram-sprinter-figure.png +0 -0
  80. package/assests/characters-vibe-types/apsx-infinite-planner/infinite-planner-figure.png +0 -0
  81. package/assests/characters-vibe-types/apvf-architect/architect-figure.png +0 -0
  82. package/assests/characters-vibe-types/apvx-prompt-priest/prompt-priest-figure.png +0 -0
  83. package/assests/characters-vibe-types/mosf-yolo-shipper/yolo-shipper-figure.png +0 -0
  84. package/assests/characters-vibe-types/mosx-agent-commander/agent-commander-figure.png +0 -0
  85. package/assests/characters-vibe-types/movf-agent-engineer/agent-engineer-figure.png +0 -0
  86. package/assests/characters-vibe-types/movx-systems-wrangler/systems-wrangler-figure.png +0 -0
  87. package/assests/characters-vibe-types/mpsf-builder/builder-figure.png +0 -0
  88. package/assests/characters-vibe-types/mpsx-tab-hoarder/tab-hoarder-figure.png +0 -0
  89. package/assests/characters-vibe-types/mpvf-debugger/debugger-figure.png +0 -0
  90. package/assests/characters-vibe-types/mpvx-context-maxxer/context-maxxer-figure.png +0 -0
  91. package/assests/live-report.html +1403 -0
  92. package/assests/notes/live-inspect-summary.json +3487 -0
  93. package/assests/scripts/compose-banners.py +148 -0
  94. package/assests/scripts/live-report-boot.js +2 -0
  95. package/assests/scripts/profiles.json +104 -0
  96. package/assests/scripts/qa-faces.mjs +72 -0
  97. package/assests/scripts/roast-extras.js +619 -0
  98. package/assests/scripts/score-engine.js +62 -0
  99. package/assests/scripts/split-badges.py +49 -0
  100. package/assests/scripts/split-reference.py +38 -0
  101. package/assests/scripts/sync-banners.mjs +45 -0
  102. package/assests/scripts/sync-cards.mjs +50 -0
  103. package/assests/scripts/sync-raster.mjs +118 -0
  104. package/assests/scripts/vibe-types.json +22 -0
  105. package/assests/source/design-system.md +50 -0
  106. package/assests/source/prompts/01-builder.md +19 -0
  107. package/assests/source/prompts/02-debugger.md +17 -0
  108. package/assests/source/prompts/03-architect.md +16 -0
  109. package/assests/source/prompts/04-codex-believer.md +13 -0
  110. package/assests/source/prompts/05-prompt-priest.md +13 -0
  111. package/assests/source/prompts/06-tab-hoarder.md +13 -0
  112. package/assests/source/prompts/07-context-maxxer.md +13 -0
  113. package/assests/source/prompts/08-yolo-shipper.md +13 -0
  114. package/assests/source/references/sbti-style-reference-board.png +0 -0
  115. package/assests/source/references/vibe-badges-v6-board.png +0 -0
  116. package/assests/source/references/vibe-mascots-v4-reference.png +0 -0
  117. package/assests/source/references/vibe-profiles-v5-board-a.png +0 -0
  118. package/assests/source/references/vibe-profiles-v5-board-b.png +0 -0
  119. package/assests/source/vibe-types-visual-brief.md +38 -0
  120. package/bin/hook.js +121 -0
  121. package/bin/vibe-roast.js +98 -0
  122. package/bin/vibe-wrapper.js +4 -0
  123. package/dashboard/dist/assets/index-DlBycTcO.css +1 -0
  124. package/dashboard/dist/assets/index-t8m64Hm9.js +168 -0
  125. package/dashboard/dist/index.html +32 -0
  126. package/package.json +58 -0
  127. package/src/extract/environment.js +160 -0
  128. package/src/extract/phrase-stats.js +684 -0
  129. package/src/extract/prompt-analysis.js +368 -0
  130. package/src/extract/text.js +30 -0
  131. package/src/hooks/install.js +184 -0
  132. package/src/inspect.js +261 -0
  133. package/src/lib/activity-metrics.js +238 -0
  134. package/src/lib/agent-score.js +187 -0
  135. package/src/lib/ai-roast.js +446 -0
  136. package/src/lib/dates.js +43 -0
  137. package/src/lib/hashtags.js +150 -0
  138. package/src/lib/jsonl.js +48 -0
  139. package/src/lib/load-env.js +59 -0
  140. package/src/lib/roast-evidence.js +186 -0
  141. package/src/lib/roast-snapshot.js +58 -0
  142. package/src/server.js +644 -0
  143. package/src/sources/aider.js +150 -0
  144. package/src/sources/amazonq.js +113 -0
  145. package/src/sources/antigravity.js +147 -0
  146. package/src/sources/claude.js +262 -0
  147. package/src/sources/cline.js +11 -0
  148. package/src/sources/codex.js +222 -0
  149. package/src/sources/common.js +133 -0
  150. package/src/sources/continue.js +67 -0
  151. package/src/sources/copilot.js +170 -0
  152. package/src/sources/cursor.js +238 -0
  153. package/src/sources/gemini.js +75 -0
  154. package/src/sources/index.js +61 -0
  155. package/src/sources/roo.js +11 -0
  156. package/src/sources/token-tracker.js +171 -0
  157. package/src/sources/vibe-tracker.js +79 -0
  158. package/src/sources/vscode-tasks.js +142 -0
  159. package/src/sources/windsurf.js +109 -0
  160. package/worker/README.md +96 -0
  161. package/worker/package-lock.json +1547 -0
  162. package/worker/package.json +20 -0
  163. package/worker/src/index.mjs +694 -0
  164. package/worker/wrangler.jsonc +40 -0
@@ -0,0 +1,684 @@
1
+ const STOP_WORDS = new Set([
2
+ "the",
3
+ "and",
4
+ "for",
5
+ "with",
6
+ "this",
7
+ "that",
8
+ "from",
9
+ "you",
10
+ "are",
11
+ "was",
12
+ "were",
13
+ "have",
14
+ "has",
15
+ "had",
16
+ "not",
17
+ "but",
18
+ "all",
19
+ "can",
20
+ "her",
21
+ "his",
22
+ "him",
23
+ "she",
24
+ "they",
25
+ "them",
26
+ "their",
27
+ "what",
28
+ "when",
29
+ "where",
30
+ "which",
31
+ "who",
32
+ "will",
33
+ "would",
34
+ "could",
35
+ "should",
36
+ "into",
37
+ "out",
38
+ "our",
39
+ "your",
40
+ "about",
41
+ "then",
42
+ "than",
43
+ "also",
44
+ "just",
45
+ "like",
46
+ "some",
47
+ "such",
48
+ "only",
49
+ "other",
50
+ "over",
51
+ "after",
52
+ "before",
53
+ "if",
54
+ "to",
55
+ "or",
56
+ "in",
57
+ "of",
58
+ "is",
59
+ "it",
60
+ "on",
61
+ "as",
62
+ "be",
63
+ "by",
64
+ "at",
65
+ "an",
66
+ "we",
67
+ "me",
68
+ "my",
69
+ "do",
70
+ "does",
71
+ "did",
72
+ "so",
73
+ "no",
74
+ "yes",
75
+ "up",
76
+ "down",
77
+ "true",
78
+ "false",
79
+ "none",
80
+ "null",
81
+ "self",
82
+ "def",
83
+ "class",
84
+ "return",
85
+ "import",
86
+ "from",
87
+ "const",
88
+ "let",
89
+ "var",
90
+ "function",
91
+ "一个",
92
+ "一下",
93
+ "这个",
94
+ "然后",
95
+ "可以",
96
+ "帮我",
97
+ "class",
98
+ "classname",
99
+ "dashboard",
100
+ "div",
101
+ "file",
102
+ "files",
103
+ "path",
104
+ "paths",
105
+ "key",
106
+ "keys",
107
+ "id",
108
+ "ids",
109
+ "model",
110
+ "models",
111
+ "name",
112
+ "names",
113
+ "provider",
114
+ "providers",
115
+ "selected",
116
+ "source",
117
+ "sources",
118
+ "text",
119
+ "usage",
120
+ "breakdown",
121
+ "active",
122
+ "blah",
123
+ "clipboard",
124
+ "codex",
125
+ "claude",
126
+ "config",
127
+ "cursor",
128
+ "range",
129
+ "gemini",
130
+ "json",
131
+ "judew",
132
+ "mentioned",
133
+ "opencode",
134
+ "png",
135
+ "pot",
136
+ "request",
137
+ "staff",
138
+ "user",
139
+ "world",
140
+ "patch",
141
+ "const",
142
+ "let",
143
+ "var",
144
+ "function",
145
+ "return",
146
+ "import",
147
+ "export",
148
+ "async",
149
+ "await",
150
+ "true",
151
+ "false",
152
+ "null",
153
+ "undefined",
154
+ "if",
155
+ "else",
156
+ "for",
157
+ "while",
158
+ "map",
159
+ "filter",
160
+ "props",
161
+ "state",
162
+ "style",
163
+ "class",
164
+ "classname",
165
+ "border",
166
+ "rounded",
167
+ "flex",
168
+ "grid",
169
+ "gray",
170
+ "white",
171
+ "black",
172
+ "bg",
173
+ "px",
174
+ "py",
175
+ "mt",
176
+ "mb",
177
+ "oai",
178
+ "md",
179
+ "use",
180
+ "using",
181
+ "used",
182
+ "make",
183
+ "made",
184
+ "get",
185
+ "got",
186
+ "need",
187
+ "needs",
188
+ "please",
189
+ "really",
190
+ "well",
191
+ "look",
192
+ "looks",
193
+ "good",
194
+ "okay",
195
+ "ok",
196
+ "project",
197
+ "projects",
198
+ "http",
199
+ "https",
200
+ "www",
201
+ "com",
202
+ ]);
203
+
204
+ const HAN_STOP_WORDS = new Set([
205
+ "还是",
206
+ "现在",
207
+ "这个",
208
+ "这些",
209
+ "那个",
210
+ "那些",
211
+ "这里",
212
+ "里面",
213
+ "一下",
214
+ "一些",
215
+ "一个",
216
+ "然后",
217
+ "以及",
218
+ "可以",
219
+ "需要",
220
+ "帮我",
221
+ "我们",
222
+ "你们",
223
+ "他们",
224
+ "什么",
225
+ "怎么",
226
+ "为什么",
227
+ "已经",
228
+ "还有",
229
+ "进行",
230
+ "相关",
231
+ "部分",
232
+ "整体",
233
+ "目前",
234
+ "但是",
235
+ "其实",
236
+ "所以",
237
+ "同时",
238
+ "直接",
239
+ "开始",
240
+ "时候",
241
+ "情况",
242
+ "内容",
243
+ "问题",
244
+ "文件",
245
+ "没有",
246
+ "不要",
247
+ "使用",
248
+ "显示",
249
+ "看看",
250
+ "我的",
251
+ "你的",
252
+ "就好",
253
+ "这样",
254
+ "做的",
255
+ "界面",
256
+ "检查",
257
+ "就是",
258
+ "不用",
259
+ "觉得",
260
+ "说明",
261
+ "效果",
262
+ "一点",
263
+ "位置",
264
+ "而是",
265
+ "放到",
266
+ "加入",
267
+ "之后",
268
+ "最后",
269
+ "下面",
270
+ "感觉",
271
+ "因为",
272
+ "生成",
273
+ "其次",
274
+ "直接",
275
+ "重新",
276
+ "继续",
277
+ "应该",
278
+ "知道",
279
+ "后面",
280
+ "出来",
281
+ "很多",
282
+ "完全",
283
+ "其他",
284
+ "自己",
285
+ "结果",
286
+ "所有",
287
+ "也要",
288
+ "选择",
289
+ "具体",
290
+ "好了",
291
+ "这种",
292
+ "不对",
293
+ "好的",
294
+ "根据",
295
+ "当前",
296
+ "整个",
297
+ "不太",
298
+ "中间",
299
+ "重要",
300
+ "对应",
301
+ "或者",
302
+ "理解",
303
+ "上面",
304
+ "也是",
305
+ "不同",
306
+ "一致",
307
+ "比如",
308
+ "进去",
309
+ "全部",
310
+ "那边",
311
+ "进来",
312
+ "每次",
313
+ "考虑",
314
+ "都要",
315
+ ]);
316
+
317
+ // Intl.Segmenter is deliberately conservative around short developer terms
318
+ // ("动效" may become "动" + "效"). This compact vocabulary restores common
319
+ // concepts without shipping a large language-specific tokenizer.
320
+ const KNOWN_HAN_TERMS = [
321
+ "人工智能",
322
+ "供应商",
323
+ "提示词",
324
+ "工作流",
325
+ "移动端",
326
+ "桌面端",
327
+ "深色模式",
328
+ "浅色模式",
329
+ "用户体验",
330
+ "响应式",
331
+ "可访问性",
332
+ "自动化",
333
+ "数据库",
334
+ "代码块",
335
+ "回归测试",
336
+ "单元测试",
337
+ "集成测试",
338
+ "错误日志",
339
+ "词云",
340
+ "动效",
341
+ "动画",
342
+ "重构",
343
+ "优化",
344
+ "页面",
345
+ "组件",
346
+ "布局",
347
+ "设计",
348
+ "逻辑",
349
+ "功能",
350
+ "测试",
351
+ "回归",
352
+ "错误",
353
+ "修复",
354
+ "解释",
355
+ "方案",
356
+ "架构",
357
+ "数据",
358
+ "模型",
359
+ "代码",
360
+ "发布",
361
+ "构建",
362
+ "性能",
363
+ "交互",
364
+ "视觉",
365
+ "按钮",
366
+ "卡片",
367
+ "主题",
368
+ "算法",
369
+ "检测",
370
+ "代理",
371
+ "文档",
372
+ "接口",
373
+ "服务",
374
+ "缓存",
375
+ "登录",
376
+ "发票",
377
+ "电子",
378
+ "胶囊",
379
+ ];
380
+
381
+ const CONCEPT_GROUPS = [
382
+ ["motion", ["motion", "animation", "animations", "animated", "动效", "动画"]],
383
+ ["testing", ["testing", "test", "tests", "测试"]],
384
+ ["refactor", ["refactor", "refactoring", "重构"]],
385
+ ["debugging", ["debugging", "debug", "bug", "bugs", "调试"]],
386
+ ["design", ["design", "designing", "ui", "ux", "设计"]],
387
+ ["performance", ["performance", "perf", "性能"]],
388
+ ["workflow", ["workflow", "workflows", "agent", "agents", "工作流"]],
389
+ ["prompt", ["prompt", "prompts", "prompting", "提示词"]],
390
+ ["component", ["component", "components", "组件"]],
391
+ ["layout", ["layout", "layouts", "布局"]],
392
+ ["model", ["model", "models", "模型"]],
393
+ ];
394
+
395
+ const CONCEPT_BY_TERM = new Map(
396
+ CONCEPT_GROUPS.flatMap(([concept, terms]) => terms.map((term) => [term, `concept:${concept}`])),
397
+ );
398
+ const CATEGORY_BY_CONCEPT = new Map([
399
+ ["concept:testing", "category:testing"],
400
+ ["concept:refactor", "category:refactor"],
401
+ ["concept:debugging", "category:debugging"],
402
+ ["concept:design", "category:ui_design"],
403
+ ["concept:workflow", "category:workflow"],
404
+ ]);
405
+ const VIBE_ASCII_TERMS = new Set([
406
+ "api",
407
+ "branch",
408
+ "build",
409
+ "canvas",
410
+ "cli",
411
+ "commit",
412
+ "css",
413
+ "deploy",
414
+ "deployment",
415
+ "frontend",
416
+ "backend",
417
+ "git",
418
+ "github",
419
+ "html",
420
+ "javascript",
421
+ "merge",
422
+ "mcp",
423
+ "node",
424
+ "npm",
425
+ "page",
426
+ "plugin",
427
+ "pr",
428
+ "prompt",
429
+ "push",
430
+ "react",
431
+ "release",
432
+ "repo",
433
+ "server",
434
+ "skill",
435
+ "sql",
436
+ "sqlite",
437
+ "swift",
438
+ "swiftui",
439
+ "token",
440
+ "typescript",
441
+ "vite",
442
+ "zsh",
443
+ ]);
444
+
445
+ function wordFrequencies(prompts, { limit = 30 } = {}) {
446
+ return wordFrequenciesFromRecords(wordCloudRecords(prompts), { limit });
447
+ }
448
+
449
+ function wordCloudRecords(prompts = []) {
450
+ return prompts.map((prompt) => {
451
+ const cloudText = promptTextForCloud(prompt.text);
452
+ const terms = tokenize(cloudText);
453
+ const byConcept = new Map();
454
+ for (const term of terms) {
455
+ const key = CONCEPT_BY_TERM.get(term) || `term:${term}`;
456
+ const acronym = isAcronymUse(term, cloudText);
457
+ const displayTerm = acronym ? term.toUpperCase() : term;
458
+ const existing = byConcept.get(key) || {
459
+ key,
460
+ kind: "term",
461
+ vibe: false,
462
+ acronym: false,
463
+ variants: {},
464
+ };
465
+ existing.vibe = existing.vibe || isVibeCodingTerm(term);
466
+ existing.acronym = existing.acronym || acronym;
467
+ existing.variants[displayTerm] = (existing.variants[displayTerm] || 0) + 1;
468
+ byConcept.set(key, existing);
469
+ }
470
+ for (const category of prompt.categories || []) {
471
+ if (category === "implementation") continue;
472
+ const key = `category:${category}`;
473
+ byConcept.set(key, {
474
+ key,
475
+ kind: "category",
476
+ vibe: true,
477
+ acronym: false,
478
+ variants: { [category]: 1 },
479
+ });
480
+ }
481
+ return {
482
+ source: prompt.source || "unknown",
483
+ timestamp: prompt.timestamp || null,
484
+ concepts: [...byConcept.values()],
485
+ };
486
+ });
487
+ }
488
+
489
+ function wordFrequenciesFromRecords(records = [], { limit = 30, vibeOnly = false } = {}) {
490
+ const concepts = new Map();
491
+ for (const record of records) {
492
+ for (const item of record.concepts || []) {
493
+ const key = item.key;
494
+ const concept = concepts.get(key) || {
495
+ key,
496
+ kind: item.kind || "term",
497
+ vibe: Boolean(item.vibe),
498
+ count: 0,
499
+ prompt_count: 0,
500
+ variants: new Map(),
501
+ };
502
+ concept.vibe = concept.vibe || Boolean(item.vibe);
503
+ let conceptCount = 0;
504
+ for (const [term, rawCount] of Object.entries(item.variants || {})) {
505
+ const count = Math.max(0, Number(rawCount) || 0);
506
+ if (count === 0) continue;
507
+ const variant = concept.variants.get(term) || { count: 0, prompt_count: 0 };
508
+ conceptCount += count;
509
+ variant.count += count;
510
+ variant.prompt_count += 1;
511
+ concept.variants.set(term, variant);
512
+ }
513
+ if (conceptCount === 0) continue;
514
+ concept.count += conceptCount;
515
+ concept.prompt_count += 1;
516
+ concepts.set(key, concept);
517
+ }
518
+ }
519
+
520
+ return Array.from(concepts.values())
521
+ .filter((concept) => {
522
+ const categoryKey = CATEGORY_BY_CONCEPT.get(concept.key);
523
+ if (categoryKey && concepts.has(categoryKey)) return false;
524
+ return !vibeOnly || concept.vibe;
525
+ })
526
+ .map((concept) => {
527
+ const term = preferredVariant(concept.variants);
528
+ // Prompt coverage is the primary signal. Repetition inside one prompt gets
529
+ // only a small logarithmic boost, so pasted/verbose prompts cannot dominate.
530
+ const repetition = Math.max(0, concept.count - concept.prompt_count);
531
+ const weight = Number(
532
+ ((concept.prompt_count + Math.log2(1 + repetition) * 0.35)
533
+ * (concept.kind === "category" ? 1.12 : 1)).toFixed(3),
534
+ );
535
+ return {
536
+ key: concept.key,
537
+ kind: concept.kind,
538
+ term,
539
+ count: concept.count,
540
+ prompt_count: concept.prompt_count,
541
+ weight,
542
+ };
543
+ })
544
+ .sort((a, b) => b.weight - a.weight || b.prompt_count - a.prompt_count || b.count - a.count || a.term.localeCompare(b.term))
545
+ .slice(0, limit);
546
+ }
547
+
548
+ function isAcronymUse(term, text) {
549
+ if (!/^[a-z][a-z0-9]{1,5}$/.test(term)) return false;
550
+ const upper = term.toUpperCase();
551
+ return new RegExp(`(^|[^A-Za-z0-9])${upper}(?=$|[^A-Za-z0-9])`).test(String(text || ""));
552
+ }
553
+
554
+ function isVibeCodingTerm(term) {
555
+ if (CONCEPT_BY_TERM.has(term)) return true;
556
+ if (/^[\p{Script=Han}]+$/u.test(term)) return KNOWN_HAN_TERMS.includes(term);
557
+ return VIBE_ASCII_TERMS.has(term);
558
+ }
559
+
560
+ function preferredVariant(variants) {
561
+ return [...variants.entries()]
562
+ .sort((a, b) => (
563
+ b[1].prompt_count - a[1].prompt_count
564
+ || b[1].count - a[1].count
565
+ || a[0].length - b[0].length
566
+ || a[0].localeCompare(b[0])
567
+ ))[0]?.[0] || "";
568
+ }
569
+
570
+ /** Prefer natural-language intent; drop fenced/code-heavy lines before tokenizing. */
571
+ function promptTextForCloud(text) {
572
+ return String(text || "")
573
+ .replace(/```[\s\S]*?```/g, " ")
574
+ .replace(/`[^`]+`/g, " ")
575
+ .replace(/\/(?:Users|Volumes|home|var|tmp|private)\/[^\s"'`]+/gi, " ")
576
+ .replace(/[A-Za-z]:\\[^\s"'`]+/g, " ")
577
+ .replace(/<[^>]*>/g, " ")
578
+ .replace(/--[A-Za-z0-9_-]+\s*:[^;]+;?/g, " ")
579
+ .replace(/\b(?:import|from\s+\S+\s+import|class\s+\w+|def\s+\w+|const\s+\w+|let\s+\w+|var\s+\w+|function\s+\w+)\b[\s\S]*$/gim, " ")
580
+ .replace(/\b[a-z][a-z0-9]*(?:_[a-z0-9]+)+\b/gi, " ")
581
+ .replace(/\b(?:torch|numpy|nn|plt|pd|np)\.[A-Za-z_][\w.]*\b/g, " ")
582
+ .split(/\r?\n/)
583
+ .filter((line) => !looksLikeCodeLine(line))
584
+ .join("\n");
585
+ }
586
+
587
+ function tokenize(text) {
588
+ const normalized = String(text || "");
589
+ const matches = normalized.match(/[\p{Script=Han}]+|[a-z0-9][a-z0-9_-]{1,}/giu) || [];
590
+ const terms = [];
591
+ for (const match of matches) {
592
+ if (/^[\p{Script=Han}]+$/u.test(match)) {
593
+ terms.push(...splitHanTerm(match));
594
+ } else {
595
+ terms.push(...splitAsciiTerm(match));
596
+ }
597
+ }
598
+ return terms.filter((term) => (
599
+ term.length > 1
600
+ && !STOP_WORDS.has(term)
601
+ && !HAN_STOP_WORDS.has(term)
602
+ && !/^\d+$/.test(term)
603
+ ));
604
+ }
605
+
606
+ function splitHanTerm(term) {
607
+ const candidates = [];
608
+ const knownCoverage = new Set();
609
+
610
+ for (const known of KNOWN_HAN_TERMS) {
611
+ let start = term.indexOf(known);
612
+ while (start >= 0) {
613
+ candidates.push({ term: known, start, end: start + known.length });
614
+ start = term.indexOf(known, start + known.length);
615
+ }
616
+ }
617
+
618
+ const matches = [];
619
+ candidates
620
+ .sort((a, b) => a.start - b.start || b.term.length - a.term.length)
621
+ .forEach((candidate) => {
622
+ const indexes = Array.from(
623
+ { length: candidate.end - candidate.start },
624
+ (_, offset) => candidate.start + offset,
625
+ );
626
+ if (indexes.some((index) => knownCoverage.has(index))) return;
627
+ matches.push(candidate);
628
+ indexes.forEach((index) => knownCoverage.add(index));
629
+ });
630
+
631
+ if (typeof Intl?.Segmenter === "function") {
632
+ const segmenter = new Intl.Segmenter("zh", { granularity: "word" });
633
+ for (const segment of segmenter.segment(term)) {
634
+ const value = segment.segment.toLowerCase();
635
+ if (!segment.isWordLike || value.length < 2) continue;
636
+ const overlapsKnown = Array.from(
637
+ { length: segment.segment.length },
638
+ (_, offset) => segment.index + offset,
639
+ ).every((index) => knownCoverage.has(index));
640
+ if (!overlapsKnown) matches.push({
641
+ term: value,
642
+ start: segment.index,
643
+ end: segment.index + segment.segment.length,
644
+ });
645
+ }
646
+ }
647
+
648
+ if (matches.length === 0 && term.length === 2) return [term.toLowerCase()];
649
+ return matches
650
+ .sort((a, b) => a.start - b.start || b.term.length - a.term.length)
651
+ .map((entry) => entry.term.toLowerCase());
652
+ }
653
+
654
+ function looksLikeCodeLine(line) {
655
+ const trimmed = String(line || "").trim();
656
+ if (!trimmed) return false;
657
+ const withoutDiff = trimmed.replace(/^[+-]\s?/, "");
658
+ const hasHan = /[\p{Script=Han}]/u.test(withoutDiff);
659
+ if (/^(const|let|var|return|import|export|function|class|if|else|for|while|await|async)\b/.test(withoutDiff)) return true;
660
+ if (/\b(className|useState|useEffect|style=|aria-|data-|set[A-Z][A-Za-z0-9_]*)\b/.test(withoutDiff)) return true;
661
+ if (!hasHan && /[{}<>;=]/.test(withoutDiff)) return true;
662
+ const styleTokens = withoutDiff.match(/\b(?:text|bg|border|rounded|flex|grid|items|justify|gap|px|py|mt|mb|w|h|dark):?-[A-Za-z0-9/[\].%-]+/g) || [];
663
+ if (!hasHan && styleTokens.length >= 3) return true;
664
+ return false;
665
+ }
666
+
667
+ function splitAsciiTerm(term) {
668
+ const normalized = String(term || "").toLowerCase();
669
+ const parts = String(term || "")
670
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
671
+ .split(/[^A-Za-z0-9]+/)
672
+ .map((part) => part.toLowerCase())
673
+ .filter((part) => part.length > 1);
674
+
675
+ return Array.from(new Set([normalized, ...parts]));
676
+ }
677
+
678
+ module.exports = {
679
+ wordFrequencies,
680
+ wordCloudRecords,
681
+ wordFrequenciesFromRecords,
682
+ tokenize,
683
+ promptTextForCloud,
684
+ };