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.
- package/.claude/agents/diagnosis-analyst.md +156 -0
- package/.claude/agents/evolution-reviewer.md +180 -0
- package/.claude/agents/integration-guide.md +212 -0
- package/.claude/agents/pattern-analyst.md +160 -0
- package/CHANGELOG.md +46 -1
- package/README.md +105 -257
- package/apps/local-dashboard/dist/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
- package/apps/local-dashboard/dist/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
- package/apps/local-dashboard/dist/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
- package/apps/local-dashboard/dist/assets/index-C4EOTFZ2.js +15 -0
- package/apps/local-dashboard/dist/assets/index-bl-Webyd.css +1 -0
- package/apps/local-dashboard/dist/assets/vendor-react-U7zYD9Rg.js +60 -0
- package/apps/local-dashboard/dist/assets/vendor-table-B7VF2Ipl.js +26 -0
- package/apps/local-dashboard/dist/assets/vendor-ui-D7_zX_qy.js +346 -0
- package/apps/local-dashboard/dist/favicon.png +0 -0
- package/apps/local-dashboard/dist/index.html +17 -0
- package/apps/local-dashboard/dist/logo.png +0 -0
- package/apps/local-dashboard/dist/logo.svg +9 -0
- package/assets/BeforeAfter.gif +0 -0
- package/assets/FeedbackLoop.gif +0 -0
- package/assets/logo.svg +9 -0
- package/assets/skill-health-badge.svg +20 -0
- package/cli/selftune/activation-rules.ts +171 -0
- package/cli/selftune/badge/badge-data.ts +108 -0
- package/cli/selftune/badge/badge-svg.ts +212 -0
- package/cli/selftune/badge/badge.ts +99 -0
- package/cli/selftune/canonical-export.ts +183 -0
- package/cli/selftune/constants.ts +103 -1
- package/cli/selftune/contribute/bundle.ts +314 -0
- package/cli/selftune/contribute/contribute.ts +214 -0
- package/cli/selftune/contribute/sanitize.ts +162 -0
- package/cli/selftune/cron/setup.ts +266 -0
- package/cli/selftune/dashboard-contract.ts +202 -0
- package/cli/selftune/dashboard-server.ts +1049 -0
- package/cli/selftune/dashboard.ts +43 -156
- package/cli/selftune/eval/baseline.ts +248 -0
- package/cli/selftune/eval/composability-v2.ts +273 -0
- package/cli/selftune/eval/composability.ts +117 -0
- package/cli/selftune/eval/generate-unit-tests.ts +143 -0
- package/cli/selftune/eval/hooks-to-evals.ts +101 -16
- package/cli/selftune/eval/import-skillsbench.ts +221 -0
- package/cli/selftune/eval/synthetic-evals.ts +172 -0
- package/cli/selftune/eval/unit-test-cli.ts +152 -0
- package/cli/selftune/eval/unit-test.ts +196 -0
- package/cli/selftune/evolution/deploy-proposal.ts +142 -1
- package/cli/selftune/evolution/evidence.ts +26 -0
- package/cli/selftune/evolution/evolve-body.ts +586 -0
- package/cli/selftune/evolution/evolve.ts +825 -116
- package/cli/selftune/evolution/extract-patterns.ts +105 -16
- package/cli/selftune/evolution/pareto.ts +314 -0
- package/cli/selftune/evolution/propose-body.ts +171 -0
- package/cli/selftune/evolution/propose-description.ts +100 -2
- package/cli/selftune/evolution/propose-routing.ts +166 -0
- package/cli/selftune/evolution/refine-body.ts +141 -0
- package/cli/selftune/evolution/rollback.ts +21 -4
- package/cli/selftune/evolution/validate-body.ts +254 -0
- package/cli/selftune/evolution/validate-proposal.ts +257 -35
- package/cli/selftune/evolution/validate-routing.ts +177 -0
- package/cli/selftune/grading/auto-grade.ts +200 -0
- package/cli/selftune/grading/grade-session.ts +513 -42
- package/cli/selftune/grading/pre-gates.ts +104 -0
- package/cli/selftune/grading/results.ts +42 -0
- package/cli/selftune/hooks/auto-activate.ts +185 -0
- package/cli/selftune/hooks/evolution-guard.ts +165 -0
- package/cli/selftune/hooks/prompt-log.ts +172 -2
- package/cli/selftune/hooks/session-stop.ts +123 -3
- package/cli/selftune/hooks/skill-change-guard.ts +112 -0
- package/cli/selftune/hooks/skill-eval.ts +119 -3
- package/cli/selftune/index.ts +415 -48
- package/cli/selftune/ingestors/claude-replay.ts +377 -0
- package/cli/selftune/ingestors/codex-rollout.ts +345 -46
- package/cli/selftune/ingestors/codex-wrapper.ts +207 -39
- package/cli/selftune/ingestors/openclaw-ingest.ts +573 -0
- package/cli/selftune/ingestors/opencode-ingest.ts +193 -17
- package/cli/selftune/init.ts +376 -16
- package/cli/selftune/last.ts +14 -5
- package/cli/selftune/localdb/db.ts +63 -0
- package/cli/selftune/localdb/materialize.ts +428 -0
- package/cli/selftune/localdb/queries.ts +376 -0
- package/cli/selftune/localdb/schema.ts +204 -0
- package/cli/selftune/memory/writer.ts +447 -0
- package/cli/selftune/monitoring/watch.ts +90 -16
- package/cli/selftune/normalization.ts +682 -0
- package/cli/selftune/observability.ts +19 -44
- package/cli/selftune/orchestrate.ts +1073 -0
- package/cli/selftune/quickstart.ts +203 -0
- package/cli/selftune/repair/skill-usage.ts +576 -0
- package/cli/selftune/schedule.ts +561 -0
- package/cli/selftune/status.ts +59 -33
- package/cli/selftune/sync.ts +627 -0
- package/cli/selftune/types.ts +525 -5
- package/cli/selftune/utils/canonical-log.ts +45 -0
- package/cli/selftune/utils/frontmatter.ts +217 -0
- package/cli/selftune/utils/hooks.ts +41 -0
- package/cli/selftune/utils/html.ts +27 -0
- package/cli/selftune/utils/llm-call.ts +103 -19
- package/cli/selftune/utils/math.ts +10 -0
- package/cli/selftune/utils/query-filter.ts +139 -0
- package/cli/selftune/utils/skill-discovery.ts +340 -0
- package/cli/selftune/utils/skill-log.ts +68 -0
- package/cli/selftune/utils/skill-usage-confidence.ts +18 -0
- package/cli/selftune/utils/transcript.ts +307 -26
- package/cli/selftune/utils/trigger-check.ts +89 -0
- package/cli/selftune/utils/tui.ts +156 -0
- package/cli/selftune/workflows/discover.ts +254 -0
- package/cli/selftune/workflows/skill-md-writer.ts +288 -0
- package/cli/selftune/workflows/workflows.ts +188 -0
- package/package.json +28 -11
- package/packages/telemetry-contract/README.md +11 -0
- package/packages/telemetry-contract/fixtures/golden.json +87 -0
- package/packages/telemetry-contract/fixtures/golden.test.ts +42 -0
- package/packages/telemetry-contract/index.ts +1 -0
- package/packages/telemetry-contract/package.json +19 -0
- package/packages/telemetry-contract/src/index.ts +2 -0
- package/packages/telemetry-contract/src/types.ts +163 -0
- package/packages/telemetry-contract/src/validators.ts +109 -0
- package/skill/SKILL.md +180 -33
- package/skill/Workflows/AutoActivation.md +145 -0
- package/skill/Workflows/Badge.md +124 -0
- package/skill/Workflows/Baseline.md +144 -0
- package/skill/Workflows/Composability.md +107 -0
- package/skill/Workflows/Contribute.md +94 -0
- package/skill/Workflows/Cron.md +132 -0
- package/skill/Workflows/Dashboard.md +214 -0
- package/skill/Workflows/Doctor.md +63 -14
- package/skill/Workflows/Evals.md +110 -18
- package/skill/Workflows/EvolutionMemory.md +154 -0
- package/skill/Workflows/Evolve.md +181 -21
- package/skill/Workflows/EvolveBody.md +159 -0
- package/skill/Workflows/Grade.md +36 -31
- package/skill/Workflows/ImportSkillsBench.md +117 -0
- package/skill/Workflows/Ingest.md +142 -21
- package/skill/Workflows/Initialize.md +91 -23
- package/skill/Workflows/Orchestrate.md +139 -0
- package/skill/Workflows/Replay.md +91 -0
- package/skill/Workflows/Rollback.md +23 -4
- package/skill/Workflows/Schedule.md +61 -0
- package/skill/Workflows/Sync.md +88 -0
- package/skill/Workflows/UnitTest.md +150 -0
- package/skill/Workflows/Watch.md +33 -1
- package/skill/Workflows/Workflows.md +129 -0
- package/skill/assets/activation-rules-default.json +26 -0
- package/skill/assets/multi-skill-settings.json +63 -0
- package/skill/assets/single-skill-settings.json +57 -0
- package/skill/references/invocation-taxonomy.md +2 -2
- package/skill/references/logs.md +164 -2
- package/skill/references/setup-patterns.md +65 -0
- package/skill/references/version-history.md +40 -0
- package/skill/settings_snippet.json +23 -0
- package/templates/activation-rules-default.json +27 -0
- package/templates/multi-skill-settings.json +64 -0
- package/templates/single-skill-settings.json +58 -0
- package/dashboard/index.html +0 -1119
|
Binary file
|
|
@@ -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>
|
|
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="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
|
package/assets/logo.svg
ADDED
|
@@ -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, "&")
|
|
125
|
+
.replace(/</g, "<")
|
|
126
|
+
.replace(/>/g, ">")
|
|
127
|
+
.replace(/"/g, """)
|
|
128
|
+
.replace(/'/g, "'");
|
|
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 ``;
|
|
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
|
+
}
|