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.
- package/LICENSE +59 -0
- package/README.md +1175 -0
- package/bin/add-agent.js +88 -0
- package/bin/add-project.js +88 -0
- package/bin/add-role.js +61 -0
- package/bin/add-skill.js +88 -0
- package/bin/ai-usage.js +81 -0
- package/bin/alma.js +99 -0
- package/bin/applications.js +69 -0
- package/bin/availability.js +70 -0
- package/bin/certifications.js +69 -0
- package/bin/certify.js +73 -0
- package/bin/change-role.js +61 -0
- package/bin/config.js +120 -0
- package/bin/experiences.js +69 -0
- package/bin/find-projects.js +89 -0
- package/bin/invitations.js +69 -0
- package/bin/lang.js +70 -0
- package/bin/login.js +283 -0
- package/bin/mcp.js +98 -0
- package/bin/me.js +69 -0
- package/bin/onboarding.js +134 -0
- package/bin/portfolios.js +69 -0
- package/bin/rate.js +70 -0
- package/bin/register.js +325 -0
- package/bin/report-html.js +134 -0
- package/bin/save.js +77 -0
- package/bin/shakers.js +344 -0
- package/bin/share.js +72 -0
- package/bin/show-project.js +72 -0
- package/bin/socials.js +70 -0
- package/bin/superadmin.js +351 -0
- package/package.json +40 -0
- package/src/agent-category.js +13 -0
- package/src/agent-evaluation.js +177 -0
- package/src/agent-org-chart.js +355 -0
- package/src/agent-synthesis.js +132 -0
- package/src/agent-usage.js +118 -0
- package/src/agents-client.js +165 -0
- package/src/ai-fluency-cell.js +72 -0
- package/src/ai-profile-client.js +45 -0
- package/src/ai-profile-preview.js +154 -0
- package/src/ai.js +66 -0
- package/src/alma-client.js +123 -0
- package/src/alma-flow.js +32 -0
- package/src/alma-repo-context.js +47 -0
- package/src/alma-shell.js +161 -0
- package/src/ansi.js +62 -0
- package/src/applications-client.js +79 -0
- package/src/applications-flow.js +58 -0
- package/src/auth-client.js +144 -0
- package/src/auth-device-client.js +118 -0
- package/src/auth-method-picker.js +44 -0
- package/src/auth-methods.js +35 -0
- package/src/auth-session-store.js +214 -0
- package/src/authorship.js +203 -0
- package/src/automations-detector.js +170 -0
- package/src/availability-client.js +92 -0
- package/src/availability-flow.js +105 -0
- package/src/backend-request.js +170 -0
- package/src/blind-approval-extract.js +48 -0
- package/src/brand-ansi.js +14 -0
- package/src/brand-svg.js +9 -0
- package/src/browser-tools-detector.js +21 -0
- package/src/build-next-level.js +56 -0
- package/src/certifications-client.js +89 -0
- package/src/certifications-flow.js +75 -0
- package/src/certify-dimension-client.js +217 -0
- package/src/certify-dimension-flow.js +193 -0
- package/src/certify-disclaimer.js +51 -0
- package/src/certify-remediation-prompt.js +23 -0
- package/src/cli-args.js +83 -0
- package/src/config-dir.js +120 -0
- package/src/config.js +778 -0
- package/src/consent-flow.js +115 -0
- package/src/consent-skip.js +27 -0
- package/src/decisions-extract.js +43 -0
- package/src/detect-repos.js +145 -0
- package/src/detected-tools.js +15 -0
- package/src/detection-evidence.js +105 -0
- package/src/detection-noise.js +132 -0
- package/src/detectors.js +221 -0
- package/src/device-auth-store.js +100 -0
- package/src/device-login.js +118 -0
- package/src/email-verification.js +152 -0
- package/src/env-paths.js +16 -0
- package/src/find-projects-cache.js +89 -0
- package/src/find-projects-client.js +138 -0
- package/src/find-projects-flow.js +164 -0
- package/src/git-activity.js +123 -0
- package/src/graph-certs.js +58 -0
- package/src/graph-generator.js +230 -0
- package/src/graph-scan.js +239 -0
- package/src/i18n.js +3264 -0
- package/src/inter-fonts.js +6 -0
- package/src/interactive-select.js +183 -0
- package/src/interview-client.js +226 -0
- package/src/interview-transcript.js +56 -0
- package/src/inventory-client.js +90 -0
- package/src/invitations-client.js +66 -0
- package/src/invitations-flow.js +50 -0
- package/src/lang-client.js +147 -0
- package/src/lang-codes.js +14 -0
- package/src/lang-flow.js +85 -0
- package/src/legal-notice.js +53 -0
- package/src/locale.js +51 -0
- package/src/markdown-ansi.js +50 -0
- package/src/maturity.js +111 -0
- package/src/mcp-ai-usage-result-tool.js +109 -0
- package/src/mcp-ai-usage-tool.js +333 -0
- package/src/mcp-alma-tools.js +91 -0
- package/src/mcp-applications-tools.js +42 -0
- package/src/mcp-ask-tools.js +50 -0
- package/src/mcp-auth-form.js +282 -0
- package/src/mcp-availability-tools.js +43 -0
- package/src/mcp-certifications-tools.js +42 -0
- package/src/mcp-certify-dimension-tools.js +58 -0
- package/src/mcp-detector.js +223 -0
- package/src/mcp-device-login-tools.js +173 -0
- package/src/mcp-find-projects-tools.js +67 -0
- package/src/mcp-install.js +245 -0
- package/src/mcp-invitations-tools.js +42 -0
- package/src/mcp-lang-tools.js +39 -0
- package/src/mcp-onboarding-tools.js +388 -0
- package/src/mcp-portfolios-tools.js +46 -0
- package/src/mcp-profile-tools.js +46 -0
- package/src/mcp-rate-tools.js +42 -0
- package/src/mcp-register-tools.js +517 -0
- package/src/mcp-report-tools.js +101 -0
- package/src/mcp-role-tools.js +112 -0
- package/src/mcp-saved-positions-tools.js +55 -0
- package/src/mcp-server.js +170 -0
- package/src/mcp-show-project-tools.js +50 -0
- package/src/mcp-socials-tools.js +39 -0
- package/src/memory-structure-detector.js +108 -0
- package/src/model-call-record.js +198 -0
- package/src/onboarding-availability.js +26 -0
- package/src/onboarding-client.js +364 -0
- package/src/onboarding-flow.js +1078 -0
- package/src/onboarding-livekit-client.js +102 -0
- package/src/onboarding-status-client.js +42 -0
- package/src/open-file.js +32 -0
- package/src/osc-link.js +16 -0
- package/src/portfolio-client.js +164 -0
- package/src/portfolio-git-info.js +61 -0
- package/src/portfolios-client.js +85 -0
- package/src/portfolios-flow.js +79 -0
- package/src/profile-client.js +125 -0
- package/src/profile-flow.js +115 -0
- package/src/profile-switch.js +46 -0
- package/src/prompt-select.js +29 -0
- package/src/rate-client.js +82 -0
- package/src/rate-flow.js +137 -0
- package/src/render-ai-fluency.js +266 -0
- package/src/render-certification.js +177 -0
- package/src/render-html.js +140 -0
- package/src/render-report-text.js +27 -0
- package/src/render-sheet.js +261 -0
- package/src/render-terminal.js +109 -0
- package/src/render-usage-report-text.js +155 -0
- package/src/repo-context.js +215 -0
- package/src/report-gating.js +49 -0
- package/src/report-store.js +226 -0
- package/src/report-theme.js +177 -0
- package/src/report-view-client.js +50 -0
- package/src/reveal-output.js +29 -0
- package/src/roadmap-content.js +453 -0
- package/src/roadmap-personalization.js +132 -0
- package/src/roadmap-prompt.js +154 -0
- package/src/roles-client.js +114 -0
- package/src/roles-flow.js +192 -0
- package/src/sanitize-network-text.js +36 -0
- package/src/saved-positions-client.js +71 -0
- package/src/saved-positions-flow.js +50 -0
- package/src/scan-exclusions.js +50 -0
- package/src/scanner.js +612 -0
- package/src/scope-signals.js +148 -0
- package/src/secret-prompt.js +72 -0
- package/src/session-gate.js +30 -0
- package/src/session-refresh.js +51 -0
- package/src/session-scan.js +336 -0
- package/src/session-signals.js +36 -0
- package/src/shakers-wordmark.js +17 -0
- package/src/share-card.js +362 -0
- package/src/share-certification.js +111 -0
- package/src/share-consent-state.js +157 -0
- package/src/share-payload.js +300 -0
- package/src/share.js +184 -0
- package/src/show-project-client.js +58 -0
- package/src/show-project-flow.js +119 -0
- package/src/signup-client.js +91 -0
- package/src/skills-client.js +102 -0
- package/src/socials-client.js +91 -0
- package/src/socials-flow.js +83 -0
- package/src/specificity-extract.js +41 -0
- package/src/start-add-agents.js +306 -0
- package/src/start-add-portfolio.js +388 -0
- package/src/start-add-skills.js +260 -0
- package/src/stdin-ask.js +129 -0
- package/src/steering-extract.js +43 -0
- package/src/strip-internal-comments.js +45 -0
- package/src/tech-detector.js +295 -0
- package/src/templates/report-sheet.html +572 -0
- package/src/terminal-agents.js +199 -0
- package/src/terminal-format.js +85 -0
- package/src/terminal-progress.js +106 -0
- package/src/terminal-sections.js +219 -0
- package/src/tier-analysis.js +126 -0
- package/src/tier-engine.js +114 -0
- package/src/tier-projection.js +117 -0
- package/src/transcript-turns.js +49 -0
- package/src/translations-client.js +84 -0
- package/src/typewriter.js +154 -0
- package/src/usage-model-calls.js +119 -0
- package/src/usage-oneshot-commands.js +180 -0
- package/src/usage-repo-scope.js +162 -0
- package/src/usage-run-steps.js +338 -0
- package/src/work-streams.js +74 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Renders the "My work with AI" preview (3x3 matrix + vision E + howIWork F +
|
|
4
|
+
// traction), terminal + HTML. Copy is inlined for the spike; promote to i18n later.
|
|
5
|
+
|
|
6
|
+
const { SETUP_CODE, CELL_NAMES } = require('./ai-fluency-cell');
|
|
7
|
+
const { detectedToolNames } = require('./detected-tools');
|
|
8
|
+
|
|
9
|
+
const SETUP_ORDER = ['ASSISTED', 'EXTENDED', 'ORCHESTRATED'];
|
|
10
|
+
const USAGE_ORDER = ['REACTIVE', 'DELIBERATE', 'RIGOROUS'];
|
|
11
|
+
|
|
12
|
+
const COPY = {
|
|
13
|
+
es: {
|
|
14
|
+
title: 'Tu trabajo con IA (Setup x Uso)',
|
|
15
|
+
usage: { REACTIVE: 'Reactivo', DELIBERATE: 'Deliberado', RIGOROUS: 'Riguroso' },
|
|
16
|
+
setup: { ASSISTED: 'Asistido', EXTENDED: 'Extendido', ORCHESTRATED: 'Orquestado' },
|
|
17
|
+
aiNative: 'AI Native',
|
|
18
|
+
pending: 'Tu perfil se está evaluando; míralo en un momento en "Mi trabajo con IA".',
|
|
19
|
+
matrixUpdating: 'Tu matriz Setup x Uso se está actualizando; aparecerá en breve.',
|
|
20
|
+
setupOnly: 'Disponible en tu perfil tras iniciar sesión y compartir el informe.',
|
|
21
|
+
visionLabel: 'Mi visión sobre la IA',
|
|
22
|
+
howIWorkLabel: 'Cómo trabajo con IA',
|
|
23
|
+
howIWorkMissing: 'Se genera tras la entrevista de onboarding.',
|
|
24
|
+
tractionLabel: 'Actividad',
|
|
25
|
+
sessions90d: 'sesiones (90d)',
|
|
26
|
+
daysPerWeek: 'días/semana',
|
|
27
|
+
agents: (d, p) => `${d} agentes detectados${p != null ? ` · ${p} en tu perfil` : ''}`,
|
|
28
|
+
toolsLabel: 'herramientas',
|
|
29
|
+
axisSetup: 'Setup',
|
|
30
|
+
axisUsage: 'Uso',
|
|
31
|
+
agentsLabel: 'Agentes',
|
|
32
|
+
},
|
|
33
|
+
en: {
|
|
34
|
+
title: 'Your work with AI (Setup x Usage)',
|
|
35
|
+
usage: { REACTIVE: 'Reactive', DELIBERATE: 'Deliberate', RIGOROUS: 'Rigorous' },
|
|
36
|
+
setup: { ASSISTED: 'Assisted', EXTENDED: 'Extended', ORCHESTRATED: 'Orchestrated' },
|
|
37
|
+
aiNative: 'AI Native',
|
|
38
|
+
pending: 'Your profile is being evaluated; check "My work with AI" shortly.',
|
|
39
|
+
matrixUpdating: 'Your Setup x Usage matrix is updating; it will appear shortly.',
|
|
40
|
+
setupOnly: 'Available on your profile after you log in and share the report.',
|
|
41
|
+
visionLabel: 'My vision on AI',
|
|
42
|
+
howIWorkLabel: 'How I work with AI',
|
|
43
|
+
howIWorkMissing: 'Generated after the onboarding interview.',
|
|
44
|
+
tractionLabel: 'Traction',
|
|
45
|
+
sessions90d: 'sessions (90d)',
|
|
46
|
+
daysPerWeek: 'days/week',
|
|
47
|
+
agents: (d, p) => `${d} agents detected${p != null ? ` · ${p} on your profile` : ''}`,
|
|
48
|
+
toolsLabel: 'tools',
|
|
49
|
+
axisSetup: 'Setup',
|
|
50
|
+
axisUsage: 'Usage',
|
|
51
|
+
agentsLabel: 'Agents',
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function copyFor(lang) {
|
|
56
|
+
return COPY[lang === 'es' ? 'es' : 'en'];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// The matrix band can lag behind E/F (independent projection). When absent, render
|
|
60
|
+
// E/F/traction anyway and swap the 3x3 grid for a short "updating" note.
|
|
61
|
+
function matrixReady(matrix) {
|
|
62
|
+
return !!(matrix && (matrix.setupLevel || matrix.usageLevel || matrix.cell));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const noColor = new Proxy({}, { get: () => '' });
|
|
66
|
+
|
|
67
|
+
const esc = (s) => String(s).replace(/[&<>"]/g, (ch) =>
|
|
68
|
+
({ '&': '&', '<': '<', '>': '>', '"': '"' }[ch]));
|
|
69
|
+
|
|
70
|
+
// Traction metrics as compact "label: value" fragments, omitting null fields.
|
|
71
|
+
function tractionParts(traction, t) {
|
|
72
|
+
if (!traction) return [];
|
|
73
|
+
const parts = [];
|
|
74
|
+
if (traction.sessions90d != null) parts.push(`${traction.sessions90d} ${t.sessions90d}`);
|
|
75
|
+
if (traction.activeDaysPerWeek != null) parts.push(`${traction.activeDaysPerWeek} ${t.daysPerWeek}`);
|
|
76
|
+
if (traction.agentsDetected != null) parts.push(t.agents(traction.agentsDetected, traction.agentsOnProfile));
|
|
77
|
+
const tools = detectedToolNames(traction.tools);
|
|
78
|
+
if (tools.length) parts.push(`${t.toolsLabel}: ${tools.join(', ')}`);
|
|
79
|
+
return parts;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function matrixTerminalLines(matrix, t, c) {
|
|
83
|
+
// Column widths from the longest label (incl. current-cell brackets). Pad BEFORE
|
|
84
|
+
// colorizing: padEnd counts ANSI escape bytes, so padding a colored string misaligns.
|
|
85
|
+
const rowLabels = SETUP_ORDER.map((s) => `${SETUP_CODE[s]} ${t.setup[s]}`);
|
|
86
|
+
const rowW = Math.max(...rowLabels.map((s) => s.length)) + 2;
|
|
87
|
+
const cellLabels = SETUP_ORDER.flatMap((s) => USAGE_ORDER.map((u) => CELL_NAMES[s][u]));
|
|
88
|
+
const headerLabels = USAGE_ORDER.map((u) => t.usage[u]);
|
|
89
|
+
const colW = Math.max(...cellLabels.map((x) => x.length), ...headerLabels.map((x) => x.length)) + 4;
|
|
90
|
+
|
|
91
|
+
const lines = [` ${c.bold}${t.title}${c.reset}`];
|
|
92
|
+
lines.push(` ${c.dim}${' '.repeat(rowW) + headerLabels.map((u) => u.padEnd(colW)).join('')}${c.reset}`);
|
|
93
|
+
for (const s of SETUP_ORDER) {
|
|
94
|
+
const rowLabel = `${SETUP_CODE[s]} ${t.setup[s]}`.padEnd(rowW);
|
|
95
|
+
const cells = USAGE_ORDER.map((u) => {
|
|
96
|
+
const name = CELL_NAMES[s][u];
|
|
97
|
+
const isCurrent = matrix.setupLevel === s && matrix.usageLevel === u;
|
|
98
|
+
if (isCurrent) return `${c.accent}${c.bold}${`[${name}]`.padEnd(colW)}${c.reset}`;
|
|
99
|
+
return `${c.muted}${name.padEnd(colW)}${c.reset}`;
|
|
100
|
+
}).join('');
|
|
101
|
+
lines.push(` ${c.dim}${rowLabel}${c.reset}${cells}`);
|
|
102
|
+
}
|
|
103
|
+
return lines;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function buildProfileSections(preview, t) {
|
|
107
|
+
const sections = [];
|
|
108
|
+
const m = (preview && preview.matrix) || {};
|
|
109
|
+
const setupExp = preview.setup && preview.setup.explanation ? preview.setup.explanation : null;
|
|
110
|
+
if (m.setupLevel) {
|
|
111
|
+
const value = [m.setupCode, t.setup[m.setupLevel]].filter(Boolean).join(' ');
|
|
112
|
+
sections.push({ kind: 'axis', label: t.axisSetup, value, explanation: setupExp });
|
|
113
|
+
}
|
|
114
|
+
const usageExp = preview.usage && preview.usage.explanation ? preview.usage.explanation : null;
|
|
115
|
+
if (m.usageLevel) {
|
|
116
|
+
sections.push({ kind: 'axis', label: t.axisUsage, value: t.usage[m.usageLevel] || m.usageLevel, explanation: usageExp });
|
|
117
|
+
}
|
|
118
|
+
if (preview.vision) sections.push({ kind: 'narrative', label: t.visionLabel, body: preview.vision });
|
|
119
|
+
if (preview.howIWork) sections.push({ kind: 'narrative', label: t.howIWorkLabel, body: preview.howIWork });
|
|
120
|
+
return sections;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const axisBody = (s) => (s.explanation ? `${s.value} — ${s.explanation}` : s.value);
|
|
124
|
+
|
|
125
|
+
// Full "My work with AI" preview, terminal. `preview` is the normalized shape from
|
|
126
|
+
// resolveAiProfilePreview; `status` drives the fallbacks.
|
|
127
|
+
function renderAiProfileTerminal({ status, preview } = {}, { lang = 'en', c = noColor } = {}) {
|
|
128
|
+
const t = copyFor(lang);
|
|
129
|
+
// Non-fatal notices (not-yet-judged / setup-only) → yellow.
|
|
130
|
+
if (status === 'unavailable') return ` ${c.warning || c.muted}${t.setupOnly}${c.reset}`;
|
|
131
|
+
if (status !== 'ready' || !preview) return ` ${c.warning || c.muted}${t.pending}${c.reset}`;
|
|
132
|
+
|
|
133
|
+
const lines = matrixReady(preview.matrix)
|
|
134
|
+
? matrixTerminalLines(preview.matrix, t, c)
|
|
135
|
+
: [` ${c.bold}${t.title}${c.reset}`, ` ${c.warning || c.muted}${t.matrixUpdating}${c.reset}`];
|
|
136
|
+
const sections = buildProfileSections(preview, t);
|
|
137
|
+
const axes = sections.filter((s) => s.kind === 'axis');
|
|
138
|
+
if (axes.length) {
|
|
139
|
+
lines.push('');
|
|
140
|
+
for (const a of axes) lines.push(` ${c.bold}${a.label}:${c.reset} ${c.muted}${axisBody(a)}${c.reset}`);
|
|
141
|
+
}
|
|
142
|
+
for (const n of sections.filter((s) => s.kind === 'narrative')) {
|
|
143
|
+
lines.push('', ` ${c.bold}${n.label}${c.reset}`, ` ${c.muted}${n.body}${c.reset}`);
|
|
144
|
+
}
|
|
145
|
+
// Traction now lives in the Activity section (terminal-agents#printActivity).
|
|
146
|
+
return lines.join('\n');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function plainAgentLines(agents) {
|
|
150
|
+
if (!Array.isArray(agents) || agents.length === 0) return [];
|
|
151
|
+
return agents.map((a) => {
|
|
152
|
+
const bits = [a.category, a.role].filter(Boolean);
|
|
153
|
+
return bits.length ? `- ${a.name} — ${bits.join(' · ')}` : `- ${a.name}`;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Full "My work with AI" report as plain markdown (NO ANSI), for the MCP tool so a chat client shows the same report the CLI terminal prints.
|
|
158
|
+
function renderAiProfilePlain({ status, preview } = {}, { lang = 'en', agents = [] } = {}) {
|
|
159
|
+
const t = copyFor(lang);
|
|
160
|
+
const out = [];
|
|
161
|
+
if (status === 'unavailable') {
|
|
162
|
+
out.push(t.setupOnly);
|
|
163
|
+
} else if (status !== 'ready' || !preview) {
|
|
164
|
+
out.push(t.pending);
|
|
165
|
+
} else {
|
|
166
|
+
if (matrixReady(preview.matrix)) {
|
|
167
|
+
const gridLines = matrixTerminalLines(preview.matrix, t, noColor).slice(1);
|
|
168
|
+
out.push(`## ${t.title}`, '', '```', ...gridLines, '```');
|
|
169
|
+
} else {
|
|
170
|
+
out.push(`## ${t.title}`, '', t.matrixUpdating);
|
|
171
|
+
}
|
|
172
|
+
const sections = buildProfileSections(preview, t);
|
|
173
|
+
const axes = sections.filter((s) => s.kind === 'axis');
|
|
174
|
+
if (axes.length) {
|
|
175
|
+
out.push('');
|
|
176
|
+
for (const a of axes) out.push(`${a.label}: ${axisBody(a)}`);
|
|
177
|
+
}
|
|
178
|
+
for (const n of sections.filter((s) => s.kind === 'narrative')) {
|
|
179
|
+
out.push('', `**${n.label}**`, n.body);
|
|
180
|
+
}
|
|
181
|
+
const tp = tractionParts(preview.traction, t);
|
|
182
|
+
if (tp.length) out.push('', `**${t.tractionLabel}:** ${tp.join(' · ')}`);
|
|
183
|
+
}
|
|
184
|
+
const agentLines = plainAgentLines(agents);
|
|
185
|
+
if (agentLines.length) out.push('', `**${t.agentsLabel}**`, ...agentLines);
|
|
186
|
+
return out.join('\n');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Full "My work with AI" preview, HTML. '' when there is nothing to show inline.
|
|
190
|
+
function renderAiProfileHtml({ status, preview } = {}, { lang = 'en' } = {}) {
|
|
191
|
+
const t = copyFor(lang);
|
|
192
|
+
if (status === 'unavailable') return `<p class="ai-fluency-note">${esc(t.setupOnly)}</p>`;
|
|
193
|
+
if (status !== 'ready' || !preview) return `<p class="ai-fluency-note">${esc(t.pending)}</p>`;
|
|
194
|
+
|
|
195
|
+
const m = preview.matrix;
|
|
196
|
+
const out = [
|
|
197
|
+
`<section class="ai-fluency" aria-label="${esc(t.title)}">`,
|
|
198
|
+
`<h3>${esc(t.title)}</h3>`,
|
|
199
|
+
];
|
|
200
|
+
if (matrixReady(m)) {
|
|
201
|
+
const rows = SETUP_ORDER.map((s) => {
|
|
202
|
+
const cells = USAGE_ORDER.map((u) => {
|
|
203
|
+
const current = m.setupLevel === s && m.usageLevel === u;
|
|
204
|
+
return `<td class="ai-fluency-cell${current ? ' is-current' : ''}">${esc(CELL_NAMES[s][u])}</td>`;
|
|
205
|
+
}).join('');
|
|
206
|
+
return `<tr><th scope="row">${esc(SETUP_CODE[s])} ${esc(t.setup[s])}</th>${cells}</tr>`;
|
|
207
|
+
}).join('');
|
|
208
|
+
const head = USAGE_ORDER.map((u) => `<th scope="col">${esc(t.usage[u])}</th>`).join('');
|
|
209
|
+
out.push(
|
|
210
|
+
'<table class="ai-fluency-matrix">',
|
|
211
|
+
`<thead><tr><th></th>${head}</tr></thead>`,
|
|
212
|
+
`<tbody>${rows}</tbody>`,
|
|
213
|
+
'</table>',
|
|
214
|
+
);
|
|
215
|
+
} else {
|
|
216
|
+
out.push(`<p class="ai-fluency-note">${esc(t.matrixUpdating)}</p>`);
|
|
217
|
+
}
|
|
218
|
+
if (m.cell) {
|
|
219
|
+
out.push(`<p class="ai-fluency-current">${esc(m.setupCode)}·${esc(t.usage[m.usageLevel] || m.usageLevel)} → <strong>${esc(m.cell)}</strong>${m.isAiNative ? ` <span class="ai-native">★ ${esc(t.aiNative)}</span>` : ''}</p>`);
|
|
220
|
+
}
|
|
221
|
+
if (preview.vision) {
|
|
222
|
+
out.push(`<div class="ai-vision"><h4>${esc(t.visionLabel)}</h4><p>${esc(preview.vision)}</p></div>`);
|
|
223
|
+
}
|
|
224
|
+
out.push(`<div class="ai-howiwork"><h4>${esc(t.howIWorkLabel)}</h4><p>${preview.howIWork ? esc(preview.howIWork) : `<em>${esc(t.howIWorkMissing)}</em>`}</p></div>`);
|
|
225
|
+
const parts = tractionParts(preview.traction, t);
|
|
226
|
+
if (parts.length) {
|
|
227
|
+
out.push(`<p class="ai-traction"><strong>${esc(t.tractionLabel)}:</strong> ${esc(parts.join(' · '))}</p>`);
|
|
228
|
+
}
|
|
229
|
+
out.push('</section>');
|
|
230
|
+
return out.join('');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Matrix-only HTML (kept for render-sheet.js; the HTML report is a separate iteration).
|
|
234
|
+
function renderAiFluencyMatrixHtml({ status, usage } = {}, { lang = 'en' } = {}) {
|
|
235
|
+
const t = copyFor(lang);
|
|
236
|
+
if (status === 'unavailable') return `<p class="ai-fluency-note">${esc(t.setupOnly)}</p>`;
|
|
237
|
+
if (status !== 'ready' || !usage || !usage.cell) return `<p class="ai-fluency-note">${esc(t.pending)}</p>`;
|
|
238
|
+
const rows = SETUP_ORDER.map((s) => {
|
|
239
|
+
const cells = USAGE_ORDER.map((u) => {
|
|
240
|
+
const current = usage.setupLevel === s && usage.usageLevel === u;
|
|
241
|
+
return `<td class="ai-fluency-cell${current ? ' is-current' : ''}">${esc(CELL_NAMES[s][u])}</td>`;
|
|
242
|
+
}).join('');
|
|
243
|
+
return `<tr><th scope="row">${esc(SETUP_CODE[s])} ${esc(t.setup[s])}</th>${cells}</tr>`;
|
|
244
|
+
}).join('');
|
|
245
|
+
const head = USAGE_ORDER.map((u) => `<th scope="col">${esc(t.usage[u])}</th>`).join('');
|
|
246
|
+
return [
|
|
247
|
+
`<section class="ai-fluency" aria-label="${esc(t.title)}">`,
|
|
248
|
+
`<h3>${esc(t.title)}</h3>`,
|
|
249
|
+
'<table class="ai-fluency-matrix">',
|
|
250
|
+
`<thead><tr><th></th>${head}</tr></thead>`,
|
|
251
|
+
`<tbody>${rows}</tbody></table>`,
|
|
252
|
+
`<p class="ai-fluency-current">${esc(usage.setupCode)}·${esc(t.usage[usage.usageLevel] || usage.usageLevel)} → <strong>${esc(usage.cell)}</strong>${usage.isAiNative ? ` <span class="ai-native">★ ${esc(t.aiNative)}</span>` : ''}</p>`,
|
|
253
|
+
'</section>',
|
|
254
|
+
].join('');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
module.exports = {
|
|
258
|
+
renderAiProfileTerminal,
|
|
259
|
+
renderAiProfilePlain,
|
|
260
|
+
renderAiProfileHtml,
|
|
261
|
+
renderAiFluencyMatrixHtml,
|
|
262
|
+
copyFor,
|
|
263
|
+
tractionParts,
|
|
264
|
+
SETUP_ORDER,
|
|
265
|
+
USAGE_ORDER,
|
|
266
|
+
};
|
|
@@ -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 };
|