shakers-cli-beta 0.8.0-beta.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 (218) hide show
  1. package/LICENSE +59 -0
  2. package/README.md +1175 -0
  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 +81 -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 +120 -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 +283 -0
  20. package/bin/mcp.js +98 -0
  21. package/bin/me.js +69 -0
  22. package/bin/onboarding.js +134 -0
  23. package/bin/portfolios.js +69 -0
  24. package/bin/rate.js +70 -0
  25. package/bin/register.js +325 -0
  26. package/bin/report-html.js +134 -0
  27. package/bin/save.js +77 -0
  28. package/bin/shakers.js +344 -0
  29. package/bin/share.js +72 -0
  30. package/bin/show-project.js +72 -0
  31. package/bin/socials.js +70 -0
  32. package/bin/superadmin.js +351 -0
  33. package/package.json +40 -0
  34. package/src/agent-category.js +13 -0
  35. package/src/agent-evaluation.js +177 -0
  36. package/src/agent-org-chart.js +355 -0
  37. package/src/agent-synthesis.js +132 -0
  38. package/src/agent-usage.js +118 -0
  39. package/src/agents-client.js +165 -0
  40. package/src/ai-fluency-cell.js +72 -0
  41. package/src/ai-profile-client.js +45 -0
  42. package/src/ai-profile-preview.js +154 -0
  43. package/src/ai.js +66 -0
  44. package/src/alma-client.js +123 -0
  45. package/src/alma-flow.js +32 -0
  46. package/src/alma-repo-context.js +47 -0
  47. package/src/alma-shell.js +161 -0
  48. package/src/ansi.js +62 -0
  49. package/src/applications-client.js +79 -0
  50. package/src/applications-flow.js +58 -0
  51. package/src/auth-client.js +144 -0
  52. package/src/auth-device-client.js +118 -0
  53. package/src/auth-method-picker.js +44 -0
  54. package/src/auth-methods.js +35 -0
  55. package/src/auth-session-store.js +214 -0
  56. package/src/authorship.js +203 -0
  57. package/src/automations-detector.js +170 -0
  58. package/src/availability-client.js +92 -0
  59. package/src/availability-flow.js +105 -0
  60. package/src/backend-request.js +170 -0
  61. package/src/blind-approval-extract.js +48 -0
  62. package/src/brand-ansi.js +14 -0
  63. package/src/brand-svg.js +9 -0
  64. package/src/browser-tools-detector.js +21 -0
  65. package/src/build-next-level.js +56 -0
  66. package/src/certifications-client.js +89 -0
  67. package/src/certifications-flow.js +75 -0
  68. package/src/certify-dimension-client.js +217 -0
  69. package/src/certify-dimension-flow.js +193 -0
  70. package/src/certify-disclaimer.js +51 -0
  71. package/src/certify-remediation-prompt.js +23 -0
  72. package/src/cli-args.js +83 -0
  73. package/src/config-dir.js +120 -0
  74. package/src/config.js +778 -0
  75. package/src/consent-flow.js +115 -0
  76. package/src/consent-skip.js +27 -0
  77. package/src/decisions-extract.js +43 -0
  78. package/src/detect-repos.js +145 -0
  79. package/src/detected-tools.js +15 -0
  80. package/src/detection-evidence.js +105 -0
  81. package/src/detection-noise.js +132 -0
  82. package/src/detectors.js +221 -0
  83. package/src/device-auth-store.js +100 -0
  84. package/src/device-login.js +118 -0
  85. package/src/email-verification.js +152 -0
  86. package/src/env-paths.js +16 -0
  87. package/src/find-projects-cache.js +89 -0
  88. package/src/find-projects-client.js +138 -0
  89. package/src/find-projects-flow.js +164 -0
  90. package/src/git-activity.js +123 -0
  91. package/src/graph-certs.js +58 -0
  92. package/src/graph-generator.js +230 -0
  93. package/src/graph-scan.js +239 -0
  94. package/src/i18n.js +3264 -0
  95. package/src/inter-fonts.js +6 -0
  96. package/src/interactive-select.js +183 -0
  97. package/src/interview-client.js +226 -0
  98. package/src/interview-transcript.js +56 -0
  99. package/src/inventory-client.js +90 -0
  100. package/src/invitations-client.js +66 -0
  101. package/src/invitations-flow.js +50 -0
  102. package/src/lang-client.js +147 -0
  103. package/src/lang-codes.js +14 -0
  104. package/src/lang-flow.js +85 -0
  105. package/src/legal-notice.js +53 -0
  106. package/src/locale.js +51 -0
  107. package/src/markdown-ansi.js +50 -0
  108. package/src/maturity.js +111 -0
  109. package/src/mcp-ai-usage-result-tool.js +109 -0
  110. package/src/mcp-ai-usage-tool.js +333 -0
  111. package/src/mcp-alma-tools.js +91 -0
  112. package/src/mcp-applications-tools.js +42 -0
  113. package/src/mcp-ask-tools.js +50 -0
  114. package/src/mcp-auth-form.js +282 -0
  115. package/src/mcp-availability-tools.js +43 -0
  116. package/src/mcp-certifications-tools.js +42 -0
  117. package/src/mcp-certify-dimension-tools.js +58 -0
  118. package/src/mcp-detector.js +223 -0
  119. package/src/mcp-device-login-tools.js +173 -0
  120. package/src/mcp-find-projects-tools.js +67 -0
  121. package/src/mcp-install.js +245 -0
  122. package/src/mcp-invitations-tools.js +42 -0
  123. package/src/mcp-lang-tools.js +39 -0
  124. package/src/mcp-onboarding-tools.js +388 -0
  125. package/src/mcp-portfolios-tools.js +46 -0
  126. package/src/mcp-profile-tools.js +46 -0
  127. package/src/mcp-rate-tools.js +42 -0
  128. package/src/mcp-register-tools.js +517 -0
  129. package/src/mcp-report-tools.js +101 -0
  130. package/src/mcp-role-tools.js +112 -0
  131. package/src/mcp-saved-positions-tools.js +55 -0
  132. package/src/mcp-server.js +170 -0
  133. package/src/mcp-show-project-tools.js +50 -0
  134. package/src/mcp-socials-tools.js +39 -0
  135. package/src/memory-structure-detector.js +108 -0
  136. package/src/model-call-record.js +198 -0
  137. package/src/onboarding-availability.js +26 -0
  138. package/src/onboarding-client.js +364 -0
  139. package/src/onboarding-flow.js +1078 -0
  140. package/src/onboarding-livekit-client.js +102 -0
  141. package/src/onboarding-status-client.js +42 -0
  142. package/src/open-file.js +32 -0
  143. package/src/osc-link.js +16 -0
  144. package/src/portfolio-client.js +164 -0
  145. package/src/portfolio-git-info.js +61 -0
  146. package/src/portfolios-client.js +85 -0
  147. package/src/portfolios-flow.js +79 -0
  148. package/src/profile-client.js +125 -0
  149. package/src/profile-flow.js +115 -0
  150. package/src/profile-switch.js +46 -0
  151. package/src/prompt-select.js +29 -0
  152. package/src/rate-client.js +82 -0
  153. package/src/rate-flow.js +137 -0
  154. package/src/render-ai-fluency.js +266 -0
  155. package/src/render-certification.js +177 -0
  156. package/src/render-html.js +140 -0
  157. package/src/render-report-text.js +27 -0
  158. package/src/render-sheet.js +261 -0
  159. package/src/render-terminal.js +109 -0
  160. package/src/render-usage-report-text.js +155 -0
  161. package/src/repo-context.js +215 -0
  162. package/src/report-gating.js +49 -0
  163. package/src/report-store.js +226 -0
  164. package/src/report-theme.js +177 -0
  165. package/src/report-view-client.js +50 -0
  166. package/src/reveal-output.js +29 -0
  167. package/src/roadmap-content.js +453 -0
  168. package/src/roadmap-personalization.js +132 -0
  169. package/src/roadmap-prompt.js +154 -0
  170. package/src/roles-client.js +114 -0
  171. package/src/roles-flow.js +192 -0
  172. package/src/sanitize-network-text.js +36 -0
  173. package/src/saved-positions-client.js +71 -0
  174. package/src/saved-positions-flow.js +50 -0
  175. package/src/scan-exclusions.js +50 -0
  176. package/src/scanner.js +612 -0
  177. package/src/scope-signals.js +148 -0
  178. package/src/secret-prompt.js +72 -0
  179. package/src/session-gate.js +30 -0
  180. package/src/session-refresh.js +51 -0
  181. package/src/session-scan.js +336 -0
  182. package/src/session-signals.js +36 -0
  183. package/src/shakers-wordmark.js +17 -0
  184. package/src/share-card.js +362 -0
  185. package/src/share-certification.js +111 -0
  186. package/src/share-consent-state.js +157 -0
  187. package/src/share-payload.js +300 -0
  188. package/src/share.js +184 -0
  189. package/src/show-project-client.js +58 -0
  190. package/src/show-project-flow.js +119 -0
  191. package/src/signup-client.js +91 -0
  192. package/src/skills-client.js +102 -0
  193. package/src/socials-client.js +91 -0
  194. package/src/socials-flow.js +83 -0
  195. package/src/specificity-extract.js +41 -0
  196. package/src/start-add-agents.js +306 -0
  197. package/src/start-add-portfolio.js +388 -0
  198. package/src/start-add-skills.js +260 -0
  199. package/src/stdin-ask.js +129 -0
  200. package/src/steering-extract.js +43 -0
  201. package/src/strip-internal-comments.js +45 -0
  202. package/src/tech-detector.js +295 -0
  203. package/src/templates/report-sheet.html +572 -0
  204. package/src/terminal-agents.js +199 -0
  205. package/src/terminal-format.js +85 -0
  206. package/src/terminal-progress.js +106 -0
  207. package/src/terminal-sections.js +219 -0
  208. package/src/tier-analysis.js +126 -0
  209. package/src/tier-engine.js +114 -0
  210. package/src/tier-projection.js +117 -0
  211. package/src/transcript-turns.js +49 -0
  212. package/src/translations-client.js +84 -0
  213. package/src/typewriter.js +154 -0
  214. package/src/usage-model-calls.js +119 -0
  215. package/src/usage-oneshot-commands.js +180 -0
  216. package/src/usage-repo-scope.js +162 -0
  217. package/src/usage-run-steps.js +338 -0
  218. package/src/work-streams.js +74 -0
