@bompus/house-rules 0.0.0-stage → 0.13.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.
Files changed (236) hide show
  1. package/CODE_OF_CONDUCT.md +83 -0
  2. package/CONTRIBUTING.md +54 -0
  3. package/LICENSE +21 -0
  4. package/README.md +378 -2
  5. package/THIRD_PARTY_NOTICES.md +188 -0
  6. package/compose.mjs +125 -0
  7. package/composition.mjs +361 -0
  8. package/config-view.mjs +209 -0
  9. package/config.mjs +381 -0
  10. package/docs/assets/banner-dark.png +0 -0
  11. package/docs/assets/banner-light.png +0 -0
  12. package/docs/assets/overview.png +0 -0
  13. package/docs/configuration.md +269 -0
  14. package/docs/skill-names.md +50 -0
  15. package/examples/person/house-rules.json +15 -0
  16. package/examples/person/rules/10-tooling.md +8 -0
  17. package/examples/person/rules/20-reporting.md +9 -0
  18. package/examples/project/AGENTS.md +18 -0
  19. package/package.json +42 -4
  20. package/rule-references.mjs +91 -0
  21. package/rules/core.md +340 -0
  22. package/rules/modifiers/backlog-outline.md +25 -0
  23. package/rules/modifiers/backlog-visual.md +16 -0
  24. package/rules/modifiers/coded-offers.md +70 -0
  25. package/rules/modifiers/cross-session-relay.md +23 -0
  26. package/rules/modifiers/effort-estimates.md +41 -0
  27. package/rules/modifiers/land-when-done.md +12 -0
  28. package/rules/modifiers/low-quota-handoff.md +20 -0
  29. package/rules/modifiers/model-default-questions.md +33 -0
  30. package/rules/modifiers/multi-agent.md +38 -0
  31. package/rules/modifiers/no-attribution.md +16 -0
  32. package/rules/modifiers/periodic-updates.md +25 -0
  33. package/rules/modifiers/plan-files.md +82 -0
  34. package/rules/modifiers/question-cards.md +48 -0
  35. package/rules/modifiers/release-batching.md +15 -0
  36. package/rules/modifiers/scratch-on-disk.md +15 -0
  37. package/rules/modifiers/shared-host-load.md +19 -0
  38. package/rules/modifiers/solo-operator.md +13 -0
  39. package/rules/modifiers/squash-landing.md +22 -0
  40. package/rules/modifiers/upstream-contributions.md +45 -0
  41. package/rules/modifiers/web-image-formats.md +35 -0
  42. package/rules/references/backlog-view.md +154 -0
  43. package/rules/references/github-actions.md +45 -0
  44. package/rules/references/landing.md +39 -0
  45. package/rules/references/multi-agent.md +51 -0
  46. package/rules/references/release-batching.md +66 -0
  47. package/rules/references/squash-landing.md +50 -0
  48. package/rules/references/thread-visuals.md +45 -0
  49. package/setup.mjs +445 -0
  50. package/skills/hr-agent-guidance-audit/LICENSE +21 -0
  51. package/skills/hr-agent-guidance-audit/SKILL.md +91 -0
  52. package/skills/hr-agent-guidance-audit/agents/openai.yaml +2 -0
  53. package/skills/hr-agent-guidance-audit/references/prune-workflow.md +72 -0
  54. package/skills/hr-agent-guidance-audit/references/records-and-lessons.md +60 -0
  55. package/skills/hr-agent-guidance-audit/references/retirement-verdicts.md +49 -0
  56. package/skills/hr-agent-guidance-refresh/LICENSE +21 -0
  57. package/skills/hr-agent-guidance-refresh/SKILL.md +36 -0
  58. package/skills/hr-agent-guidance-refresh/agents/openai.yaml +2 -0
  59. package/skills/hr-agent-guidance-refresh/references/verification.md +47 -0
  60. package/skills/hr-api-exposure-check/SKILL.md +159 -0
  61. package/skills/hr-api-exposure-check/agents/openai.yaml +3 -0
  62. package/skills/hr-audit-choices/SKILL.md +67 -0
  63. package/skills/hr-audit-choices/agents/openai.yaml +2 -0
  64. package/skills/hr-benchmarking/NOTICE.md +36 -0
  65. package/skills/hr-benchmarking/SKILL.md +279 -0
  66. package/skills/hr-benchmarking/agents/openai.yaml +2 -0
  67. package/skills/hr-benchmarking/references/javascript.md +130 -0
  68. package/skills/hr-benchmarking/references/tools.md +165 -0
  69. package/skills/hr-benchmarking/scripts/process-attribution.ps1 +20 -0
  70. package/skills/hr-benchmarking/scripts/process-attribution.py +107 -0
  71. package/skills/hr-better-accessibility/LICENSE +22 -0
  72. package/skills/hr-better-accessibility/NOTICE.md +46 -0
  73. package/skills/hr-better-accessibility/SKILL.md +119 -0
  74. package/skills/hr-better-accessibility/agents/openai.yaml +3 -0
  75. package/skills/hr-better-accessibility/focus-and-keyboard.md +130 -0
  76. package/skills/hr-better-accessibility/forms.md +82 -0
  77. package/skills/hr-better-accessibility/hit-areas.md +98 -0
  78. package/skills/hr-better-accessibility/motion-and-zoom.md +81 -0
  79. package/skills/hr-better-accessibility/screen-readers.md +102 -0
  80. package/skills/hr-better-accessibility/semantics-and-aria.md +84 -0
  81. package/skills/hr-better-colors/LICENSE +21 -0
  82. package/skills/hr-better-colors/NOTICE.md +37 -0
  83. package/skills/hr-better-colors/SKILL.md +114 -0
  84. package/skills/hr-better-colors/agents/openai.yaml +3 -0
  85. package/skills/hr-better-colors/color-formats.md +90 -0
  86. package/skills/hr-better-colors/color-usage.md +140 -0
  87. package/skills/hr-better-colors/contrast.md +80 -0
  88. package/skills/hr-better-colors/palette-generation.md +104 -0
  89. package/skills/hr-better-colors/palette-structure.md +76 -0
  90. package/skills/hr-better-colors/token-naming.md +97 -0
  91. package/skills/hr-better-layout/LICENSE +22 -0
  92. package/skills/hr-better-layout/NOTICE.md +48 -0
  93. package/skills/hr-better-layout/SKILL.md +92 -0
  94. package/skills/hr-better-layout/agents/openai.yaml +3 -0
  95. package/skills/hr-better-layout/grouping-and-alignment.md +144 -0
  96. package/skills/hr-better-layout/spacing-and-adaptivity.md +215 -0
  97. package/skills/hr-better-typography/LICENSE +22 -0
  98. package/skills/hr-better-typography/NOTICE.md +46 -0
  99. package/skills/hr-better-typography/SKILL.md +172 -0
  100. package/skills/hr-better-typography/agents/openai.yaml +3 -0
  101. package/skills/hr-better-typography/choosing-fonts.md +69 -0
  102. package/skills/hr-better-typography/css-cheat-sheet.md +65 -0
  103. package/skills/hr-better-typography/details-and-accessibility.md +119 -0
  104. package/skills/hr-better-typography/spacing-and-sizing.md +131 -0
  105. package/skills/hr-better-typography/variable-fonts-and-opentype.md +105 -0
  106. package/skills/hr-better-typography/wrapping-and-punctuation.md +57 -0
  107. package/skills/hr-better-writing/LICENSE +22 -0
  108. package/skills/hr-better-writing/NOTICE.md +46 -0
  109. package/skills/hr-better-writing/SKILL.md +122 -0
  110. package/skills/hr-better-writing/agents/openai.yaml +3 -0
  111. package/skills/hr-code-review/LICENSE +21 -0
  112. package/skills/hr-code-review/NOTICE.md +26 -0
  113. package/skills/hr-code-review/SKILL.md +163 -0
  114. package/skills/hr-code-review/agents/openai.yaml +3 -0
  115. package/skills/hr-code-review/references/acp-client.md +43 -0
  116. package/skills/hr-code-review/references/evidence.md +57 -0
  117. package/skills/hr-code-review/references/impact.md +103 -0
  118. package/skills/hr-code-review/references/panel-adjudication.md +58 -0
  119. package/skills/hr-code-review/references/panel-plan.md +61 -0
  120. package/skills/hr-code-review/references/panel-recommendations.json +105 -0
  121. package/skills/hr-code-review/references/panel-recommendations.md +46 -0
  122. package/skills/hr-code-review/references/panel-setup.md +54 -0
  123. package/skills/hr-code-review/references/reviewer-role.md +31 -0
  124. package/skills/hr-code-review/scripts/acp-panel-client.mjs +299 -0
  125. package/skills/hr-code-review/scripts/check-receipts.mjs +263 -0
  126. package/skills/hr-code-review/scripts/panel-plan.mjs +69 -0
  127. package/skills/hr-code-review/scripts/panel-preferences.mjs +220 -0
  128. package/skills/hr-design-exploration/SKILL.md +105 -0
  129. package/skills/hr-design-exploration/agents/openai.yaml +4 -0
  130. package/skills/hr-diagnosing-bugs/NOTICE.md +45 -0
  131. package/skills/hr-diagnosing-bugs/SKILL.md +108 -0
  132. package/skills/hr-diagnosing-bugs/agents/openai.yaml +3 -0
  133. package/skills/hr-diagnosing-bugs/references/reporting.md +97 -0
  134. package/skills/hr-diagnosing-bugs/scripts/check-pr-offer.mjs +47 -0
  135. package/skills/hr-explain-code/NOTICE.md +37 -0
  136. package/skills/hr-explain-code/SKILL.md +110 -0
  137. package/skills/hr-explain-code/agents/openai.yaml +5 -0
  138. package/skills/hr-explain-code/references/helper-brief.md +39 -0
  139. package/skills/hr-extract-shared-steps/SKILL.md +134 -0
  140. package/skills/hr-extract-shared-steps/agents/openai.yaml +3 -0
  141. package/skills/hr-forgotten-work-audit/SKILL.md +130 -0
  142. package/skills/hr-forgotten-work-audit/references/brief-template.md +56 -0
  143. package/skills/hr-forgotten-work-audit/references/result-format.md +46 -0
  144. package/skills/hr-handoff/LICENSE +21 -0
  145. package/skills/hr-handoff/NOTICE.md +29 -0
  146. package/skills/hr-handoff/SKILL.md +114 -0
  147. package/skills/hr-handoff/agents/openai.yaml +5 -0
  148. package/skills/hr-handoff/assets/ledger.example.json +10 -0
  149. package/skills/hr-handoff/assets/receipt.example.json +76 -0
  150. package/skills/hr-handoff/assets/source.example.json +20 -0
  151. package/skills/hr-handoff/references/reconciliation.md +124 -0
  152. package/skills/hr-handoff/references/successor-thread.md +76 -0
  153. package/skills/hr-handoff/scripts/check-reconciliation.mjs +324 -0
  154. package/skills/hr-house-rules-setup/SKILL.md +114 -0
  155. package/skills/hr-house-rules-setup/agents/openai.yaml +5 -0
  156. package/skills/hr-issue-tracker-setup/SKILL.md +49 -0
  157. package/skills/hr-issue-tracker-setup/agents/openai.yaml +5 -0
  158. package/skills/hr-lean-plan/SKILL.md +146 -0
  159. package/skills/hr-lean-plan/agents/openai.yaml +5 -0
  160. package/skills/hr-maintainability-review/SKILL.md +186 -0
  161. package/skills/hr-maintainability-review/agents/openai.yaml +5 -0
  162. package/skills/hr-manual-qa/NOTICE.md +36 -0
  163. package/skills/hr-manual-qa/SKILL.md +37 -0
  164. package/skills/hr-manual-qa/agents/openai.yaml +3 -0
  165. package/skills/hr-navigation-retro/LICENSE +21 -0
  166. package/skills/hr-navigation-retro/NOTICE.md +5 -0
  167. package/skills/hr-navigation-retro/SKILL.md +86 -0
  168. package/skills/hr-navigation-retro/agents/openai.yaml +4 -0
  169. package/skills/hr-navigation-retro/references/inputs.md +67 -0
  170. package/skills/hr-navigation-retro/scripts/retro.py +552 -0
  171. package/skills/hr-navigation-retro/scripts/transcripts.py +205 -0
  172. package/skills/hr-ordering-tests/SKILL.md +51 -0
  173. package/skills/hr-ordering-tests/agents/openai.yaml +2 -0
  174. package/skills/hr-plain-prose/SKILL.md +264 -0
  175. package/skills/hr-plain-prose/agents/openai.yaml +3 -0
  176. package/skills/hr-plain-prose/scripts/check.mjs +331 -0
  177. package/skills/hr-pr-followup/LICENSE +21 -0
  178. package/skills/hr-pr-followup/NOTICE.md +9 -0
  179. package/skills/hr-pr-followup/SKILL.md +74 -0
  180. package/skills/hr-pr-followup/agents/openai.yaml +2 -0
  181. package/skills/hr-pr-followup/references/review-helpers.md +111 -0
  182. package/skills/hr-pr-followup/scripts/check-landing.py +204 -0
  183. package/skills/hr-pr-followup/scripts/check-pr-review.ts +510 -0
  184. package/skills/hr-pr-followup/scripts/merge-pr.sh +100 -0
  185. package/skills/hr-pr-followup/scripts/resolve-pr-thread.ts +219 -0
  186. package/skills/hr-pr-followup/scripts/verify-landed.sh +184 -0
  187. package/skills/hr-progress-report/SKILL.md +49 -0
  188. package/skills/hr-progress-report/agents/openai.yaml +5 -0
  189. package/skills/hr-read-reddit/SKILL.md +44 -0
  190. package/skills/hr-read-reddit/agents/openai.yaml +2 -0
  191. package/skills/hr-read-reddit/scripts/reddit.ts +252 -0
  192. package/skills/hr-read-x-links/SKILL.md +42 -0
  193. package/skills/hr-read-x-links/agents/openai.yaml +2 -0
  194. package/skills/hr-read-x-links/scripts/x-post.ts +264 -0
  195. package/skills/hr-research-shared-links/SKILL.md +103 -0
  196. package/skills/hr-split-to-prs/LICENSE +21 -0
  197. package/skills/hr-split-to-prs/NOTICE.md +9 -0
  198. package/skills/hr-split-to-prs/SKILL.md +61 -0
  199. package/skills/hr-split-to-prs/agents/openai.yaml +2 -0
  200. package/skills/hr-sqlite/SKILL.md +174 -0
  201. package/skills/hr-sqlite/agents/openai.yaml +2 -0
  202. package/skills/hr-sqlite/references/evidence.md +94 -0
  203. package/skills/hr-sqlite/references/tool-decisions.md +39 -0
  204. package/skills/hr-sqlite/scripts/inspect-sqlite.py +245 -0
  205. package/skills/hr-stock-ui-audit/SKILL.md +132 -0
  206. package/skills/hr-stock-ui-audit/agents/openai.yaml +3 -0
  207. package/skills/hr-stock-ui-audit/scripts/scan.mjs +619 -0
  208. package/skills/hr-tdd/LICENSE +21 -0
  209. package/skills/hr-tdd/NOTICE.md +10 -0
  210. package/skills/hr-tdd/SKILL.md +40 -0
  211. package/skills/hr-tdd/agents/openai.yaml +3 -0
  212. package/skills/hr-tdd/mocking.md +39 -0
  213. package/skills/hr-tdd/tests.md +77 -0
  214. package/skills/hr-test-audit/LICENSE +21 -0
  215. package/skills/hr-test-audit/NOTICE.md +67 -0
  216. package/skills/hr-test-audit/SKILL.md +146 -0
  217. package/skills/hr-test-audit/agents/openai.yaml +3 -0
  218. package/skills/hr-test-audit/scripts/floor-guard.ts +137 -0
  219. package/skills/hr-usage-report/SKILL.md +90 -0
  220. package/skills/hr-usage-report/agents/openai.yaml +2 -0
  221. package/skills/hr-usage-report/assets/example-report.json +125 -0
  222. package/skills/hr-usage-report/assets/report-template.html +377 -0
  223. package/skills/hr-usage-report/scripts/render-report.ts +86 -0
  224. package/skills/hr-usage-report/scripts/usage-by-model.ts +499 -0
  225. package/skills/hr-what-next/SKILL.md +151 -0
  226. package/skills/hr-what-next/agents/openai.yaml +4 -0
  227. package/skills/hr-what-next/references/reporting.md +59 -0
  228. package/skills/hr-what-next/scripts/check-open-work.mjs +711 -0
  229. package/skills/hr-writing-for-agents/NOTICE.md +46 -0
  230. package/skills/hr-writing-for-agents/SKILL-MECHANICS.md +83 -0
  231. package/skills/hr-writing-for-agents/SKILL.md +108 -0
  232. package/skills/hr-writing-for-agents/agents/openai.yaml +3 -0
  233. package/skills/hr-writing-pr/NOTICE.md +48 -0
  234. package/skills/hr-writing-pr/SKILL.md +119 -0
  235. package/skills/hr-writing-pr/agents/openai.yaml +2 -0
  236. package/skills/hr-writing-pr/scripts/benefit-chart.py +200 -0
