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,17 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Shakers hand-drawn "shakers" WORDMARK, inlined VERBATIM from the design asset shakers-hub-frontend/apps/hub/public/images/shakers-text-logo.svg (viewBox 0 0 87 18, 7 glyph <path>s).
|
|
4
|
+
|
|
5
|
+
const WORDMARK_VIEWBOX = { w: 87, h: 18 };
|
|
6
|
+
|
|
7
|
+
const WORDMARK_PATHS = [
|
|
8
|
+
'M19.2845 8.74573C19.2845 8.74573 16.556 10.0876 15.7698 10.5041C15.7236 11.4295 15.6311 12.355 15.5848 13.2804C15.5386 14.3447 15.4924 14.9462 15.4461 16.0105C15.4461 16.2419 15.4461 16.4732 15.3999 16.6583C15.2611 17.445 14.8449 17.9077 14.2437 17.9077C13.55 17.9077 13.0876 17.3987 13.0876 16.5658C13.0876 15.1776 13.1801 14.2059 13.2263 12.8177C13.2263 12.4475 13.2263 11.8923 13.2263 11.4758C13.2263 11.4758 12.8101 11.6609 12.5326 11.7072C12.0701 11.6609 11.7002 11.4295 11.5614 10.9668C11.4227 10.5041 11.5614 10.0876 11.9314 9.76372C12.0239 9.71745 13.5963 8.74573 13.5963 8.74573C13.6425 6.80228 13.735 4.07221 13.8737 2.17503C13.92 1.80485 13.9662 1.43467 14.105 1.11077C14.2437 0.694314 14.6137 0.462951 15.0761 0.509223C15.5386 0.509223 15.8623 0.786858 15.9548 1.20331C16.0473 1.57349 16.0935 1.98994 16.0935 2.4064C16.0473 3.93339 16.0011 4.95138 15.9548 6.47837C15.9548 6.84856 15.9548 7.21873 15.9548 7.774C17.7122 6.9411 17.8971 6.70974 19.562 5.92311C19.6545 4.53493 19.7007 3.65575 19.7932 2.26758C19.8395 1.80485 19.8857 1.29586 19.9782 0.833131C20.117 0.324133 20.4869 0.000226092 21.0419 0.0464986C21.5968 0.0927711 21.9205 0.416678 22.013 0.925676C22.0593 1.34213 22.0593 1.71231 22.0593 2.12876C22.0593 3.05421 21.9668 3.7483 21.9668 4.81257C22.2905 4.72002 22.7067 4.30357 23.0304 4.2573C23.6779 4.07221 24.2791 4.34984 24.5103 4.90511C24.7416 5.41411 24.5566 5.96938 24.0016 6.33956C23.5854 6.52465 21.7356 7.68146 21.7356 7.68146C21.6893 10.6892 21.5968 12.8177 21.5043 15.8717C21.5043 16.1493 21.5043 16.4732 21.4581 16.7509C21.3193 17.445 20.8569 17.8614 20.2557 17.8151C19.6545 17.7689 19.2383 17.3524 19.1458 16.6583C19.0995 16.3807 19.192 12.4013 19.2845 9.76372C19.2845 9.39354 19.2845 9.20845 19.2845 8.74573Z',
|
|
9
|
+
'M64.097 4.67388C63.542 4.72016 63.1258 4.76643 62.7558 4.76643C61.9697 4.76643 61.5072 4.48879 61.4147 3.88725C61.3222 3.28571 61.6459 2.82299 62.3859 2.59162C62.9408 2.40653 63.542 2.31399 64.097 2.22144C65.9931 1.99008 67.8429 1.89754 69.6927 2.45281C70.3402 2.6379 70.9414 2.91553 71.4963 3.28571C72.8837 4.21116 73.1612 5.73815 72.2825 7.1726C71.7738 8.0055 70.9876 8.60705 70.1552 9.06977C69.6002 9.39368 69.0453 9.67131 68.3978 9.99522C68.8603 10.4117 73.1149 13.5582 73.4849 14.0209C73.8549 14.4837 73.8086 15.1315 73.4386 15.5479C73.0687 15.9644 72.56 16.0569 72.005 15.8256C71.6813 15.6405 71.4038 15.4091 71.0801 15.1777C69.8315 14.206 67.6579 12.6328 66.3168 11.5685C66.2705 12.0312 65.9931 15.0389 65.9468 16.1032C65.9468 16.3808 65.9468 16.7047 65.8543 16.9824C65.7156 17.5376 65.2069 17.8615 64.6982 17.8153C64.1432 17.769 63.7733 17.4914 63.6345 16.9824C63.542 16.5659 63.542 16.1032 63.542 15.6867C63.727 12.6328 64.097 5.18288 64.097 4.67388ZM66.4555 8.14432C67.7504 7.63532 68.9528 7.1726 69.8777 6.24715C70.2477 5.87697 70.1552 5.27543 69.6927 5.04406C68.6753 4.58134 67.6117 4.53507 66.548 4.44252L66.4555 8.14432Z',
|
|
10
|
+
'M32.2335 12.8178C31.9097 13.1879 31.6785 13.4656 31.4473 13.6969C30.6149 14.4836 29.8287 15.3627 28.95 16.0568C28.4413 16.4733 27.7939 16.7047 27.1464 16.8897C25.7128 17.2599 24.4641 16.427 24.2792 14.8538C24.1867 14.0208 24.2329 13.0954 24.4641 12.2625C25.3428 9.02342 27.1002 6.29334 29.6437 4.1648C30.2449 3.65581 30.9386 3.28563 31.6323 3.00799C33.5746 2.22136 35.1007 3.8409 35.1007 5.55298C35.0082 6.5247 34.7308 12.4939 34.6845 14.4373C34.6845 14.7612 34.6845 15.1314 34.592 15.4553C34.4533 16.0568 33.9446 16.4733 33.3896 16.427C32.8809 16.3807 32.4185 16.0106 32.326 15.409C32.2335 14.8538 32.2797 14.2985 32.2797 13.6969C32.2797 13.5118 32.2797 13.2342 32.2335 12.8178ZM26.9614 14.3448C29.6899 13.2805 32.6497 8.51442 32.3722 5.64552C32.3722 5.46043 32.1872 5.36789 32.0485 5.46043C29.6899 6.5247 26.684 11.337 26.684 14.1134C26.684 14.2522 26.8227 14.391 26.9614 14.3448Z',
|
|
11
|
+
'M40.4189 7.91296C41.9451 6.84869 43.2399 5.92324 44.5348 5.04406C44.8586 4.8127 45.1823 4.58134 45.5522 4.39625C46.2459 4.02607 46.8934 4.16489 47.2171 4.67388C47.5408 5.18288 47.4483 5.73815 46.8934 6.24715C46.5234 6.57106 46.1534 6.84869 45.7372 7.12633C44.3498 8.09805 42.9625 9.06977 41.4363 10.134C41.8063 10.3191 41.9913 10.5042 42.2688 10.5968C43.8874 11.3834 45.4598 12.1238 47.0784 12.8641C47.4483 13.0492 47.8183 13.2343 48.0958 13.5119C48.512 13.8821 48.5582 14.3448 48.327 14.8538C48.0958 15.3166 47.6333 15.5479 47.1246 15.4554C46.7546 15.4091 46.4309 15.224 46.1072 15.0852C44.5811 14.3448 43.055 13.6045 41.4826 12.9104C41.2051 12.7716 40.8814 12.6328 40.4652 12.4939C40.4189 12.8178 40.3727 13.0492 40.3727 13.2806C40.3727 14.3448 40.3264 14.9464 40.3264 16.0569C40.3264 16.3808 40.3264 16.7047 40.2802 17.0286C40.1415 17.6765 39.7252 18.0466 39.1703 18.0466C38.6153 18.0466 38.1529 17.6765 38.0141 17.0286C37.9679 16.7047 37.9679 16.3808 37.9679 16.0569C38.0141 10.643 38.1066 7.58905 38.1529 2.17517C38.1529 1.75872 38.1991 1.34227 38.2454 0.972085C38.3379 0.463088 38.7078 0.139181 39.2628 0.092908C39.7715 0.000363052 40.234 0.370543 40.4189 0.925813C40.5114 1.29599 40.5114 2.03635 40.5114 2.03635C40.5114 4.16489 40.4189 4.48879 40.3727 6.61733C40.3727 6.94124 40.3727 7.31142 40.4189 7.91296Z',
|
|
12
|
+
'M51.5643 9.20847C50.7781 8.88456 50.5468 8.09793 51.0093 7.35757C51.148 7.12621 51.3793 6.89485 51.6105 6.70976C53.8766 4.48868 55.4027 3.19305 57.6687 1.01824C57.9462 0.740604 58.2237 0.509241 58.5012 0.324151C59.0099 0.000243835 59.5648 0.0465163 59.9348 0.416696C60.3048 0.786876 60.351 1.34215 60.0273 1.85114C59.796 2.17505 59.5186 2.49896 59.2411 2.77659C57.6687 4.34986 56.8825 4.9514 55.3102 6.52467C55.079 6.75603 54.8015 7.03366 54.4315 7.45012C55.5877 7.68148 56.5126 7.91284 57.4837 8.1442C57.9462 8.23675 58.4087 8.32929 58.8711 8.46811C59.3798 8.60693 59.796 8.93084 59.8885 9.53238C59.981 10.0876 59.7036 10.5504 59.1024 10.828C57.9462 11.337 56.7901 11.7535 55.5877 12.2162C54.894 12.4938 54.154 12.7715 53.2754 13.1416C53.6453 13.373 56.1889 14.7149 57.345 15.3627C57.6225 15.5478 57.9462 15.6866 58.1774 15.918C58.6399 16.3344 58.7786 16.936 58.5012 17.3987C58.2237 17.8614 57.6687 18.0928 57.0675 17.9077C56.8363 17.8152 56.6051 17.7226 56.3738 17.5838C54.339 16.427 52.4429 15.5478 50.4081 14.3447C50.2694 14.2522 50.0844 14.1596 49.9456 14.0671C49.067 13.373 49.1132 12.355 50.1306 11.846C50.9168 11.4296 55.2177 9.9951 55.2177 9.9951C55.2177 9.9951 51.9342 9.39356 51.5643 9.20847Z',
|
|
13
|
+
'M83.0116 9.9949C82.0866 9.90236 81.4392 9.85608 80.7455 9.80981C79.7281 9.71727 78.7107 9.62472 77.6933 9.53218C77.4158 9.4859 77.1383 9.43963 76.8608 9.30081C75.566 8.79182 75.1497 7.49619 76.0284 6.2931C76.4446 5.69156 76.9533 5.18256 77.5083 4.71984C79.4044 3.1003 81.5779 2.12858 83.7977 1.20313C83.9827 1.11058 84.214 1.01804 84.4452 1.06431C84.7227 1.11058 85.0464 1.2494 85.1389 1.48076C85.2314 1.7584 85.2314 2.22112 85.0926 2.45248C84.8152 2.82266 84.4452 3.05403 84.0752 3.28539C83.0116 3.88693 81.9016 4.4422 80.8842 5.04374C79.9593 5.59901 79.0806 6.15428 78.2482 7.21855C78.8494 7.26482 79.3119 7.35737 79.7743 7.40364C81.4392 7.54246 83.1041 7.635 84.7689 7.77382C85.1389 7.82009 85.5088 7.86637 85.8326 8.00518C86.9425 8.37536 87.2199 9.43963 86.4338 10.3651C86.0175 10.8741 85.4626 11.3368 84.9076 11.707C83.7052 12.5399 82.4104 13.3265 81.208 14.2057C80.2368 14.8998 79.2656 15.5939 78.2945 16.288C77.9245 16.5193 77.5545 16.797 77.1383 16.9358C76.6296 17.1209 76.2134 16.8895 75.9359 16.4268C75.6584 15.9641 75.7047 15.5013 76.0284 15.0849C76.2596 14.8072 76.5834 14.5759 76.8608 14.3445C78.7107 13.0489 80.5605 11.7995 82.3641 10.5502C82.5029 10.4576 82.6416 10.3188 83.0116 9.9949Z',
|
|
14
|
+
'M3.56097 8.32919C4.53213 8.51428 5.22582 8.65309 5.96576 8.74564C6.79819 8.88446 7.63061 8.93073 8.50929 9.02327C9.20298 9.11582 9.80418 9.39345 10.0354 10.1338C10.2666 10.9204 9.89667 11.4757 9.34172 11.9384C9.06424 12.1698 8.74052 12.4012 8.46304 12.6325C6.38197 14.1132 4.3009 15.594 2.21983 17.0747C2.08109 17.1672 1.94236 17.306 1.75737 17.3986C1.24867 17.6762 0.693714 17.5837 0.323746 17.1672C-0.0462222 16.7508 -0.0924682 16.1955 0.231254 15.6865C0.462484 15.3163 0.786206 15.0387 1.10993 14.8073C2.63605 13.6968 4.16216 12.6325 5.87327 11.3832C5.36456 11.2444 5.04084 11.1981 4.71712 11.1055C3.83844 10.9204 2.95977 10.7816 2.12734 10.504C0.601222 9.94872 0.231254 8.60682 1.29491 7.31119C1.9886 6.47829 2.82103 5.73793 3.65346 5.04384C4.80961 4.16466 6.05825 3.37803 7.30689 2.5914C8.27806 1.94358 8.97175 1.98985 9.38796 2.63767C9.80418 3.28548 9.5267 3.9333 8.60178 4.62739C7.44563 5.46029 6.24323 6.24692 5.08708 7.07983C4.62462 7.40374 4.20841 7.77392 3.56097 8.32919Z',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
module.exports = { WORDMARK_VIEWBOX, WORDMARK_PATHS };
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const { getCatalog } = require('./i18n');
|
|
7
|
+
const { setupLevelForTier } = require('./tier-engine');
|
|
8
|
+
const { renderDocument } = require('./report-theme');
|
|
9
|
+
const { loadState, configDir, projectSlug, fileUrl } = require('./report-store');
|
|
10
|
+
const { WORDMARK_VIEWBOX, WORDMARK_PATHS } = require('./shakers-wordmark');
|
|
11
|
+
|
|
12
|
+
// LinkedIn shares render best at 1.91:1 (1200×627). Fixed so the browser-side
|
|
13
|
+
// canvas export writes exactly these pixels.
|
|
14
|
+
const CARD_W = 1200;
|
|
15
|
+
const CARD_H = 627;
|
|
16
|
+
|
|
17
|
+
// Brand palette (from shakers-design-system/design-spec/tokens.css).
|
|
18
|
+
const BRAND = {
|
|
19
|
+
lime: '#d8e637', // hero accent (tier, score arc, stat numbers)
|
|
20
|
+
dark: '#181B1A', // card background
|
|
21
|
+
white: '#ffffff',
|
|
22
|
+
zinc300: '#d4d4d8', // stat labels / technology names (light, secondary)
|
|
23
|
+
zinc400: '#a1a1aa', // "/ 100", muted labels
|
|
24
|
+
zinc500: '#71717a', // shakersworks.com, separators
|
|
25
|
+
teal500: '#0e7d69', // teal-500 — accent stripe + eyebrow
|
|
26
|
+
teal700: '#08473c', // teal-700 — band pill + score-ring track
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Font stack shared with the reports (Inter -> system fallback).
|
|
30
|
+
const FONT_STACK =
|
|
31
|
+
"Inter, ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif";
|
|
32
|
+
|
|
33
|
+
function esc(s) {
|
|
34
|
+
return String(s == null ? '' : s).replace(/[&<>"']/g, (ch) => ({
|
|
35
|
+
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
|
36
|
+
}[ch]));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function loadProjectFootprint(root, { load = loadState } = {}) {
|
|
40
|
+
const absRoot = path.resolve(root || process.cwd());
|
|
41
|
+
let state;
|
|
42
|
+
try {
|
|
43
|
+
state = load();
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const project = state && state.projects && state.projects[absRoot];
|
|
48
|
+
const fp = project && project.footprint;
|
|
49
|
+
const m = fp && fp.maturity;
|
|
50
|
+
if (!m || m.tierKey == null || typeof m.score !== 'number') return null;
|
|
51
|
+
|
|
52
|
+
// Derived signals from the persisted report (defensive: any missing piece
|
|
53
|
+
// reads as 0 / empty — an older stored report never crashes the card).
|
|
54
|
+
const report = (fp && fp.report) || {};
|
|
55
|
+
const counts = report.agentCounts || {};
|
|
56
|
+
const toolsDetected = Array.isArray(report.tools)
|
|
57
|
+
? report.tools.filter((tl) => tl && tl.detected).length
|
|
58
|
+
: 0;
|
|
59
|
+
const mcpTotal = report.mcp && typeof report.mcp.total === 'number'
|
|
60
|
+
? report.mcp.total
|
|
61
|
+
: (typeof counts.mcpServers === 'number' ? counts.mcpServers : 0);
|
|
62
|
+
const technologies = Array.isArray(report.technologies)
|
|
63
|
+
? report.technologies.filter((s) => typeof s === 'string' && s.trim())
|
|
64
|
+
: [];
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
root: absRoot,
|
|
68
|
+
tier: m.tier,
|
|
69
|
+
tierKey: m.tierKey,
|
|
70
|
+
score: m.score,
|
|
71
|
+
level: m.level,
|
|
72
|
+
levelKey: m.key,
|
|
73
|
+
// ADR-016: Setup Level for the card pill (replaces the 0-4 band). Prefer the
|
|
74
|
+
// persisted `setupLevel`; derive from the tier for older stored reports.
|
|
75
|
+
setupLevelKey: (m.setupLevel && m.setupLevel.key)
|
|
76
|
+
|| (typeof m.tier === 'number' ? setupLevelForTier(m.tier).key : 'none'),
|
|
77
|
+
generatedAt: fp.generatedAt || null,
|
|
78
|
+
signals: {
|
|
79
|
+
toolsDetected,
|
|
80
|
+
mcpServers: mcpTotal,
|
|
81
|
+
agents: typeof counts.agents === 'number' ? counts.agents : 0,
|
|
82
|
+
skills: typeof counts.skills === 'number' ? counts.skills : 0,
|
|
83
|
+
commands: typeof counts.commands === 'number' ? counts.commands : 0,
|
|
84
|
+
hooks: typeof counts.hooks === 'number' ? counts.hooks : 0,
|
|
85
|
+
technologies,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// How many top technologies to name on the card (kept small — social card).
|
|
91
|
+
const MAX_TECHNOLOGIES = 4;
|
|
92
|
+
|
|
93
|
+
// Maps the raw footprint into the card's display model.
|
|
94
|
+
function buildCardModel(fp) {
|
|
95
|
+
const en = getCatalog('en');
|
|
96
|
+
const tierKey = fp.tierKey;
|
|
97
|
+
const tierName = (en.tierNames && en.tierNames[tierKey]) || tierKey;
|
|
98
|
+
// ADR-016: Setup Level label for the pill (English-fixed like the tier name).
|
|
99
|
+
const setupKey = fp.setupLevelKey || 'none';
|
|
100
|
+
const bandName = (en.setupLevels && en.setupLevels[setupKey] && en.setupLevels[setupKey].label) || '';
|
|
101
|
+
|
|
102
|
+
const s = fp.signals || {};
|
|
103
|
+
const n = (v) => (typeof v === 'number' && isFinite(v) ? v : 0);
|
|
104
|
+
// Only stats with a value > 0 make the card — never a "0 skills" filler.
|
|
105
|
+
// Order is the reading order the label list implies (tools -> hooks).
|
|
106
|
+
const stats = [
|
|
107
|
+
{ value: n(s.toolsDetected), label: 'AI tools' },
|
|
108
|
+
{ value: n(s.mcpServers), label: 'MCP' },
|
|
109
|
+
{ value: n(s.agents), label: 'agents' },
|
|
110
|
+
{ value: n(s.skills), label: 'skills' },
|
|
111
|
+
{ value: n(s.commands), label: 'commands' },
|
|
112
|
+
{ value: n(s.hooks), label: 'hooks' },
|
|
113
|
+
].filter((st) => st.value > 0);
|
|
114
|
+
const technologies = Array.isArray(s.technologies) ? s.technologies.slice(0, MAX_TECHNOLOGIES) : [];
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
tierKey,
|
|
118
|
+
tierName,
|
|
119
|
+
bandName,
|
|
120
|
+
score: Math.max(0, Math.min(100, Math.round(fp.score))),
|
|
121
|
+
stats,
|
|
122
|
+
technologies,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Suggested LinkedIn caption, built deterministically from the model. English.
|
|
127
|
+
function buildSuggestedText(model) {
|
|
128
|
+
return (
|
|
129
|
+
`I just measured my AI tooling maturity with the Shakers AI Usage Evaluator: `
|
|
130
|
+
+ `tier ${model.tierKey} (${model.tierName}), scoring ${model.score}/100. `
|
|
131
|
+
+ `How do you work with AI? Measure yours at shakersworks.com `
|
|
132
|
+
+ `#AI #DeveloperExperience #AITooling #Shakers`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Builds the branded card as a SELF-CONTAINED SVG (1200×627).
|
|
137
|
+
function renderCardSvg(model, { id = 'share-card-svg' } = {}) {
|
|
138
|
+
const { tierKey, tierName, bandName, score, stats, technologies } = model;
|
|
139
|
+
|
|
140
|
+
// Logo: the REAL hand-drawn "shakers" wordmark (87×18 asset), ~44px tall, top-left, in white.
|
|
141
|
+
const wordmarkScale = 44 / WORDMARK_VIEWBOX.h; // ≈ 2.44
|
|
142
|
+
const wordmark = WORDMARK_PATHS
|
|
143
|
+
.map((d) => `<path d="${d}" fill="${BRAND.white}"/>`)
|
|
144
|
+
.join('');
|
|
145
|
+
|
|
146
|
+
// Score donut ring (right). Deterministic geometry from the score.
|
|
147
|
+
const cx = 980;
|
|
148
|
+
const cy = 262;
|
|
149
|
+
const r = 118;
|
|
150
|
+
const circ = 2 * Math.PI * r;
|
|
151
|
+
const dashOffset = (circ * (1 - score / 100)).toFixed(2);
|
|
152
|
+
|
|
153
|
+
// Stats: a single LIGHT line — only signals with a value > 0 (buildCardModel
|
|
154
|
+
// already filtered zeros out). Number in lime, label muted, " · " separators.
|
|
155
|
+
const SEP = `<tspan fill="${BRAND.zinc500}"> · </tspan>`;
|
|
156
|
+
const statsInline = (Array.isArray(stats) ? stats : [])
|
|
157
|
+
.map((st) => `<tspan font-weight="800" fill="${BRAND.lime}">${esc(st.value)}</tspan><tspan fill="${BRAND.zinc300}"> ${esc(st.label)}</tspan>`)
|
|
158
|
+
.join(SEP);
|
|
159
|
+
const statsLine = stats && stats.length
|
|
160
|
+
? `<text x="82" y="500" font-size="27" fill="${BRAND.zinc300}">${statsInline}</text>`
|
|
161
|
+
: '';
|
|
162
|
+
|
|
163
|
+
// Technologies: one muted line ("Top: A · B · C"), only when present.
|
|
164
|
+
const techLine = Array.isArray(technologies) && technologies.length
|
|
165
|
+
? `<text x="82" y="546" font-size="22" fill="${BRAND.zinc400}">Top: <tspan fill="${BRAND.white}">${esc(technologies.join(' · '))}</tspan></text>`
|
|
166
|
+
: '';
|
|
167
|
+
|
|
168
|
+
// Band pill (teal, secondary) with a lime status dot before the band name.
|
|
169
|
+
const pillWidth = Math.max(150, 58 + bandName.length * 12);
|
|
170
|
+
|
|
171
|
+
return `<svg id="${id}" width="${CARD_W}" height="${CARD_H}" viewBox="0 0 ${CARD_W} ${CARD_H}"`
|
|
172
|
+
+ ` xmlns="http://www.w3.org/2000/svg" font-family="${esc(FONT_STACK)}">
|
|
173
|
+
<rect width="${CARD_W}" height="${CARD_H}" fill="${BRAND.dark}"/>
|
|
174
|
+
<rect x="0" y="0" width="12" height="${CARD_H}" fill="${BRAND.teal500}"/>
|
|
175
|
+
|
|
176
|
+
<!-- logo: real hand-drawn shakers wordmark (inline vector paths, self-contained) -->
|
|
177
|
+
<g transform="translate(80,56) scale(${wordmarkScale.toFixed(4)})">${wordmark}</g>
|
|
178
|
+
<text x="1160" y="92" text-anchor="end" font-size="19" fill="${BRAND.zinc500}">shakersworks.com</text>
|
|
179
|
+
|
|
180
|
+
<!-- eyebrow (teal, letterspaced) -->
|
|
181
|
+
<text x="82" y="176" font-size="25" letter-spacing="4" font-weight="700" fill="${BRAND.teal500}">AI TOOLING MATURITY</text>
|
|
182
|
+
|
|
183
|
+
<!-- tier (hero, lime) + tier name -->
|
|
184
|
+
<text x="76" y="330" font-size="150" font-weight="800" fill="${BRAND.lime}">${esc(tierKey)}</text>
|
|
185
|
+
<text x="82" y="392" font-size="40" font-weight="700" fill="${BRAND.white}">${esc(tierName)}</text>
|
|
186
|
+
|
|
187
|
+
<!-- maturity band pill (teal, secondary) with a lime status dot -->
|
|
188
|
+
<g transform="translate(82,414)">
|
|
189
|
+
<rect x="0" y="0" rx="15" ry="15" width="${pillWidth}" height="40" fill="${BRAND.teal700}"/>
|
|
190
|
+
<circle cx="24" cy="20" r="6" fill="${BRAND.lime}"/>
|
|
191
|
+
<text x="42" y="27" font-size="19" font-weight="700" fill="${BRAND.white}">${esc(bandName)}</text>
|
|
192
|
+
</g>
|
|
193
|
+
|
|
194
|
+
<!-- score donut (hero, lime arc on a teal track) -->
|
|
195
|
+
<g transform="translate(${cx},${cy})">
|
|
196
|
+
<circle r="${r}" fill="none" stroke="${BRAND.teal700}" stroke-width="22"/>
|
|
197
|
+
<circle r="${r}" fill="none" stroke="${BRAND.lime}" stroke-width="22" stroke-linecap="round"
|
|
198
|
+
stroke-dasharray="${circ.toFixed(2)}" stroke-dashoffset="${dashOffset}" transform="rotate(-90)"/>
|
|
199
|
+
<text text-anchor="middle" y="14" font-size="86" font-weight="800" fill="${BRAND.white}">${score}</text>
|
|
200
|
+
<text text-anchor="middle" y="56" font-size="24" letter-spacing="2" fill="${BRAND.zinc400}">/ 100</text>
|
|
201
|
+
</g>
|
|
202
|
+
|
|
203
|
+
<!-- light stats line + technologies (no panel, no footer; bottom edge breathes) -->
|
|
204
|
+
${statsLine}
|
|
205
|
+
${techLine}
|
|
206
|
+
</svg>`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Component CSS for the HTML wrapper (the page around the card). Reuses the
|
|
210
|
+
// report theme tokens (var(--...)) from report-theme.js's renderDocument.
|
|
211
|
+
const SHARE_CSS = `
|
|
212
|
+
.card-frame{margin:8px 0 22px}
|
|
213
|
+
.card-svg-wrap{max-width:760px;border-radius:var(--r-xl);overflow:hidden;
|
|
214
|
+
box-shadow:var(--shadow-lg);border:1px solid var(--border);background:#181B1A}
|
|
215
|
+
.card-svg-wrap svg{display:block;width:100%;height:auto}
|
|
216
|
+
.actions{display:flex;flex-wrap:wrap;gap:12px;margin:0 0 18px}
|
|
217
|
+
.btn{display:inline-flex;align-items:center;gap:8px;font:inherit;font-size:15px;
|
|
218
|
+
font-weight:600;padding:11px 20px;border-radius:var(--r-md);border:1px solid transparent;
|
|
219
|
+
cursor:pointer;text-decoration:none;line-height:1}
|
|
220
|
+
.btn-primary{background:var(--primary);color:var(--primary-fg)}
|
|
221
|
+
.btn-primary:hover{background:var(--emphasis-strong)}
|
|
222
|
+
.btn-secondary{background:var(--accent-lime);color:var(--accent-lime-fg)}
|
|
223
|
+
.btn-secondary:hover{filter:brightness(.96)}
|
|
224
|
+
.flow-note{display:flex;gap:12px;padding:16px 18px;border-radius:var(--r-lg);
|
|
225
|
+
background:var(--secondary);color:var(--secondary-fg);margin:0 0 22px;font-size:14px;line-height:1.55}
|
|
226
|
+
.flow-note ol{margin:6px 0 0;padding-left:20px}
|
|
227
|
+
.flow-note li{margin:2px 0}
|
|
228
|
+
.suggested{margin:0 0 8px}
|
|
229
|
+
.sug-head{display:flex;align-items:center;justify-content:space-between;margin:0 0 8px}
|
|
230
|
+
.sug-head span{font-size:13px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--faint)}
|
|
231
|
+
.copy{font:inherit;font-size:12.5px;font-weight:600;padding:6px 12px;border-radius:var(--r-sm);
|
|
232
|
+
border:1px solid var(--border);background:var(--surface);color:var(--muted);cursor:pointer}
|
|
233
|
+
.copy.copied{background:var(--secondary);color:var(--secondary-fg);border-color:var(--secondary)}
|
|
234
|
+
#li-text{margin:0;padding:16px 18px;border:1px solid var(--border);border-radius:var(--r-md);
|
|
235
|
+
background:var(--track);color:var(--fg);font-size:14.5px;line-height:1.6}
|
|
236
|
+
`;
|
|
237
|
+
|
|
238
|
+
// Browser-side PNG export + LinkedIn share wiring.
|
|
239
|
+
const SHARE_SCRIPT = `
|
|
240
|
+
(function(){
|
|
241
|
+
var CARD_W=${CARD_W}, CARD_H=${CARD_H};
|
|
242
|
+
var svg=document.getElementById('share-card-svg');
|
|
243
|
+
var dl=document.getElementById('dl-png');
|
|
244
|
+
var li=document.getElementById('li-share');
|
|
245
|
+
var liText=document.getElementById('li-text');
|
|
246
|
+
if(li){
|
|
247
|
+
var text=liText?liText.textContent:'';
|
|
248
|
+
li.setAttribute('href','https://www.linkedin.com/feed/?shareActive=true&text='+encodeURIComponent(text));
|
|
249
|
+
}
|
|
250
|
+
if(dl&&svg){
|
|
251
|
+
dl.addEventListener('click',function(){
|
|
252
|
+
// Serialize the inline SVG and load it as an image. encodeURIComponent
|
|
253
|
+
// (not btoa) so any non-ASCII survives; charset is declared for safety.
|
|
254
|
+
var xml=new XMLSerializer().serializeToString(svg);
|
|
255
|
+
var src='data:image/svg+xml;charset=utf-8,'+encodeURIComponent(xml);
|
|
256
|
+
var img=new Image();
|
|
257
|
+
img.onload=function(){
|
|
258
|
+
var canvas=document.createElement('canvas');
|
|
259
|
+
canvas.width=CARD_W; canvas.height=CARD_H;
|
|
260
|
+
var ctx=canvas.getContext('2d');
|
|
261
|
+
ctx.fillStyle='#181B1A'; ctx.fillRect(0,0,CARD_W,CARD_H);
|
|
262
|
+
ctx.drawImage(img,0,0,CARD_W,CARD_H);
|
|
263
|
+
var png;
|
|
264
|
+
try{ png=canvas.toDataURL('image/png'); }
|
|
265
|
+
catch(e){ /* self-contained SVG shouldn't taint; bail quietly if it did */ return; }
|
|
266
|
+
var a=document.createElement('a');
|
|
267
|
+
a.href=png; a.download='shakers-ai-maturity.png';
|
|
268
|
+
document.body.appendChild(a); a.click(); a.remove();
|
|
269
|
+
};
|
|
270
|
+
img.onerror=function(){};
|
|
271
|
+
img.src=src;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
})();
|
|
275
|
+
`;
|
|
276
|
+
|
|
277
|
+
// Assembles the full, self-contained HTML page around the card, using the shared Shakers report theme (report-theme.js#renderDocument).
|
|
278
|
+
function renderShareCardHtml(model) {
|
|
279
|
+
const svg = renderCardSvg(model);
|
|
280
|
+
const suggested = buildSuggestedText(model);
|
|
281
|
+
|
|
282
|
+
const body = `<header>
|
|
283
|
+
<span class="badge"><span class="spark"></span>SHAKERS</span>
|
|
284
|
+
<h1>Share your AI maturity</h1>
|
|
285
|
+
<p class="sub">A branded card to post on LinkedIn.</p>
|
|
286
|
+
</header>
|
|
287
|
+
|
|
288
|
+
<div class="card-frame">
|
|
289
|
+
<div class="card-svg-wrap">${svg}</div>
|
|
290
|
+
</div>
|
|
291
|
+
|
|
292
|
+
<div class="actions">
|
|
293
|
+
<button type="button" id="dl-png" class="btn btn-primary">Download PNG</button>
|
|
294
|
+
<a id="li-share" class="btn btn-secondary" href="https://www.linkedin.com/feed/?shareActive=true" target="_blank" rel="noopener">Share on LinkedIn</a>
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
<div class="flow-note">
|
|
298
|
+
<span aria-hidden="true">💡</span>
|
|
299
|
+
<div>
|
|
300
|
+
<strong>LinkedIn can't attach an image from a URL.</strong> To post the card:
|
|
301
|
+
<ol>
|
|
302
|
+
<li>Click <strong>Download PNG</strong> to save the image.</li>
|
|
303
|
+
<li>Click <strong>Share on LinkedIn</strong> to open the composer (with the suggested text).</li>
|
|
304
|
+
<li>Attach the downloaded PNG to your post.</li>
|
|
305
|
+
</ol>
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
|
|
309
|
+
<section class="suggested">
|
|
310
|
+
<div class="sug-head">
|
|
311
|
+
<span>Suggested text</span>
|
|
312
|
+
<button type="button" class="copy" data-copy-target="li-text" data-copied-label="Copied ✓">Copy</button>
|
|
313
|
+
</div>
|
|
314
|
+
<p id="li-text">${esc(suggested)}</p>
|
|
315
|
+
</section>
|
|
316
|
+
|
|
317
|
+
<footer>
|
|
318
|
+
<div class="priv">
|
|
319
|
+
<span class="lock" aria-hidden="true">🔒</span>
|
|
320
|
+
<span>Generated locally on your machine. The card and the PNG are built entirely in your browser — nothing is uploaded.</span>
|
|
321
|
+
</div>
|
|
322
|
+
</footer>`;
|
|
323
|
+
|
|
324
|
+
return renderDocument({
|
|
325
|
+
lang: 'en',
|
|
326
|
+
title: 'Shakers — Share your AI maturity',
|
|
327
|
+
componentCss: SHARE_CSS,
|
|
328
|
+
body,
|
|
329
|
+
script: SHARE_SCRIPT,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Per-project card file, alongside the reports in ~/.config/ai-footprint/ (override via SHAKERS_CLI_CONFIG_DIR).
|
|
334
|
+
function cardPathFor(absRoot) {
|
|
335
|
+
return path.join(configDir(), `share-${projectSlug(path.resolve(absRoot))}.html`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Orchestrates the command: load the project's footprint, and either return a "no footprint" signal or render + write the card and return its file:// link.
|
|
339
|
+
function generateShareCard({ root, load } = {}) {
|
|
340
|
+
const absRoot = path.resolve(root || process.cwd());
|
|
341
|
+
const fp = loadProjectFootprint(absRoot, load ? { load } : {});
|
|
342
|
+
if (!fp) return { ok: false, reason: 'no-footprint', root: absRoot };
|
|
343
|
+
|
|
344
|
+
const model = buildCardModel(fp);
|
|
345
|
+
const html = renderShareCardHtml(model);
|
|
346
|
+
fs.mkdirSync(configDir(), { recursive: true });
|
|
347
|
+
const htmlPath = cardPathFor(absRoot);
|
|
348
|
+
fs.writeFileSync(htmlPath, html);
|
|
349
|
+
return { ok: true, root: absRoot, model, html, htmlPath, fileUrl: fileUrl(htmlPath) };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
module.exports = {
|
|
353
|
+
CARD_W,
|
|
354
|
+
CARD_H,
|
|
355
|
+
loadProjectFootprint,
|
|
356
|
+
buildCardModel,
|
|
357
|
+
buildSuggestedText,
|
|
358
|
+
renderCardSvg,
|
|
359
|
+
renderShareCardHtml,
|
|
360
|
+
cardPathFor,
|
|
361
|
+
generateShareCard,
|
|
362
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Certification-persistence payload (skill-code-certification, issue 005), extracted from src/share.js (structure refactor, issue 020).
|
|
4
|
+
|
|
5
|
+
const { scrubSecrets } = require('./agent-synthesis');
|
|
6
|
+
|
|
7
|
+
const CERTIFY_SEND_THROTTLE_MS = 60 * 60 * 1000;
|
|
8
|
+
const CERT_SCHEMA_VERSION = 1;
|
|
9
|
+
|
|
10
|
+
// Strict whitelist: the analyzed RESULT + observability provenance (ADR-017), rebuilt field-by-field (never spread).
|
|
11
|
+
const CERT_DIMENSION_KEYS = ['idiomatic', 'correctness', 'depth', 'structure', 'testing'];
|
|
12
|
+
function deriveDimensionScores(dimensions) {
|
|
13
|
+
if (!dimensions || typeof dimensions !== 'object') return null;
|
|
14
|
+
const out = {};
|
|
15
|
+
let any = false;
|
|
16
|
+
for (const key of CERT_DIMENSION_KEYS) {
|
|
17
|
+
const v = dimensions[key];
|
|
18
|
+
if (typeof v === 'number' && Number.isInteger(v) && v >= 0 && v <= 4) {
|
|
19
|
+
out[key] = v;
|
|
20
|
+
any = true;
|
|
21
|
+
} else {
|
|
22
|
+
out[key] = null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return any ? out : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const MAX_INTERVIEW_EVIDENCE_TOKEN_CHARS = 4096;
|
|
29
|
+
function interviewEvidenceTokenOf(item) {
|
|
30
|
+
const t = item && item.interviewEvidenceToken;
|
|
31
|
+
return typeof t === 'string' && t ? t.slice(0, MAX_INTERVIEW_EVIDENCE_TOKEN_CHARS) : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const MAX_CERTIFY_EVIDENCE_TOKEN_CHARS = 4096;
|
|
35
|
+
function certifyEvidenceTokenOf(item) {
|
|
36
|
+
const t = item && item.result && item.result.evidenceToken;
|
|
37
|
+
return typeof t === 'string' && t ? t.slice(0, MAX_CERTIFY_EVIDENCE_TOKEN_CHARS) : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function deriveCertificationPayload(items, { repository = null, commitRange = null, toolVersion = null } = {}) {
|
|
41
|
+
const list = Array.isArray(items) ? items : [];
|
|
42
|
+
const skillCodeAssessments = list
|
|
43
|
+
.filter((i) => i && i.result && typeof i.result === 'object')
|
|
44
|
+
.map((i) => {
|
|
45
|
+
const assessment = {
|
|
46
|
+
skillId: i.skillId != null ? i.skillId : null,
|
|
47
|
+
skillName: typeof i.skillName === 'string' ? i.skillName : null,
|
|
48
|
+
technology: typeof i.technology === 'string' ? i.technology : null,
|
|
49
|
+
score: typeof i.result.score === 'number' ? i.result.score : null,
|
|
50
|
+
rationale: scrubSecrets(typeof i.result.rationale === 'string' ? i.result.rationale : ''),
|
|
51
|
+
improvements: Array.isArray(i.result.improvements)
|
|
52
|
+
? i.result.improvements.filter((x) => typeof x === 'string' && x).map((x) => scrubSecrets(x))
|
|
53
|
+
: [],
|
|
54
|
+
sampled: !!(i.sampling && i.sampling.sampleable),
|
|
55
|
+
model: typeof i.result.model === 'string' && i.result.model ? i.result.model : null,
|
|
56
|
+
// Paths only (never content). The attribution gate already filtered
|
|
57
|
+
// these to the Talent's own files (bin/certify.js).
|
|
58
|
+
sampledFiles: Array.isArray(i.sampledFiles)
|
|
59
|
+
? i.sampledFiles.filter((p) => typeof p === 'string' && p)
|
|
60
|
+
: [],
|
|
61
|
+
// Verified-authorship evidence: every considered author-email + match flag.
|
|
62
|
+
authorEmails: Array.isArray(i.authorEmails)
|
|
63
|
+
? i.authorEmails
|
|
64
|
+
.filter((a) => a && typeof a.email === 'string' && a.email)
|
|
65
|
+
.map((a) => ({ email: a.email, matched: !!a.matched }))
|
|
66
|
+
: [],
|
|
67
|
+
// Model's per-file scores; `[]` when the model degraded to aggregate-only.
|
|
68
|
+
perFileBreakdown: Array.isArray(i.result.perFileBreakdown)
|
|
69
|
+
? i.result.perFileBreakdown
|
|
70
|
+
.filter((f) => f && typeof f.path === 'string' && typeof f.score === 'number')
|
|
71
|
+
.map((f) => ({
|
|
72
|
+
path: f.path,
|
|
73
|
+
score: f.score,
|
|
74
|
+
note: typeof f.note === 'string' && f.note ? scrubSecrets(f.note) : null,
|
|
75
|
+
}))
|
|
76
|
+
: [],
|
|
77
|
+
// ADR-024 rubric dimensions (0-4 or null) — the inputs the server used to
|
|
78
|
+
// compute the deterministic score. Rebuilt key-by-key, never spread.
|
|
79
|
+
dimensionScores: deriveDimensionScores(i.result.dimensions),
|
|
80
|
+
};
|
|
81
|
+
const certifyToken = certifyEvidenceTokenOf(i);
|
|
82
|
+
if (certifyToken) assessment.evidenceToken = certifyToken;
|
|
83
|
+
// ADR-049 / issue 128: the attested combined level, bound ONLY when an
|
|
84
|
+
// interview ran. Added conditionally so an item without one is byte-identical.
|
|
85
|
+
const token = interviewEvidenceTokenOf(i);
|
|
86
|
+
if (token) assessment.interviewEvidenceToken = token;
|
|
87
|
+
return assessment;
|
|
88
|
+
});
|
|
89
|
+
return {
|
|
90
|
+
schemaVersion: CERT_SCHEMA_VERSION,
|
|
91
|
+
generatedAt: new Date().toISOString(),
|
|
92
|
+
kind: 'skill-code-assessment',
|
|
93
|
+
skillCodeAssessments,
|
|
94
|
+
toolVersion: typeof toolVersion === 'string' && toolVersion ? toolVersion : null,
|
|
95
|
+
repository: typeof repository === 'string' && repository ? repository : null,
|
|
96
|
+
commitRange: typeof commitRange === 'string' && commitRange ? commitRange : null,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isCertifyThrottled(state, now = Date.now()) {
|
|
101
|
+
if (!state || !state.lastCertifySentAt) return false;
|
|
102
|
+
const last = new Date(state.lastCertifySentAt).getTime();
|
|
103
|
+
if (Number.isNaN(last)) return false;
|
|
104
|
+
return now - last < CERTIFY_SEND_THROTTLE_MS;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = {
|
|
108
|
+
deriveCertificationPayload,
|
|
109
|
+
isCertifyThrottled,
|
|
110
|
+
CERTIFY_SEND_THROTTLE_MS,
|
|
111
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Consent state + email identity + client-side throttle, extracted from src/share.js (structure refactor, issue 020).
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { getConfigDir } = require('./config-dir');
|
|
8
|
+
|
|
9
|
+
function configDir() {
|
|
10
|
+
return getConfigDir(process.env);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function consentPath() {
|
|
14
|
+
return path.join(configDir(), 'consent.json');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Client-side throttle: don't retry a submission if the last one was less than 1h ago.
|
|
18
|
+
const SEND_THROTTLE_MS = 60 * 60 * 1000;
|
|
19
|
+
|
|
20
|
+
// consent state (~/.config/ai-footprint/consent.json) Replaces the old `credentials.json` (token model).
|
|
21
|
+
|
|
22
|
+
function loadConsentState() {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(fs.readFileSync(consentPath(), 'utf8'));
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function saveConsentState(state) {
|
|
31
|
+
fs.mkdirSync(configDir(), { recursive: true });
|
|
32
|
+
fs.writeFileSync(consentPath(), JSON.stringify(state, null, 2));
|
|
33
|
+
try { fs.chmodSync(consentPath(), 0o600); } catch { /* e.g. Windows */ }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Returns 'granted' | 'denied' | null ("no decision persisted yet").
|
|
37
|
+
function getConsentDecision(state) {
|
|
38
|
+
if (!state || state.consent === undefined || state.consent === null) return null;
|
|
39
|
+
return state.consent;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function hasTraceContentConsent() {
|
|
43
|
+
return getConsentDecision(loadConsentState()) === 'granted';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* ---------- email ---------- */
|
|
47
|
+
|
|
48
|
+
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
49
|
+
|
|
50
|
+
// Basic format validation only (specs.md: "validación de formato básica").
|
|
51
|
+
function isValidEmail(value) {
|
|
52
|
+
return typeof value === 'string' && EMAIL_RE.test(value.trim());
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Same normalization the backend applies (specs.md Data model): trim +
|
|
56
|
+
// lowercase, so client and server agree on what "the same email" means.
|
|
57
|
+
function normalizeEmail(value) {
|
|
58
|
+
return String(value).trim().toLowerCase();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Persists a consent decision.
|
|
62
|
+
function recordConsent(decision, email = null, { verified } = {}) {
|
|
63
|
+
if (decision !== 'granted' && decision !== 'denied') {
|
|
64
|
+
throw new Error(`Decisión de consentimiento no válida: ${decision}`);
|
|
65
|
+
}
|
|
66
|
+
if (decision === 'granted' && !isValidEmail(email)) {
|
|
67
|
+
throw new Error('No se puede conceder consentimiento sin un correo válido.');
|
|
68
|
+
}
|
|
69
|
+
const state = loadConsentState() || {};
|
|
70
|
+
state.consent = decision;
|
|
71
|
+
if (email !== null) state.email = normalizeEmail(email);
|
|
72
|
+
if (state.lastSentAt === undefined) state.lastSentAt = null;
|
|
73
|
+
// Email-ownership verification flag (skill-code-certification / ADR-006).
|
|
74
|
+
if (decision === 'granted' && verified !== undefined) {
|
|
75
|
+
state.emailVerified = verified === true;
|
|
76
|
+
}
|
|
77
|
+
saveConsentState(state);
|
|
78
|
+
return state;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// consent management (issue 007: status / revoke / change email) One-shot actions (do not scan), same pattern the retired `--enroll` used.
|
|
82
|
+
|
|
83
|
+
// Read-only snapshot for `--consent-status`. Never throws.
|
|
84
|
+
function getConsentStatus() {
|
|
85
|
+
const state = loadConsentState();
|
|
86
|
+
return {
|
|
87
|
+
consent: getConsentDecision(state),
|
|
88
|
+
email: state && state.email ? state.email : null,
|
|
89
|
+
emailVerified: state ? state.emailVerified !== false : false,
|
|
90
|
+
lastSentAt: state && state.lastSentAt ? state.lastSentAt : null,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Revokes consent unconditionally (works even with no prior decision, or after a prior `denied` — idempotent): from this point on `autoShare` skips with `consent-denied`.
|
|
95
|
+
function revokeConsent() {
|
|
96
|
+
const state = recordConsent('denied');
|
|
97
|
+
return { ok: true, state };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function resetConsent() {
|
|
101
|
+
const state = loadConsentState() || {};
|
|
102
|
+
delete state.consent;
|
|
103
|
+
// A re-grant is "concede de cero" (ADR-006): drop the verification flag so the OTP runs again from scratch and nothing is sent under the old email until it's re-verified.
|
|
104
|
+
delete state.emailVerified;
|
|
105
|
+
if (state.lastSentAt === undefined) state.lastSentAt = null;
|
|
106
|
+
saveConsentState(state);
|
|
107
|
+
return { ok: true, state };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function clearConsentState() {
|
|
111
|
+
try {
|
|
112
|
+
fs.rmSync(consentPath(), { force: true });
|
|
113
|
+
} catch { /* no-op */ }
|
|
114
|
+
return { ok: true };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Changes the persisted email WITHOUT touching the consent decision (specs.md: "sin tocar la decisión de consentimiento").
|
|
118
|
+
function setEmail(newEmail) {
|
|
119
|
+
if (!isValidEmail(newEmail)) {
|
|
120
|
+
return { ok: false, reason: 'invalid-email' };
|
|
121
|
+
}
|
|
122
|
+
const state = loadConsentState() || {};
|
|
123
|
+
state.email = normalizeEmail(newEmail);
|
|
124
|
+
// A new email is unverified until proven (ADR-006): reset the flag so
|
|
125
|
+
// autoShare won't send under an address whose ownership wasn't checked.
|
|
126
|
+
state.emailVerified = false;
|
|
127
|
+
saveConsentState(state);
|
|
128
|
+
return { ok: true, state };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* ---------- client-side throttle ---------- */
|
|
132
|
+
|
|
133
|
+
function isThrottled(state, now = Date.now()) {
|
|
134
|
+
if (!state || !state.lastSentAt) return false;
|
|
135
|
+
const last = new Date(state.lastSentAt).getTime();
|
|
136
|
+
if (Number.isNaN(last)) return false;
|
|
137
|
+
return now - last < SEND_THROTTLE_MS;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = {
|
|
141
|
+
configDir,
|
|
142
|
+
consentPath,
|
|
143
|
+
SEND_THROTTLE_MS,
|
|
144
|
+
loadConsentState,
|
|
145
|
+
saveConsentState,
|
|
146
|
+
getConsentDecision,
|
|
147
|
+
hasTraceContentConsent,
|
|
148
|
+
isValidEmail,
|
|
149
|
+
normalizeEmail,
|
|
150
|
+
recordConsent,
|
|
151
|
+
getConsentStatus,
|
|
152
|
+
revokeConsent,
|
|
153
|
+
resetConsent,
|
|
154
|
+
clearConsentState,
|
|
155
|
+
setEmail,
|
|
156
|
+
isThrottled,
|
|
157
|
+
};
|