@@ -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
+ };
@@ -0,0 +1,239 @@
1
+ 'use strict';
2
+
3
+ // graph-scan.js — the DETECTOR → scan adapter for the LOCAL report (`map`).
4
+
5
+ const path = require('path');
6
+ const { scan } = require('./scanner');
7
+ const { classify } = require('./maturity');
8
+ const { setupLevelForTier } = require('./tier-engine');
9
+ const { sanitizeRenderText } = require('./sanitize-network-text');
10
+ // The ONE agent-shaping layer, shared with the terminal report (issue 089).
11
+ const { buildAgentCardTree } = require('./render-html');
12
+
13
+ function slug(s) {
14
+ return String(s || '')
15
+ .toLowerCase()
16
+ .replace(/[^a-z0-9]+/g, '-')
17
+ .replace(/^-+|-+$/g, '')
18
+ .slice(0, 48) || 'x';
19
+ }
20
+
21
+ // Bare Claude aliases → the current EXACT model id (Claude 5 family + Opus 4.8 + Haiku 4.5).
22
+ const CLAUDE_ALIASES = {
23
+ opus: 'claude-opus-4-8',
24
+ sonnet: 'claude-sonnet-5',
25
+ haiku: 'claude-haiku-4-5-20251001',
26
+ };
27
+
28
+ // Normalize a raw agent `model` string to a graph model node keyed by the EXACT model id (one node per distinct exact id — never collapsed to a vendor family).
29
+ function modelNode(raw) {
30
+ const original = String(raw || '').trim();
31
+ const m = original.toLowerCase();
32
+ if (!m) return null;
33
+ // `inherit` (or the session default): be honest — no specific id invented.
34
+ if (m === 'inherit' || m === 'default') return { id: 'inherit', label: 'inherit', domain: null };
35
+ // Bare Claude alias → exact id.
36
+ if (CLAUDE_ALIASES[m]) return { id: CLAUDE_ALIASES[m], label: CLAUDE_ALIASES[m], domain: 'claude.ai' };
37
+ // Already an exact/qualified id — keep it verbatim, key by it.
38
+ if (/claude/.test(m)) return { id: m, label: original, domain: 'claude.ai' };
39
+ if (/gemini/.test(m)) return { id: m, label: original, domain: 'gemini.google.com' };
40
+ if (/gpt|o1|o3|openai/.test(m)) return { id: m, label: original, domain: 'openai.com' };
41
+ // unknown but present: keep it as a node so the agent has a call target
42
+ return { id: slug(m), label: original, domain: null };
43
+ }
44
+
45
+ // Light, deterministic store hints from technologies (fed to the LLM pass only
46
+ // as hints; never emitted as authoritative store nodes here).
47
+ function storeHints(technologies) {
48
+ const t = (technologies || []).map((x) => String(x).toLowerCase());
49
+ const hints = [];
50
+ if (t.some((x) => /prisma|postgres|pg\b/.test(x))) hints.push('postgresql');
51
+ if (t.some((x) => /mongo/.test(x))) hints.push('mongodb');
52
+ if (t.some((x) => /redis/.test(x))) hints.push('redis');
53
+ if (t.some((x) => /s3|aws-sdk/.test(x))) hints.push('aws-s3');
54
+ return hints;
55
+ }
56
+
57
+ function buildGraphScan(root, { scanFn = scan, classifyFn = classify } = {}) {
58
+ const abs = path.resolve(root || process.cwd());
59
+ const report = scanFn({ root: abs });
60
+ const maturity = classifyFn(report);
61
+
62
+ const agentsRaw = Array.isArray(report.agents) ? report.agents : [];
63
+ const technologies = Array.isArray(report.technologies) ? report.technologies : [];
64
+
65
+ // agents (+ their models), deduped by slug(name)
66
+ const seenAgent = new Set();
67
+ const agents = [];
68
+ const modelsById = new Map();
69
+ const nameToId = new Map();
70
+ for (const a of agentsRaw) {
71
+ const id = slug(a.name);
72
+ if (!a.name || seenAgent.has(id)) continue;
73
+ seenAgent.add(id);
74
+ nameToId.set(a.name, id);
75
+ const mn = modelNode(a.model);
76
+ if (mn && !modelsById.has(mn.id)) modelsById.set(mn.id, mn);
77
+ agents.push({
78
+ id,
79
+ label: a.name,
80
+ _parentName: a.parent || null,
81
+ ...(mn ? { model: mn.id } : {}),
82
+ ...(a.aiProduct ? { group: a.aiProduct } : {}),
83
+ // Sub-label = the EXACT resolved model id (not the raw alias like "opus").
84
+ ...(mn ? { sub: mn.label } : a.aiProduct ? { sub: a.aiProduct } : {}),
85
+ });
86
+ }
87
+ // second pass: resolve parent NAME -> parent agent id (orchestrator hierarchy)
88
+ for (const ag of agents) {
89
+ const pid = ag._parentName ? nameToId.get(ag._parentName) : null;
90
+ if (pid && pid !== ag.id) ag.parent = pid;
91
+ delete ag._parentName;
92
+ }
93
+
94
+ const scanOut = {
95
+ project: {
96
+ name: path.basename(abs) || 'project',
97
+ slug: slug(path.basename(abs)),
98
+ date: new Date().toISOString().slice(0, 10),
99
+ },
100
+ agents,
101
+ models: Array.from(modelsById.values()),
102
+ tools: [],
103
+ integrations: [],
104
+ technologies,
105
+ // hints for the LLM enrichment pass (structural only)
106
+ entrypoints: [],
107
+ stores: storeHints(technologies),
108
+ };
109
+
110
+ return {
111
+ scan: scanOut,
112
+ footprint: buildFootprintDrawer(report, maturity),
113
+ report,
114
+ maturity,
115
+ };
116
+ }
117
+
118
+ // Setup-level ladder for the drawer (ADR-016): the 3 Setup Levels + "Not certified", replacing the retired 0-4 maturity ladder.
119
+ const SETUP_LADDER = [
120
+ { rank: 0, key: 'none', label: 'Sin certificar' },
121
+ { rank: 1, key: 'S1', label: 'S1 · Asistido' },
122
+ { rank: 2, key: 'S2', label: 'S2 · Extendido' },
123
+ { rank: 3, key: 'S3', label: 'S3 · Orquestado' },
124
+ ];
125
+
126
+ // #3 footprint drawer payload from the live scan (defensive). Detected agents =
127
+ // a FLAT list with a depth number, not a tree object (issue 089).
128
+ function drawerAgents(report, t) {
129
+ let tree;
130
+ try {
131
+ tree = buildAgentCardTree(report, t);
132
+ } catch {
133
+ return [];
134
+ }
135
+ // WHICH agents an evaluation actually covered.
136
+ const evaluatedNames = new Set(
137
+ report && report.agentEvaluation && Array.isArray(report.agentEvaluation.evaluations)
138
+ ? report.agentEvaluation.evaluations
139
+ .filter((e) => e && typeof e.name === 'string')
140
+ .map((e) => sanitizeRenderText(e.name))
141
+ : [],
142
+ );
143
+ const rows = [];
144
+ const walk = (card, depth, seen) => {
145
+ if (seen.has(card.name)) return; // malformed parent chain: never loop
146
+ const nextSeen = new Set(seen);
147
+ nextSeen.add(card.name);
148
+ const children = tree.childrenByParent.get(card.name) || [];
149
+ const cls = card.classification && card.classification.catalogId ? card.classification : null;
150
+ rows.push({
151
+ name: sanitizeRenderText(card.name),
152
+ model: card.model ? sanitizeRenderText(card.model) : (card.aiProduct ? sanitizeRenderText(card.aiProduct) : null),
153
+ // `whatItDoes` is already the merged description (synthesis > frontmatter >
154
+ // humanised-name fallback) and already sanitised by the shaping layer.
155
+ whatItDoes: card.whatItDoes || null,
156
+ // The category is a REQUIREMENT of the design, and it is the agent → catalog-seed association, not the Talent's tier.
157
+ category: cls ? cls.category : null,
158
+ // THREE STATES, NOT TWO (issue 106).
159
+ evaluated: evaluatedNames.has(card.name),
160
+ // COPIED from the card, never recomputed (2026-08-03).
161
+ evaluationState: card.evaluationState || (evaluatedNames.has(card.name) ? (cls ? 'classified' : 'unclassified') : 'not-evaluated'),
162
+ role: cls && cls.role ? sanitizeRenderText(cls.role) : null,
163
+ depth,
164
+ hasChildren: children.length > 0,
165
+ });
166
+ for (const child of children) walk(child, depth + 1, nextSeen);
167
+ };
168
+ for (const root of tree.roots) walk(root, 0, new Set());
169
+ return rows;
170
+ }
171
+
172
+ function buildFootprintDrawer(report, maturity, t) {
173
+ const score = maturity && typeof maturity.score === 'number' ? maturity.score : 0;
174
+ const tierKey = (maturity && (maturity.tierKey || maturity.key)) || '';
175
+ const tierNum = maturity && typeof maturity.tier === 'number'
176
+ ? maturity.tier
177
+ : (/^T([0-7])$/.test(tierKey) ? Number(tierKey.slice(1)) : null);
178
+ const tierName = (maturity && maturity.tierName) || '—';
179
+ // Setup Level (ADR-016) replaces the 0-4 band as the drawer's headline rollup.
180
+ const setup = (maturity && maturity.setupLevel)
181
+ || (tierNum != null ? setupLevelForTier(tierNum) : { key: 'none', code: null, rank: 0, emoji: '○' });
182
+ const setupLabel = (SETUP_LADDER.find((s) => s.key === setup.key) || SETUP_LADDER[0]).label;
183
+ // Issue 055: this drawer payload feeds the shareable sheet AND the graph report, and both only `esc()`.
184
+ const technologies = (Array.isArray(report.technologies) ? report.technologies : [])
185
+ .map((x) => (typeof x === 'string' ? sanitizeRenderText(x) : x));
186
+ // Installed AI dev tools (assistants).
187
+ const toolList = Array.isArray(report.tools)
188
+ ? report.tools
189
+ : report.tools && typeof report.tools === 'object'
190
+ ? Object.entries(report.tools).map(([id, v]) => ({ id, ...(v && typeof v === 'object' ? v : {}) }))
191
+ : [];
192
+ const tools = [];
193
+ for (const t of toolList) {
194
+ if (!t || !(t.detected || t.installed || t.present)) continue;
195
+ const nm = (typeof t.name === 'string' && t.name) || prettyTool(t.id || t.key || '');
196
+ if (nm) tools.push(sanitizeRenderText(nm));
197
+ }
198
+
199
+ return {
200
+ score,
201
+ // `setup` is the ADR-016 rollup shown in the drawer hero; `tier` keeps the tierKey · tierName chip.
202
+ setup: { key: setup.key, code: setup.code, rank: setup.rank, emoji: setup.emoji, label: setupLabel },
203
+ tier: { key: tierKey || 'none', name: tierName, label: tierKey ? `${tierKey} · ${tierName}` : tierName },
204
+ ladder: SETUP_LADDER,
205
+ // "Lectura": a deterministic, human-readable one-liner from the scan (no LLM,
206
+ // no fabricated content). Previously hardcoded '' → an empty box in the drawer.
207
+ summary: buildReading({ setupLabel, tierKey, tierName, score, nTools: tools.length, nTech: technologies.length }),
208
+ tools,
209
+ technologies,
210
+ // Issue 089: ALL detected agents, always.
211
+ agents: drawerAgents(report, t),
212
+ // Issue 110: the SERVICES behind the MCP servers, shaped ONCE here for both consumers of this drawer (the shareable sheet and the graph report), the same way `agents` is.
213
+ mcp: {
214
+ services: (report.mcp && Array.isArray(report.mcp.services) ? report.mcp.services : [])
215
+ .map((x) => ({ label: sanitizeRenderText(x.label), count: x.count })),
216
+ unidentified: (report.mcp && typeof report.mcp.unidentified === 'number') ? report.mcp.unidentified : 0,
217
+ total: (report.mcp && typeof report.mcp.total === 'number') ? report.mcp.total : 0,
218
+ },
219
+ };
220
+ }
221
+
222
+ // Deterministic Spanish reading of the footprint (the drawer is es), composed ONLY from real scan numbers — never invented prose.
223
+ function buildReading({ setupLabel, tierKey, tierName, score, nTools, nTech }) {
224
+ const tier = tierKey ? `${tierKey} · ${tierName}` : tierName;
225
+ const toolsPart = nTools > 0 ? `${nTools} herramienta${nTools === 1 ? '' : 's'} de IA detectada${nTools === 1 ? '' : 's'}` : 'sin herramientas de IA detectadas';
226
+ const techPart = nTech > 0 ? `${nTech} tecnología${nTech === 1 ? '' : 's'} en el proyecto` : 'sin tecnologías detectadas';
227
+ return `Nivel de setup ${setupLabel} (${tier}), score ${score}/100. ${toolsPart[0].toUpperCase()}${toolsPart.slice(1)} y ${techPart}.`;
228
+ }
229
+
230
+ function prettyTool(key) {
231
+ const map = {
232
+ 'claude-code': 'Claude Code', cursor: 'Cursor', 'github-copilot': 'GitHub Copilot',
233
+ windsurf: 'Windsurf', aider: 'Aider', continue: 'Continue', 'gemini-cli': 'Gemini CLI',
234
+ 'codex-cli': 'Codex CLI', trae: 'Trae',
235
+ };
236
+ return map[key] || key;
237
+ }
238
+
239
+ module.exports = { buildGraphScan, buildFootprintDrawer, modelNode, slug };