shakers-cli 0.0.0-stage → 0.14.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 (237) hide show
  1. package/LICENSE +59 -0
  2. package/README.md +1233 -2
  3. package/bin/add-agent.js +88 -0
  4. package/bin/add-project.js +88 -0
  5. package/bin/add-role.js +61 -0
  6. package/bin/add-skill.js +88 -0
  7. package/bin/ai-usage.js +99 -0
  8. package/bin/alma.js +99 -0
  9. package/bin/applications.js +69 -0
  10. package/bin/availability.js +70 -0
  11. package/bin/certifications.js +69 -0
  12. package/bin/certify.js +73 -0
  13. package/bin/change-role.js +61 -0
  14. package/bin/config.js +124 -0
  15. package/bin/experiences.js +69 -0
  16. package/bin/find-projects.js +89 -0
  17. package/bin/invitations.js +69 -0
  18. package/bin/lang.js +70 -0
  19. package/bin/login.js +216 -0
  20. package/bin/mcp.js +124 -0
  21. package/bin/mcpb.js +5 -0
  22. package/bin/me.js +69 -0
  23. package/bin/onboarding.js +134 -0
  24. package/bin/portfolios.js +69 -0
  25. package/bin/rate.js +70 -0
  26. package/bin/register.js +337 -0
  27. package/bin/report-html.js +134 -0
  28. package/bin/save.js +77 -0
  29. package/bin/shakers.js +363 -0
  30. package/bin/share.js +72 -0
  31. package/bin/show-project.js +72 -0
  32. package/bin/socials.js +70 -0
  33. package/bin/superadmin.js +351 -0
  34. package/bin/update-check-worker.js +70 -0
  35. package/bin/update.js +111 -0
  36. package/package.json +40 -4
  37. package/src/agent-category.js +13 -0
  38. package/src/agent-evaluation.js +177 -0
  39. package/src/agent-org-chart.js +355 -0
  40. package/src/agent-synthesis.js +132 -0
  41. package/src/agent-usage.js +118 -0
  42. package/src/agents-client.js +165 -0
  43. package/src/ai-fluency-cell.js +72 -0
  44. package/src/ai-profile-client.js +45 -0
  45. package/src/ai-profile-preview.js +154 -0
  46. package/src/ai.js +66 -0
  47. package/src/alma-client.js +123 -0
  48. package/src/alma-flow.js +32 -0
  49. package/src/alma-repo-context.js +47 -0
  50. package/src/alma-shell.js +161 -0
  51. package/src/ansi.js +62 -0
  52. package/src/applications-client.js +79 -0
  53. package/src/applications-flow.js +58 -0
  54. package/src/auth-client.js +144 -0
  55. package/src/auth-device-client.js +118 -0
  56. package/src/auth-method-picker.js +44 -0
  57. package/src/auth-methods.js +35 -0
  58. package/src/auth-session-store.js +215 -0
  59. package/src/authorship-mismatch.js +62 -0
  60. package/src/authorship.js +204 -0
  61. package/src/automations-detector.js +170 -0
  62. package/src/availability-client.js +93 -0
  63. package/src/availability-flow.js +124 -0
  64. package/src/backend-request.js +191 -0
  65. package/src/blind-approval-extract.js +48 -0
  66. package/src/brand-ansi.js +14 -0
  67. package/src/brand-svg.js +9 -0
  68. package/src/browser-tools-detector.js +21 -0
  69. package/src/build-next-level.js +56 -0
  70. package/src/certifications-client.js +89 -0
  71. package/src/certifications-flow.js +75 -0
  72. package/src/certify-dimension-client.js +163 -0
  73. package/src/certify-dimension-flow.js +135 -0
  74. package/src/certify-disclaimer.js +51 -0
  75. package/src/certify-remediation-prompt.js +23 -0
  76. package/src/cli-args.js +83 -0
  77. package/src/command-audit.js +105 -0
  78. package/src/config-dir.js +120 -0
  79. package/src/config.js +877 -0
  80. package/src/consent-flow.js +115 -0
  81. package/src/consent-skip.js +27 -0
  82. package/src/cv-file.js +62 -0
  83. package/src/cv-text.js +92 -0
  84. package/src/decisions-extract.js +43 -0
  85. package/src/detect-repos.js +145 -0
  86. package/src/detected-tools.js +15 -0
  87. package/src/detection-evidence.js +105 -0
  88. package/src/detection-noise.js +132 -0
  89. package/src/detectors.js +221 -0
  90. package/src/device-auth-store.js +100 -0
  91. package/src/device-login.js +120 -0
  92. package/src/email-verification.js +152 -0
  93. package/src/env-paths.js +30 -0
  94. package/src/find-projects-cache.js +89 -0
  95. package/src/find-projects-client.js +148 -0
  96. package/src/find-projects-flow.js +164 -0
  97. package/src/git-activity.js +124 -0
  98. package/src/git-log-prefetch.js +53 -0
  99. package/src/graph-certs.js +58 -0
  100. package/src/graph-generator.js +230 -0
  101. package/src/graph-scan.js +239 -0
  102. package/src/i18n.js +3341 -0
  103. package/src/inter-fonts.js +6 -0
  104. package/src/interactive-select.js +183 -0
  105. package/src/interview-client.js +226 -0
  106. package/src/interview-transcript.js +56 -0
  107. package/src/inventory-client.js +90 -0
  108. package/src/invitations-client.js +66 -0
  109. package/src/invitations-flow.js +50 -0
  110. package/src/lang-client.js +147 -0
  111. package/src/lang-codes.js +14 -0
  112. package/src/lang-flow.js +85 -0
  113. package/src/legal-notice.js +53 -0
  114. package/src/locale.js +72 -0
  115. package/src/markdown-ansi.js +50 -0
  116. package/src/maturity.js +111 -0
  117. package/src/mcp-ai-usage-result-tool.js +111 -0
  118. package/src/mcp-ai-usage-tool.js +401 -0
  119. package/src/mcp-alma-tools.js +95 -0
  120. package/src/mcp-applications-tools.js +44 -0
  121. package/src/mcp-auth-form.js +480 -0
  122. package/src/mcp-availability-tools.js +45 -0
  123. package/src/mcp-certifications-tools.js +44 -0
  124. package/src/mcp-certify-dimension-tools.js +66 -0
  125. package/src/mcp-choice.js +95 -0
  126. package/src/mcp-detector.js +223 -0
  127. package/src/mcp-device-login-tools.js +177 -0
  128. package/src/mcp-find-projects-tools.js +69 -0
  129. package/src/mcp-install.js +275 -0
  130. package/src/mcp-instructions.js +27 -0
  131. package/src/mcp-invitations-tools.js +44 -0
  132. package/src/mcp-lang-tools.js +41 -0
  133. package/src/mcp-onboarding-tools.js +379 -0
  134. package/src/mcp-portfolios-tools.js +50 -0
  135. package/src/mcp-profile-tools.js +48 -0
  136. package/src/mcp-prompts.js +73 -0
  137. package/src/mcp-rate-tools.js +44 -0
  138. package/src/mcp-register-tools.js +441 -0
  139. package/src/mcp-report-tools.js +105 -0
  140. package/src/mcp-role-tools.js +372 -0
  141. package/src/mcp-saved-positions-tools.js +59 -0
  142. package/src/mcp-server.js +290 -0
  143. package/src/mcp-show-project-tools.js +52 -0
  144. package/src/mcp-signup-client.js +118 -0
  145. package/src/mcp-signup-tools.js +1171 -0
  146. package/src/mcp-socials-tools.js +41 -0
  147. package/src/memory-structure-detector.js +108 -0
  148. package/src/model-call-record.js +198 -0
  149. package/src/onboarding-availability.js +26 -0
  150. package/src/onboarding-client.js +374 -0
  151. package/src/onboarding-flow.js +1162 -0
  152. package/src/onboarding-funnel.js +272 -0
  153. package/src/onboarding-livekit-client.js +102 -0
  154. package/src/onboarding-status-client.js +57 -0
  155. package/src/open-file.js +32 -0
  156. package/src/osc-link.js +16 -0
  157. package/src/portfolio-client.js +164 -0
  158. package/src/portfolio-git-info.js +61 -0
  159. package/src/portfolios-client.js +85 -0
  160. package/src/portfolios-flow.js +79 -0
  161. package/src/profile-client.js +133 -0
  162. package/src/profile-flow.js +115 -0
  163. package/src/profile-switch.js +46 -0
  164. package/src/prompt-select.js +66 -0
  165. package/src/rate-client.js +81 -0
  166. package/src/rate-flow.js +137 -0
  167. package/src/register-context.js +145 -0
  168. package/src/render-ai-fluency.js +266 -0
  169. package/src/render-certification.js +177 -0
  170. package/src/render-html.js +140 -0
  171. package/src/render-report-text.js +27 -0
  172. package/src/render-sheet.js +261 -0
  173. package/src/render-terminal.js +109 -0
  174. package/src/render-usage-report-text.js +155 -0
  175. package/src/repo-context.js +215 -0
  176. package/src/report-gating.js +49 -0
  177. package/src/report-store.js +226 -0
  178. package/src/report-theme.js +177 -0
  179. package/src/report-view-client.js +50 -0
  180. package/src/reveal-output.js +29 -0
  181. package/src/roadmap-content.js +453 -0
  182. package/src/roadmap-personalization.js +132 -0
  183. package/src/roadmap-prompt.js +154 -0
  184. package/src/roles-client.js +112 -0
  185. package/src/roles-flow.js +192 -0
  186. package/src/sanitize-network-text.js +36 -0
  187. package/src/saved-positions-client.js +71 -0
  188. package/src/saved-positions-flow.js +50 -0
  189. package/src/scan-exclusions.js +50 -0
  190. package/src/scanner.js +624 -0
  191. package/src/scope-signals.js +148 -0
  192. package/src/secret-prompt.js +72 -0
  193. package/src/session-gate.js +30 -0
  194. package/src/session-normalizers.js +174 -0
  195. package/src/session-refresh.js +51 -0
  196. package/src/session-scan.js +475 -0
  197. package/src/session-signals.js +36 -0
  198. package/src/shakers-wordmark.js +17 -0
  199. package/src/share-card.js +362 -0
  200. package/src/share-certification.js +111 -0
  201. package/src/share-consent-state.js +157 -0
  202. package/src/share-payload.js +300 -0
  203. package/src/share.js +185 -0
  204. package/src/show-project-client.js +58 -0
  205. package/src/show-project-flow.js +119 -0
  206. package/src/signup-client.js +103 -0
  207. package/src/signup-copy.js +450 -0
  208. package/src/signup-language.js +21 -0
  209. package/src/skills-client.js +102 -0
  210. package/src/socials-client.js +91 -0
  211. package/src/socials-flow.js +83 -0
  212. package/src/specificity-extract.js +41 -0
  213. package/src/start-add-agents.js +306 -0
  214. package/src/start-add-portfolio.js +388 -0
  215. package/src/start-add-skills.js +260 -0
  216. package/src/stdin-ask.js +129 -0
  217. package/src/steering-extract.js +43 -0
  218. package/src/strip-internal-comments.js +45 -0
  219. package/src/tech-detector.js +295 -0
  220. package/src/templates/report-sheet.html +572 -0
  221. package/src/terminal-agents.js +207 -0
  222. package/src/terminal-format.js +85 -0
  223. package/src/terminal-progress.js +106 -0
  224. package/src/terminal-sections.js +219 -0
  225. package/src/tier-analysis.js +126 -0
  226. package/src/tier-engine.js +114 -0
  227. package/src/tier-projection.js +117 -0
  228. package/src/transcript-turns.js +49 -0
  229. package/src/translations-client.js +84 -0
  230. package/src/typewriter.js +154 -0
  231. package/src/update-flow.js +223 -0
  232. package/src/update-notifier.js +113 -0
  233. package/src/usage-model-calls.js +119 -0
  234. package/src/usage-oneshot-commands.js +180 -0
  235. package/src/usage-repo-scope.js +162 -0
  236. package/src/usage-run-steps.js +354 -0
  237. package/src/work-streams.js +79 -0