@@ -0,0 +1,125 @@
1
+ {
2
+ "title": "Example: three models across effort levels (illustrative data)",
3
+ "series": {
4
+ "small": { "name": "Model S", "slot": 0 },
5
+ "mid": { "name": "Model M", "slot": 1 },
6
+ "large": { "name": "Model L", "slot": 2 },
7
+ "previous": { "name": "Model L (previous)", "reference": true }
8
+ },
9
+ "sections": [
10
+ {
11
+ "type": "curve",
12
+ "title": "Score against cost per task, every effort level",
13
+ "sub": "Illustrative values. Each line runs low to max on a log cost axis.",
14
+ "xLog": true,
15
+ "xDomain": [0.05, 20],
16
+ "yDomain": [20, 60],
17
+ "xTicks": [0.1, 0.3, 1, 3, 10],
18
+ "xPrefix": "$",
19
+ "yTicks": [20, 30, 40, 50, 60],
20
+ "ySuffix": "%",
21
+ "xLabel": "cost per task (log)",
22
+ "series": [
23
+ {
24
+ "key": "small",
25
+ "points": [
26
+ { "label": "low", "x": 0.08, "y": 28, "tip": "28% · $0.08/task" },
27
+ { "label": "medium", "x": 0.2, "y": 34, "tip": "34% · $0.20/task" },
28
+ { "label": "max", "x": 1, "y": 45, "tip": "45% · $1.00/task" }
29
+ ]
30
+ },
31
+ {
32
+ "key": "mid",
33
+ "points": [
34
+ { "label": "low", "x": 0.5, "y": 35, "tip": "35% · $0.50/task" },
35
+ { "label": "medium", "x": 0.8, "y": 40, "tip": "40% · $0.80/task" },
36
+ { "label": "max", "x": 8, "y": 54, "tip": "54% · $8.00/task" }
37
+ ]
38
+ },
39
+ {
40
+ "key": "large",
41
+ "points": [
42
+ { "label": "low", "x": 1.2, "y": 44, "tip": "44% · $1.20/task" },
43
+ { "label": "medium", "x": 3, "y": 52, "tip": "52% · $3.00/task" },
44
+ { "label": "max", "x": 12, "y": 57, "tip": "57% · $12.00/task" }
45
+ ]
46
+ },
47
+ {
48
+ "key": "previous",
49
+ "points": [
50
+ { "label": "low", "x": 5, "y": 43, "tip": "43% · $5.00/task" },
51
+ { "label": "max", "x": 16, "y": 50, "tip": "50% · $16.00/task" }
52
+ ]
53
+ }
54
+ ]
55
+ },
56
+ {
57
+ "type": "ladder",
58
+ "title": "Review: bugs found per effort level",
59
+ "sub": "Illustrative values; notes carry run counts.",
60
+ "categories": ["low", "medium", "high", "max"],
61
+ "yDomain": [0, 50],
62
+ "yTicks": [0, 25, 50],
63
+ "series": [
64
+ {
65
+ "key": "small",
66
+ "values": [10, 12, 16, null],
67
+ "notes": ["1 run", "1 run", "2 runs", "no row"]
68
+ },
69
+ {
70
+ "key": "mid",
71
+ "values": [18, 23, 34, 45],
72
+ "notes": ["3 runs", "3 runs", "3 runs", "3 runs"]
73
+ },
74
+ {
75
+ "key": "large",
76
+ "values": [22, 30, 32, 41],
77
+ "notes": ["3 runs", "3 runs", "3 runs", "3 runs"]
78
+ }
79
+ ]
80
+ },
81
+ {
82
+ "type": "pair",
83
+ "items": [
84
+ {
85
+ "type": "bars",
86
+ "title": "Generation speed: output tokens/s",
87
+ "max": 400,
88
+ "rows": [
89
+ { "label": "Model S", "key": "small", "value": 380 },
90
+ { "label": "Model M", "key": "mid", "value": 220 },
91
+ { "label": "Model L", "key": "large", "value": 125 }
92
+ ]
93
+ },
94
+ {
95
+ "type": "bars",
96
+ "title": "Task time: minutes per task",
97
+ "max": 3,
98
+ "rows": [
99
+ { "label": "Model S", "key": "small", "value": 2.6, "display": "2:36" },
100
+ { "label": "Model M", "key": "mid", "value": 1, "display": "1:00" },
101
+ { "label": "Model L", "key": "large", "value": 2.1, "display": "2:06" }
102
+ ]
103
+ }
104
+ ]
105
+ },
106
+ {
107
+ "type": "table",
108
+ "title": "Model S: where it fits",
109
+ "columns": ["Role", "Key evidence", "Verdict"],
110
+ "rows": [
111
+ [
112
+ "Narrow read-only lookups",
113
+ "Equal accuracy on known-answer lookups at a fraction of the cost",
114
+ { "text": "Yes", "verdict": "yes" }
115
+ ],
116
+ [
117
+ "Mechanical edits",
118
+ "Matches Model M medium at lower cost; untested on your work",
119
+ { "text": "Try on a named task", "verdict": "maybe" }
120
+ ],
121
+ ["Review", "Finds about half of what Model L finds", { "text": "No", "verdict": "no" }]
122
+ ]
123
+ }
124
+ ]
125
+ }
@@ -0,0 +1,377 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <!-- Model comparison report. scripts/render-report.ts fills #report-data from a data file;
6
+ section types and rules: SKILL.md. Colors: up to three categorical series
7
+ (validated dark-surface slots) plus one dashed neutral "reference" series. -->
8
+ <style>
9
+ :root {
10
+ --s1: #3987e5;
11
+ --s2: #d95926;
12
+ --s3: #199e70;
13
+ --ref: #a8a7a0;
14
+ --fg: var(--foreground, #f2f2f0);
15
+ --mut: var(--muted-foreground, #b5b4ad);
16
+ --bd: var(--border, #3a3a38);
17
+ --bg: var(--background, #1a1a19);
18
+ --grid: color-mix(in srgb, var(--fg) 12%, transparent);
19
+ }
20
+ html,
21
+ body {
22
+ background: var(--bg);
23
+ }
24
+ body {
25
+ font-family: var(--font-sans, system-ui, sans-serif);
26
+ color: var(--fg);
27
+ margin: 0;
28
+ }
29
+ .wrap {
30
+ display: flex;
31
+ flex-direction: column;
32
+ gap: 22px;
33
+ }
34
+ h2 {
35
+ font-size: 18px;
36
+ margin: 0;
37
+ }
38
+ h3 {
39
+ font-size: 15px;
40
+ margin: 0 0 2px;
41
+ }
42
+ .sub {
43
+ color: var(--mut);
44
+ font-size: 12.5px;
45
+ margin: 0 0 8px;
46
+ line-height: 1.4;
47
+ }
48
+ .legend {
49
+ display: flex;
50
+ gap: 14px;
51
+ flex-wrap: wrap;
52
+ font-size: 12.5px;
53
+ color: var(--mut);
54
+ margin-bottom: 6px;
55
+ }
56
+ .legend span {
57
+ display: inline-flex;
58
+ align-items: center;
59
+ gap: 6px;
60
+ }
61
+ .sw {
62
+ width: 18px;
63
+ height: 0;
64
+ border-top: 2px solid;
65
+ }
66
+ .grid2 {
67
+ display: grid;
68
+ grid-template-columns: 1fr 1fr;
69
+ gap: 18px;
70
+ }
71
+ @media (max-width: 560px) {
72
+ .grid2 {
73
+ grid-template-columns: 1fr;
74
+ }
75
+ }
76
+ svg {
77
+ display: block;
78
+ width: 100%;
79
+ overflow: visible;
80
+ }
81
+ svg text {
82
+ font-family: inherit;
83
+ font-size: 11px;
84
+ fill: var(--mut);
85
+ }
86
+ svg .lbl {
87
+ fill: var(--fg);
88
+ }
89
+ .tip {
90
+ position: fixed;
91
+ pointer-events: none;
92
+ background: var(--popover, #262624);
93
+ color: var(--popover-foreground, var(--fg));
94
+ border: 1px solid var(--bd);
95
+ border-radius: 6px;
96
+ padding: 6px 8px;
97
+ font-size: 12px;
98
+ opacity: 0;
99
+ transition: opacity 0.08s;
100
+ z-index: 5;
101
+ max-width: 260px;
102
+ }
103
+ table {
104
+ border-collapse: collapse;
105
+ width: 100%;
106
+ font-size: 12.5px;
107
+ }
108
+ th,
109
+ td {
110
+ text-align: left;
111
+ padding: 6px 8px;
112
+ border-bottom: 1px solid var(--bd);
113
+ vertical-align: top;
114
+ }
115
+ th {
116
+ color: var(--mut);
117
+ font-weight: 600;
118
+ }
119
+ td.n {
120
+ font-variant-numeric: tabular-nums;
121
+ white-space: nowrap;
122
+ }
123
+ .yes {
124
+ color: var(--success, #3fb37f);
125
+ }
126
+ .no {
127
+ color: var(--mut);
128
+ }
129
+ .maybe {
130
+ color: var(--warning, #e0a63a);
131
+ }
132
+ </style>
133
+ </head>
134
+ <body>
135
+ <div class="wrap" id="root"></div>
136
+ <div class="tip" id="tip"></div>
137
+ <script id="report-data" type="application/json">
138
+ __REPORT_DATA__
139
+ </script>
140
+ <script>
141
+ const DATA = JSON.parse(document.getElementById("report-data").textContent);
142
+ const SLOTS = ["var(--s1)", "var(--s2)", "var(--s3)"];
143
+ const NS = "http://www.w3.org/2000/svg";
144
+ const tip = document.getElementById("tip");
145
+ const esc = (s) =>
146
+ String(s).replace(
147
+ /[&<>"]/g,
148
+ (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c],
149
+ );
150
+ function showTip(e, h) {
151
+ tip.innerHTML = h;
152
+ tip.style.opacity = 1;
153
+ tip.style.left = Math.min(e.clientX + 12, innerWidth - 270) + "px";
154
+ tip.style.top = e.clientY + 12 + "px";
155
+ }
156
+ function hideTip() {
157
+ tip.style.opacity = 0;
158
+ }
159
+ function el(t, a, p) {
160
+ const n = document.createElementNS(NS, t);
161
+ for (const k in a) n.setAttribute(k, a[k]);
162
+ if (p) p.appendChild(n);
163
+ return n;
164
+ }
165
+ function hover(node, html) {
166
+ node.addEventListener("mousemove", (e) => showTip(e, html));
167
+ node.addEventListener("mouseleave", hideTip);
168
+ }
169
+ // Color follows the entity: a series key keeps its slot across every chart in the report.
170
+ const keys = DATA.series || {};
171
+ const color = (k) => (keys[k]?.reference ? "var(--ref)" : SLOTS[keys[k]?.slot ?? 0]);
172
+ const isRef = (k) => !!keys[k]?.reference;
173
+ const name = (k) => keys[k]?.name ?? k;
174
+ const legend = (ks) =>
175
+ `<div class="legend">${ks.map((k) => `<span><i class="sw" style="border-color:${color(k)};${isRef(k) ? "border-top-style:dashed" : ""}"></i>${esc(name(k))}</span>`).join("")}</div>`;
176
+ function marker(svg, x, y, k, html) {
177
+ el(
178
+ "circle",
179
+ {
180
+ cx: x,
181
+ cy: y,
182
+ r: 4.5,
183
+ fill: isRef(k) ? "var(--bg)" : color(k),
184
+ stroke: isRef(k) ? color(k) : "var(--bg)",
185
+ "stroke-width": 2,
186
+ },
187
+ svg,
188
+ );
189
+ hover(el("circle", { cx: x, cy: y, r: 11, fill: "transparent" }, svg), html);
190
+ }
191
+ function endLabels(svg, items) {
192
+ items.sort((a, b) => a.y - b.y);
193
+ let last = -99;
194
+ for (const it of items) {
195
+ const y = Math.max(it.y, last + 13);
196
+ last = y;
197
+ el("text", { x: it.x + 8, y: y + 4, class: "lbl" }, svg).textContent = it.text;
198
+ }
199
+ }
200
+ const draw = {
201
+ // Score against cost (or any two measures): one polyline per series through its effort points.
202
+ curve(s, svg) {
203
+ const W = svg.clientWidth,
204
+ H = s.height || 300,
205
+ m = { l: 44, r: 74, t: 10, b: 34 };
206
+ svg.setAttribute("height", H);
207
+ const xs = s.series.flatMap((r) => r.points.map((p) => p.x)),
208
+ ys = s.series.flatMap((r) => r.points.map((p) => p.y));
209
+ const [x0, x1] = s.xDomain || [Math.min(...xs) * 0.7, Math.max(...xs) * 1.4],
210
+ [y0, y1] = s.yDomain || [
211
+ Math.floor(Math.min(...ys) / 10) * 10,
212
+ Math.ceil(Math.max(...ys) / 10) * 10,
213
+ ];
214
+ const tx = (v) =>
215
+ (s.xLog ? Math.log10(v) - Math.log10(x0) : v - x0) /
216
+ (s.xLog ? Math.log10(x1) - Math.log10(x0) : x1 - x0);
217
+ const x = (v) => m.l + tx(v) * (W - m.l - m.r),
218
+ y = (v) => H - m.b - ((v - y0) / (y1 - y0)) * (H - m.t - m.b);
219
+ for (const v of s.xTicks || []) {
220
+ el("line", { x1: x(v), x2: x(v), y1: m.t, y2: H - m.b, stroke: "var(--grid)" }, svg);
221
+ el("text", { x: x(v), y: H - m.b + 15, "text-anchor": "middle" }, svg).textContent =
222
+ (s.xPrefix || "") + v + (s.xSuffix || "");
223
+ }
224
+ for (const v of s.yTicks || []) {
225
+ el("line", { x1: m.l, x2: W - m.r, y1: y(v), y2: y(v), stroke: "var(--grid)" }, svg);
226
+ el("text", { x: m.l - 6, y: y(v) + 4, "text-anchor": "end" }, svg).textContent =
227
+ v + (s.ySuffix || "");
228
+ }
229
+ el("text", { x: W / 2, y: H - 2, "text-anchor": "middle" }, svg).textContent =
230
+ s.xLabel || "";
231
+ const ends = [];
232
+ for (const r of s.series) {
233
+ el(
234
+ "polyline",
235
+ {
236
+ points: r.points.map((p) => x(p.x) + "," + y(p.y)).join(" "),
237
+ fill: "none",
238
+ stroke: color(r.key),
239
+ "stroke-width": 2,
240
+ "stroke-dasharray": isRef(r.key) ? "5 4" : "",
241
+ },
242
+ svg,
243
+ );
244
+ for (const p of r.points)
245
+ marker(
246
+ svg,
247
+ x(p.x),
248
+ y(p.y),
249
+ r.key,
250
+ `<b>${esc(name(r.key))} ${esc(p.label)}</b><br>${esc(p.tip || "")}`,
251
+ );
252
+ const L = r.points[r.points.length - 1];
253
+ ends.push({ x: x(L.x), y: y(L.y), text: name(r.key).split(" ")[0] });
254
+ }
255
+ endLabels(svg, ends);
256
+ },
257
+ // One value per category per series (effort ladders).
258
+ ladder(s, svg) {
259
+ const W = svg.clientWidth,
260
+ H = s.height || 250,
261
+ m = { l: 34, r: 74, t: 10, b: 26 },
262
+ n = s.categories.length;
263
+ svg.setAttribute("height", H);
264
+ const [y0, y1] = s.yDomain,
265
+ x = (i) => m.l + (i / (n - 1)) * (W - m.l - m.r),
266
+ y = (v) => H - m.b - ((v - y0) / (y1 - y0)) * (H - m.t - m.b);
267
+ for (const v of s.yTicks || []) {
268
+ el("line", { x1: m.l, x2: W - m.r, y1: y(v), y2: y(v), stroke: "var(--grid)" }, svg);
269
+ el("text", { x: m.l - 6, y: y(v) + 4, "text-anchor": "end" }, svg).textContent = v;
270
+ }
271
+ s.categories.forEach((c, i) => {
272
+ el("text", { x: x(i), y: H - m.b + 16, "text-anchor": "middle" }, svg).textContent = c;
273
+ });
274
+ const ends = [];
275
+ for (const r of s.series) {
276
+ const pts = r.values.map((v, i) => [i, v]).filter((p) => p[1] !== null);
277
+ el(
278
+ "polyline",
279
+ {
280
+ points: pts.map(([i, v]) => x(i) + "," + y(v)).join(" "),
281
+ fill: "none",
282
+ stroke: color(r.key),
283
+ "stroke-width": 2,
284
+ "stroke-dasharray": isRef(r.key) ? "5 4" : "",
285
+ },
286
+ svg,
287
+ );
288
+ for (const [i, v] of pts)
289
+ marker(
290
+ svg,
291
+ x(i),
292
+ y(v),
293
+ r.key,
294
+ `<b>${esc(name(r.key))} ${esc(s.categories[i])}</b><br>${esc(v)}${r.notes?.[i] ? " · " + esc(r.notes[i]) : ""}`,
295
+ );
296
+ const L = pts[pts.length - 1];
297
+ ends.push({ x: x(L[0]), y: y(L[1]), text: name(r.key).split(" ")[0] });
298
+ }
299
+ endLabels(svg, ends);
300
+ },
301
+ // Horizontal bars; rows name their series for color.
302
+ bars(s, svg) {
303
+ const W = svg.clientWidth,
304
+ H = s.height || 190,
305
+ lw = s.labelWidth || 96,
306
+ rh = (H - 6) / s.rows.length,
307
+ max = s.max || Math.max(...s.rows.map((r) => r.value)) * 1.1;
308
+ svg.setAttribute("height", H);
309
+ s.rows.forEach((r, i) => {
310
+ const w = ((W - lw - 46) * r.value) / max,
311
+ yy = i * rh + 4,
312
+ bh = rh - 12;
313
+ el("text", { x: 0, y: yy + bh / 2 + 4 }, svg).textContent = r.label;
314
+ const b = el(
315
+ "rect",
316
+ {
317
+ x: lw,
318
+ y: yy,
319
+ width: Math.max(w, 2),
320
+ height: bh,
321
+ rx: 4,
322
+ fill: isRef(r.key) ? "none" : color(r.key),
323
+ stroke: isRef(r.key) ? color(r.key) : "none",
324
+ "stroke-dasharray": isRef(r.key) ? "5 4" : "",
325
+ "stroke-width": isRef(r.key) ? 1.5 : 0,
326
+ },
327
+ svg,
328
+ );
329
+ el("text", { x: lw + w + 6, y: yy + bh / 2 + 4, class: "lbl" }, svg).textContent =
330
+ r.display ?? r.value;
331
+ hover(b, `<b>${esc(r.label)}</b><br>${esc(r.tip || "")}`);
332
+ });
333
+ },
334
+ };
335
+ function section(s) {
336
+ const box = document.createElement("section");
337
+ box.innerHTML = `<h3>${esc(s.title || "")}</h3>${s.sub ? `<p class="sub">${esc(s.sub)}</p>` : ""}`;
338
+ if (s.type === "pair") {
339
+ box.className = "grid2";
340
+ box.innerHTML = "";
341
+ for (const c of s.items) box.appendChild(section(c));
342
+ return box;
343
+ }
344
+ if (s.type === "table") {
345
+ const cls = (v) => ({ yes: "yes", no: "no", maybe: "maybe" })[v] || "";
346
+ box.innerHTML += `<table><thead><tr>${s.columns.map((c) => `<th>${esc(c)}</th>`).join("")}</tr></thead><tbody>${s.rows
347
+ .map(
348
+ (row) =>
349
+ `<tr>${row
350
+ .map((c, i) => {
351
+ const v = typeof c === "object" && c ? c : { text: c };
352
+ return `<td class="${s.numeric?.includes(i) ? "n" : ""} ${cls(v.verdict)}">${esc(v.text)}</td>`;
353
+ })
354
+ .join("")}</tr>`,
355
+ )
356
+ .join("")}</tbody></table>`;
357
+ return box;
358
+ }
359
+ const ks = s.series ? s.series.map((r) => r.key) : [];
360
+ if (ks.length > 1) box.insertAdjacentHTML("beforeend", legend(ks));
361
+ const svg = el("svg", { height: s.height || 200 }, box);
362
+ box._draw = () => {
363
+ svg.innerHTML = "";
364
+ draw[s.type](s, svg);
365
+ };
366
+ return box;
367
+ }
368
+ const root = document.getElementById("root");
369
+ if (DATA.title) root.insertAdjacentHTML("beforeend", `<h2>${esc(DATA.title)}</h2>`);
370
+ const nodes = DATA.sections.map(section);
371
+ nodes.forEach((n) => root.appendChild(n));
372
+ const redraw = () => root.querySelectorAll("section").forEach((n) => n._draw && n._draw());
373
+ redraw();
374
+ addEventListener("resize", redraw);
375
+ </script>
376
+ </body>
377
+ </html>
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env bun
2
+ // Render a model comparison report: data JSON in, self-contained HTML out.
3
+ //
4
+ // bun render-report.ts <data.json> <out.html>
5
+ //
6
+ // Data shape and section types: ../SKILL.md. Example data:
7
+ // ../assets/example-report.json.
8
+ import { readFileSync, writeFileSync } from "node:fs";
9
+ import { join } from "node:path";
10
+
11
+ type Series = { name: string; slot?: number; reference?: boolean };
12
+ type Section = {
13
+ type: string;
14
+ items?: Section[];
15
+ series?: { key: string }[];
16
+ rows?: { key?: string }[];
17
+ };
18
+ export type ReportData = { title?: string; series: Record<string, Series>; sections: Section[] };
19
+
20
+ const TYPES = new Set(["curve", "ladder", "bars", "table", "pair"]);
21
+
22
+ /** Problems that would make the page wrong or unreadable; empty when the data is usable. */
23
+ export function check(data: ReportData): string[] {
24
+ const errors: string[] = [];
25
+ const entries = Object.entries(data.series);
26
+ const colored = entries.filter(([, s]) => !s.reference);
27
+ // The template's three dark-surface slots pass the all-pairs color-vision check;
28
+ // a fourth hue does not, so further series must be a dashed reference or a separate chart.
29
+ if (colored.length > 3) {
30
+ errors.push(
31
+ `at most 3 colored series (got ${colored.length}); mark one more "reference": true or put the rest in a separate report`,
32
+ );
33
+ }
34
+ if (entries.filter(([, s]) => s.reference).length > 1) {
35
+ errors.push("at most one reference series");
36
+ }
37
+ const slots = colored.map(([, s]) => s.slot);
38
+ if (
39
+ slots.some((s) => s === undefined || s < 0 || s > 2) ||
40
+ new Set(slots).size !== slots.length
41
+ ) {
42
+ errors.push("each colored series needs a distinct slot 0, 1 or 2");
43
+ }
44
+ const walk = (sections: Section[], path: string) =>
45
+ sections.forEach((s, i) => {
46
+ const where = `${path}[${i}]`;
47
+ if (!TYPES.has(s.type)) {
48
+ errors.push(`${where}: unknown type ${s.type}`);
49
+ }
50
+ for (const k of [
51
+ ...(s.series ?? []).map((r) => r.key),
52
+ ...(s.rows ?? []).map((r) => r.key),
53
+ ]) {
54
+ if (k !== undefined && !data.series[k]) {
55
+ errors.push(`${where}: series key ${k} is not declared`);
56
+ }
57
+ }
58
+ if (s.items) {
59
+ walk(s.items, `${where}.items`);
60
+ }
61
+ });
62
+ walk(data.sections, "sections");
63
+ return errors;
64
+ }
65
+
66
+ export function render(data: ReportData, template: string): string {
67
+ const errors = check(data);
68
+ if (errors.length) {
69
+ throw new Error(errors.join("\n"));
70
+ }
71
+ // Inside <script>, a literal "</" would close the element early.
72
+ return template.replace("__REPORT_DATA__", () => JSON.stringify(data).replaceAll("</", "<\\/"));
73
+ }
74
+
75
+ if (import.meta.main) {
76
+ const [input, output] = process.argv.slice(2);
77
+ if (!input || !output) {
78
+ throw new Error("usage: bun render-report.ts <data.json> <out.html>");
79
+ }
80
+ const template = readFileSync(
81
+ join(import.meta.dir, "..", "assets", "report-template.html"),
82
+ "utf8",
83
+ );
84
+ writeFileSync(output, render(JSON.parse(readFileSync(input, "utf8")), template));
85
+ console.log(output);
86
+ }