selftune 0.1.4 → 0.2.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 (153) hide show
  1. package/.claude/agents/diagnosis-analyst.md +156 -0
  2. package/.claude/agents/evolution-reviewer.md +180 -0
  3. package/.claude/agents/integration-guide.md +212 -0
  4. package/.claude/agents/pattern-analyst.md +160 -0
  5. package/CHANGELOG.md +46 -1
  6. package/README.md +105 -257
  7. package/apps/local-dashboard/dist/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
  8. package/apps/local-dashboard/dist/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
  9. package/apps/local-dashboard/dist/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
  10. package/apps/local-dashboard/dist/assets/index-C4EOTFZ2.js +15 -0
  11. package/apps/local-dashboard/dist/assets/index-bl-Webyd.css +1 -0
  12. package/apps/local-dashboard/dist/assets/vendor-react-U7zYD9Rg.js +60 -0
  13. package/apps/local-dashboard/dist/assets/vendor-table-B7VF2Ipl.js +26 -0
  14. package/apps/local-dashboard/dist/assets/vendor-ui-D7_zX_qy.js +346 -0
  15. package/apps/local-dashboard/dist/favicon.png +0 -0
  16. package/apps/local-dashboard/dist/index.html +17 -0
  17. package/apps/local-dashboard/dist/logo.png +0 -0
  18. package/apps/local-dashboard/dist/logo.svg +9 -0
  19. package/assets/BeforeAfter.gif +0 -0
  20. package/assets/FeedbackLoop.gif +0 -0
  21. package/assets/logo.svg +9 -0
  22. package/assets/skill-health-badge.svg +20 -0
  23. package/cli/selftune/activation-rules.ts +171 -0
  24. package/cli/selftune/badge/badge-data.ts +108 -0
  25. package/cli/selftune/badge/badge-svg.ts +212 -0
  26. package/cli/selftune/badge/badge.ts +99 -0
  27. package/cli/selftune/canonical-export.ts +183 -0
  28. package/cli/selftune/constants.ts +103 -1
  29. package/cli/selftune/contribute/bundle.ts +314 -0
  30. package/cli/selftune/contribute/contribute.ts +214 -0
  31. package/cli/selftune/contribute/sanitize.ts +162 -0
  32. package/cli/selftune/cron/setup.ts +266 -0
  33. package/cli/selftune/dashboard-contract.ts +202 -0
  34. package/cli/selftune/dashboard-server.ts +1049 -0
  35. package/cli/selftune/dashboard.ts +43 -156
  36. package/cli/selftune/eval/baseline.ts +248 -0
  37. package/cli/selftune/eval/composability-v2.ts +273 -0
  38. package/cli/selftune/eval/composability.ts +117 -0
  39. package/cli/selftune/eval/generate-unit-tests.ts +143 -0
  40. package/cli/selftune/eval/hooks-to-evals.ts +101 -16
  41. package/cli/selftune/eval/import-skillsbench.ts +221 -0
  42. package/cli/selftune/eval/synthetic-evals.ts +172 -0
  43. package/cli/selftune/eval/unit-test-cli.ts +152 -0
  44. package/cli/selftune/eval/unit-test.ts +196 -0
  45. package/cli/selftune/evolution/deploy-proposal.ts +142 -1
  46. package/cli/selftune/evolution/evidence.ts +26 -0
  47. package/cli/selftune/evolution/evolve-body.ts +586 -0
  48. package/cli/selftune/evolution/evolve.ts +825 -116
  49. package/cli/selftune/evolution/extract-patterns.ts +105 -16
  50. package/cli/selftune/evolution/pareto.ts +314 -0
  51. package/cli/selftune/evolution/propose-body.ts +171 -0
  52. package/cli/selftune/evolution/propose-description.ts +100 -2
  53. package/cli/selftune/evolution/propose-routing.ts +166 -0
  54. package/cli/selftune/evolution/refine-body.ts +141 -0
  55. package/cli/selftune/evolution/rollback.ts +21 -4
  56. package/cli/selftune/evolution/validate-body.ts +254 -0
  57. package/cli/selftune/evolution/validate-proposal.ts +257 -35
  58. package/cli/selftune/evolution/validate-routing.ts +177 -0
  59. package/cli/selftune/grading/auto-grade.ts +200 -0
  60. package/cli/selftune/grading/grade-session.ts +513 -42
  61. package/cli/selftune/grading/pre-gates.ts +104 -0
  62. package/cli/selftune/grading/results.ts +42 -0
  63. package/cli/selftune/hooks/auto-activate.ts +185 -0
  64. package/cli/selftune/hooks/evolution-guard.ts +165 -0
  65. package/cli/selftune/hooks/prompt-log.ts +172 -2
  66. package/cli/selftune/hooks/session-stop.ts +123 -3
  67. package/cli/selftune/hooks/skill-change-guard.ts +112 -0
  68. package/cli/selftune/hooks/skill-eval.ts +119 -3
  69. package/cli/selftune/index.ts +415 -48
  70. package/cli/selftune/ingestors/claude-replay.ts +377 -0
  71. package/cli/selftune/ingestors/codex-rollout.ts +345 -46
  72. package/cli/selftune/ingestors/codex-wrapper.ts +207 -39
  73. package/cli/selftune/ingestors/openclaw-ingest.ts +573 -0
  74. package/cli/selftune/ingestors/opencode-ingest.ts +193 -17
  75. package/cli/selftune/init.ts +376 -16
  76. package/cli/selftune/last.ts +14 -5
  77. package/cli/selftune/localdb/db.ts +63 -0
  78. package/cli/selftune/localdb/materialize.ts +428 -0
  79. package/cli/selftune/localdb/queries.ts +376 -0
  80. package/cli/selftune/localdb/schema.ts +204 -0
  81. package/cli/selftune/memory/writer.ts +447 -0
  82. package/cli/selftune/monitoring/watch.ts +90 -16
  83. package/cli/selftune/normalization.ts +682 -0
  84. package/cli/selftune/observability.ts +19 -44
  85. package/cli/selftune/orchestrate.ts +1073 -0
  86. package/cli/selftune/quickstart.ts +203 -0
  87. package/cli/selftune/repair/skill-usage.ts +576 -0
  88. package/cli/selftune/schedule.ts +561 -0
  89. package/cli/selftune/status.ts +59 -33
  90. package/cli/selftune/sync.ts +627 -0
  91. package/cli/selftune/types.ts +525 -5
  92. package/cli/selftune/utils/canonical-log.ts +45 -0
  93. package/cli/selftune/utils/frontmatter.ts +217 -0
  94. package/cli/selftune/utils/hooks.ts +41 -0
  95. package/cli/selftune/utils/html.ts +27 -0
  96. package/cli/selftune/utils/llm-call.ts +103 -19
  97. package/cli/selftune/utils/math.ts +10 -0
  98. package/cli/selftune/utils/query-filter.ts +139 -0
  99. package/cli/selftune/utils/skill-discovery.ts +340 -0
  100. package/cli/selftune/utils/skill-log.ts +68 -0
  101. package/cli/selftune/utils/skill-usage-confidence.ts +18 -0
  102. package/cli/selftune/utils/transcript.ts +307 -26
  103. package/cli/selftune/utils/trigger-check.ts +89 -0
  104. package/cli/selftune/utils/tui.ts +156 -0
  105. package/cli/selftune/workflows/discover.ts +254 -0
  106. package/cli/selftune/workflows/skill-md-writer.ts +288 -0
  107. package/cli/selftune/workflows/workflows.ts +188 -0
  108. package/package.json +28 -11
  109. package/packages/telemetry-contract/README.md +11 -0
  110. package/packages/telemetry-contract/fixtures/golden.json +87 -0
  111. package/packages/telemetry-contract/fixtures/golden.test.ts +42 -0
  112. package/packages/telemetry-contract/index.ts +1 -0
  113. package/packages/telemetry-contract/package.json +19 -0
  114. package/packages/telemetry-contract/src/index.ts +2 -0
  115. package/packages/telemetry-contract/src/types.ts +163 -0
  116. package/packages/telemetry-contract/src/validators.ts +109 -0
  117. package/skill/SKILL.md +180 -33
  118. package/skill/Workflows/AutoActivation.md +145 -0
  119. package/skill/Workflows/Badge.md +124 -0
  120. package/skill/Workflows/Baseline.md +144 -0
  121. package/skill/Workflows/Composability.md +107 -0
  122. package/skill/Workflows/Contribute.md +94 -0
  123. package/skill/Workflows/Cron.md +132 -0
  124. package/skill/Workflows/Dashboard.md +214 -0
  125. package/skill/Workflows/Doctor.md +63 -14
  126. package/skill/Workflows/Evals.md +110 -18
  127. package/skill/Workflows/EvolutionMemory.md +154 -0
  128. package/skill/Workflows/Evolve.md +181 -21
  129. package/skill/Workflows/EvolveBody.md +159 -0
  130. package/skill/Workflows/Grade.md +36 -31
  131. package/skill/Workflows/ImportSkillsBench.md +117 -0
  132. package/skill/Workflows/Ingest.md +142 -21
  133. package/skill/Workflows/Initialize.md +91 -23
  134. package/skill/Workflows/Orchestrate.md +139 -0
  135. package/skill/Workflows/Replay.md +91 -0
  136. package/skill/Workflows/Rollback.md +23 -4
  137. package/skill/Workflows/Schedule.md +61 -0
  138. package/skill/Workflows/Sync.md +88 -0
  139. package/skill/Workflows/UnitTest.md +150 -0
  140. package/skill/Workflows/Watch.md +33 -1
  141. package/skill/Workflows/Workflows.md +129 -0
  142. package/skill/assets/activation-rules-default.json +26 -0
  143. package/skill/assets/multi-skill-settings.json +63 -0
  144. package/skill/assets/single-skill-settings.json +57 -0
  145. package/skill/references/invocation-taxonomy.md +2 -2
  146. package/skill/references/logs.md +164 -2
  147. package/skill/references/setup-patterns.md +65 -0
  148. package/skill/references/version-history.md +40 -0
  149. package/skill/settings_snippet.json +23 -0
  150. package/templates/activation-rules-default.json +27 -0
  151. package/templates/multi-skill-settings.json +64 -0
  152. package/templates/single-skill-settings.json +58 -0
  153. package/dashboard/index.html +0 -1119
