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,619 @@
1
+ /** Shared helpers for static roast pages (live-report / result). */
2
+
3
+ export const ARCHETYPE_TAGS = {
4
+ "01-builder": { en: "Builder", zh: "构建侠" },
5
+ "02-debugger": { en: "Debugger", zh: "调试猎手" },
6
+ "03-architect": { en: "Architect", zh: "架构师" },
7
+ "04-codex-believer": { en: "CodexBeliever", zh: "Codex信徒" },
8
+ "05-prompt-priest": { en: "PromptPriest", zh: "提示祭司" },
9
+ "06-tab-hoarder": { en: "TabHoarder", zh: "标签囤积癖" },
10
+ "07-context-maxxer": { en: "ContextMaxxer", zh: "上下文极限党" },
11
+ "08-yolo-shipper": { en: "YoloShipper", zh: "YOLO上线狂" },
12
+ };
13
+
14
+ export const CODE_TAGS = {
15
+ SHIP: { en: "ShipIt", zh: "直接上线" },
16
+ HUNT: { en: "BugHunt", zh: "猎虫" },
17
+ DRAW: { en: "Blueprint", zh: "蓝图先行" },
18
+ FAITH: { en: "CodexFaith", zh: "Codex信仰" },
19
+ SPELL: { en: "SpellCraft", zh: "法术工艺" },
20
+ TABS: { en: "TabHoard", zh: "标签囤积" },
21
+ METER: { en: "CtxMeter", zh: "上下文表" },
22
+ YOLO: { en: "YoloShip", zh: "YOLO发车" },
23
+ };
24
+
25
+ const CATEGORY_TAGS = {
26
+ planning: { en: "Planning", zh: "规划" },
27
+ debugging: { en: "Debugging", zh: "调试" },
28
+ implementation: { en: "Implementation", zh: "实现" },
29
+ refactor: { en: "Refactor", zh: "重构" },
30
+ testing: { en: "Testing", zh: "测试" },
31
+ packaging: { en: "Packaging", zh: "打包" },
32
+ explanation: { en: "Explanation", zh: "讲解" },
33
+ research: { en: "Research", zh: "调研" },
34
+ ui_design: { en: "UIDesign", zh: "UI设计" },
35
+ workflow: { en: "Workflow", zh: "工作流" },
36
+ };
37
+
38
+ const TIER_TAGS = {
39
+ GOD: { en: "GOD", zh: "神级" },
40
+ ELITE: { en: "ELITE", zh: "精英" },
41
+ SOLID: { en: "SOLID", zh: "扎实" },
42
+ NPC: { en: "NPC", zh: "路人甲" },
43
+ TRASH: { en: "TRASH", zh: "垃分" },
44
+ };
45
+
46
+ function pickLocale(entry, zh) {
47
+ if (!entry) return "";
48
+ if (typeof entry === "string") return entry;
49
+ return zh ? entry.zh || entry.en : entry.en || entry.zh;
50
+ }
51
+
52
+ function pascalCase(value) {
53
+ return String(value || "")
54
+ .replace(/[_-]+/g, " ")
55
+ .replace(/\s+/g, " ")
56
+ .trim()
57
+ .split(" ")
58
+ .filter(Boolean)
59
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
60
+ .join("");
61
+ }
62
+
63
+ function categoryCount(categories, name) {
64
+ const row = categories?.[name];
65
+ if (row == null) return 0;
66
+ if (typeof row === "number") return Number.isFinite(row) ? row : 0;
67
+ const count = Number(row.count);
68
+ return Number.isFinite(count) ? count : 0;
69
+ }
70
+
71
+ function normalizeOpts(opts) {
72
+ if (typeof opts === "number") return { limit: opts };
73
+ return opts || {};
74
+ }
75
+
76
+ /** @param {{ limit?: number, locale?: "en"|"zh" }|number} [opts] */
77
+ export function buildHashtags(vibe, categories = {}, opts = {}) {
78
+ const options = normalizeOpts(opts);
79
+ const limit = options.limit ?? 8;
80
+ const zh = options.locale === "zh";
81
+ const tags = [];
82
+ const seen = new Set();
83
+ function push(raw) {
84
+ const bare = String(raw || "")
85
+ .replace(/^#+/, "")
86
+ .replace(/[^A-Za-z0-9\u4e00-\u9fff]/g, "");
87
+ if (!bare) return;
88
+ const tag = `#${bare}`;
89
+ const key = tag.toLowerCase();
90
+ if (seen.has(key)) return;
91
+ seen.add(key);
92
+ tags.push(tag);
93
+ }
94
+ const arch = vibe?.archetype || {};
95
+ const archId = arch.id || "";
96
+ if (ARCHETYPE_TAGS[archId]) push(pickLocale(ARCHETYPE_TAGS[archId], zh));
97
+ else if (arch.title) push(zh ? arch.title : pascalCase(arch.title));
98
+ if (arch.code && CODE_TAGS[arch.code]) push(pickLocale(CODE_TAGS[arch.code], zh));
99
+ else if (arch.code) push(pascalCase(arch.code));
100
+ const tierId = vibe?.tier?.id;
101
+ if (tierId && TIER_TAGS[tierId]) push(pickLocale(TIER_TAGS[tierId], zh));
102
+ else if (tierId) push(tierId);
103
+ const ranked = Object.keys(CATEGORY_TAGS)
104
+ .map((key) => [key, categoryCount(categories, key)])
105
+ .filter(([, count]) => count > 0)
106
+ .sort((a, b) => b[1] - a[1]);
107
+ for (const [key] of ranked) {
108
+ push(pickLocale(CATEGORY_TAGS[key], zh));
109
+ if (tags.length >= limit) break;
110
+ }
111
+ const hookSrc = zh ? arch.hookZh || arch.hook : arch.hook;
112
+ if (hookSrc) {
113
+ const hookWord = String(hookSrc).split(/[\s·•\-–—,/,、]+/)[0];
114
+ if (hookWord && hookWord.length > 1) push(zh ? hookWord : pascalCase(hookWord));
115
+ }
116
+ return tags.slice(0, limit);
117
+ }
118
+
119
+ export function renderWordCloud(canvas, words, accent = "#ff5a1f") {
120
+ if (!canvas || !words?.length) return;
121
+ const parent = canvas.parentElement;
122
+ const width = Math.max(200, Math.floor(parent?.clientWidth || canvas.clientWidth || 640));
123
+ const height = Math.max(120, Math.floor(parent?.clientHeight || canvas.clientHeight || 140));
124
+ const dpr = Math.min(window.devicePixelRatio || 1, 2.5);
125
+ canvas.width = Math.floor(width * dpr);
126
+ canvas.height = Math.floor(height * dpr);
127
+ canvas.style.width = "100%";
128
+ canvas.style.height = `${height}px`;
129
+ const ctx = canvas.getContext("2d");
130
+ // CSS-pixel coords + DPR transform (do not also multiply placement by dpr).
131
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
132
+ ctx.clearRect(0, 0, width, height);
133
+
134
+ const max = Math.max(1, Number(words[0]?.count) || 1);
135
+ const palette = [accent, "#c45c26", "#f0c14a", "#23d6a5", "#5b8cff", "#6b6560", "#d97706", "#e07a3a"];
136
+ const placed = [];
137
+ const shuffled = words.slice(0, 80);
138
+ const pad = 1;
139
+ const gap = 0.5;
140
+
141
+ function collides(x, y, w, h) {
142
+ for (const box of placed) {
143
+ if (x < box.x + box.w + gap && x + w + gap > box.x && y < box.y + box.h + gap && y + h + gap > box.y) {
144
+ return true;
145
+ }
146
+ }
147
+ return false;
148
+ }
149
+
150
+ for (let i = 0; i < shuffled.length; i++) {
151
+ const { term, count } = shuffled[i];
152
+ const weight = Math.max(0, Number(count) || 0) / max;
153
+ const size = 8 + Math.pow(weight, 0.55) * Math.min(34, height * 0.28);
154
+ const rotate = i % 11 === 0 ? (i % 2 === 0 ? -0.16 : 0.16) : 0;
155
+ ctx.font = `700 ${size}px Outfit, system-ui, sans-serif`;
156
+ const tw = ctx.measureText(term).width;
157
+ const th = size;
158
+ let placedOk = false;
159
+ for (let attempt = 0; attempt < 220; attempt++) {
160
+ const angle = attempt * 0.38;
161
+ const radius = 0.5 + attempt * 1.2;
162
+ const cx = width / 2 + Math.cos(angle) * radius * (width / Math.max(height, 1)) * 0.52;
163
+ const cy = height / 2 + Math.sin(angle) * radius * 0.68;
164
+ const x = cx - tw / 2;
165
+ const y = cy + th * 0.32;
166
+ if (x < pad || y < th + pad * 0.25 || x + tw > width - pad || y > height - pad) continue;
167
+ if (collides(x, y - th, tw, th)) continue;
168
+ ctx.save();
169
+ ctx.translate(cx, cy);
170
+ if (rotate) ctx.rotate(rotate);
171
+ ctx.fillStyle = palette[i % palette.length];
172
+ ctx.globalAlpha = 0.62 + weight * 0.38;
173
+ ctx.textAlign = "center";
174
+ ctx.textBaseline = "middle";
175
+ ctx.fillText(term, 0, 0);
176
+ ctx.restore();
177
+ placed.push({ x, y: y - th, w: tw, h: th });
178
+ placedOk = true;
179
+ break;
180
+ }
181
+ if (!placedOk && i < 14) {
182
+ // Soft fallback near center rather than dumping into a corner grid.
183
+ ctx.fillStyle = palette[i % palette.length];
184
+ ctx.globalAlpha = 0.45;
185
+ ctx.textAlign = "left";
186
+ ctx.textBaseline = "alphabetic";
187
+ ctx.fillText(term, pad + (i % 5) * (width / 5.2), height / 2 + (Math.floor(i / 5) - 1) * 16);
188
+ }
189
+ }
190
+ ctx.globalAlpha = 1;
191
+ ctx.textAlign = "left";
192
+ ctx.textBaseline = "alphabetic";
193
+ }
194
+
195
+ function rotatePoint(x, y, z, yaw, pitch) {
196
+ const cosY = Math.cos(yaw);
197
+ const sinY = Math.sin(yaw);
198
+ const x1 = x * cosY - y * sinY;
199
+ const y1 = x * sinY + y * cosY;
200
+ const cosP = Math.cos(pitch);
201
+ const sinP = Math.sin(pitch);
202
+ return { x: x1, y: y1 * cosP - z * sinP, z: y1 * sinP + z * cosP };
203
+ }
204
+
205
+ function shadeColor(hex, factor) {
206
+ const m = /^#([0-9a-f]{6})$/i.exec(hex);
207
+ if (!m) return hex;
208
+ const num = parseInt(m[1], 16);
209
+ const r = (num >> 16) & 0xff;
210
+ const g = (num >> 8) & 0xff;
211
+ const b = num & 0xff;
212
+ const c = (n) => Math.max(0, Math.min(255, Math.round(n * factor)));
213
+ return `rgb(${c(r)}, ${c(g)}, ${c(b)})`;
214
+ }
215
+
216
+ function buildWeeksFromDaily(dailyRows = [], weeks = 20) {
217
+ const now = new Date();
218
+ const end = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
219
+ const start = new Date(end);
220
+ start.setUTCDate(start.getUTCDate() - (weeks * 7 - 1));
221
+ const startDow = start.getUTCDay();
222
+ start.setUTCDate(start.getUTCDate() - startDow);
223
+
224
+ const byDay = new Map();
225
+ for (const row of dailyRows) {
226
+ if (!row?.day) continue;
227
+ byDay.set(row.day, Number(row.value || row.billable_total_tokens || row.total_tokens || 0));
228
+ }
229
+
230
+ const values = [];
231
+ const cells = [];
232
+ const totalDays = Math.floor((end - start) / 86400000) + 1;
233
+ const weekCount = Math.ceil(totalDays / 7);
234
+ for (let w = 0; w < weekCount; w++) {
235
+ const week = [];
236
+ for (let d = 0; d < 7; d++) {
237
+ const dt = new Date(start);
238
+ dt.setUTCDate(dt.getUTCDate() + w * 7 + d);
239
+ if (dt > end) {
240
+ week.push(null);
241
+ continue;
242
+ }
243
+ const day = dt.toISOString().slice(0, 10);
244
+ const value = byDay.get(day) || 0;
245
+ if (value > 0) values.push(value);
246
+ week.push({ day, value });
247
+ }
248
+ cells.push(week);
249
+ }
250
+ values.sort((a, b) => a - b);
251
+ const q = (pct) => {
252
+ if (!values.length) return 0;
253
+ const pos = (values.length - 1) * pct;
254
+ const lo = Math.floor(pos);
255
+ const hi = Math.min(values.length - 1, lo + 1);
256
+ return Math.round(values[lo] + (values[hi] - values[lo]) * (pos - lo));
257
+ };
258
+ const t1 = q(0.5);
259
+ const t2 = q(0.75);
260
+ const t3 = q(0.9);
261
+ for (const week of cells) {
262
+ for (const cell of week) {
263
+ if (!cell) continue;
264
+ if (!cell.value) cell.level = 0;
265
+ else if (cell.value <= t1) cell.level = 1;
266
+ else if (cell.value <= t2) cell.level = 2;
267
+ else if (cell.value <= t3) cell.level = 3;
268
+ else cell.level = 4;
269
+ }
270
+ }
271
+ return cells.length > weeks ? cells.slice(cells.length - weeks) : cells;
272
+ }
273
+
274
+ const EMERALD = ["#ebedf0", "#a7f3d0", "#6ee7b7", "#34d399", "#10b981"];
275
+
276
+ function compactNumber(value) {
277
+ const n = Number(value) || 0;
278
+ if (n < 1000) return String(Math.round(n));
279
+ if (n < 1000000) return `${(n / 1000).toFixed(n >= 10000 ? 0 : 1)}K`;
280
+ if (n < 10000000) return `${(n / 1000000).toFixed(2)}M`;
281
+ if (n < 1000000000) return `${(n / 1000000).toFixed(1)}M`;
282
+ return `${(n / 1000000000).toFixed(2)}B`;
283
+ }
284
+
285
+ export function renderActivityIso(container, activity, weeks = 52, { svgHeight = 180, animate = true } = {}) {
286
+ if (!container) return;
287
+ const dailyRows = activity?.daily_rows || [];
288
+ const weeksData = buildWeeksFromDaily(dailyRows, weeks);
289
+ const yaw = -0.2;
290
+ const pitch = 0.88;
291
+ const UNIT = 11;
292
+ const GAP = 1.2;
293
+ const SIZE = UNIT - GAP;
294
+ const HEIGHT_MAX = 28;
295
+ const isTokens = activity?.metric === "tokens" && Number(activity?.total_tokens || 0) > 0;
296
+
297
+ const cells = [];
298
+ weeksData.forEach((week, wi) => {
299
+ (week || []).forEach((cell, di) => {
300
+ if (!cell) return;
301
+ cells.push({ col: wi, row: di, level: cell.level || 0, value: cell.value, day: cell.day });
302
+ });
303
+ });
304
+
305
+ if (!cells.length) {
306
+ container.innerHTML = `<p class="sub" style="margin:0;text-align:center">No activity days in snapshot</p>`;
307
+ return;
308
+ }
309
+
310
+ const W = weeksData.length;
311
+ const peak = activity?.peak_day;
312
+ const kpi = `
313
+ <div class="activity-kpi">
314
+ <div><span>${isTokens ? "Total tokens" : "Active days"}</span><strong>${isTokens ? compactNumber(activity.total_tokens) : (activity.active_day_count || dailyRows.length)}</strong></div>
315
+ <div><span>Streak</span><strong>${activity.longest_streak || 0}d</strong></div>
316
+ <div><span>Peak</span><strong>${peak?.value ? compactNumber(peak.value) : "—"}</strong></div>
317
+ <div><span>Provider</span><strong>${(activity.top_provider || "—").toString().toUpperCase()}</strong></div>
318
+ </div>`;
319
+
320
+ const projectAt = (growth) => {
321
+ const projected = cells.map((c) => {
322
+ const targetH = Math.max(1.8, (c.level / 4) * HEIGHT_MAX);
323
+ const dist = Math.sqrt(Math.pow(c.col - W / 2, 2) + Math.pow(c.row - 3.5, 2));
324
+ const maxDist = Math.sqrt(Math.pow(W / 2, 2) + Math.pow(3.5, 2)) || 1;
325
+ const delay = (dist / maxDist) * 0.4;
326
+ const progress = Math.min(1, Math.max(0, (growth - delay) * (1 / 0.6)));
327
+ const h = targetH * progress;
328
+ const xc = (c.col - W / 2) * UNIT;
329
+ const yc = (c.row - 3.5) * UNIT;
330
+ const half = SIZE / 2;
331
+ const pts = [
332
+ { x: xc - half, y: yc - half, z: 0 },
333
+ { x: xc + half, y: yc - half, z: 0 },
334
+ { x: xc + half, y: yc + half, z: 0 },
335
+ { x: xc - half, y: yc + half, z: 0 },
336
+ { x: xc - half, y: yc - half, z: h },
337
+ { x: xc + half, y: yc - half, z: h },
338
+ { x: xc + half, y: yc + half, z: h },
339
+ { x: xc - half, y: yc + half, z: h },
340
+ ].map((p) => rotatePoint(p.x, p.y, p.z, yaw, pitch));
341
+ const center = rotatePoint(xc, yc, h / 2, yaw, pitch);
342
+ const base = EMERALD[Math.min(4, c.level)];
343
+ const faces = [
344
+ { indices: [4, 5, 6, 7], scale: 1 },
345
+ { indices: [1, 2, 6, 5], scale: 0.75 },
346
+ { indices: [0, 1, 5, 4], scale: 0.85 },
347
+ { indices: [3, 0, 4, 7], scale: 0.55 },
348
+ ];
349
+ const paths = faces.map((f) => {
350
+ const [a, b, c2, d] = f.indices.map((i) => pts[i]);
351
+ return `<path d="M${a.x},${a.y} L${b.x},${b.y} L${c2.x},${c2.y} L${d.x},${d.y} Z" fill="${shadeColor(base, f.scale)}" stroke="rgba(16,185,129,0.08)" stroke-width="0.4" />`;
352
+ });
353
+ return { center, paths: paths.join(""), z: center.z };
354
+ });
355
+ projected.sort((a, b) => a.z - b.z);
356
+ let minX = Infinity;
357
+ let minY = Infinity;
358
+ let maxX = -Infinity;
359
+ let maxY = -Infinity;
360
+ for (const p of projected) {
361
+ minX = Math.min(minX, p.center.x - UNIT * 2);
362
+ maxX = Math.max(maxX, p.center.x + UNIT * 2);
363
+ minY = Math.min(minY, p.center.y - UNIT * 2);
364
+ maxY = Math.max(maxY, p.center.y + UNIT * 2);
365
+ }
366
+ const pad = 6;
367
+ const vb = `${minX - pad} ${minY - pad} ${maxX - minX + pad * 2} ${maxY - minY + pad * 2}`;
368
+ return { projected, vb };
369
+ };
370
+
371
+ const paint = (growth) => {
372
+ const { projected, vb } = projectAt(growth);
373
+ const svg = container.querySelector("[data-iso-svg]");
374
+ if (svg) {
375
+ svg.setAttribute("viewBox", vb);
376
+ svg.innerHTML = projected.map((p) => p.paths).join("");
377
+ return;
378
+ }
379
+ container.innerHTML = `${kpi}<svg data-iso-svg viewBox="${vb}" role="img" aria-label="3D activity" style="width:100%;height:${svgHeight}px">${projected.map((p) => p.paths).join("")}</svg>`;
380
+ };
381
+
382
+ if (!animate || (typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches)) {
383
+ paint(1);
384
+ return;
385
+ }
386
+
387
+ paint(0);
388
+ const start = performance.now();
389
+ const tick = (now) => {
390
+ const p = Math.min(1, (now - start) / 1200);
391
+ const eased = 1 - Math.pow(1 - p, 3);
392
+ paint(eased);
393
+ if (p < 1) requestAnimationFrame(tick);
394
+ };
395
+ requestAnimationFrame(tick);
396
+ }
397
+
398
+ export function renderActivity2D(container, activity, weeks = 52) {
399
+ if (!container) return;
400
+ const dailyRows = activity?.daily_rows || [];
401
+ const weeksData = buildWeeksFromDaily(dailyRows, weeks);
402
+ const isTokens = activity?.metric === "tokens" && Number(activity?.total_tokens || 0) > 0;
403
+ const dayLabels = ["", "Mon", "", "Wed", "", "Fri", ""];
404
+
405
+ if (!weeksData.some((w) => (w || []).some(Boolean))) {
406
+ container.innerHTML = `<p class="sub" style="margin:0;text-align:center">No activity days in snapshot</p>`;
407
+ return;
408
+ }
409
+
410
+ const cellSize = 18;
411
+ const cellGap = 2;
412
+ const labelWidth = 16;
413
+ const totalWidth = Math.max(weeksData.length * (cellSize + cellGap) + labelWidth, 120);
414
+ const height = 7 * (cellSize + cellGap) + 4;
415
+ const rects = [];
416
+ weeksData.forEach((week, wi) => {
417
+ (week || []).forEach((c, di) => {
418
+ if (!c) return;
419
+ const fill = EMERALD[Math.min(4, c.level || 0)];
420
+ rects.push(
421
+ `<rect x="${labelWidth + wi * (cellSize + cellGap)}" y="${di * (cellSize + cellGap)}" width="${cellSize}" height="${cellSize}" rx="2" fill="${fill}"><title>${c.day}: ${Number(c.value || 0).toLocaleString()}</title></rect>`,
422
+ );
423
+ });
424
+ });
425
+ const labels = dayLabels
426
+ .map((label, i) =>
427
+ label
428
+ ? `<text x="0" y="${i * (cellSize + cellGap) + cellSize}" font-size="9" fill="#8b8680">${label}</text>`
429
+ : "",
430
+ )
431
+ .join("");
432
+ const peak = activity?.peak_day;
433
+ const kpi = `
434
+ <div class="activity-kpi">
435
+ <div><span>${isTokens ? "Total tokens" : "Active days"}</span><strong>${isTokens ? compactNumber(activity.total_tokens) : (activity.active_day_count || dailyRows.length)}</strong></div>
436
+ <div><span>Streak</span><strong>${activity.longest_streak || 0}d</strong></div>
437
+ <div><span>Peak</span><strong>${peak?.value ? compactNumber(peak.value) : "—"}</strong></div>
438
+ <div><span>Provider</span><strong>${(activity.top_provider || "—").toString().toUpperCase()}</strong></div>
439
+ </div>`;
440
+ container.innerHTML = `${kpi}<div class="activity-2d-wrap" style="min-height:230px;align-items:center"><svg width="${totalWidth}" height="${height}" role="img" aria-label="2D activity heatmap">${labels}${rects.join("")}</svg></div>`;
441
+ }
442
+
443
+ /** Compact activity map with 2D default and optional 3D toggle. */
444
+ export function renderActivityMap(container, activity, weeks = 52, { defaultMode = "2d" } = {}) {
445
+ if (!container) return;
446
+ let mode = defaultMode === "3d" ? "3d" : "2d";
447
+ let busy = false;
448
+
449
+ const paintBody = (body) => {
450
+ body.classList.remove("is-exit");
451
+ body.classList.add("is-enter");
452
+ if (mode === "3d") renderActivityIso(body, activity, weeks, { svgHeight: 240, animate: true });
453
+ else renderActivity2D(body, activity, weeks);
454
+ };
455
+
456
+ const paint = () => {
457
+ container.innerHTML = `
458
+ <div class="activity-view-toggle" role="group" aria-label="Activity view">
459
+ <button type="button" data-mode="2d" class="${mode === "2d" ? "is-active" : ""}">2d</button>
460
+ <button type="button" data-mode="3d" class="${mode === "3d" ? "is-active" : ""}">3d</button>
461
+ </div>
462
+ <div class="activity-view-body is-enter"></div>`;
463
+ const body = container.querySelector(".activity-view-body");
464
+ paintBody(body);
465
+ container.querySelectorAll("[data-mode]").forEach((btn) => {
466
+ btn.addEventListener("click", () => {
467
+ const next = btn.getAttribute("data-mode");
468
+ if (next === mode || busy) return;
469
+ busy = true;
470
+ const current = container.querySelector(".activity-view-body");
471
+ if (current) {
472
+ current.classList.remove("is-enter");
473
+ current.classList.add("is-exit");
474
+ }
475
+ window.setTimeout(() => {
476
+ mode = next;
477
+ container.querySelectorAll("[data-mode]").forEach((b) => {
478
+ b.classList.toggle("is-active", b.getAttribute("data-mode") === mode);
479
+ });
480
+ const nextBody = document.createElement("div");
481
+ nextBody.className = "activity-view-body";
482
+ current?.replaceWith(nextBody);
483
+ paintBody(nextBody);
484
+ busy = false;
485
+ }, 160);
486
+ });
487
+ });
488
+ };
489
+
490
+ paint();
491
+ }
492
+
493
+ function loadImage(src) {
494
+ return new Promise((resolve, reject) => {
495
+ const img = new Image();
496
+ img.onload = () => resolve(img);
497
+ img.onerror = () => reject(new Error("image load failed"));
498
+ img.src = src;
499
+ });
500
+ }
501
+
502
+ function roundRect(ctx, x, y, w, h, r) {
503
+ const radius = Math.min(r, w / 2, h / 2);
504
+ ctx.beginPath();
505
+ ctx.moveTo(x + radius, y);
506
+ ctx.arcTo(x + w, y, x + w, y + h, radius);
507
+ ctx.arcTo(x + w, y + h, x, y + h, radius);
508
+ ctx.arcTo(x, y + h, x, y, radius);
509
+ ctx.arcTo(x, y, x + w, y, radius);
510
+ ctx.closePath();
511
+ }
512
+
513
+ export async function renderSharePoster({ vibe, hashtags = [], lang = "zh" } = {}) {
514
+ const zh = lang === "zh";
515
+ const width = 900;
516
+ const height = 1200;
517
+ const canvas = document.createElement("canvas");
518
+ canvas.width = width;
519
+ canvas.height = height;
520
+ const ctx = canvas.getContext("2d");
521
+ const accent = vibe?.archetype?.accent || "#ff5a1f";
522
+ const pad = 48;
523
+
524
+ const grad = ctx.createLinearGradient(0, 0, width, height);
525
+ grad.addColorStop(0, "#fff7f0");
526
+ grad.addColorStop(0.5, "#f3f1ec");
527
+ grad.addColorStop(1, "#e8e4db");
528
+ ctx.fillStyle = grad;
529
+ ctx.fillRect(0, 0, width, height);
530
+
531
+ ctx.fillStyle = "#1a1a1a";
532
+ ctx.font = "700 22px Outfit, system-ui, sans-serif";
533
+ ctx.fillText("Vibe Roaster", pad, pad + 8);
534
+ ctx.fillStyle = "#6b6560";
535
+ ctx.font = "600 16px Outfit, system-ui, sans-serif";
536
+ ctx.fillText(zh ? "分享海报 · 3:4" : "Share poster · 3:4", pad, pad + 32);
537
+
538
+ let figureY = pad + 56;
539
+ try {
540
+ const img = await loadImage(vibe.figure);
541
+ const maxW = width * 0.58;
542
+ const maxH = height * 0.4;
543
+ const scale = Math.min(maxW / img.width, maxH / img.height);
544
+ const iw = img.width * scale;
545
+ const ih = img.height * scale;
546
+ ctx.drawImage(img, (width - iw) / 2, figureY, iw, ih);
547
+ figureY += ih + 18;
548
+ } catch {
549
+ figureY += 280;
550
+ }
551
+
552
+ ctx.textAlign = "center";
553
+ ctx.fillStyle = accent;
554
+ ctx.font = "800 42px Outfit, system-ui, sans-serif";
555
+ ctx.fillText(vibe.archetype?.title || "Vibe", width / 2, figureY);
556
+
557
+ ctx.fillStyle = "#1a1a1a";
558
+ ctx.font = "700 56px 'JetBrains Mono', monospace";
559
+ ctx.fillText(Number(vibe.total).toFixed(1), width / 2, figureY + 64);
560
+ ctx.fillStyle = "#6b6560";
561
+ ctx.font = "600 20px Outfit, system-ui, sans-serif";
562
+ ctx.fillText("/ 100", width / 2, figureY + 92);
563
+
564
+ ctx.fillStyle = vibe.tier?.color || accent;
565
+ ctx.font = "800 28px Outfit, system-ui, sans-serif";
566
+ ctx.fillText(`${vibe.tier?.emoji || ""} ${vibe.tier?.id || ""}`.trim(), width / 2, figureY + 132);
567
+
568
+ ctx.font = "700 18px Outfit, system-ui, sans-serif";
569
+ let tagX = pad;
570
+ let tagY = figureY + 168;
571
+ for (const tag of hashtags.slice(0, 6)) {
572
+ const tw = ctx.measureText(tag).width + 24;
573
+ if (tagX + tw > width - pad) {
574
+ tagX = pad;
575
+ tagY += 36;
576
+ }
577
+ roundRect(ctx, tagX, tagY - 22, tw, 30, 15);
578
+ ctx.fillStyle = `${accent}22`;
579
+ ctx.fill();
580
+ ctx.fillStyle = accent;
581
+ ctx.textAlign = "left";
582
+ ctx.fillText(tag, tagX + 12, tagY);
583
+ tagX += tw + 10;
584
+ }
585
+
586
+ const tldr = zh ? vibe.tldrZh || vibe.tldr : vibe.tldr || vibe.tldrZh;
587
+ roundRect(ctx, pad, height - 220, width - pad * 2, 150, 20);
588
+ ctx.fillStyle = "#fffcf7";
589
+ ctx.fill();
590
+ ctx.fillStyle = "#6b6560";
591
+ ctx.font = "800 13px Outfit, system-ui, sans-serif";
592
+ ctx.textAlign = "left";
593
+ ctx.fillText("TL;DR", pad + 24, height - 180);
594
+ ctx.fillStyle = "#1a1a1a";
595
+ ctx.font = "600 18px Outfit, system-ui, sans-serif";
596
+ const max = zh ? 28 : 46;
597
+ let rest = String(tldr || "");
598
+ let ly = height - 150;
599
+ for (let i = 0; i < 3 && rest; i++) {
600
+ ctx.fillText(rest.slice(0, max), pad + 24, ly);
601
+ rest = rest.slice(max);
602
+ ly += 28;
603
+ }
604
+
605
+ ctx.textAlign = "center";
606
+ ctx.fillStyle = "#8b8680";
607
+ ctx.font = "600 13px Outfit, system-ui, sans-serif";
608
+ ctx.fillText(zh ? "vibe-roast · 本地 roast" : "vibe-roast · local roast", width / 2, height - 28);
609
+
610
+ return canvas;
611
+ }
612
+
613
+ export function downloadCanvasPng(canvas, filename = "vibe-roast-poster.png") {
614
+ const url = canvas.toDataURL("image/png");
615
+ const a = document.createElement("a");
616
+ a.href = url;
617
+ a.download = filename;
618
+ a.click();
619
+ }
@@ -0,0 +1,62 @@
1
+ /** Browser-side reference for src/lib/agent-score.js. Keep formulas aligned. */
2
+ export const DIMENSIONS = [
3
+ ["orchestration", "Agent orchestration", "Agent 编排", "Workflow delegation density", "工作流与委派密度"],
4
+ ["promptCraft", "Prompt craft", "提示词手艺", "Intent clarity vs pasted noise", "清晰意图 vs 粘贴噪音"],
5
+ ["build", "Build drive", "构建驱动力", "Implementation and packaging", "实现与打包倾向"],
6
+ ["debug", "Verification drive", "验证驱动力", "Debug, tests, and refactors", "调试、测试与重构"],
7
+ ["context", "Context appetite", "上下文胃口", "Long prompts and reference payloads", "长提示与引用负载"],
8
+ ["ship", "Shipping bias", "交付倾向", "Execution over deliberation", "执行相对思考的倾向"],
9
+ ].map(([key, label, labelZh, hint, hintZh]) => ({ key, label, labelZh, hint, hintZh, max: 100 }));
10
+
11
+ const AXES = [
12
+ ["maker_architect", ["M", "Maker", "造物派"], ["A", "Architect", "架构派"]],
13
+ ["orchestrator_promptsmith", ["O", "Orchestrator", "编排派"], ["P", "Promptsmith", "提示派"]],
14
+ ["verifier_shipper", ["V", "Verifier", "验证派"], ["S", "Shipper", "交付派"]],
15
+ ["focused_maxcontext", ["F", "Focused", "聚焦派"], ["X", "Max-context", "满上下文派"]],
16
+ ].map(([key, left, right]) => ({ key, left, right }));
17
+
18
+ function count(categories, name) {
19
+ const row = categories?.[name];
20
+ const value = typeof row === "number" ? row : Number(row?.count || 0);
21
+ return Number.isFinite(value) ? value : 0;
22
+ }
23
+
24
+ function ratioScore(n, d) { return d > 0 ? Math.round(Math.min(1, Math.max(0, n / d)) * 1000) / 10 : 0; }
25
+
26
+ export function scoreFromCategories(categories = {}, _env = {}, promptAnalysis = {}) {
27
+ const c = (name) => count(categories, name);
28
+ const useful = ["implementation", "debugging", "planning", "research", "ui_design", "explanation", "testing", "refactor", "packaging", "workflow"].reduce((sum, key) => sum + c(key), 0);
29
+ if (!useful) return Object.fromEntries(DIMENSIONS.map((dim) => [dim.key, 0]));
30
+ const usefulRatio = Number(promptAnalysis.useful_ratio ?? useful / Math.max(useful + c("reference"), 1));
31
+ const longRatio = Number(promptAnalysis.long_prompt_ratio || 0);
32
+ return {
33
+ orchestration: ratioScore(c("workflow") * 2, useful * 0.45),
34
+ promptCraft: Math.round(Math.min(1, usefulRatio * 0.8 + ((c("planning") + c("explanation")) / useful) * 0.5) * 1000) / 10,
35
+ build: ratioScore(c("implementation") + c("packaging") + c("ui_design") * 0.5, useful * 0.75),
36
+ debug: ratioScore(c("debugging") + c("testing") + c("refactor") * 0.6, useful * 0.55),
37
+ context: Math.round(Math.min(1, longRatio * 1.5 + (1 - usefulRatio) * 0.7) * 1000) / 10,
38
+ ship: ratioScore(c("implementation") + c("packaging"), c("implementation") + c("packaging") + c("planning") + c("research") + c("debugging") + c("testing")),
39
+ };
40
+ }
41
+
42
+ function split(def, leftEvidence, rightEvidence) {
43
+ const total = leftEvidence + rightEvidence;
44
+ const lp = total ? Math.round(leftEvidence / total * 100) : 50;
45
+ const rp = 100 - lp;
46
+ const chosen = lp >= rp ? def.left : def.right;
47
+ return { key: def.key, letter: chosen[0], label: chosen[1], labelZh: chosen[2], left: { code: def.left[0], label: def.left[1], labelZh: def.left[2], percent: lp }, right: { code: def.right[0], label: def.right[1], labelZh: def.right[2], percent: rp }, margin: Math.abs(lp - rp) };
48
+ }
49
+
50
+ export function classifyType(categories = {}, promptAnalysis = {}) {
51
+ const c = (name) => count(categories, name);
52
+ const usefulRatio = Number(promptAnalysis.useful_ratio ?? 1);
53
+ const longRatio = Number(promptAnalysis.long_prompt_ratio || 0);
54
+ const referenceRatio = Math.max(0, 1 - usefulRatio);
55
+ const axes = [
56
+ split(AXES[0], c("implementation") + c("packaging") + c("testing") + c("refactor") + c("debugging") + c("ui_design") * 0.5, c("planning") + c("research") + c("explanation") + c("ui_design") * 0.5),
57
+ split(AXES[1], c("workflow") * 2 + c("planning") * 0.2, c("implementation") * 0.2 + c("explanation") + c("ui_design") * 0.35 + 0.25),
58
+ split(AXES[2], c("debugging") + c("testing") + c("refactor") * 0.7, c("implementation") + c("packaging") + c("ui_design") * 0.25),
59
+ split(AXES[3], Math.max(0.05, 1 - longRatio - referenceRatio), longRatio * 1.4 + referenceRatio * 1.2),
60
+ ];
61
+ return { code: axes.map((axis) => axis.letter).join(""), axes };
62
+ }