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,43 @@
1
+ function dayBounds(from, to) {
2
+ return {
3
+ fromMs: from ? Date.parse(`${from}T00:00:00.000Z`) : null,
4
+ toMs: to ? Date.parse(`${to}T23:59:59.999Z`) : null,
5
+ };
6
+ }
7
+
8
+ function isInRange(timestamp, range) {
9
+ const bounds = normalizeRange(range);
10
+ const hasActiveRange = bounds.fromMs != null || bounds.toMs != null;
11
+ if (!timestamp) return !hasActiveRange;
12
+ const ms = Date.parse(timestamp);
13
+ if (!Number.isFinite(ms)) return !hasActiveRange;
14
+ if (bounds.fromMs != null && ms < bounds.fromMs) return false;
15
+ if (bounds.toMs != null && ms > bounds.toMs) return false;
16
+ return true;
17
+ }
18
+
19
+ function normalizeRange(range) {
20
+ if (!range) return { fromMs: null, toMs: null };
21
+ return {
22
+ fromMs: finiteNumber(range.fromMs) ?? parseBoundary(range.from, "start"),
23
+ toMs: finiteNumber(range.toMs) ?? parseBoundary(range.to, "end"),
24
+ };
25
+ }
26
+
27
+ function parseBoundary(value, side) {
28
+ if (!value) return null;
29
+ const raw = String(value);
30
+ const stamp = raw.includes("T")
31
+ ? raw
32
+ : `${raw}T${side === "start" ? "00:00:00.000Z" : "23:59:59.999Z"}`;
33
+ const ms = Date.parse(stamp);
34
+ return Number.isFinite(ms) ? ms : null;
35
+ }
36
+
37
+ function finiteNumber(value) {
38
+ if (value == null) return null;
39
+ const number = Number(value);
40
+ return Number.isFinite(number) ? number : null;
41
+ }
42
+
43
+ module.exports = { dayBounds, isInRange };
@@ -0,0 +1,150 @@
1
+ /** Derive shareable #hashtags from vibe profile + category signals (EN / ZH). */
2
+
3
+ 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
+ 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
+ function buildHashtags(vibe, categories = {}, opts = {}) {
77
+ const options = normalizeOpts(opts);
78
+ const limit = options.limit ?? 8;
79
+ const zh = options.locale === "zh";
80
+ const tags = [];
81
+ const seen = new Set();
82
+
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
+
95
+ const arch = vibe?.archetype || {};
96
+ const archId = arch.id || "";
97
+ if (ARCHETYPE_TAGS[archId]) {
98
+ push(pickLocale(ARCHETYPE_TAGS[archId], zh));
99
+ } else if (arch.title) {
100
+ push(zh ? arch.titleZh || arch.title : pascalCase(arch.title));
101
+ }
102
+
103
+ if (arch.code && CODE_TAGS[arch.code]) {
104
+ push(pickLocale(CODE_TAGS[arch.code], zh));
105
+ } else if (arch.code) {
106
+ push(/^[MA][OP][VS][FX]$/.test(arch.code) ? arch.code : pascalCase(arch.code));
107
+ }
108
+
109
+ const tierId = vibe?.tier?.id;
110
+ if (!vibe?.type_code && tierId && TIER_TAGS[tierId]) push(pickLocale(TIER_TAGS[tierId], zh));
111
+ else if (!vibe?.type_code && tierId) push(tierId);
112
+
113
+ const ranked = Object.keys(CATEGORY_TAGS)
114
+ .map((key) => [key, categoryCount(categories, key)])
115
+ .filter(([, count]) => count > 0)
116
+ .sort((a, b) => b[1] - a[1]);
117
+
118
+ for (const [key] of ranked) {
119
+ push(pickLocale(CATEGORY_TAGS[key], zh));
120
+ if (tags.length >= limit) break;
121
+ }
122
+
123
+ const hookSrc = zh ? arch.hookZh || arch.hook : arch.hook;
124
+ if (hookSrc) {
125
+ const hookWord = String(hookSrc).split(/[\s·•\-–—,/,、]+/)[0];
126
+ if (hookWord && hookWord.length > 1) {
127
+ push(zh ? hookWord : pascalCase(hookWord));
128
+ }
129
+ }
130
+
131
+ return tags.slice(0, limit);
132
+ }
133
+
134
+ function mergeHashtags(generated = [], limit = 8) {
135
+ const tags = [];
136
+ const seen = new Set();
137
+ for (const raw of generated || []) {
138
+ const bare = String(raw || "")
139
+ .replace(/^#+/, "")
140
+ .replace(/[^A-Za-z0-9\u4e00-\u9fff]/g, "");
141
+ if (!bare) continue;
142
+ const key = bare.toLowerCase();
143
+ if (seen.has(key)) continue;
144
+ seen.add(key);
145
+ tags.push(`#${bare}`);
146
+ }
147
+ return tags.slice(0, limit);
148
+ }
149
+
150
+ module.exports = { buildHashtags, mergeHashtags, CODE_TAGS, ARCHETYPE_TAGS, CATEGORY_TAGS, TIER_TAGS };
@@ -0,0 +1,48 @@
1
+ const fs = require("node:fs/promises");
2
+ const fssync = require("node:fs");
3
+ const path = require("node:path");
4
+ const readline = require("node:readline");
5
+
6
+ async function walkFiles(rootDir, predicate) {
7
+ const out = [];
8
+ const stack = [rootDir];
9
+
10
+ while (stack.length > 0) {
11
+ const dir = stack.pop();
12
+ let entries;
13
+ try {
14
+ entries = await fs.readdir(dir, { withFileTypes: true });
15
+ } catch {
16
+ continue;
17
+ }
18
+
19
+ for (const entry of entries) {
20
+ const filePath = path.join(dir, entry.name);
21
+ if (entry.isDirectory()) {
22
+ stack.push(filePath);
23
+ } else if (entry.isFile() && predicate(filePath, entry.name)) {
24
+ out.push(filePath);
25
+ }
26
+ }
27
+ }
28
+
29
+ out.sort((a, b) => a.localeCompare(b));
30
+ return out;
31
+ }
32
+
33
+ async function readJsonl(filePath, onObject) {
34
+ const stream = fssync.createReadStream(filePath, { encoding: "utf8" });
35
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
36
+
37
+ for await (const line of rl) {
38
+ const trimmed = line.trim();
39
+ if (!trimmed) continue;
40
+ try {
41
+ await onObject(JSON.parse(trimmed));
42
+ } catch {
43
+ // Session logs can contain partial tail writes. Skip malformed lines.
44
+ }
45
+ }
46
+ }
47
+
48
+ module.exports = { walkFiles, readJsonl };
@@ -0,0 +1,59 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+
4
+ function parseEnvValue(raw) {
5
+ const value = String(raw || "").trim();
6
+ if (value.length >= 2 && value[0] === "\"" && value[value.length - 1] === "\"") {
7
+ return value.slice(1, -1)
8
+ .replace(/\\n/g, "\n")
9
+ .replace(/\\r/g, "\r")
10
+ .replace(/\\t/g, "\t")
11
+ .replace(/\\"/g, "\"")
12
+ .replace(/\\\\/g, "\\");
13
+ }
14
+ if (value.length >= 2 && value[0] === "'" && value[value.length - 1] === "'") {
15
+ return value.slice(1, -1);
16
+ }
17
+ return value.replace(/\s+#.*$/, "").trim();
18
+ }
19
+
20
+ function loadEnvFallback(filePath) {
21
+ const text = fs.readFileSync(filePath, "utf8");
22
+ for (const rawLine of text.split(/\r?\n/)) {
23
+ const line = rawLine.trim();
24
+ if (!line || line.startsWith("#")) continue;
25
+ const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
26
+ if (!match || process.env[match[1]] !== undefined) continue;
27
+ process.env[match[1]] = parseEnvValue(match[2]);
28
+ }
29
+ }
30
+
31
+ function resolveLocalEnv(cwd = process.cwd()) {
32
+ for (const name of [".env.local", ".env"]) {
33
+ const candidate = path.resolve(cwd, name);
34
+ try {
35
+ if (fs.statSync(candidate).isFile()) return candidate;
36
+ } catch (error) {
37
+ if (error?.code !== "ENOENT") throw error;
38
+ }
39
+ }
40
+ return null;
41
+ }
42
+
43
+ function loadLocalEnv(filePath) {
44
+ const resolved = filePath || resolveLocalEnv();
45
+ if (!resolved) return false;
46
+ if (typeof process.loadEnvFile === "function") {
47
+ process.loadEnvFile(resolved);
48
+ } else {
49
+ loadEnvFallback(resolved);
50
+ }
51
+ return true;
52
+ }
53
+
54
+ module.exports = {
55
+ loadLocalEnv,
56
+ loadEnvFallback,
57
+ parseEnvValue,
58
+ resolveLocalEnv,
59
+ };
@@ -0,0 +1,186 @@
1
+ /** Build the privacy-bounded, deterministic evidence packet consumed by an AI roast writer. */
2
+
3
+ function finiteNumber(value, fallback = 0) {
4
+ const number = Number(value);
5
+ return Number.isFinite(number) ? number : fallback;
6
+ }
7
+
8
+ function categoryRows(categories = {}) {
9
+ return Object.entries(categories)
10
+ .map(([key, row]) => ({
11
+ key,
12
+ count: finiteNumber(typeof row === "number" ? row : row?.count),
13
+ }))
14
+ .filter((row) => row.count > 0)
15
+ .sort((a, b) => b.count - a.count || a.key.localeCompare(b.key));
16
+ }
17
+
18
+ function conceptRows(wordFrequencies = [], limit = 16) {
19
+ return (wordFrequencies || [])
20
+ .map((row) => ({
21
+ term: String(row?.term || "").trim().slice(0, 48),
22
+ occurrence_count: finiteNumber(row?.count),
23
+ prompt_coverage: finiteNumber(row?.prompt_count),
24
+ weight: finiteNumber(row?.weight),
25
+ }))
26
+ .filter((row) => row.term && row.occurrence_count > 0)
27
+ .slice(0, limit);
28
+ }
29
+
30
+ function domainConceptRows(records = [], limit = 12) {
31
+ const concepts = new Map();
32
+ for (const record of records || []) {
33
+ const seen = new Set();
34
+ for (const item of record?.concepts || []) {
35
+ if (!item?.key || item.kind === "category" || item.vibe) continue;
36
+ const row = concepts.get(item.key) || {
37
+ key: item.key,
38
+ acronym: Boolean(item.acronym),
39
+ occurrence_count: 0,
40
+ prompt_coverage: 0,
41
+ variants: new Map(),
42
+ };
43
+ for (const [term, rawCount] of Object.entries(item.variants || {})) {
44
+ const count = Math.max(0, finiteNumber(rawCount));
45
+ if (!term || count <= 0) continue;
46
+ row.occurrence_count += count;
47
+ row.variants.set(term, (row.variants.get(term) || 0) + count);
48
+ }
49
+ if (!seen.has(item.key)) {
50
+ row.prompt_coverage += 1;
51
+ seen.add(item.key);
52
+ }
53
+ concepts.set(item.key, row);
54
+ }
55
+ }
56
+
57
+ return [...concepts.values()]
58
+ .filter((row) => row.prompt_coverage >= 3)
59
+ .map((row) => {
60
+ const term = [...row.variants.entries()]
61
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))[0]?.[0] || row.key;
62
+ const repetition = row.occurrence_count / Math.max(1, row.prompt_coverage);
63
+ return {
64
+ term: String(term).slice(0, 48),
65
+ occurrence_count: row.occurrence_count,
66
+ prompt_coverage: row.prompt_coverage,
67
+ acronym: row.acronym,
68
+ salience: Math.round(
69
+ Math.log2(1 + row.prompt_coverage) * Math.min(3, repetition) * (row.acronym ? 1.25 : 1) * 1000,
70
+ ) / 1000,
71
+ };
72
+ })
73
+ .sort((a, b) => (
74
+ b.salience - a.salience
75
+ || b.prompt_coverage - a.prompt_coverage
76
+ || b.occurrence_count - a.occurrence_count
77
+ || a.term.localeCompare(b.term)
78
+ ))
79
+ .slice(0, limit);
80
+ }
81
+
82
+ function dimensionRows(dimensions = []) {
83
+ return (dimensions || []).map((dimension) => ({
84
+ key: dimension.key,
85
+ label: dimension.label,
86
+ label_zh: dimension.labelZh,
87
+ value: finiteNumber(dimension.value),
88
+ }));
89
+ }
90
+
91
+ function axisRows(axes = []) {
92
+ return (axes || []).map((axis) => ({
93
+ key: axis.key,
94
+ chosen: axis.letter,
95
+ left: { code: axis.left?.code, percent: finiteNumber(axis.left?.percent) },
96
+ right: { code: axis.right?.code, percent: finiteNumber(axis.right?.percent) },
97
+ margin: finiteNumber(axis.margin),
98
+ }));
99
+ }
100
+
101
+ function buildContradictions(vibe, categories) {
102
+ const notes = [];
103
+ const dimensions = [...(vibe?.dimensions || [])]
104
+ .sort((a, b) => finiteNumber(a.value) - finiteNumber(b.value));
105
+ const closest = [...(vibe?.type_axes || [])]
106
+ .sort((a, b) => finiteNumber(a.margin) - finiteNumber(b.margin))[0];
107
+ const rows = categoryRows(categories);
108
+ const counts = Object.fromEntries(rows.map((row) => [row.key, row.count]));
109
+
110
+ if (dimensions.length >= 2) {
111
+ notes.push({
112
+ kind: "strongest_vs_weakest_dimension",
113
+ strongest: dimensions[dimensions.length - 1].key,
114
+ strongest_value: finiteNumber(dimensions[dimensions.length - 1].value),
115
+ weakest: dimensions[0].key,
116
+ weakest_value: finiteNumber(dimensions[0].value),
117
+ });
118
+ }
119
+ if (closest) {
120
+ notes.push({
121
+ kind: "closest_personality_split",
122
+ axis: closest.key,
123
+ left: `${closest.left?.code}:${finiteNumber(closest.left?.percent)}`,
124
+ right: `${closest.right?.code}:${finiteNumber(closest.right?.percent)}`,
125
+ });
126
+ }
127
+
128
+ const building = finiteNumber(counts.implementation) + finiteNumber(counts.packaging) + finiteNumber(counts.ui_design);
129
+ const verifying = finiteNumber(counts.testing) + finiteNumber(counts.debugging) + finiteNumber(counts.refactor);
130
+ if (building + verifying > 0) {
131
+ notes.push({
132
+ kind: "building_vs_verifying",
133
+ building,
134
+ verifying,
135
+ ratio: Math.round((building / Math.max(verifying, 1)) * 10) / 10,
136
+ });
137
+ }
138
+ return notes;
139
+ }
140
+
141
+ function buildRoastEvidence(report = {}) {
142
+ const vibe = report.vibe_profile || {};
143
+ const categories = report.profile_signals?.prompt_analysis?.categories || {};
144
+ const activity = report.activity || {};
145
+ const summary = report.summary || {};
146
+
147
+ return {
148
+ schema_version: 1,
149
+ immutable_profile: {
150
+ type_code: vibe.type_code,
151
+ personality: vibe.personality?.title,
152
+ personality_zh: vibe.personality?.titleZh,
153
+ confidence: finiteNumber(vibe.confidence),
154
+ status: vibe.status,
155
+ axes: axisRows(vibe.type_axes),
156
+ dimensions: dimensionRows(vibe.dimensions),
157
+ },
158
+ prompt_behavior: {
159
+ useful_prompt_count: finiteNumber(report.profile_signals?.prompt_analysis?.useful_prompt_count),
160
+ categories: categoryRows(categories),
161
+ recurring_behavior_terms: conceptRows(report.word_frequencies, 12),
162
+ recurring_domain_concepts: domainConceptRows(report.word_cloud_records),
163
+ },
164
+ activity: {
165
+ metric: activity.metric,
166
+ total_tokens: finiteNumber(activity.total_tokens),
167
+ active_days: finiteNumber(activity.active_day_count),
168
+ longest_streak: finiteNumber(activity.longest_streak),
169
+ active_rate: activity.active_rate || null,
170
+ top_agent: activity.top_agent || null,
171
+ top_provider: activity.top_provider || null,
172
+ top_model: activity.top_model || null,
173
+ active_agents: (summary.active_sources || []).slice(0, 12),
174
+ },
175
+ roast_targets: buildContradictions(vibe, categories),
176
+ guardrails: {
177
+ profile_is_immutable: true,
178
+ aggregate_evidence_only: true,
179
+ raw_prompts_included: false,
180
+ attack_behavior_not_identity: true,
181
+ unsupported_claims_forbidden: true,
182
+ },
183
+ };
184
+ }
185
+
186
+ module.exports = { buildRoastEvidence, buildContradictions, domainConceptRows };
@@ -0,0 +1,58 @@
1
+ const SNAPSHOT_VERSION = 1;
2
+ const WRITER_PROMPT_VERSION = 3;
3
+
4
+ function finite(value) {
5
+ const number = Number(value);
6
+ return Number.isFinite(number) ? number : 0;
7
+ }
8
+
9
+ function normalizedTerm(value) {
10
+ return String(value || "").trim().toLocaleLowerCase().slice(0, 48);
11
+ }
12
+
13
+ function buildRoastSnapshot(evidence = {}) {
14
+ const profile = evidence.immutable_profile || {};
15
+ const categories = evidence.prompt_behavior?.categories || [];
16
+ const categoryTotal = categories.reduce((total, row) => total + Math.max(0, finite(row?.count)), 0);
17
+ const concepts = [
18
+ ...(evidence.prompt_behavior?.recurring_domain_concepts || []),
19
+ ...(evidence.prompt_behavior?.recurring_behavior_terms || []),
20
+ ];
21
+
22
+ return {
23
+ snapshot_version: SNAPSHOT_VERSION,
24
+ writer_prompt_version: WRITER_PROMPT_VERSION,
25
+ evidence_schema_version: finite(evidence.schema_version),
26
+ type_code: String(profile.type_code || ""),
27
+ status: String(profile.status || ""),
28
+ confidence: Math.round(finite(profile.confidence)),
29
+ axes: (profile.axes || []).map((axis) => ({
30
+ key: String(axis?.key || ""),
31
+ chosen: String(axis?.chosen || ""),
32
+ left_percent: Math.round(finite(axis?.left?.percent)),
33
+ })).filter((axis) => axis.key),
34
+ dimensions: (profile.dimensions || []).map((dimension) => ({
35
+ key: String(dimension?.key || ""),
36
+ value: Math.round(finite(dimension?.value)),
37
+ })).filter((dimension) => dimension.key),
38
+ category_shares: categories.map((row) => ({
39
+ key: String(row?.key || ""),
40
+ share: categoryTotal > 0
41
+ ? Math.round((Math.max(0, finite(row?.count)) / categoryTotal) * 100)
42
+ : 0,
43
+ })).filter((row) => row.key),
44
+ concepts: [...new Set(concepts.map((row) => normalizedTerm(row?.term)).filter(Boolean))].slice(0, 8),
45
+ top_agent: String(evidence.activity?.top_agent || ""),
46
+ };
47
+ }
48
+
49
+ function encodeRoastSnapshot(evidence) {
50
+ return Buffer.from(JSON.stringify(buildRoastSnapshot(evidence)), "utf8").toString("base64url");
51
+ }
52
+
53
+ module.exports = {
54
+ SNAPSHOT_VERSION,
55
+ WRITER_PROMPT_VERSION,
56
+ buildRoastSnapshot,
57
+ encodeRoastSnapshot,
58
+ };