@@ -0,0 +1,124 @@
1
+ 'use strict';
2
+
3
+ const { execFileSync } = require('child_process');
4
+ const path = require('path');
5
+
6
+ const { takePrefetchedGitLog } = require('./git-log-prefetch');
7
+
8
+ const GIT_TIMEOUT_MS = 15000;
9
+ const MAX_FILE_TYPES = 50;
10
+
11
+ // The one history read behind the activity numbers (\x01 = commit marker, \x1f = field sep).
12
+ const GIT_ACTIVITY_LOG_ARGS = ['log', '--no-merges', '--numstat', '--pretty=format:\x01%ae\x1f%at'];
13
+
14
+ function runGit(root, args) {
15
+ try {
16
+ return execFileSync('git', ['-C', root, ...args], {
17
+ encoding: 'utf8',
18
+ timeout: GIT_TIMEOUT_MS,
19
+ stdio: ['ignore', 'pipe', 'ignore'],
20
+ maxBuffer: 64 * 1024 * 1024,
21
+ });
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ function isGitRepo(root) {
28
+ return runGit(root, ['rev-parse', '--is-inside-work-tree']) !== null;
29
+ }
30
+
31
+ function extensionOf(rawPath) {
32
+ let p = rawPath;
33
+ const arrow = p.indexOf('=>');
34
+ if (arrow !== -1) {
35
+ p = p
36
+ .slice(arrow + 2)
37
+ .replace(/[{}]/g, '')
38
+ .trim();
39
+ }
40
+ const base = path.basename(p.trim());
41
+ const ext = path.extname(base).replace(/^\./, '').toLowerCase();
42
+ return ext || 'none';
43
+ }
44
+
45
+ function bumpFileType(filesByType, ext) {
46
+ if (Object.prototype.hasOwnProperty.call(filesByType, ext)) {
47
+ filesByType[ext] += 1;
48
+ return;
49
+ }
50
+ if (Object.keys(filesByType).length >= MAX_FILE_TYPES) return;
51
+ filesByType[ext] = 1;
52
+ }
53
+
54
+ function round2(n) {
55
+ return Math.round(n * 100) / 100;
56
+ }
57
+
58
+ // Deterministic git-activity signals for the project where `shakers ai-usage` runs (hybrid evaluation, Slice 1).
59
+ function collectGitActivity(root, verifiedEmail) {
60
+ if (!isGitRepo(root)) return null;
61
+
62
+ const sep = String.fromCharCode(1);
63
+ const unit = String.fromCharCode(31);
64
+ const prefetchedLog = takePrefetchedGitLog(root, GIT_ACTIVITY_LOG_ARGS);
65
+ const log = prefetchedLog !== undefined ? prefetchedLog : runGit(root, GIT_ACTIVITY_LOG_ARGS);
66
+ if (log === null) return null;
67
+
68
+ const target = String(verifiedEmail || '')
69
+ .trim()
70
+ .toLowerCase();
71
+
72
+ let commitCount = 0;
73
+ let authoredCommitCount = 0;
74
+ let linesAdded = 0;
75
+ let linesDeleted = 0;
76
+ let minTs = null;
77
+ let maxTs = null;
78
+ const filesByType = {};
79
+
80
+ for (const rawLine of log.split('\n')) {
81
+ if (rawLine.startsWith(sep)) {
82
+ commitCount += 1;
83
+ const [email, atRaw] = rawLine.slice(1).split(unit);
84
+ if (target && String(email || '').trim().toLowerCase() === target) {
85
+ authoredCommitCount += 1;
86
+ }
87
+ const ts = Number.parseInt(atRaw, 10);
88
+ if (Number.isFinite(ts)) {
89
+ if (minTs === null || ts < minTs) minTs = ts;
90
+ if (maxTs === null || ts > maxTs) maxTs = ts;
91
+ }
92
+ continue;
93
+ }
94
+ const line = rawLine.trim();
95
+ if (!line) continue;
96
+ const parts = line.split('\t');
97
+ if (parts.length < 3) continue;
98
+ const [addedRaw, deletedRaw] = parts;
99
+ const filePath = parts.slice(2).join('\t');
100
+ if (addedRaw !== '-') linesAdded += Number.parseInt(addedRaw, 10) || 0;
101
+ if (deletedRaw !== '-') linesDeleted += Number.parseInt(deletedRaw, 10) || 0;
102
+ bumpFileType(filesByType, extensionOf(filePath));
103
+ }
104
+
105
+ if (commitCount === 0) return null;
106
+
107
+ const spanDays =
108
+ minTs !== null && maxTs !== null
109
+ ? Math.max(0, Math.floor((maxTs - minTs) / 86400))
110
+ : 0;
111
+ const velocity = round2(commitCount / Math.max(spanDays, 1));
112
+
113
+ return {
114
+ commitCount,
115
+ authoredCommitCount,
116
+ linesAdded,
117
+ linesDeleted,
118
+ filesByType,
119
+ velocity,
120
+ spanDays,
121
+ };
122
+ }
123
+
124
+ module.exports = { collectGitActivity, extensionOf, isGitRepo, GIT_ACTIVITY_LOG_ARGS };
@@ -0,0 +1,53 @@
1
+ 'use strict';
2
+
3
+ const { execFile } = require('child_process');
4
+
5
+ // Runs the aggregation's per-repo git logs concurrently with the collectors' exact args; each result is taken once and expires.
6
+
7
+ const GIT_TIMEOUT_MS = 15000;
8
+ const GIT_MAX_BUFFER = 64 * 1024 * 1024;
9
+ const MAX_CONCURRENT = 8;
10
+ const TTL_MS = 60 * 1000;
11
+
12
+ const prefetched = new Map();
13
+
14
+ const keyOf = (root, args) => JSON.stringify([root, args]);
15
+
16
+ function runAsync(root, args) {
17
+ return new Promise((resolve) => {
18
+ execFile(
19
+ 'git',
20
+ ['-C', root, ...args],
21
+ { encoding: 'utf8', timeout: GIT_TIMEOUT_MS, maxBuffer: GIT_MAX_BUFFER },
22
+ (err, stdout) => resolve(err ? null : stdout),
23
+ );
24
+ });
25
+ }
26
+
27
+ // Runs every (root, args) pair, at most MAX_CONCURRENT at a time. Never throws.
28
+ async function prefetchGitLogs(roots, argSets) {
29
+ const jobs = [];
30
+ for (const root of roots || []) {
31
+ for (const args of argSets) jobs.push({ root, args });
32
+ }
33
+ let next = 0;
34
+ const worker = async () => {
35
+ while (next < jobs.length) {
36
+ const { root, args } = jobs[next++];
37
+ const out = await runAsync(root, args);
38
+ if (out !== null) prefetched.set(keyOf(root, args), { out, at: Date.now() });
39
+ }
40
+ };
41
+ await Promise.all(Array.from({ length: Math.min(MAX_CONCURRENT, jobs.length) }, worker));
42
+ }
43
+
44
+ // The prefetched stdout for exactly this command, once; `undefined` on a miss.
45
+ function takePrefetchedGitLog(root, args) {
46
+ const key = keyOf(root, args);
47
+ const hit = prefetched.get(key);
48
+ if (!hit) return undefined;
49
+ prefetched.delete(key);
50
+ return Date.now() - hit.at <= TTL_MS ? hit.out : undefined;
51
+ }
52
+
53
+ module.exports = { prefetchGitLogs, takePrefetchedGitLog };
@@ -0,0 +1,58 @@
1
+ 'use strict';
2
+
3
+ // graph-certs.js — report-store → `map` cert-drawer payload adapter.
4
+
5
+ const { getCatalog } = require('./i18n');
6
+ const { scoreBand, skillLevelForScore } = require('./render-certification');
7
+ const { sanitizeRenderText } = require('./sanitize-network-text');
8
+
9
+ function levelInfoFromCombinedLevel(combinedLevel) {
10
+ const key = typeof combinedLevel === 'string' ? combinedLevel.trim().toLowerCase() : '';
11
+ if (key === 'expert') return { key: 'expert', band: 'high' };
12
+ if (key === 'senior') return { key: 'senior', band: 'mid' };
13
+ if (key === 'middle') return { key: 'middle', band: 'low' };
14
+ return null;
15
+ }
16
+
17
+ // buildCertsPayload(project, lang) -> cert drawer payload | null `project` is a report-store project entry (state.projects[absRoot]).
18
+ function buildCertsPayload(project, lang) {
19
+ const t = getCatalog(lang);
20
+
21
+ const certObj = (project && project.certifications) || {};
22
+ const skills = Object.values(certObj)
23
+ .map((e) => e && e.item)
24
+ .filter(Boolean)
25
+ .map((item) => {
26
+ const res = item.result || {};
27
+ // `skillName` and `technology` both come off the wire (the service's certifiable list / verdict), and `technology` was one of the two fields issue 055 was opened for.
28
+ const base = sanitizeRenderText(item.skillName) || (item.skillId != null ? String(item.skillId) : 'Skill');
29
+ const name = item.technology ? `${base} · ${sanitizeRenderText(item.technology)}` : base;
30
+ // ADR-016: named level (Middle/Senior/Expert) instead of the numeric grade.
31
+ const skillReport = (t.certify && t.certify.report) || {};
32
+ const skillLevelNames = skillReport.skillLevels || {};
33
+ const { key: levelKey, band } = levelInfoFromCombinedLevel(res.combinedLevel) || skillLevelForScore(res.score);
34
+ return {
35
+ name,
36
+ band, // retained for the badge colour (unchanged palette)
37
+ levelKey,
38
+ levelName: levelKey ? (skillLevelNames[levelKey] || levelKey) : (skillLevelNames.na || '—'),
39
+ // Model-written text off the wire; sanitised (issue 055 — left raw by 052).
40
+ rationale: typeof res.rationale === 'string' ? sanitizeRenderText(res.rationale) : '',
41
+ improvements: Array.isArray(res.improvements)
42
+ ? res.improvements.map((i) => (typeof i === 'string' ? sanitizeRenderText(i) : i))
43
+ : [],
44
+ };
45
+ });
46
+
47
+ if (!skills.length) return null;
48
+
49
+ const labels = {
50
+ skillsTitle: lang === 'es' ? 'Skills evaluadas' : 'Skills evaluated',
51
+ empty: lang === 'es' ? 'Aún no hay certificaciones para este proyecto.' : 'No certifications for this project yet.',
52
+ improvements: lang === 'es' ? 'Mejoras sugeridas' : 'Suggested improvements',
53
+ };
54
+
55
+ return { labels, skills };
56
+ }
57
+
58
+ module.exports = { buildCertsPayload, levelInfoFromCombinedLevel };
@@ -0,0 +1,230 @@
1
+ 'use strict';
2
+
3
+ // graph-generator.js — HYBRID generation of the foglamp graph contract.
4
+
5
+ const KINDS = new Set(['entry', 'cron', 'agent', 'model', 'tool', 'service', 'store', 'external']);
6
+ const LLM_INTRODUCIBLE = new Set(['entry', 'cron', 'service', 'store']);
7
+ const EDGE_KINDS = new Set(['calls', 'reads', 'writes', 'triggers']);
8
+
9
+ const CAPS = {
10
+ nodes: 60,
11
+ edges: 120,
12
+ topModels: 3,
13
+ topTools: 10,
14
+ topIntegrations: 10,
15
+ label: 28,
16
+ sub: 40,
17
+ edgeLabel: 24,
18
+ detail: 200,
19
+ };
20
+
21
+ function clampStr(s, max) {
22
+ if (s == null) return undefined;
23
+ s = String(s);
24
+ return s.length > max ? s.slice(0, max - 1) + '…' : s;
25
+ }
26
+
27
+ // Secret-ish patterns; the structural summary should never carry these, but we
28
+ // belt-and-braces scrub every string that leaves the machine toward the LLM.
29
+ const SECRET_RE = [
30
+ /(?:sk|pk|ghp|gho|xox[baprs]|AKIA|ya29)[-_][A-Za-z0-9]{6,}/g, // known key prefixes
31
+ /[A-Za-z0-9_-]{32,}\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}/g, // JWT-ish
32
+ // long base64 blobs — excludes '/' so file PATHS (many slashes) are NOT
33
+ // mistaken for secrets; real token blobs are contiguous/URL-safe.
34
+ /\b[A-Za-z0-9+]{40,}={0,2}\b/g,
35
+ /(?:password|secret|token|api[_-]?key|bearer)\s*[:=]\s*\S+/gi,
36
+ ];
37
+
38
+ function scrubString(s) {
39
+ if (s == null) return s;
40
+ let out = String(s);
41
+ for (const re of SECRET_RE) out = out.replace(re, '[redacted]');
42
+ return out;
43
+ }
44
+
45
+ // DETERMINISTIC assembly.
46
+ function assembleDeterministic(scan) {
47
+ const nodes = [];
48
+ const edges = [];
49
+ const seen = new Set();
50
+ const push = (n) => {
51
+ if (!n.id || seen.has(n.id) || !KINDS.has(n.kind)) return;
52
+ seen.add(n.id);
53
+ nodes.push({
54
+ id: n.id,
55
+ label: clampStr(n.label || n.id, CAPS.label),
56
+ kind: n.kind,
57
+ ...(n.sub ? { sub: clampStr(n.sub, CAPS.sub) } : {}),
58
+ ...(n.domain ? { domain: n.domain } : {}),
59
+ ...(n.group ? { group: clampStr(n.group, CAPS.label) } : {}),
60
+ ...(n.detail ? { detail: clampStr(n.detail, CAPS.detail) } : {}),
61
+ ...(n.sourceRef ? { sourceRef: n.sourceRef } : {}),
62
+ });
63
+ };
64
+
65
+ (scan.models || []).forEach((m) => push({ ...m, kind: 'model' }));
66
+ (scan.tools || []).forEach((t) => push({ ...t, kind: 'tool' }));
67
+ (scan.integrations || []).forEach((i) => push({ ...i, kind: 'external' }));
68
+ (scan.agents || []).forEach((a) => {
69
+ push({ id: a.id, label: a.label, kind: 'agent', sub: a.sub, group: a.group, detail: a.detail, sourceRef: a.sourceRef });
70
+ // deterministic call edge agent -> model|tool (we know what each agent calls)
71
+ const target = a.model || a.toolId || a.tool;
72
+ if (target && seen.has(target)) edges.push({ from: a.id, to: target, kind: 'calls' });
73
+ });
74
+ // orchestrator -> subagent hierarchy (parent triggers child), deterministic
75
+ (scan.agents || []).forEach((a) => {
76
+ if (a.parent && seen.has(a.parent) && seen.has(a.id) && a.parent !== a.id) {
77
+ edges.push({ from: a.parent, to: a.id, kind: 'triggers' });
78
+ }
79
+ });
80
+
81
+ const stats = {
82
+ agents: (scan.agents || []).length,
83
+ models: (scan.models || []).length,
84
+ tools: (scan.tools || []).length,
85
+ integrations: (scan.integrations || []).length,
86
+ };
87
+ const topModels = (scan.models || []).slice(0, CAPS.topModels).map((m) => ({ id: m.id, label: m.label, ...(m.domain ? { domain: m.domain } : {}) }));
88
+ const topTools = (scan.tools || []).slice(0, CAPS.topTools).map((t) => ({ id: t.id, label: t.label, ...(t.domain ? { domain: t.domain } : {}) }));
89
+ const topIntegrations = (scan.integrations || []).slice(0, CAPS.topIntegrations).map((i) => ({ id: i.id, label: i.label, ...(i.domain ? { domain: i.domain } : {}) }));
90
+
91
+ return { nodes, edges, stats, topModels, topTools, topIntegrations };
92
+ }
93
+
94
+ // Build the CONTENT-FREE structural summary handed to the LLM.
95
+ function buildScrubbedSummary(scan, base) {
96
+ return {
97
+ project: { name: scrubString(scan.project.name), slug: scan.project.slug },
98
+ nodes: base.nodes.map((n) => ({
99
+ id: n.id,
100
+ label: scrubString(n.label),
101
+ kind: n.kind,
102
+ ...(n.sub ? { sub: scrubString(n.sub) } : {}),
103
+ ...(n.group ? { group: n.group } : {}),
104
+ ...(n.sourceRef ? { sourceRef: scrubString(n.sourceRef) } : {}),
105
+ })),
106
+ detectedEdges: base.edges.map((e) => ({ from: e.from, to: e.to, kind: e.kind })),
107
+ hints: {
108
+ entrypoints: (scan.entrypoints || []).map((p) => scrubString(p)),
109
+ stores: (scan.stores || []).map((s) => scrubString(s)),
110
+ technologies: scan.technologies || [],
111
+ },
112
+ };
113
+ }
114
+
115
+ // MERGE + VALIDATE the LLM enrichment onto the deterministic base.
116
+ function mergeEnrichment(base, enrichment) {
117
+ const byId = new Map(base.nodes.map((n) => [n.id, n]));
118
+ const nodes = base.nodes.slice();
119
+
120
+ for (const raw of (enrichment && enrichment.nodes) || []) {
121
+ if (!raw || !raw.id) continue;
122
+ const existing = byId.get(raw.id);
123
+ if (existing) {
124
+ // may only FILL missing group/detail/sub — never override kind
125
+ if (!existing.group && raw.group) existing.group = clampStr(raw.group, CAPS.label);
126
+ if (!existing.detail && raw.detail) existing.detail = clampStr(raw.detail, CAPS.detail);
127
+ if (!existing.sub && raw.sub) existing.sub = clampStr(raw.sub, CAPS.sub);
128
+ continue;
129
+ }
130
+ // new node: only entry/cron/service/store are LLM-introducible
131
+ if (!LLM_INTRODUCIBLE.has(raw.kind)) continue;
132
+ const n = {
133
+ id: raw.id,
134
+ label: clampStr(raw.label || raw.id, CAPS.label),
135
+ kind: raw.kind,
136
+ ...(raw.sub ? { sub: clampStr(raw.sub, CAPS.sub) } : {}),
137
+ ...(raw.group ? { group: clampStr(raw.group, CAPS.label) } : {}),
138
+ ...(raw.detail ? { detail: clampStr(raw.detail, CAPS.detail) } : {}),
139
+ ...(raw.sourceRef ? { sourceRef: raw.sourceRef } : {}),
140
+ };
141
+ byId.set(n.id, n);
142
+ nodes.push(n);
143
+ if (nodes.length >= CAPS.nodes) break;
144
+ }
145
+
146
+ // edges: deterministic first, then valid LLM edges; dedupe; cap
147
+ const edges = [];
148
+ const edgeSeen = new Set();
149
+ const addEdge = (e) => {
150
+ if (!e || !byId.has(e.from) || !byId.has(e.to)) return;
151
+ if (e.from === e.to) return;
152
+ const kind = EDGE_KINDS.has(e.kind) ? e.kind : 'calls';
153
+ const key = e.from + '>' + e.to + '>' + kind;
154
+ if (edgeSeen.has(key)) return;
155
+ edgeSeen.add(key);
156
+ edges.push({ from: e.from, to: e.to, kind, ...(e.label ? { label: clampStr(e.label, CAPS.edgeLabel) } : {}) });
157
+ };
158
+ base.edges.forEach(addEdge);
159
+ for (const e of (enrichment && enrichment.edges) || []) {
160
+ if (edges.length >= CAPS.edges) break;
161
+ addEdge(e);
162
+ }
163
+
164
+ return { nodes: nodes.slice(0, CAPS.nodes), edges: edges.slice(0, CAPS.edges) };
165
+ }
166
+
167
+ async function generateGraph({ scan, llm = null, onTrace = null } = {}) {
168
+ if (!scan || !scan.project) throw new Error('graph-generator: scan.project required');
169
+ const base = assembleDeterministic(scan);
170
+
171
+ let enrichment = { nodes: [], edges: [] };
172
+ if (llm && typeof llm.inferGraph === 'function') {
173
+ const summary = buildScrubbedSummary(scan, base);
174
+ const started = Date.now();
175
+ try {
176
+ const res = await llm.inferGraph(summary);
177
+ enrichment = res && typeof res === 'object' ? res : enrichment;
178
+ emitTrace(onTrace, {
179
+ event: 'graph.infer',
180
+ ok: true,
181
+ model: (res && res.__model) || (llm.model) || 'unknown',
182
+ inputTokens: res && res.__inputTokens,
183
+ outputTokens: res && res.__outputTokens,
184
+ costUsd: res && res.__costUsd,
185
+ latencyMs: Date.now() - started,
186
+ contentFree: true,
187
+ });
188
+ } catch (err) {
189
+ // enrichment is best-effort; a failed LLM pass yields the deterministic
190
+ // graph, never a crash and never a fabricated flow.
191
+ emitTrace(onTrace, { event: 'graph.infer', ok: false, error: err && err.name, latencyMs: Date.now() - started, contentFree: true });
192
+ }
193
+ }
194
+
195
+ const graph = mergeEnrichment(base, enrichment);
196
+ return {
197
+ version: 1,
198
+ project: {
199
+ name: clampStr(scan.project.name, 60),
200
+ slug: scan.project.slug,
201
+ ...(scan.project.tagline ? { tagline: clampStr(scan.project.tagline, 120) } : {}),
202
+ ...(scan.project.iconDomain ? { iconDomain: scan.project.iconDomain } : {}),
203
+ date: scan.project.date || new Date().toISOString().slice(0, 10),
204
+ },
205
+ stats: base.stats,
206
+ topModels: base.topModels,
207
+ topTools: base.topTools,
208
+ topIntegrations: base.topIntegrations,
209
+ graph,
210
+ };
211
+ }
212
+
213
+ function emitTrace(onTrace, evt) {
214
+ if (typeof onTrace === 'function') {
215
+ try { onTrace(evt); } catch { /* instrumentation must never break generation */ }
216
+ }
217
+ }
218
+
219
+ module.exports = {
220
+ generateGraph,
221
+ assembleDeterministic,
222
+ buildScrubbedSummary,
223
+ mergeEnrichment,
224
+ scrubString,
225
+ clampStr,
226
+ CAPS,
227
+ KINDS,
228
+ EDGE_KINDS,
229
+ LLM_INTRODUCIBLE,
230
+ };