shakers-cli 0.0.0-stage → 0.14.1

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,177 @@
1
+ 'use strict';
2
+
3
+ const { getCatalog } = require('./i18n');
4
+ const { buildRemediationPrompt } = require('./certify-remediation-prompt');
5
+ const { sanitizeRenderText } = require('./sanitize-network-text');
6
+ const { palette } = require('./ansi');
7
+ const { BRAND_ANSI } = require('./brand-ansi');
8
+ const { DEFAULT_REPORT_GATE } = require('./report-gating');
9
+
10
+ // Local shorthand: this file has ~15 paint sites and spelling the full name at
11
+ // each one buried the markup. Non-strings degrade to '' (see the primitive).
12
+ const display = (s) => sanitizeRenderText(s);
13
+
14
+ const C = palette({
15
+ reset: '\x1b[0m',
16
+ bold: '\x1b[1m',
17
+ dim: '\x1b[2m',
18
+ primary: BRAND_ANSI.primary,
19
+ accent: BRAND_ANSI.accent,
20
+ success: BRAND_ANSI.success,
21
+ warning: BRAND_ANSI.warning,
22
+ danger: BRAND_ANSI.danger,
23
+ });
24
+
25
+ function anyTruncated(items) {
26
+ return (items || []).some((i) => i && i.sampling && i.sampling.truncated);
27
+ }
28
+
29
+ // ADR-024 rubric dimension order (terminal).
30
+ const DIMENSION_KEYS = ['idiomatic', 'correctness', 'depth', 'structure', 'testing'];
31
+
32
+ // Score band -> semantic (shared with the HTML/graph CSS class via `graph-certs.js`, which still imports this).
33
+ function scoreBand(score) {
34
+ if (typeof score !== 'number') return 'mid';
35
+ if (score >= 70) return 'high';
36
+ if (score >= 40) return 'mid';
37
+ return 'low';
38
+ }
39
+
40
+ // ADR-016: named skill level (by decision power, not points) shown INSTEAD of the numeric grade.
41
+ function skillLevelForScore(score) {
42
+ const band = scoreBand(score);
43
+ if (typeof score !== 'number') return { key: null, band };
44
+ return { key: band === 'high' ? 'expert' : band === 'low' ? 'middle' : 'senior', band };
45
+ }
46
+
47
+ // Localized level label from the certify.report.skillLevels catalog.
48
+ function skillLevelLabel(key, r) {
49
+ const names = (r && r.skillLevels) || {};
50
+ return key ? (names[key] || key) : (names.na || '—');
51
+ }
52
+
53
+ const RULE = '────────────────────────────────────────';
54
+
55
+ // Terminal-condense (CPO feedback): the LLM rationale can be several sentences.
56
+ const RATIONALE_MAX = 220;
57
+ function conciseRationale(text) {
58
+ const s = String(text == null ? '' : text).replace(/\s+/g, ' ').trim();
59
+ if (s.length <= RATIONALE_MAX) return s;
60
+ const window = s.slice(0, RATIONALE_MAX);
61
+ const lastStop = Math.max(window.lastIndexOf('. '), window.lastIndexOf('! '), window.lastIndexOf('? '));
62
+ if (lastStop >= 80) return window.slice(0, lastStop + 1);
63
+ return `${window.replace(/[\s.,;:]+$/, '')}…`;
64
+ }
65
+
66
+ function renderCertificationTerminal(certification, lang, opts = {}) {
67
+ const r = getCatalog(lang).certify.report;
68
+ const items = Array.isArray(certification && certification.items) ? certification.items : [];
69
+ const lines = [];
70
+ // issue 123 / ADR-044: the SAME identity gate the footprint report reads.
71
+ const gate = opts.gate || DEFAULT_REPORT_GATE;
72
+
73
+ // Header stands out; disclaimer + cost note are deliberately quiet (dim) so
74
+ // the per-Skill results below are visually dominant (issue 011 feedback).
75
+ lines.push(`${C.bold}${C.primary}${r.heading}${C.reset}`);
76
+ // talents-ai-score, ADR-051: this whole render is now a FALLBACK — reached only when the mandatory interview did not complete.
77
+ if (opts.noInterview && r.noInterviewNote) lines.push(`${C.bold}${C.warning}${r.noInterviewNote}${C.reset}`);
78
+ lines.push('');
79
+ lines.push(`${C.dim}${r.disclaimer}${C.reset}`);
80
+ lines.push(`${C.dim}${r.costNote}${C.reset}`);
81
+ // ADR-025 authorship receipt (run-level): repo + commit range + honest note,
82
+ // shown once. Attribution is based on git authorship, NOT cryptographic proof.
83
+ const authorship = (certification && certification.authorship) || null;
84
+ const rc = r.receipt;
85
+ if (rc && authorship && (authorship.repository || authorship.commitRange)) {
86
+ const bits = [];
87
+ if (authorship.repository) bits.push(`${rc.repoLabel}: ${display(authorship.repository)}`);
88
+ if (authorship.commitRange) bits.push(`${rc.commitRangeLabel}: ${authorship.commitRange}`);
89
+ lines.push(`${C.dim}${rc.label} — ${bits.join(' · ')}${C.reset}`);
90
+ }
91
+ if (rc) lines.push(`${C.dim}${rc.note}${C.reset}`);
92
+ if (anyTruncated(items)) lines.push(`\n ${C.warning}${r.partialSampleWarning}${C.reset}`);
93
+ lines.push('');
94
+
95
+ if (items.length === 0) {
96
+ lines.push(` ${r.noItems}`);
97
+ return lines.join('\n');
98
+ }
99
+
100
+ for (const item of items) {
101
+ const title = `${display(item.skillName)}${item.technology ? ` (${display(item.technology)})` : ''}`;
102
+ lines.push(`${C.bold}${C.primary}╭─ ${title}${C.reset}`);
103
+
104
+ if (item.sampling && item.sampling.sampleable === false) {
105
+ lines.push(`${C.primary}│${C.reset} ${r.notSampleableNote(display(item.technology))}`);
106
+ lines.push(`${C.primary}╰─${C.reset}`);
107
+ lines.push('');
108
+ continue;
109
+ }
110
+ if (!item.result) {
111
+ lines.push(`${C.primary}│${C.reset} ${r.notCertified}`);
112
+ lines.push(`${C.primary}╰─${C.reset}`);
113
+ lines.push('');
114
+ continue;
115
+ }
116
+
117
+ // ADR-016: the HEADLINE is the named level (Middle/Senior/Expert) instead of the numeric 0-100 grade.
118
+ const { key: levelKey } = skillLevelForScore(item.result.score);
119
+ lines.push(`${C.primary}│${C.reset} ${C.bold}${C.accent}${r.levelLine(skillLevelLabel(levelKey, r))}${C.reset}`);
120
+ // ADR-024: show the anchored rubric dimensions so the level is explainable.
121
+ if (item.result.dimensions && r.dimensionsLabel && r.dimensionLabels) {
122
+ lines.push(`${C.primary}│${C.reset} ${C.bold}${r.dimensionsLabel}:${C.reset}`);
123
+ for (const key of DIMENSION_KEYS) {
124
+ const v = item.result.dimensions[key];
125
+ const shown = typeof v === 'number' ? `${v}/4` : r.dimensionNA;
126
+ lines.push(`${C.primary}│${C.reset} ${C.dim}${r.dimensionLabels[key] || key}:${C.reset} ${shown}`);
127
+ }
128
+ }
129
+ if (item.result.rationale) {
130
+ lines.push(`${C.primary}│${C.reset} ${C.bold}${r.rationaleLabel}:${C.reset} ${conciseRationale(display(item.result.rationale))}`);
131
+ }
132
+ if (Array.isArray(item.result.improvements) && item.result.improvements.length > 0) {
133
+ lines.push(`${C.primary}│${C.reset} ${C.bold}${r.improvementsLabel}:${C.reset}`);
134
+ for (const imp of item.result.improvements) lines.push(`${C.primary}│${C.reset} • ${display(imp)}`);
135
+ }
136
+ if (item.sampling) {
137
+ const summary = r.sampleSummary(item.sampling.includedCount, item.sampling.candidateCount, item.sampling.estTokens);
138
+ const tag = item.sampling.truncated ? ` ${r.partialTag}` : '';
139
+ lines.push(`${C.primary}│${C.reset} ${C.dim}${summary}${tag}${C.reset}`);
140
+ }
141
+ // ADR-025 per-Skill authorship receipt (compact in terminal): attributed
142
+ // file count + the author emails confirmed against the identity.
143
+ if (rc && Array.isArray(item.fileAttribution) && item.fileAttribution.length > 0) {
144
+ const attributed = item.fileAttribution.filter((f) => f.attributed).length;
145
+ lines.push(
146
+ `${C.primary}│${C.reset} ${C.bold}${rc.label}:${C.reset} ${rc.summary(attributed, item.fileAttribution.length)}`,
147
+ );
148
+ const matched = (item.authorEmails || []).filter((a) => a && a.matched).map((a) => display(a.email));
149
+ if (matched.length > 0) {
150
+ lines.push(`${C.primary}│${C.reset} ${C.dim}${rc.confirmedLabel}: ${matched.join(', ')}${C.reset}`);
151
+ }
152
+ }
153
+
154
+ // Remediation prompt (issue 011): a clearly delimited copyable block.
155
+ const remediation = gate.showCertifyRemediation ? buildRemediationPrompt(item, lang) : null;
156
+ if (remediation) {
157
+ lines.push(`${C.primary}│${C.reset}`);
158
+ lines.push(`${C.primary}│${C.reset} ${C.bold}${r.remediationHeading}${C.reset}`);
159
+ lines.push(`${C.primary}│${C.reset} ${C.dim}${r.remediationHint}${C.reset}`);
160
+ lines.push(`${C.primary}│${C.reset} ${C.dim}${RULE}${C.reset}`);
161
+ for (const l of remediation.split('\n')) lines.push(`${C.primary}│${C.reset} ${display(l)}`);
162
+ lines.push(`${C.primary}│${C.reset} ${C.dim}${RULE}${C.reset}`);
163
+ }
164
+
165
+ lines.push(`${C.primary}╰─${C.reset}`);
166
+ lines.push('');
167
+ }
168
+
169
+ return lines.join('\n').replace(/\n+$/, '');
170
+ }
171
+
172
+ module.exports = {
173
+ renderCertificationTerminal,
174
+ anyTruncated,
175
+ scoreBand,
176
+ skillLevelForScore,
177
+ };
@@ -0,0 +1,140 @@
1
+ 'use strict';
2
+
3
+ const { sanitizeRenderText } = require('./sanitize-network-text');
4
+
5
+ // SHARED SHAPING FOR AGENT CARDS AND CERTIFICATION EVIDENCE — no HTML.
6
+
7
+ function normalizeAgentName(name) {
8
+ return String(name || '').trim().toLowerCase().replace(/^[`'"*]+|[`'"*]+$/g, '');
9
+ }
10
+
11
+ // talents-ai-score, description-always-present (real-browser user feedback): TWO earlier approaches were tried and rejected before this one.
12
+
13
+ function humanizeAgentName(name) {
14
+ const spaced = String(name || '').replace(/[-_]+/g, ' ').replace(/\s+/g, ' ').trim();
15
+ return spaced.replace(/^\S/, (c) => c.toUpperCase());
16
+ }
17
+
18
+ function cleanRawDescription(text) {
19
+ return String(text || '')
20
+ .replace(/\\[nrt]/g, ' ') // literal 2-char escape sequences, as typed in the source frontmatter
21
+ .replace(/[\n\r\t]/g, ' ') // real control characters (e.g. from a `|` block scalar)
22
+ .replace(/\|/g, ' ') // stray YAML block-scalar/table-like pipe artifacts
23
+ .replace(/\s+/g, ' ')
24
+ .trim();
25
+ }
26
+
27
+ const CARD_EXCERPT_MAX_LEN = 160;
28
+
29
+ function excerptForCard(text, maxLen = CARD_EXCERPT_MAX_LEN) {
30
+ if (!text) return text;
31
+ const sentenceMatch = text.match(/[.!?](?=\s|$)/);
32
+ const sentenceEndIdx = sentenceMatch ? sentenceMatch.index + 1 : null;
33
+
34
+ if (sentenceEndIdx !== null && sentenceEndIdx <= maxLen) {
35
+ return text.slice(0, sentenceEndIdx).trim();
36
+ }
37
+ if (text.length <= maxLen) return text;
38
+ return `${text.slice(0, maxLen).trim()}…`;
39
+ }
40
+
41
+ // Every card field that is TALENT-AUTHORED or came OFF THE WIRE, neutralised once, here — issue 055.
42
+
43
+ // Every card field that is TALENT-AUTHORED or came OFF THE WIRE, neutralised once, here — issue 055.
44
+ function sanitizeCardText(v) {
45
+ return typeof v === 'string' ? sanitizeRenderText(v) : v;
46
+ }
47
+
48
+ function sanitizeCardList(v) {
49
+ return Array.isArray(v) ? v.map((x) => (typeof x === 'string' ? sanitizeRenderText(x) : x)) : v;
50
+ }
51
+
52
+ function buildAgentCardTree(report, t) {
53
+ const agents = Array.isArray(report.agents) ? report.agents : [];
54
+ const synthesisAgents =
55
+ report.agentSynthesis && Array.isArray(report.agentSynthesis.agents) ? report.agentSynthesis.agents : [];
56
+ const synthesisByName = new Map(synthesisAgents.map((a) => [normalizeAgentName(a.name), a]));
57
+ const rawDescriptions = Array.isArray(report.agentDescriptions) ? report.agentDescriptions : [];
58
+ const rawDescByName = new Map(rawDescriptions.map((d) => [normalizeAgentName(d.name), d.description]));
59
+ const byName = new Set(agents.map((a) => a.name));
60
+
61
+ // ADR-016 agent evaluation: the ephemeral LLM definition-quality score (+ rationale) keyed by normalized name, and the LOCAL usage count keyed by exact name.
62
+ const evaluations =
63
+ report.agentEvaluation && Array.isArray(report.agentEvaluation.evaluations)
64
+ ? report.agentEvaluation.evaluations
65
+ : [];
66
+ // The agents the evaluation call answered about but did NOT cover — declared by
67
+ // the service and cross-checked against what was sent (src/agent-evaluation.js).
68
+ const omittedNames = new Set(
69
+ report.agentEvaluation && Array.isArray(report.agentEvaluation.omittedAgentNames)
70
+ ? report.agentEvaluation.omittedAgentNames.filter((n) => typeof n === 'string' && n)
71
+ : [],
72
+ );
73
+ const evalByName = new Map(evaluations.map((e) => [normalizeAgentName(e.name), e]));
74
+
75
+ const cards = agents.map((a) => {
76
+ const key = normalizeAgentName(a.name);
77
+ const synth = synthesisByName.get(key);
78
+ const parentKey = a.parent && byName.has(a.parent) && a.parent !== a.name ? a.parent : null;
79
+ const tools = Array.isArray(a.tools) ? a.tools : [];
80
+ // AI product derived from the agent's SOURCE (agent-org-chart#deriveAiProduct) — shown on the card in place of the LLM model.
81
+ const aiProduct = typeof a.aiProduct === 'string' && a.aiProduct ? a.aiProduct : null;
82
+
83
+ const ev = evalByName.get(key);
84
+ const evalPhrase =
85
+ ev && typeof ev.description === 'string' && ev.description.trim() ? ev.description.trim() : null;
86
+ const synthPhrase =
87
+ synth && typeof synth.whatItDoes === 'string' && synth.whatItDoes.trim() ? synth.whatItDoes.trim() : null;
88
+ const rawRaw = rawDescByName.get(key);
89
+ const rawPhrase =
90
+ typeof rawRaw === 'string' && rawRaw.trim() ? excerptForCard(cleanRawDescription(rawRaw)) : null;
91
+ const fallbackPhrase = t && t.html.agentDescriptionFromName ? t.html.agentDescriptionFromName(humanizeAgentName(a.name)) : null;
92
+ const whatItDoes = evalPhrase || synthPhrase || rawPhrase || fallbackPhrase;
93
+
94
+ // The numeric score AND the local usage signal were removed from footprint cards (user decision) — not carried onto the card.
95
+ const rationale = ev && typeof ev.rationale === 'string' && ev.rationale.trim() ? ev.rationale.trim() : null;
96
+ // v4 (agent classification): closest catalog agent + how it was matched, and 2-3 improvement tips.
97
+ const classification = ev
98
+ ? ev.classification && ev.classification.catalogId
99
+ ? ev.classification
100
+ : { catalogId: null, category: null, role: null, level: null, method: 'unclassified' }
101
+ : null;
102
+ const improvements = ev && Array.isArray(ev.improvements) ? ev.improvements : [];
103
+ // THE FOUR-VALUE EVALUATION STATE, RESOLVED HERE AND NOWHERE ELSE (2026-08-03).
104
+ const evaluationState = ev
105
+ ? (classification && classification.catalogId ? 'classified' : 'unclassified')
106
+ : (omittedNames.has(a.name) ? 'omitted' : 'not-evaluated');
107
+
108
+ // The single render-safety pass for both the HTML and the terminal card (see sanitizeCardText above).
109
+ return {
110
+ name: sanitizeCardText(a.name),
111
+ symbolicName: sanitizeCardText(synth && synth.symbolicName ? synth.symbolicName : null),
112
+ whatItDoes: sanitizeCardText(whatItDoes),
113
+ tools: sanitizeCardList(tools),
114
+ // The declared LLM model (issue 089).
115
+ model: sanitizeCardText(a.model),
116
+ aiProduct,
117
+ parent: sanitizeCardText(parentKey),
118
+ rationale: sanitizeCardText(rationale),
119
+ classification: classification
120
+ ? { ...classification, role: sanitizeCardText(classification.role) }
121
+ : classification,
122
+ improvements: sanitizeCardList(improvements),
123
+ // Issue 106's provenance rule, applied to the fourth state: ONE field, on the card both surfaces read, so the terminal and the shared report cannot disagree about the same agent.
124
+ evaluationState,
125
+ };
126
+ });
127
+
128
+ const childrenByParent = new Map();
129
+ for (const card of cards) {
130
+ if (!childrenByParent.has(card.parent)) childrenByParent.set(card.parent, []);
131
+ childrenByParent.get(card.parent).push(card);
132
+ }
133
+
134
+ return { childrenByParent, roots: childrenByParent.get(null) || [] };
135
+ }
136
+
137
+ // Maps an AI-product key (e.g. 'claude-code') to its display name via i18n,
138
+ // falling back to the raw key. Product names are proper nouns (same es/en).
139
+
140
+ module.exports = { buildAgentCardTree };
@@ -0,0 +1,27 @@
1
+ 'use strict';
2
+
3
+ const { renderTerminal } = require('./render-terminal');
4
+ const { renderAiProfilePlain } = require('./render-ai-fluency');
5
+ const { reportGateForIdentity } = require('./report-gating');
6
+
7
+ const ANSI_RE = /\x1b\[[0-9;]*m/g;
8
+ const stripAnsi = (s) => String(s).replace(ANSI_RE, '');
9
+
10
+ function renderFullReportText(report, maturity, aiProfile, { lang = 'en', gate } = {}) {
11
+ const profile = renderAiProfilePlain(
12
+ aiProfile || { status: 'unavailable', preview: null },
13
+ { lang },
14
+ );
15
+ let terminal = '';
16
+ try {
17
+ terminal = stripAnsi(renderTerminal(report, maturity, lang, { gate: gate || reportGateForIdentity() }))
18
+ .replace(/[ \t]+$/gm, '')
19
+ .replace(/\n{3,}/g, '\n\n')
20
+ .trim();
21
+ } catch {
22
+ terminal = '';
23
+ }
24
+ return terminal ? `\`\`\`\n${terminal}\n\`\`\`\n\n${profile}` : profile;
25
+ }
26
+
27
+ module.exports = { renderFullReportText, stripAnsi };
@@ -0,0 +1,261 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { buildFootprintDrawer } = require('./graph-scan');
6
+ const { buildCertsPayload } = require('./graph-certs');
7
+ const { getCatalog, label } = require('./i18n');
8
+ const { sanitizeRenderText } = require('./sanitize-network-text');
9
+ const { isFloorCategory } = require('./agent-category');
10
+ const { stripTemplateComments } = require('./strip-internal-comments');
11
+ const { DEFAULT_REPORT_GATE } = require('./report-gating');
12
+ // 3x3 AI-Fluency matrix block (Usage axis persisted on the report).
13
+ const { renderAiFluencyMatrixHtml } = require('./render-ai-fluency');
14
+
15
+ // Rendered only when the Usage axis was judged and persisted on the report.
16
+ function aiFluencyHtml(report, lang) {
17
+ const usage = report && report.aiFluencyUsage;
18
+ if (!usage) return '';
19
+ return `<div class="card reveal">${renderAiFluencyMatrixHtml({ status: 'ready', usage }, { lang })}</div>`;
20
+ }
21
+
22
+ const TEMPLATE_PATH = path.join(__dirname, 'templates', 'report-sheet.html');
23
+ let _tpl = null;
24
+ // Issue 098: the template's comments are OURS and this document is the one the talent shares, so they are swept here — at load, before any talent data is in the string.
25
+ function template() {
26
+ if (_tpl == null) _tpl = stripTemplateComments(fs.readFileSync(TEMPLATE_PATH, 'utf8'));
27
+ return _tpl;
28
+ }
29
+
30
+ function esc(s) {
31
+ return String(s == null ? '' : s)
32
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
33
+ .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
34
+ }
35
+
36
+ // Literal, GLOBAL placeholder substitution.
37
+ function fill(tpl, token, value) {
38
+ return tpl.split(token).join(value);
39
+ }
40
+
41
+ // Escapes a value for injection inside a single-quoted JS string literal in the template's inline <script>.
42
+ function jsStr(s) {
43
+ return String(s == null ? '' : s)
44
+ .replace(/\\/g, '\\\\').replace(/'/g, "\\'")
45
+ .replace(/</g, '\\u003c').replace(/\r?\n/g, ' ');
46
+ }
47
+
48
+ // Tier name, translated by STABLE KEY — same rule as render-terminal.js and render-html.js.
49
+ function tierName(fp, t) {
50
+ const key = fp && fp.tier && fp.tier.key;
51
+ return (key && key !== 'none' && t.tierNames && t.tierNames[key])
52
+ || (fp && fp.tier && fp.tier.name) || '';
53
+ }
54
+
55
+ function bandClass(b) { return b === 'high' ? 'band-high' : b === 'mid' ? 'band-mid' : 'band-low'; }
56
+
57
+ // Localized Setup Level label from the drawer payload (ADR-016): prefer the
58
+ // i18n `setupLevels` catalog by key, fall back to the label baked into `fp`.
59
+ function setupLabel(fp, t) {
60
+ const key = (fp.setup && fp.setup.key) || 'none';
61
+ return (t && t.setupLevels && t.setupLevels[key] && t.setupLevels[key].label)
62
+ || (fp.setup && fp.setup.label) || key;
63
+ }
64
+
65
+ function heroHtml(fp, c, t) {
66
+ const rank = (fp.setup && typeof fp.setup.rank === 'number') ? fp.setup.rank : 0;
67
+ const label = setupLabel(fp, t);
68
+ // Four pips: Not certified + S1/S2/S3, current filled (ADR-016).
69
+ const pills = Array.from({ length: 4 }, (_, i) => `<span class="${i <= rank ? 'on' : ''}"></span>`).join('');
70
+ const chipK = esc(fp.tier.key && fp.tier.key !== 'none' ? fp.tier.key : (fp.setup && fp.setup.code) || '');
71
+ return `<div class="card reveal">
72
+ <div class="hero">
73
+ <div class="ring" id="ring">
74
+ <svg width="132" height="132" viewBox="0 0 132 132">
75
+ <circle class="track" cx="66" cy="66" r="54"></circle>
76
+ <circle class="fill" id="ringFill" cx="66" cy="66" r="54"></circle>
77
+ </svg>
78
+ <div class="num"><b id="scoreNum">0</b><small>/ 100</small></div>
79
+ </div>
80
+ <div class="hero-meta">
81
+ <span class="tier-chip"><span class="k">${chipK}</span> ${esc(tierName(fp, t))}</span>
82
+ <div class="band-line">${c.bandLine(esc(label))}</div>
83
+ <div class="lvl-pills" title="S1–S3">${pills}</div>
84
+ </div>
85
+ </div>
86
+ </div>`;
87
+ }
88
+
89
+ // The Setup Level ladder (ADR-016), with each rung's tiers and short meaning inline — meanings from the localized i18n `setupLevels` catalog (never invented).
90
+ function ladderHtml(fp, c, t) {
91
+ const cur = (fp.setup && typeof fp.setup.rank === 'number') ? fp.setup.rank : 0;
92
+ const catalog = (t && t.setupLevels) || {};
93
+ const items = (fp.ladder || []).map((r) => {
94
+ const cls = r.rank < cur ? 'done' : r.rank === cur ? 'current' : 'pending';
95
+ const node = r.rank < cur ? '✓' : '';
96
+ const copy = catalog[r.key] || {};
97
+ const label = copy.label || r.label || r.key;
98
+ // desc starts with the label prefix ("Assisted: …") — strip it so it isn't
99
+ // doubled next to the name we already show.
100
+ const meaning = (copy.desc || '').replace(/^[^:]+:\s*/, '');
101
+ const here = r.rank === cur ? ` — ${esc(c.hereYouAre)}` : '';
102
+ const line = meaning ? `<div class="desc">${esc(meaning)}${here}</div>` : (here ? `<div class="desc">${esc(c.hereYouAre)}</div>` : '');
103
+ return `<li class="${cls}"><span class="node">${node}</span><span class="name">${esc(label)}</span>${line}</li>`;
104
+ }).join('');
105
+ return `<div class="card reveal"><h3>${esc(c.ladderT)}</h3><p class="sub">${esc(c.ladderS)}</p><ul class="ladder">${items}</ul></div>`;
106
+ }
107
+
108
+ function chipsCard(title, sub, items, withDot) {
109
+ const chips = (items || []).map((x) => `<span class="chip">${withDot ? '<span class="d"></span>' : ''}${esc(x)}</span>`).join('');
110
+ const body = chips || '<span class="chip" style="opacity:.6">—</span>';
111
+ return `<div class="card reveal"><h3>${esc(title)}</h3><p class="sub">${esc(sub)}</p><div class="chips">${body}</div></div>`;
112
+ }
113
+
114
+ // MCP SERVICES (issue 110).
115
+ function mcpCard(mcp, c) {
116
+ const services = (mcp && Array.isArray(mcp.services) ? mcp.services : []);
117
+ const unidentified = (mcp && typeof mcp.unidentified === 'number') ? mcp.unidentified : 0;
118
+ if (services.length === 0 && unidentified === 0) {
119
+ return `<div class="card reveal"><h3>${esc(c.mcpT)}</h3><p class="sub">${esc(c.mcpEmpty)}</p></div>`;
120
+ }
121
+ const chips = services
122
+ .map((s) => `<span class="chip">${esc(s.label)}${s.count > 1 ? ` ×${s.count}` : ''}</span>`)
123
+ .join('');
124
+ // The unidentified servers are stated, never omitted: the list must account for
125
+ // every server the tier counted.
126
+ const note = unidentified > 0
127
+ ? `<p class="sub" style="margin:8px 0 0">${esc(c.mcpUnidentified(unidentified))}</p>`
128
+ : '';
129
+ return `<div class="card reveal"><h3>${esc(c.mcpT)}</h3><p class="sub">${esc(c.mcpS)}</p><div class="chips">${chips}</div>${note}</div>`;
130
+ }
131
+
132
+ // DETECTED AGENTS, left column (issue 089).
133
+ function agentRow(a, c, t) {
134
+ const cats = (t.classification && t.classification.categories) || {};
135
+ const model = a.model ? `<span class="model">${esc(a.model)}</span>` : `<span class="model" style="opacity:.6">${esc(c.agentNoModel)}</span>`;
136
+ // The category is the agent → catalog-seed association (per agent), not the Talent's tier.
137
+ const catLabel = a.category ? (cats[a.category] || sanitizeRenderText(a.category)) : null;
138
+ // ONE COLOUR PER CATEGORY (user request), and THREE distinct facts, not two: - a known catalog category -> its own colour token (`.cat-developer` …).
139
+ // FOUR facts now, not three (issue 120).
140
+ const coloured = !!(a.category && cats[a.category] && !isFloorCategory(a.category));
141
+ const catClass = coloured ? `cat cat-${a.category}` : 'cat cat-none';
142
+ // Issue 106: the empty badge says WHICH kind of empty it is.
143
+ const cls = t.classification || {};
144
+ const emptyLabel = a.evaluationState === 'omitted'
145
+ ? cls.agentEvalOmitted
146
+ : ((a.evaluationState === 'not-evaluated' || a.evaluated === false) ? c.agentNotEvaluated : c.agentNoCategory);
147
+ const category = catLabel
148
+ ? `<span class="chip ${catClass}">${esc(catLabel)}</span>`
149
+ : `<span class="chip cat cat-none">${esc(emptyLabel)}</span>`;
150
+ const orchestrates = a.hasChildren ? `<span class="usage">${esc(c.agentOrchestrates)}</span>` : '';
151
+ const desc = a.whatItDoes ? `<p class="whatdo">${esc(a.whatItDoes)}</p>` : '';
152
+ return `<li class="agent${a.depth > 0 ? ' child' : ''}">
153
+ <div class="agent-row">
154
+ <span class="an">${esc(a.name)}</span>
155
+ ${model}
156
+ <span class="spacer"></span>
157
+ ${orchestrates}
158
+ ${category}
159
+ </div>
160
+ ${desc}
161
+ </li>`;
162
+ }
163
+
164
+ function agentsCard(agents, c, t) {
165
+ const list = Array.isArray(agents) ? agents : [];
166
+ // SAID ONCE, VISIBLY (issue 106): if nobody was evaluated, the reason the badges are empty is that the run could not classify, not that the agents have no category.
167
+ const noneEvaluated = list.length > 0 && list.every((a) => a.evaluated === false);
168
+ // AND THE PARTIAL RUN, NAMED (2026-08-03).
169
+ const omitted = list.filter((a) => a.evaluationState === 'omitted').map((a) => a.name);
170
+ const cls = (t && t.classification) || {};
171
+ // OMITTED WINS OVER "NOBODY WAS EVALUATED", and the order is the fix.
172
+ const notice = (omitted.length && typeof cls.agentsEvalPartial === 'function')
173
+ ? `<p class="sub" style="margin:0 0 10px">${esc(cls.agentsEvalPartial(omitted.join(', '), omitted.length))}</p>`
174
+ : (noneEvaluated ? `<p class="sub" style="margin:0 0 10px">${esc(c.agentsEvalMissing)}</p>` : '');
175
+ // The empty state is explicit on purpose: "you have none" and "the tool does not
176
+ // look" are different facts and used to look identical (nothing at all).
177
+ const body = list.length
178
+ ? `<ul class="tree">${list.map((a) => agentRow(a, c, t)).join('')}</ul>`
179
+ : `<p class="sub" style="margin:0">${esc(label(c.agentsEmpty, 'No configured AI agents detected.'))}</p>`;
180
+ return `<div class="card reveal"><h3>${esc(label(c.agentsT, 'AI agents'))}</h3><p class="sub">${esc(label(c.agentsS, ''))}</p>${notice}${body}</div>`;
181
+ }
182
+
183
+ function skillAcc(s, c, gate = DEFAULT_REPORT_GATE) {
184
+ const imps = gate.showCertifyRemediation ? (s.improvements || []).map((i) => `<li>${esc(i)}</li>`).join('') : '';
185
+ const improveBlk = imps ? `<div class="blk"><h5>${esc(c.comoMejorar)}</h5><ul>${imps}</ul></div>` : '';
186
+ return `<div class="acc"><button class="acc-head"><span class="caret">▸</span><span class="title">${esc(s.name)}</span><span class="spacer"></span><span class="score-badge ${bandClass(s.band)}">${esc(s.levelName || '—')}</span></button>
187
+ <div class="acc-body-wrap"><div class="acc-body-inner"><div class="acc-body">
188
+ <div class="blk"><h5>${esc(c.valoracion)}</h5><p>${esc(s.rationale) || '—'}</p></div>
189
+ ${improveBlk}
190
+ </div></div></div></div>`;
191
+ }
192
+
193
+ // renderSheet(project, lang) -> full self-contained HTML string with the mockup design, populated from live report-store data.
194
+ function renderSheet(project, lang, gate = DEFAULT_REPORT_GATE) {
195
+ const L = lang === 'en' ? 'en' : 'es';
196
+ const t = getCatalog(L);
197
+ const c = t.sheet;
198
+ const hasFoot = !!(project && project.footprint && project.footprint.report);
199
+ // `t` is passed now (issue 089): the drawer's agent rows use the catalog for the
200
+ // description fallback and nothing else — it stays optional for the graph report.
201
+ const fp = hasFoot ? buildFootprintDrawer(project.footprint.report, project.footprint.maturity, t) : null;
202
+ // buildCertsPayload returns null when the project has no Skill certs — the sheet still renders both columns, so normalize to an empty list (clean state).
203
+ const certs = buildCertsPayload(project, L) || { skills: [] };
204
+
205
+ // LEFT column — footprint (or a clean empty state). The tier explainer now
206
+ // lives INSIDE the maturity ladder (per rung), so no separate tiers card.
207
+ const left = hasFoot
208
+ ? heroHtml(fp, c, t) + aiFluencyHtml(project.footprint.report, L) + ladderHtml(fp, c, t) + agentsCard(fp.agents, c, t)
209
+ + chipsCard(c.toolsT, c.toolsS, fp.tools, true) + chipsCard(c.techT, c.techS, fp.technologies, false)
210
+ + mcpCard(fp.mcp, c)
211
+ : `<div class="card reveal"><p class="sub">${esc(c.noFoot)}</p></div>`;
212
+
213
+ // RIGHT column — Skill certifications (or a clean empty state).
214
+ const skillsBody = certs.skills.length
215
+ ? certs.skills.map((s) => skillAcc(s, c, gate)).join('')
216
+ : `<div class="card reveal"><p class="sub">${esc(c.noSkills)}</p></div>`;
217
+
218
+ const body = `
219
+ <section class="col-left">
220
+ <div class="col-head"><span class="dot"></span><h2>${esc(c.footprint)}</h2></div>
221
+ ${left}
222
+ </section>
223
+ <section class="col-right">
224
+ <div class="col-head"><span class="dot"></span><h2>${esc(c.certs)}</h2></div>
225
+ <div class="tabs" id="tabs">
226
+ <span class="tab-ind" id="tabInd"></span>
227
+ <button class="tab active" data-tab="skills">${esc(c.skills)}<span class="cnt">${certs.skills.length}</span></button>
228
+ </div>
229
+ <div class="panel-controls"><button class="mini-btn" id="expandAll">${esc(c.expandAll)}</button></div>
230
+ <div class="tabpanel show" data-panel="skills">${skillsBody}</div>
231
+ </section>`;
232
+
233
+ const projectName = project && project.root
234
+ ? sanitizeRenderText(path.basename(String(project.root)))
235
+ : 'project';
236
+
237
+ let out = template();
238
+ // Document/page chrome. HTML-escaped for the markup slots, JS-escaped for the
239
+ // three that land inside the template's inline <script>.
240
+ for (const [token, value] of [
241
+ ['__LANG__', L],
242
+ ['__SCORE__', String(fp ? Math.max(0, Math.min(100, Math.round(fp.score))) : 0)],
243
+ ['__PROJECT__', esc(String(projectName))],
244
+ ['__FOOTER__', esc(c.footer)],
245
+ ['__REPORT_TITLE__', esc(c.reportTitle)],
246
+ ['__THEME_TOGGLE__', esc(c.themeToggle)],
247
+ ['__THEME_DARK__', esc(c.themeDark)],
248
+ ['__THEME_LIGHT__', esc(c.themeLight)],
249
+ ['__JS_THEME_DARK__', jsStr(c.themeDark)],
250
+ ['__JS_THEME_LIGHT__', jsStr(c.themeLight)],
251
+ ['__JS_COPIED__', jsStr(c.copied)],
252
+ ['__JS_EXPAND_ALL__', jsStr(c.expandAll)],
253
+ ['__JS_COLLAPSE_ALL__', jsStr(c.collapseAll)],
254
+ ['__BODY__', body],
255
+ ]) {
256
+ out = fill(out, token, value);
257
+ }
258
+ return out;
259
+ }
260
+
261
+ module.exports = { renderSheet, agentsCard };