@@ -0,0 +1,17 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>selftune — Dashboard</title>
7
+ <link rel="icon" type="image/png" href="/favicon.png" />
8
+ <script type="module" crossorigin src="/assets/index-C4EOTFZ2.js"></script>
9
+ <link rel="modulepreload" crossorigin href="/assets/vendor-react-U7zYD9Rg.js">
10
+ <link rel="modulepreload" crossorigin href="/assets/vendor-ui-D7_zX_qy.js">
11
+ <link rel="modulepreload" crossorigin href="/assets/vendor-table-B7VF2Ipl.js">
12
+ <link rel="stylesheet" crossorigin href="/assets/index-bl-Webyd.css">
13
+ </head>
14
+ <body>
15
+ <div id="root"></div>
16
+ </body>
17
+ </html>
@@ -0,0 +1,9 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="250" height="250" viewBox="0 0 250 250" fill="none">
2
+ <path d="M 190.16,31.49 C 187.91,29.88 184.51,32.19 185.88,35.16 C 186.31,36.11 187.08,36.54 187.71,37.01 C 218.75,59.86 237.63,92.71 237.63,128.82 C 237.63,175.99 205.12,218.56 153.82,234.69 C 149.89,235.93 150.91,241.71 154.91,240.66 C 205.98,226.96 243.01,181.94 243,128.45 C 242.99,90.87 223.47,56.18 190.16,31.49 Z" fill="currentColor"/>
3
+ <path d="M 125.19,243.91 C 138.08,243.91 147.18,236.44 151.21,225.01 C 193.72,217.79 226.98,184.02 226.98,140.81 C 226.98,121.17 219.82,103.78 209.93,87.04 C 191.42,55.45 165.15,34.72 117.71,28.65 C 112.91,28.04 113.77,34.35 117.19,34.82 C 161.67,39.33 185.84,56.71 203.76,86.42 C 213.87,103.68 220.68,119.61 220.68,140.81 C 220.68,179.96 190.81,211.95 148.71,219.16 C 147.11,219.47 146.27,220.32 145.92,221.8 C 142.95,231.11 135.72,238.02 125.19,237.66 C 64.48,237.66 11.67,191.61 11.67,127.51 C 11.67,79.61 44.82,36.38 93.89,27.77 L 94.11,27.73 L 94.38,26.64 C 97.04,16.61 104.57,11.82 114.19,11.82 C 134.12,13.36 152.91,18.15 170.48,26.08 C 171.92,26.78 173.81,27.09 174.76,25.59 C 176.05,23.72 175.31,21.07 173.01,20.34 C 154.78,11.96 137.21,7.17 114.47,6 H 113.52 C 101.91,6 93.46,12.16 89.49,21.78 C 42.36,31.26 6.17,74.76 6.17,128.08 C 6.17,190.05 57.92,243.91 125.19,243.91 Z" fill="currentColor"/>
4
+ <path d="M 93.67,40.64 C 100.51,52.07 109.54,51.33 114.05,52.17 C 128.72,53.91 141.48,55.78 157.38,62.16 C 162.72,64.47 162.29,58.19 159.18,57.01 C 145.11,51.33 132.48,49.79 111.31,47.48 C 101.83,46.29 95.45,41.18 93.75,32.81 C 55.21,39.46 22.06,72.17 22.06,112.48 C 22.06,131.98 30.36,149.82 43.26,164.49 C 46.23,167.59 50.19,164.13 48.32,161.02 C 36.21,145.54 28.42,129.78 28.42,112.4 C 28.42,79.11 54.91,48.36 89.91,40.36 C 90.76,40.15 91.04,39.87 91.62,40.01 C 92.62,40.01 93.04,39.65 93.67,40.64 Z" fill="currentColor"/>
5
+ <path d="M 152.72,82.77 C 126.61,82.77 113.07,99.44 103.01,119.33 C 100.56,123.36 103.74,125.03 105.61,123.92 C 107.15,123.22 107.89,121.05 108.73,119.61 C 118.22,102.16 130.33,88.56 152.72,88.56 C 181.62,88.56 201.91,116.01 201.91,147.31 C 201.91,175.12 183.47,199.96 152.51,205.75 C 151.84,205.96 151.63,206.03 151.56,205.54 C 147.74,195.37 139.36,188.15 128.07,186.48 C 113.2,184.24 101.23,182.36 83.8,176.81 C 79.3,175.48 77.91,182.36 82.41,183.09 C 97.21,187.46 108.09,189.47 126.25,192.65 C 136.78,194.31 145.41,201.71 147.11,210.95 C 147.74,213.05 149.13,213.41 150.15,213.26 C 183.75,208.61 208.26,180.93 208.26,147.24 C 208.26,115.06 186.94,82.77 152.72,82.77 Z" fill="currentColor"/>
6
+ <path d="M 129.77,105.21 C 122.93,112.05 118.97,122.73 113.77,130.41 C 111.31,133.45 114.56,136.63 117.46,134.46 C 123.75,126.23 127.43,115.62 135.15,108.71 C 138.22,105.81 134.73,101.09 129.77,105.21 Z" fill="currentColor"/>
7
+ <path d="M 136.78,120.31 C 127.71,136.71 120.12,154.91 93.74,154.91 C 66.07,154.91 47.76,128.53 47.76,104.78 C 47.76,84.47 58.57,66.08 77.66,56.25 C 82.23,54.21 79.85,47.76 75.34,49.93 C 54.77,59.72 42.01,80.11 42.01,104.71 C 42.01,131.77 61.86,161.31 93.67,161.31 C 114.77,161.31 128.91,147.24 139.86,124.06 C 142.76,120.45 139.15,117.73 136.78,120.31 Z" fill="currentColor"/>
8
+ <path d="M 30.73,154.7 C 27.76,152.97 23.87,155.93 25.41,158.76 C 41.73,188.36 68.94,199.79 105.75,206.41 C 112.25,207.66 122.07,208.75 123.46,209.03 C 128.07,209.95 128.07,220.18 121.78,220.18 C 107.64,218.94 92.06,215.98 76.23,211.33 C 72.13,210.24 71.04,216.69 75.27,217.64 C 90.41,222.22 103.95,224.74 120.47,226.54 C 133.73,226.54 136.56,209.03 126.03,203.38 C 123.75,202.13 122.73,202.56 112.04,200.76 C 78.09,195.04 54.06,188.98 32.12,155.65 C 31.77,155.23 31.28,154.91 30.73,154.7 Z" fill="currentColor"/>
9
+ </svg>
Binary file
Binary file
@@ -0,0 +1,9 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="250" height="250" viewBox="0 0 250 250" fill="none">
2
+ <path d="M 190.16,31.49 C 187.91,29.88 184.51,32.19 185.88,35.16 C 186.31,36.11 187.08,36.54 187.71,37.01 C 218.75,59.86 237.63,92.71 237.63,128.82 C 237.63,175.99 205.12,218.56 153.82,234.69 C 149.89,235.93 150.91,241.71 154.91,240.66 C 205.98,226.96 243.01,181.94 243,128.45 C 242.99,90.87 223.47,56.18 190.16,31.49 Z" fill="#E8DED0"/>
3
+ <path d="M 125.19,243.91 C 138.08,243.91 147.18,236.44 151.21,225.01 C 193.72,217.79 226.98,184.02 226.98,140.81 C 226.98,121.17 219.82,103.78 209.93,87.04 C 191.42,55.45 165.15,34.72 117.71,28.65 C 112.91,28.04 113.77,34.35 117.19,34.82 C 161.67,39.33 185.84,56.71 203.76,86.42 C 213.87,103.68 220.68,119.61 220.68,140.81 C 220.68,179.96 190.81,211.95 148.71,219.16 C 147.11,219.47 146.27,220.32 145.92,221.8 C 142.95,231.11 135.72,238.02 125.19,237.66 C 64.48,237.66 11.67,191.61 11.67,127.51 C 11.67,79.61 44.82,36.38 93.89,27.77 L 94.11,27.73 L 94.38,26.64 C 97.04,16.61 104.57,11.82 114.19,11.82 C 134.12,13.36 152.91,18.15 170.48,26.08 C 171.92,26.78 173.81,27.09 174.76,25.59 C 176.05,23.72 175.31,21.07 173.01,20.34 C 154.78,11.96 137.21,7.17 114.47,6 H 113.52 C 101.91,6 93.46,12.16 89.49,21.78 C 42.36,31.26 6.17,74.76 6.17,128.08 C 6.17,190.05 57.92,243.91 125.19,243.91 Z" fill="#E8DED0"/>
4
+ <path d="M 93.67,40.64 C 100.51,52.07 109.54,51.33 114.05,52.17 C 128.72,53.91 141.48,55.78 157.38,62.16 C 162.72,64.47 162.29,58.19 159.18,57.01 C 145.11,51.33 132.48,49.79 111.31,47.48 C 101.83,46.29 95.45,41.18 93.75,32.81 C 55.21,39.46 22.06,72.17 22.06,112.48 C 22.06,131.98 30.36,149.82 43.26,164.49 C 46.23,167.59 50.19,164.13 48.32,161.02 C 36.21,145.54 28.42,129.78 28.42,112.4 C 28.42,79.11 54.91,48.36 89.91,40.36 C 90.76,40.15 91.04,39.87 91.62,40.01 C 92.62,40.01 93.04,39.65 93.67,40.64 Z" fill="#E8DED0"/>
5
+ <path d="M 152.72,82.77 C 126.61,82.77 113.07,99.44 103.01,119.33 C 100.56,123.36 103.74,125.03 105.61,123.92 C 107.15,123.22 107.89,121.05 108.73,119.61 C 118.22,102.16 130.33,88.56 152.72,88.56 C 181.62,88.56 201.91,116.01 201.91,147.31 C 201.91,175.12 183.47,199.96 152.51,205.75 C 151.84,205.96 151.63,206.03 151.56,205.54 C 147.74,195.37 139.36,188.15 128.07,186.48 C 113.2,184.24 101.23,182.36 83.8,176.81 C 79.3,175.48 77.91,182.36 82.41,183.09 C 97.21,187.46 108.09,189.47 126.25,192.65 C 136.78,194.31 145.41,201.71 147.11,210.95 C 147.74,213.05 149.13,213.41 150.15,213.26 C 183.75,208.61 208.26,180.93 208.26,147.24 C 208.26,115.06 186.94,82.77 152.72,82.77 Z" fill="#E8DED0"/>
6
+ <path d="M 129.77,105.21 C 122.93,112.05 118.97,122.73 113.77,130.41 C 111.31,133.45 114.56,136.63 117.46,134.46 C 123.75,126.23 127.43,115.62 135.15,108.71 C 138.22,105.81 134.73,101.09 129.77,105.21 Z" fill="#E8DED0"/>
7
+ <path d="M 136.78,120.31 C 127.71,136.71 120.12,154.91 93.74,154.91 C 66.07,154.91 47.76,128.53 47.76,104.78 C 47.76,84.47 58.57,66.08 77.66,56.25 C 82.23,54.21 79.85,47.76 75.34,49.93 C 54.77,59.72 42.01,80.11 42.01,104.71 C 42.01,131.77 61.86,161.31 93.67,161.31 C 114.77,161.31 128.91,147.24 139.86,124.06 C 142.76,120.45 139.15,117.73 136.78,120.31 Z" fill="#E8DED0"/>
8
+ <path d="M 30.73,154.7 C 27.76,152.97 23.87,155.93 25.41,158.76 C 41.73,188.36 68.94,199.79 105.75,206.41 C 112.25,207.66 122.07,208.75 123.46,209.03 C 128.07,209.95 128.07,220.18 121.78,220.18 C 107.64,218.94 92.06,215.98 76.23,211.33 C 72.13,210.24 71.04,216.69 75.27,217.64 C 90.41,222.22 103.95,224.74 120.47,226.54 C 133.73,226.54 136.56,209.03 126.03,203.38 C 123.75,202.13 122.73,202.56 112.04,200.76 C 78.09,195.04 54.06,188.98 32.12,155.65 C 31.77,155.23 31.28,154.91 30.73,154.7 Z" fill="#E8DED0"/>
9
+ </svg>
@@ -0,0 +1,20 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="138" height="20" role="img" aria-label="Skill Health: no data">
2
+ <linearGradient id="b" x2="0" y2="100%">
3
+ <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
4
+ <stop offset="1" stop-opacity=".1"/>
5
+ </linearGradient>
6
+ <clipPath id="a">
7
+ <rect width="138" height="20" rx="3" fill="#fff"/>
8
+ </clipPath>
9
+ <g clip-path="url(#a)">
10
+ <rect width="78" height="20" fill="#555"/>
11
+ <rect x="79" width="59" height="20" fill="#9f9f9f"/>
12
+ <rect width="138" height="20" fill="url(#b)"/>
13
+ </g>
14
+ <g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
15
+ <text x="39" y="15" fill="#010101" fill-opacity=".3">Skill Health</text>
16
+ <text x="39" y="14">Skill Health</text>
17
+ <text x="108.5" y="15" fill="#010101" fill-opacity=".3">no data</text>
18
+ <text x="108.5" y="14">no data</text>
19
+ </g>
20
+ </svg>
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Default activation rules for the auto-activate hook.
3
+ *
4
+ * Each rule evaluates session context and returns a suggestion string
5
+ * (or null if the rule doesn't fire). Rules must be pure functions
6
+ * that read from the filesystem — no network calls, no imports from
7
+ * evolution/monitoring/grading layers.
8
+ */
9
+
10
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
11
+ import { dirname, join } from "node:path";
12
+ import type { ActivationContext, ActivationRule } from "./types.js";
13
+ import { readJsonl } from "./utils/jsonl.js";
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Rule: post-session diagnostic
17
+ // ---------------------------------------------------------------------------
18
+
19
+ const postSessionDiagnostic: ActivationRule = {
20
+ id: "post-session-diagnostic",
21
+ description: "Suggest `selftune last` when session has >2 unmatched queries",
22
+ evaluate(ctx: ActivationContext): string | null {
23
+ // Count queries for this session
24
+ const queries = readJsonl<{ session_id: string; query: string }>(ctx.query_log_path);
25
+ const sessionQueries = queries.filter((q) => q.session_id === ctx.session_id);
26
+
27
+ if (sessionQueries.length === 0) return null;
28
+
29
+ // Count skill usages for this session (skill log is in the same dir as query log)
30
+ const skillLogPath = join(dirname(ctx.query_log_path), "skill_usage_log.jsonl");
31
+ const skillUsages = existsSync(skillLogPath)
32
+ ? readJsonl<{ session_id: string }>(skillLogPath).filter(
33
+ (s) => s.session_id === ctx.session_id,
34
+ )
35
+ : [];
36
+
37
+ const unmatchedCount = sessionQueries.length - skillUsages.length;
38
+
39
+ if (unmatchedCount > 2) {
40
+ return `Run \`selftune last\` — ${unmatchedCount} unmatched queries detected in this session.`;
41
+ }
42
+
43
+ return null;
44
+ },
45
+ };
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Rule: grading threshold breach
49
+ // ---------------------------------------------------------------------------
50
+
51
+ const gradingThresholdBreach: ActivationRule = {
52
+ id: "grading-threshold-breach",
53
+ description: "Suggest `selftune evolve` when session pass rate < 0.6",
54
+ evaluate(ctx: ActivationContext): string | null {
55
+ // Look for grading results in the selftune dir
56
+ const gradingDir = join(ctx.selftune_dir, "grading");
57
+ if (!existsSync(gradingDir)) return null;
58
+
59
+ try {
60
+ const files = readdirSync(gradingDir).filter(
61
+ (f) => f.startsWith("result-") && f.endsWith(".json"),
62
+ );
63
+
64
+ for (const file of files) {
65
+ const content = readFileSync(join(gradingDir, file), "utf-8");
66
+ const result = JSON.parse(content) as {
67
+ session_id: string;
68
+ summary?: { pass_rate: number };
69
+ };
70
+
71
+ if (result.session_id === ctx.session_id && result.summary) {
72
+ if (result.summary.pass_rate < 0.6) {
73
+ return `Run \`selftune evolve\` — session pass rate is ${(result.summary.pass_rate * 100).toFixed(0)}% (below 60% threshold).`;
74
+ }
75
+ }
76
+ }
77
+ } catch {
78
+ // fail-open
79
+ }
80
+
81
+ return null;
82
+ },
83
+ };
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // Rule: stale evolution
87
+ // ---------------------------------------------------------------------------
88
+
89
+ const staleEvolution: ActivationRule = {
90
+ id: "stale-evolution",
91
+ description:
92
+ "Suggest `selftune evolve` when no evolution in >7 days and pending false negatives exist",
93
+ evaluate(ctx: ActivationContext): string | null {
94
+ const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
95
+
96
+ // Check last evolution timestamp
97
+ const auditEntries = readJsonl<{ timestamp: string; action: string }>(
98
+ ctx.evolution_audit_log_path,
99
+ );
100
+
101
+ if (auditEntries.length === 0) {
102
+ // No evolution has ever run — check for false negatives
103
+ return checkFalseNegatives(ctx)
104
+ ? "Run `selftune evolve` — no evolution history found and pending false negatives exist."
105
+ : null;
106
+ }
107
+
108
+ const lastEntry = auditEntries[auditEntries.length - 1];
109
+ const lastTimestamp = new Date(lastEntry.timestamp).getTime();
110
+ const ageMs = Date.now() - lastTimestamp;
111
+
112
+ if (ageMs > SEVEN_DAYS_MS && checkFalseNegatives(ctx)) {
113
+ return `Run \`selftune evolve\` — no evolution in >7 days and pending false negatives detected.`;
114
+ }
115
+
116
+ return null;
117
+ },
118
+ };
119
+
120
+ function checkFalseNegatives(ctx: ActivationContext): boolean {
121
+ const fnPath = join(ctx.selftune_dir, "false-negatives", "pending.json");
122
+ if (!existsSync(fnPath)) return false;
123
+
124
+ try {
125
+ const data = JSON.parse(readFileSync(fnPath, "utf-8"));
126
+ return Array.isArray(data) && data.length > 0;
127
+ } catch {
128
+ return false;
129
+ }
130
+ }
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // Rule: regression detected
134
+ // ---------------------------------------------------------------------------
135
+
136
+ const regressionDetected: ActivationRule = {
137
+ id: "regression-detected",
138
+ description: "Suggest `selftune rollback` when watch snapshot shows regression",
139
+ evaluate(ctx: ActivationContext): string | null {
140
+ const snapshotPath = join(ctx.selftune_dir, "monitoring", "latest-snapshot.json");
141
+ if (!existsSync(snapshotPath)) return null;
142
+
143
+ try {
144
+ const snapshot = JSON.parse(readFileSync(snapshotPath, "utf-8")) as {
145
+ regression_detected: boolean;
146
+ skill_name?: string;
147
+ pass_rate?: number;
148
+ };
149
+
150
+ if (snapshot.regression_detected) {
151
+ const skillInfo = snapshot.skill_name ? ` for skill "${snapshot.skill_name}"` : "";
152
+ return `Run \`selftune rollback\` — regression detected${skillInfo}.`;
153
+ }
154
+ } catch {
155
+ // fail-open
156
+ }
157
+
158
+ return null;
159
+ },
160
+ };
161
+
162
+ // ---------------------------------------------------------------------------
163
+ // Exported defaults
164
+ // ---------------------------------------------------------------------------
165
+
166
+ export const DEFAULT_RULES: ActivationRule[] = [
167
+ postSessionDiagnostic,
168
+ gradingThresholdBreach,
169
+ staleEvolution,
170
+ regressionDetected,
171
+ ];
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Badge data computation for selftune skill health badges.
3
+ *
4
+ * Maps SkillStatus into display-ready BadgeData with color coding,
5
+ * trend arrows, and formatted messages. Pure functions, zero deps.
6
+ */
7
+
8
+ import type { SkillStatus, StatusResult } from "../status.js";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Types
12
+ // ---------------------------------------------------------------------------
13
+
14
+ export interface BadgeData {
15
+ label: string;
16
+ passRate: number | null;
17
+ trend: "up" | "down" | "stable" | "unknown";
18
+ status: "HEALTHY" | "WARNING" | "CRITICAL" | "UNGRADED" | "UNKNOWN";
19
+ color: string;
20
+ message: string;
21
+ }
22
+
23
+ export type BadgeFormat = "svg" | "markdown" | "url";
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Constants
27
+ // ---------------------------------------------------------------------------
28
+
29
+ export const BADGE_THRESHOLDS = {
30
+ /** Above this → green */
31
+ GREEN: 0.8,
32
+ /** At or above this → yellow; below → red */
33
+ YELLOW: 0.6,
34
+ } as const;
35
+
36
+ export const BADGE_COLORS = {
37
+ GREEN: "#4c1",
38
+ YELLOW: "#dfb317",
39
+ RED: "#e05d44",
40
+ GRAY: "#9f9f9f",
41
+ } as const;
42
+
43
+ export const TREND_ARROWS: Record<BadgeData["trend"], string> = {
44
+ up: "\u2191",
45
+ down: "\u2193",
46
+ stable: "\u2192",
47
+ unknown: "",
48
+ };
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // computeBadgeData
52
+ // ---------------------------------------------------------------------------
53
+
54
+ /**
55
+ * Convert a SkillStatus into display-ready badge data.
56
+ *
57
+ * Color thresholds:
58
+ * - green (#4c1) passRate > 0.8
59
+ * - yellow (#dfb317) passRate 0.6 - 0.8 (inclusive)
60
+ * - red (#e05d44) passRate < 0.6
61
+ * - gray (#9f9f9f) passRate is null (no data)
62
+ */
63
+ export function computeBadgeData(skill: SkillStatus): BadgeData {
64
+ const { passRate, trend, status } = skill;
65
+
66
+ let color: string;
67
+ let message: string;
68
+
69
+ if (passRate === null) {
70
+ color = BADGE_COLORS.GRAY;
71
+ message = "no data";
72
+ } else {
73
+ if (passRate > BADGE_THRESHOLDS.GREEN) {
74
+ color = BADGE_COLORS.GREEN;
75
+ } else if (passRate >= BADGE_THRESHOLDS.YELLOW) {
76
+ color = BADGE_COLORS.YELLOW;
77
+ } else {
78
+ color = BADGE_COLORS.RED;
79
+ }
80
+
81
+ const pct = `${Math.round(passRate * 100)}%`;
82
+ const arrow = TREND_ARROWS[trend];
83
+ message = arrow ? `${pct} ${arrow}` : pct;
84
+ }
85
+
86
+ return {
87
+ label: "Skill Health",
88
+ passRate,
89
+ trend,
90
+ status,
91
+ color,
92
+ message,
93
+ };
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // findSkillBadgeData
98
+ // ---------------------------------------------------------------------------
99
+
100
+ /**
101
+ * Find a skill by name in a StatusResult and return its BadgeData,
102
+ * or null if the skill is not found.
103
+ */
104
+ export function findSkillBadgeData(result: StatusResult, name: string): BadgeData | null {
105
+ const skill = result.skills.find((s) => s.name === name);
106
+ if (!skill) return null;
107
+ return computeBadgeData(skill);
108
+ }
@@ -0,0 +1,212 @@
1
+ /**
2
+ * SVG renderer and format router for selftune skill health badges.
3
+ *
4
+ * Generates shields.io flat-style SVG badges using template literals.
5
+ * Uses a per-character width table for Verdana 11px text width estimation.
6
+ * Zero external dependencies, pure functions only.
7
+ */
8
+
9
+ import type { BadgeData, BadgeFormat } from "./badge-data.js";
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Character width table (Verdana 11px)
13
+ // ---------------------------------------------------------------------------
14
+
15
+ const CHAR_WIDTHS: Record<string, number> = {
16
+ " ": 3.3,
17
+ "!": 3.3,
18
+ "%": 7.3,
19
+ "(": 3.6,
20
+ ")": 3.6,
21
+ "+": 7.3,
22
+ "-": 3.9,
23
+ ".": 3.3,
24
+ "/": 3.6,
25
+ "0": 6.6,
26
+ "1": 6.6,
27
+ "2": 6.6,
28
+ "3": 6.6,
29
+ "4": 6.6,
30
+ "5": 6.6,
31
+ "6": 6.6,
32
+ "7": 6.6,
33
+ "8": 6.6,
34
+ "9": 6.6,
35
+ ":": 3.3,
36
+ A: 7.5,
37
+ B: 7.5,
38
+ C: 7.2,
39
+ D: 7.8,
40
+ E: 6.8,
41
+ F: 6.3,
42
+ G: 7.8,
43
+ H: 7.8,
44
+ I: 3.0,
45
+ J: 5.0,
46
+ K: 7.2,
47
+ L: 6.2,
48
+ M: 8.9,
49
+ N: 7.8,
50
+ O: 7.8,
51
+ P: 6.6,
52
+ Q: 7.8,
53
+ R: 7.2,
54
+ S: 7.2,
55
+ T: 6.5,
56
+ U: 7.8,
57
+ V: 7.2,
58
+ W: 10.0,
59
+ X: 6.8,
60
+ Y: 6.5,
61
+ Z: 6.8,
62
+ a: 6.2,
63
+ b: 6.6,
64
+ c: 5.6,
65
+ d: 6.6,
66
+ e: 6.2,
67
+ f: 3.6,
68
+ g: 6.6,
69
+ h: 6.6,
70
+ i: 2.8,
71
+ j: 2.8,
72
+ k: 6.2,
73
+ l: 2.8,
74
+ m: 10.0,
75
+ n: 6.6,
76
+ o: 6.6,
77
+ p: 6.6,
78
+ q: 6.6,
79
+ r: 3.9,
80
+ s: 5.6,
81
+ t: 3.6,
82
+ u: 6.6,
83
+ v: 6.2,
84
+ w: 8.9,
85
+ x: 5.9,
86
+ y: 5.9,
87
+ z: 5.6,
88
+ "\u2191": 6.6,
89
+ "\u2193": 6.6,
90
+ "\u2192": 6.6,
91
+ };
92
+
93
+ const DEFAULT_CHAR_WIDTH = 6.8;
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Logo constants
97
+ // ---------------------------------------------------------------------------
98
+
99
+ const LOGO_SIZE = 14;
100
+ const LOGO_PAD = 3; // gap between logo and text
101
+ const LOGO_EXTRA = LOGO_SIZE + LOGO_PAD; // 17px added to label section
102
+
103
+ const LOGO_SVG_BASE64 =
104
+ "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNTAiIGhlaWdodD0iMjUwIiB2aWV3Qm94PSIwIDAgMjUwIDI1MCIgZmlsbD0ibm9uZSI+CjxwYXRoIGQ9Ik0gMTkwLjE2LDMxLjQ5IEMgMTg3LjkxLDI5Ljg4IDE4NC41MSwzMi4xOSAxODUuODgsMzUuMTYgQyAxODYuMzEsMzYuMTEgMTg3LjA4LDM2LjU0IDE4Ny43MSwzNy4wMSBDIDIxOC43NSw1OS44NiAyMzcuNjMsOTIuNzEgMjM3LjYzLDEyOC44MiBDIDIzNy42MywxNzUuOTkgMjA1LjEyLDIxOC41NiAxNTMuODIsMjM0LjY5IEMgMTQ5Ljg5LDIzNS45MyAxNTAuOTEsMjQxLjcxIDE1NC45MSwyNDAuNjYgQyAyMDUuOTgsMjI2Ljk2IDI0My4wMSwxODEuOTQgMjQzLDEyOC40NSBDIDI0Mi45OSw5MC44NyAyMjMuNDcsNTYuMTggMTkwLjE2LDMxLjQ5IFoiIGZpbGw9IiNmZmYiLz4KPHBhdGggZD0iTSAxMjUuMTksMjQzLjkxIEMgMTM4LjA4LDI0My45MSAxNDcuMTgsMjM2LjQ0IDE1MS4yMSwyMjUuMDEgQyAxOTMuNzIsMjE3Ljc5IDIyNi45OCwxODQuMDIgMjI2Ljk4LDE0MC44MSBDIDIyNi45OCwxMjEuMTcgMjE5LjgyLDEwMy43OCAyMDkuOTMsODcuMDQgQyAxOTEuNDIsNTUuNDUgMTY1LjE1LDM0LjcyIDExNy43MSwyOC42NSBDIDExMi45MSwyOC4wNCAxMTMuNzcsMzQuMzUgMTE3LjE5LDM0LjgyIEMgMTYxLjY3LDM5LjMzIDE4NS44NCw1Ni43MSAyMDMuNzYsODYuNDIgQyAyMTMuODcsMTAzLjY4IDIyMC42OCwxMTkuNjEgMjIwLjY4LDE0MC44MSBDIDIyMC42OCwxNzkuOTYgMTkwLjgxLDIxMS45NSAxNDguNzEsMjE5LjE2IEMgMTQ3LjExLDIxOS40NyAxNDYuMjcsMjIwLjMyIDE0NS45MiwyMjEuOCBDIDE0Mi45NSwyMzEuMTEgMTM1LjcyLDIzOC4wMiAxMjUuMTksMjM3LjY2IEMgNjQuNDgsMjM3LjY2IDExLjY3LDE5MS42MSAxMS42NywxMjcuNTEgQyAxMS42Nyw3OS42MSA0NC44MiwzNi4zOCA5My44OSwyNy43NyBMIDk0LjExLDI3LjczIEwgOTQuMzgsMjYuNjQgQyA5Ny4wNCwxNi42MSAxMDQuNTcsMTEuODIgMTE0LjE5LDExLjgyIEMgMTM0LjEyLDEzLjM2IDE1Mi45MSwxOC4xNSAxNzAuNDgsMjYuMDggQyAxNzEuOTIsMjYuNzggMTczLjgxLDI3LjA5IDE3NC43NiwyNS41OSBDIDE3Ni4wNSwyMy43MiAxNzUuMzEsMjEuMDcgMTczLjAxLDIwLjM0IEMgMTU0Ljc4LDExLjk2IDEzNy4yMSw3LjE3IDExNC40Nyw2IEggMTEzLjUyIEMgMTAxLjkxLDYgOTMuNDYsMTIuMTYgODkuNDksMjEuNzggQyA0Mi4zNiwzMS4yNiA2LjE3LDc0Ljc2IDYuMTcsMTI4LjA4IEMgNi4xNywxOTAuMDUgNTcuOTIsMjQzLjkxIDEyNS4xOSwyNDMuOTEgWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNIDkzLjY3LDQwLjY0IEMgMTAwLjUxLDUyLjA3IDEwOS41NCw1MS4zMyAxMTQuMDUsNTIuMTcgQyAxMjguNzIsNTMuOTEgMTQxLjQ4LDU1Ljc4IDE1Ny4zOCw2Mi4xNiBDIDE2Mi43Miw2NC40NyAxNjIuMjksNTguMTkgMTU5LjE4LDU3LjAxIEMgMTQ1LjExLDUxLjMzIDEzMi40OCw0OS43OSAxMTEuMzEsNDcuNDggQyAxMDEuODMsNDYuMjkgOTUuNDUsNDEuMTggOTMuNzUsMzIuODEgQyA1NS4yMSwzOS40NiAyMi4wNiw3Mi4xNyAyMi4wNiwxMTIuNDggQyAyMi4wNiwxMzEuOTggMzAuMzYsMTQ5LjgyIDQzLjI2LDE2NC40OSBDIDQ2LjIzLDE2Ny41OSA1MC4xOSwxNjQuMTMgNDguMzIsMTYxLjAyIEMgMzYuMjEsMTQ1LjU0IDI4LjQyLDEyOS43OCAyOC40MiwxMTIuNCBDIDI4LjQyLDc5LjExIDU0LjkxLDQ4LjM2IDg5LjkxLDQwLjM2IEMgOTAuNzYsNDAuMTUgOTEuMDQsMzkuODcgOTEuNjIsNDAuMDEgQyA5Mi42Miw0MC4wMSA5My4wNCwzOS42NSA5My42Nyw0MC42NCBaIiBmaWxsPSIjZmZmIi8+CjxwYXRoIGQ9Ik0gMTUyLjcyLDgyLjc3IEMgMTI2LjYxLDgyLjc3IDExMy4wNyw5OS40NCAxMDMuMDEsMTE5LjMzIEMgMTAwLjU2LDEyMy4zNiAxMDMuNzQsMTI1LjAzIDEwNS42MSwxMjMuOTIgQyAxMDcuMTUsMTIzLjIyIDEwNy44OSwxMjEuMDUgMTA4LjczLDExOS42MSBDIDExOC4yMiwxMDIuMTYgMTMwLjMzLDg4LjU2IDE1Mi43Miw4OC41NiBDIDE4MS42Miw4OC41NiAyMDEuOTEsMTE2LjAxIDIwMS45MSwxNDcuMzEgQyAyMDEuOTEsMTc1LjEyIDE4My40NywxOTkuOTYgMTUyLjUxLDIwNS43NSBDIDE1MS44NCwyMDUuOTYgMTUxLjYzLDIwNi4wMyAxNTEuNTYsMjA1LjU0IEMgMTQ3Ljc0LDE5NS4zNyAxMzkuMzYsMTg4LjE1IDEyOC4wNywxODYuNDggQyAxMTMuMiwxODQuMjQgMTAxLjIzLDE4Mi4zNiA4My44LDE3Ni44MSBDIDc5LjMsMTc1LjQ4IDc3LjkxLDE4Mi4zNiA4Mi40MSwxODMuMDkgQyA5Ny4yMSwxODcuNDYgMTA4LjA5LDE4OS40NyAxMjYuMjUsMTkyLjY1IEMgMTM2Ljc4LDE5NC4zMSAxNDUuNDEsMjAxLjcxIDE0Ny4xMSwyMTAuOTUgQyAxNDcuNzQsMjEzLjA1IDE0OS4xMywyMTMuNDEgMTUwLjE1LDIxMy4yNiBDIDE4My43NSwyMDguNjEgMjA4LjI2LDE4MC45MyAyMDguMjYsMTQ3LjI0IEMgMjA4LjI2LDExNS4wNiAxODYuOTQsODIuNzcgMTUyLjcyLDgyLjc3IFoiIGZpbGw9IiNmZmYiLz4KPHBhdGggZD0iTSAxMjkuNzcsMTA1LjIxIEMgMTIyLjkzLDExMi4wNSAxMTguOTcsMTIyLjczIDExMy43NywxMzAuNDEgQyAxMTEuMzEsMTMzLjQ1IDExNC41NiwxMzYuNjMgMTE3LjQ2LDEzNC40NiBDIDEyMy43NSwxMjYuMjMgMTI3LjQzLDExNS42MiAxMzUuMTUsMTA4LjcxIEMgMTM4LjIyLDEwNS44MSAxMzQuNzMsMTAxLjA5IDEyOS43NywxMDUuMjEgWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNIDEzNi43OCwxMjAuMzEgQyAxMjcuNzEsMTM2LjcxIDEyMC4xMiwxNTQuOTEgOTMuNzQsMTU0LjkxIEMgNjYuMDcsMTU0LjkxIDQ3Ljc2LDEyOC41MyA0Ny43NiwxMDQuNzggQyA0Ny43Niw4NC40NyA1OC41Nyw2Ni4wOCA3Ny42Niw1Ni4yNSBDIDgyLjIzLDU0LjIxIDc5Ljg1LDQ3Ljc2IDc1LjM0LDQ5LjkzIEMgNTQuNzcsNTkuNzIgNDIuMDEsODAuMTEgNDIuMDEsMTA0LjcxIEMgNDIuMDEsMTMxLjc3IDYxLjg2LDE2MS4zMSA5My42NywxNjEuMzEgQyAxMTQuNzcsMTYxLjMxIDEyOC45MSwxNDcuMjQgMTM5Ljg2LDEyNC4wNiBDIDE0Mi43NiwxMjAuNDUgMTM5LjE1LDExNy43MyAxMzYuNzgsMTIwLjMxIFoiIGZpbGw9IiNmZmYiLz4KPHBhdGggZD0iTSAzMC43MywxNTQuNyBDIDI3Ljc2LDE1Mi45NyAyMy44NywxNTUuOTMgMjUuNDEsMTU4Ljc2IEMgNDEuNzMsMTg4LjM2IDY4Ljk0LDE5OS43OSAxMDUuNzUsMjA2LjQxIEMgMTEyLjI1LDIwNy42NiAxMjIuMDcsMjA4Ljc1IDEyMy40NiwyMDkuMDMgQyAxMjguMDcsMjA5Ljk1IDEyOC4wNywyMjAuMTggMTIxLjc4LDIyMC4xOCBDIDEwNy42NCwyMTguOTQgOTIuMDYsMjE1Ljk4IDc2LjIzLDIxMS4zMyBDIDcyLjEzLDIxMC4yNCA3MS4wNCwyMTYuNjkgNzUuMjcsMjE3LjY0IEMgOTAuNDEsMjIyLjIyIDEwMy45NSwyMjQuNzQgMTIwLjQ3LDIyNi41NCBDIDEzMy43MywyMjYuNTQgMTM2LjU2LDIwOS4wMyAxMjYuMDMsMjAzLjM4IEMgMTIzLjc1LDIwMi4xMyAxMjIuNzMsMjAyLjU2IDExMi4wNCwyMDAuNzYgQyA3OC4wOSwxOTUuMDQgNTQuMDYsMTg4Ljk4IDMyLjEyLDE1NS42NSBDIDMxLjc3LDE1NS4yMyAzMS4yOCwxNTQuOTEgMzAuNzMsMTU0LjcgWiIgZmlsbD0iI2ZmZiIvPgo8L3N2Zz4=";
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Text width estimation
108
+ // ---------------------------------------------------------------------------
109
+
110
+ function measureText(text: string): number {
111
+ let width = 0;
112
+ for (const ch of text) {
113
+ width += CHAR_WIDTHS[ch] ?? DEFAULT_CHAR_WIDTH;
114
+ }
115
+ return width;
116
+ }
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // SVG escaping
120
+ // ---------------------------------------------------------------------------
121
+
122
+ function escapeXml(text: string): string {
123
+ return text
124
+ .replace(/&/g, "&amp;")
125
+ .replace(/</g, "&lt;")
126
+ .replace(/>/g, "&gt;")
127
+ .replace(/"/g, "&quot;")
128
+ .replace(/'/g, "&apos;");
129
+ }
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // renderBadgeSvg
133
+ // ---------------------------------------------------------------------------
134
+
135
+ /**
136
+ * Render a shields.io flat-style SVG badge from BadgeData.
137
+ *
138
+ * Layout: [label (gray #555)] [value (colored)]
139
+ * Each half has 10px padding on each side, 1px gap between halves.
140
+ */
141
+ export function renderBadgeSvg(data: BadgeData): string {
142
+ const labelText = data.label;
143
+ const valueText = data.message;
144
+
145
+ const labelTextWidth = measureText(labelText);
146
+ const valueTextWidth = measureText(valueText);
147
+
148
+ // 10px padding on each side of text + logo space in label
149
+ const labelWidth = Math.round(labelTextWidth + 20 + LOGO_EXTRA);
150
+ const valueWidth = Math.round(valueTextWidth + 20);
151
+ const totalWidth = labelWidth + 1 + valueWidth; // 1px gap
152
+
153
+ const labelTextX = (labelWidth + LOGO_EXTRA) / 2;
154
+ const valueX = labelWidth + 1 + valueWidth / 2;
155
+
156
+ const height = 20;
157
+ const labelColor = "#555";
158
+ const valueColor = data.color;
159
+
160
+ const escapedLabel = escapeXml(labelText);
161
+ const escapedValue = escapeXml(valueText);
162
+
163
+ return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${totalWidth}" height="${height}" role="img" aria-label="${escapedLabel}: ${escapedValue}">
164
+ <linearGradient id="b" x2="0" y2="100%">
165
+ <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
166
+ <stop offset="1" stop-opacity=".1"/>
167
+ </linearGradient>
168
+ <clipPath id="a">
169
+ <rect width="${totalWidth}" height="${height}" rx="3" fill="#fff"/>
170
+ </clipPath>
171
+ <g clip-path="url(#a)">
172
+ <rect width="${labelWidth}" height="${height}" fill="${labelColor}"/>
173
+ <rect x="${labelWidth + 1}" width="${valueWidth}" height="${height}" fill="${valueColor}"/>
174
+ <rect width="${totalWidth}" height="${height}" fill="url(#b)"/>
175
+ </g>
176
+ <image x="3" y="3" width="${LOGO_SIZE}" height="${LOGO_SIZE}" xlink:href="${LOGO_SVG_BASE64}"/>
177
+ <g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
178
+ <text x="${labelTextX}" y="15" fill="#010101" fill-opacity=".3">${escapedLabel}</text>
179
+ <text x="${labelTextX}" y="14">${escapedLabel}</text>
180
+ <text x="${valueX}" y="15" fill="#010101" fill-opacity=".3">${escapedValue}</text>
181
+ <text x="${valueX}" y="14">${escapedValue}</text>
182
+ </g>
183
+ </svg>`;
184
+ }
185
+
186
+ // ---------------------------------------------------------------------------
187
+ // formatBadgeOutput
188
+ // ---------------------------------------------------------------------------
189
+
190
+ /**
191
+ * Route badge data to the requested output format.
192
+ *
193
+ * - "svg" local SVG string via renderBadgeSvg
194
+ * - "markdown" shields.io markdown image link
195
+ * - "url" shields.io badge URL
196
+ */
197
+ export function formatBadgeOutput(data: BadgeData, skillName: string, format: BadgeFormat): string {
198
+ if (format === "svg") {
199
+ return renderBadgeSvg(data);
200
+ }
201
+
202
+ const label = encodeURIComponent(data.label);
203
+ const message = encodeURIComponent(data.message);
204
+ const color = data.color.replace("#", "");
205
+ const url = `https://img.shields.io/badge/${label}-${message}-${color}`;
206
+
207
+ if (format === "markdown") {
208
+ return `![Skill Health: ${skillName}](${url})`;
209
+ }
210
+
211
+ return url;
212
+ }
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * selftune badge -- Generate skill health badges for READMEs.
4
+ *
5
+ * Usage:
6
+ * selftune badge --skill <name> [--format svg|markdown|url] [--output <path>]
7
+ */
8
+
9
+ import { writeFileSync } from "node:fs";
10
+ import { parseArgs } from "node:util";
11
+ import { EVOLUTION_AUDIT_LOG, QUERY_LOG, TELEMETRY_LOG } from "../constants.js";
12
+ import { doctor } from "../observability.js";
13
+ import { computeStatus } from "../status.js";
14
+ import type { EvolutionAuditEntry, QueryLogRecord, SessionTelemetryRecord } from "../types.js";
15
+ import { readJsonl } from "../utils/jsonl.js";
16
+ import { readEffectiveSkillUsageRecords } from "../utils/skill-log.js";
17
+ import type { BadgeFormat } from "./badge-data.js";
18
+ import { findSkillBadgeData } from "./badge-data.js";
19
+ import { formatBadgeOutput } from "./badge-svg.js";
20
+
21
+ const HELP = `selftune badge \u2014 Generate skill health badges
22
+
23
+ Usage: selftune badge --skill <name> [options]
24
+
25
+ Options:
26
+ --skill <name> Skill name (required)
27
+ --format <type> Output format: svg, markdown, url (default: svg)
28
+ --output <path> Write to file instead of stdout
29
+ --help Show this help`;
30
+
31
+ const VALID_FORMATS = new Set<BadgeFormat>(["svg", "markdown", "url"]);
32
+
33
+ export function cliMain(): void {
34
+ const { values } = parseArgs({
35
+ args: process.argv.slice(2),
36
+ options: {
37
+ skill: { type: "string" },
38
+ format: { type: "string" },
39
+ output: { type: "string" },
40
+ help: { type: "boolean" },
41
+ },
42
+ strict: true,
43
+ });
44
+
45
+ if (values.help) {
46
+ console.log(HELP);
47
+ return;
48
+ }
49
+
50
+ if (!values.skill) {
51
+ console.error("Error: --skill is required\n");
52
+ console.error(HELP);
53
+ process.exit(1);
54
+ }
55
+
56
+ if (values.format && !VALID_FORMATS.has(values.format as BadgeFormat)) {
57
+ console.error(`Error: invalid format '${values.format}'. Must be one of: svg, markdown, url\n`);
58
+ console.error(HELP);
59
+ process.exit(1);
60
+ }
61
+
62
+ const format: BadgeFormat =
63
+ values.format && VALID_FORMATS.has(values.format as BadgeFormat)
64
+ ? (values.format as BadgeFormat)
65
+ : "svg";
66
+
67
+ // Read log files
68
+ const telemetry = readJsonl<SessionTelemetryRecord>(TELEMETRY_LOG);
69
+ const skillRecords = readEffectiveSkillUsageRecords();
70
+ const queryRecords = readJsonl<QueryLogRecord>(QUERY_LOG);
71
+ const auditEntries = readJsonl<EvolutionAuditEntry>(EVOLUTION_AUDIT_LOG);
72
+
73
+ // Run doctor for system health
74
+ const doctorResult = doctor();
75
+
76
+ // Compute status
77
+ const result = computeStatus(telemetry, skillRecords, queryRecords, auditEntries, doctorResult);
78
+
79
+ // Find skill badge data
80
+ const badgeData = findSkillBadgeData(result, values.skill);
81
+ if (!badgeData) {
82
+ console.error(`Skill not found: ${values.skill}`);
83
+ process.exit(1);
84
+ }
85
+
86
+ // Generate output
87
+ const output = formatBadgeOutput(badgeData, values.skill, format);
88
+
89
+ if (values.output) {
90
+ writeFileSync(values.output, output, "utf-8");
91
+ console.log(`Badge written to ${values.output}`);
92
+ } else {
93
+ console.log(output);
94
+ }
95
+ }
96
+
97
+ if (import.meta.main) {
98
+ cliMain();
99
+ }