@bompus/house-rules 0.11.0 → 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 (111) hide show
  1. package/CODE_OF_CONDUCT.md +83 -0
  2. package/README.md +91 -60
  3. package/compose.mjs +8 -2
  4. package/composition.mjs +11 -3
  5. package/config-view.mjs +18 -3
  6. package/config.mjs +74 -13
  7. package/docs/assets/overview.png +0 -0
  8. package/docs/configuration.md +85 -9
  9. package/package.json +4 -2
  10. package/rules/core.md +118 -209
  11. package/rules/modifiers/backlog-outline.md +25 -0
  12. package/rules/modifiers/backlog-visual.md +16 -0
  13. package/rules/modifiers/coded-offers.md +47 -17
  14. package/rules/modifiers/cross-session-relay.md +23 -0
  15. package/rules/modifiers/low-quota-handoff.md +4 -3
  16. package/rules/modifiers/model-default-questions.md +33 -0
  17. package/rules/modifiers/multi-agent.md +6 -2
  18. package/rules/modifiers/no-attribution.md +7 -1
  19. package/rules/modifiers/periodic-updates.md +25 -0
  20. package/rules/modifiers/plan-files.md +35 -3
  21. package/rules/modifiers/release-batching.md +4 -2
  22. package/rules/modifiers/shared-host-load.md +13 -42
  23. package/rules/modifiers/squash-landing.md +1 -1
  24. package/rules/modifiers/upstream-contributions.md +45 -0
  25. package/rules/modifiers/web-image-formats.md +35 -0
  26. package/rules/references/backlog-view.md +154 -0
  27. package/rules/references/landing.md +14 -14
  28. package/rules/references/multi-agent.md +6 -2
  29. package/rules/references/release-batching.md +27 -4
  30. package/rules/references/squash-landing.md +10 -4
  31. package/rules/references/thread-visuals.md +45 -0
  32. package/setup.mjs +87 -17
  33. package/skills/hr-agent-guidance-audit/SKILL.md +10 -5
  34. package/skills/hr-agent-guidance-audit/references/prune-workflow.md +72 -0
  35. package/skills/hr-agent-guidance-audit/references/records-and-lessons.md +1 -1
  36. package/skills/hr-agent-guidance-audit/references/retirement-verdicts.md +49 -0
  37. package/skills/hr-agent-guidance-refresh/SKILL.md +25 -57
  38. package/skills/hr-agent-guidance-refresh/references/verification.md +1 -1
  39. package/skills/hr-audit-choices/SKILL.md +1 -1
  40. package/skills/hr-benchmarking/SKILL.md +122 -159
  41. package/skills/hr-benchmarking/references/javascript.md +113 -0
  42. package/skills/hr-benchmarking/references/tools.md +62 -57
  43. package/skills/hr-better-accessibility/SKILL.md +1 -1
  44. package/skills/hr-better-accessibility/screen-readers.md +1 -0
  45. package/skills/hr-code-review/SKILL.md +19 -3
  46. package/skills/hr-code-review/agents/openai.yaml +1 -1
  47. package/skills/hr-code-review/references/acp-client.md +43 -0
  48. package/skills/{hr-change-impact/SKILL.md → hr-code-review/references/impact.md} +2 -12
  49. package/skills/hr-code-review/references/panel-adjudication.md +58 -0
  50. package/skills/hr-code-review/references/panel-plan.md +61 -0
  51. package/skills/hr-code-review/references/panel-recommendations.json +105 -0
  52. package/skills/hr-code-review/references/panel-recommendations.md +46 -0
  53. package/skills/hr-code-review/references/panel-setup.md +54 -0
  54. package/skills/hr-code-review/references/reviewer-role.md +31 -0
  55. package/skills/hr-code-review/scripts/acp-panel-client.mjs +299 -0
  56. package/skills/hr-code-review/scripts/check-receipts.mjs +263 -0
  57. package/skills/hr-code-review/scripts/panel-plan.mjs +69 -0
  58. package/skills/hr-code-review/scripts/panel-preferences.mjs +220 -0
  59. package/skills/hr-design-exploration/SKILL.md +24 -1
  60. package/skills/hr-design-exploration/agents/openai.yaml +2 -2
  61. package/skills/hr-diagnosing-bugs/SKILL.md +2 -2
  62. package/skills/hr-diagnosing-bugs/references/reporting.md +29 -4
  63. package/skills/hr-diagnosing-bugs/scripts/check-pr-offer.mjs +47 -0
  64. package/skills/hr-explain-code/SKILL.md +1 -1
  65. package/skills/hr-extract-shared-steps/SKILL.md +1 -1
  66. package/skills/hr-forgotten-work-audit/SKILL.md +130 -0
  67. package/skills/hr-forgotten-work-audit/references/brief-template.md +56 -0
  68. package/skills/hr-forgotten-work-audit/references/result-format.md +46 -0
  69. package/skills/hr-handoff/LICENSE +21 -0
  70. package/skills/hr-handoff/NOTICE.md +29 -0
  71. package/skills/hr-handoff/SKILL.md +24 -1
  72. package/skills/hr-handoff/references/reconciliation.md +25 -6
  73. package/skills/hr-handoff/references/successor-thread.md +76 -0
  74. package/skills/hr-handoff/scripts/check-reconciliation.mjs +48 -6
  75. package/skills/hr-house-rules-setup/SKILL.md +22 -8
  76. package/skills/hr-lean-plan/SKILL.md +4 -0
  77. package/skills/hr-maintainability-review/SKILL.md +14 -4
  78. package/skills/hr-maintainability-review/agents/openai.yaml +1 -1
  79. package/skills/hr-manual-qa/SKILL.md +1 -1
  80. package/skills/hr-ordering-tests/SKILL.md +1 -1
  81. package/skills/hr-plain-prose/SKILL.md +7 -4
  82. package/skills/hr-pr-followup/SKILL.md +6 -3
  83. package/skills/hr-pr-followup/references/review-helpers.md +21 -0
  84. package/skills/hr-pr-followup/scripts/check-landing.py +204 -0
  85. package/skills/hr-pr-followup/scripts/merge-pr.sh +100 -0
  86. package/skills/hr-pr-followup/scripts/verify-landed.sh +184 -0
  87. package/skills/hr-read-reddit/SKILL.md +1 -1
  88. package/skills/hr-research-shared-links/SKILL.md +103 -0
  89. package/skills/hr-sqlite/SKILL.md +174 -0
  90. package/skills/hr-sqlite/agents/openai.yaml +2 -0
  91. package/skills/hr-sqlite/references/evidence.md +94 -0
  92. package/skills/hr-sqlite/references/tool-decisions.md +39 -0
  93. package/skills/hr-sqlite/scripts/inspect-sqlite.py +245 -0
  94. package/skills/hr-stock-ui-audit/SKILL.md +1 -1
  95. package/skills/hr-test-audit/LICENSE +21 -0
  96. package/skills/hr-test-audit/NOTICE.md +67 -0
  97. package/skills/hr-usage-report/SKILL.md +90 -0
  98. package/skills/hr-usage-report/agents/openai.yaml +2 -0
  99. package/skills/hr-usage-report/assets/example-report.json +125 -0
  100. package/skills/hr-usage-report/assets/report-template.html +377 -0
  101. package/skills/hr-usage-report/scripts/render-report.ts +86 -0
  102. package/skills/hr-usage-report/scripts/usage-by-model.ts +499 -0
  103. package/skills/hr-what-next/SKILL.md +100 -10
  104. package/skills/hr-what-next/references/reporting.md +13 -22
  105. package/skills/hr-what-next/scripts/check-open-work.mjs +711 -0
  106. package/skills/hr-writing-pr/SKILL.md +29 -14
  107. package/skills/hr-writing-pr/scripts/benefit-chart.py +200 -0
  108. package/docs/npm-release.md +0 -45
  109. package/rules/modifiers/swarmail.md +0 -12
  110. package/skills/hr-benchmarking/references/priority-findings-20261006.md +0 -115
  111. package/skills/hr-change-impact/agents/openai.yaml +0 -5
@@ -0,0 +1,67 @@
1
+ # Source notices
2
+
3
+ This notice accompanies the hr-test-audit bundle when copied on its own.
4
+ Each source below is published under the MIT License. The licence text at the
5
+ end applies to each of them, with the copyright line shown for that source.
6
+
7
+ ## obra/superpowers
8
+
9
+ Source: https://github.com/obra/superpowers (compared at revision `8ca22db`)
10
+
11
+ - `SKILL.md`: cites `writing-good-tests.md`.
12
+
13
+ Copyright (c) 2025 Jesse Vincent
14
+
15
+ ## addyosmani/agent-skills
16
+
17
+ Source: https://github.com/addyosmani/agent-skills (compared at revision `9d0c60d`)
18
+
19
+ - `scripts/floor-guard.ts`: adapted from constraint-driven-development.
20
+
21
+ Copyright (c) 2025 Addy Osmani
22
+
23
+ ## openclaw/openclaw
24
+
25
+ Source: https://github.com/openclaw/openclaw (linked at revision `65f1e4d`, compared at `1fda703`)
26
+
27
+ - `SKILL.md`: the authoring gate, junk patterns, retention bar and evidence
28
+ fields are adapted from `.agents/skills/test-audit/`.
29
+
30
+ Copyright (c) 2026 OpenClaw Foundation
31
+
32
+ ## no-human-ai/no_human
33
+
34
+ Source: https://github.com/no-human-ai/no_human (compared at revision `9b50bf6`)
35
+
36
+ - `SKILL.md`: the rule that weakening a test counts as removing it is adapted
37
+ from its tamper guard.
38
+
39
+ Copyright (c) 2026 Eyal Golan
40
+
41
+ ## omkamal/pypict-claude-skill
42
+
43
+ Source: https://github.com/omkamal/pypict-claude-skill (compared at revision `fbda212`)
44
+
45
+ - `SKILL.md`: the pairwise-coverage guidance is adapted from this skill.
46
+
47
+ Copyright (c) 2025 pypict-claude-skill contributors
48
+
49
+ ## MIT License text
50
+
51
+ Permission is hereby granted, free of charge, to any person obtaining a copy
52
+ of this software and associated documentation files (the "Software"), to deal
53
+ in the Software without restriction, including without limitation the rights
54
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
55
+ copies of the Software, and to permit persons to whom the Software is
56
+ furnished to do so, subject to the following conditions:
57
+
58
+ The above copyright notice and this permission notice shall be included in all
59
+ copies or substantial portions of the Software.
60
+
61
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
62
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
63
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
64
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
65
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
66
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
67
+ SOFTWARE.
@@ -0,0 +1,90 @@
1
+ ---
2
+ name: hr-usage-report
3
+ description: Measure where Claude Code, Codex and OpenCode usage went by provider, model and role, and chart model comparisons as HTML. Use for quota or cost questions and to show a model or role recommendation visually.
4
+ ---
5
+
6
+ # Usage report
7
+
8
+ Answer quota and model-role questions from measured usage and published
9
+ evidence, then show the result as charts and tables. Run the scripts from this
10
+ skill's directory with Bun.
11
+
12
+ ## 1. Measure where usage went
13
+
14
+ ```bash
15
+ bun scripts/usage-by-model.ts --days 7 [--providers claude,codex,opencode] [--claude-projects <dir,dir>] [--prices <api.json>] [--json]
16
+ ```
17
+
18
+ It reads Claude Code transcripts (`projects` under `~/.claude` and under every
19
+ other `~/.claude_*` or `~/.claude-*` configuration directory, so sessions run
20
+ with a separate `CLAUDE_CONFIG_DIR` count; `--claude-projects` replaces that
21
+ list), Codex rollouts
22
+ (`~/.codex/sessions`) and the OpenCode database, all read-only and local. It
23
+ prices each request at [models.dev](https://models.dev) list rates, including
24
+ long-context tiers, and splits usage by provider, model and role: main session,
25
+ subagent, advisor. Without `--prices` it downloads `https://models.dev/api.json`;
26
+ that request carries no local data. Use `--since <ISO>` for a fixed start.
27
+
28
+ Read the output beside the provider's live quota readings:
29
+
30
+ - **Shares, not dollars.** List price stands in for subscription quota; no
31
+ provider publishes the conversion. Say so in the report.
32
+ - **Cache reads.** When cache reads dominate a row's cost, context length, not
33
+ the model's output, drives usage. Compare the main-request context median
34
+ with the session-start median: the gap is conversation growth; the start is
35
+ fixed overhead (system prompt, rules, tool schemas).
36
+ - **Advisor rows.** Each advisor call re-reads the whole transcript uncached at
37
+ the advisor's rates; a few calls can outweigh many main turns.
38
+ - **Cross-provider work.** The same table shows what the author's sessions and
39
+ another vendor's reviewer each cost.
40
+ - **Unpriced models.** The summary lists models models.dev has no price for;
41
+ report them rather than dropping them.
42
+
43
+ ## 2. Pick evidence by question
44
+
45
+ | Question | Evidence | Read |
46
+ | ------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------- |
47
+ | Quality per dollar for implementation | Agentic coding boards that publish effort, harness and cost per task | Score against cost at every effort; cost per solved task |
48
+ | Review and bug finding | Bug-finding boards with run counts per row | The effort curve; treat single runs as noisy |
49
+ | Raw speed | Output tokens per second | Generation speed only |
50
+ | Task speed | Boards that time whole tasks | Time to finish; a model that generates faster can still take longer |
51
+ | Fit for your own work | A known-answer trial on your own tasks (`hr-benchmarking`) | Correctness and model time per task shape |
52
+
53
+ Compare models at the same effort and harness, or say they differ. A vendor
54
+ table that sets one model's top effort against another's lower effort is not a
55
+ matched comparison. Keep token speed and task time in separate charts; never
56
+ infer one from the other.
57
+
58
+ ## 3. Render the report
59
+
60
+ Write a data file, then:
61
+
62
+ ```bash
63
+ bun scripts/render-report.ts <data.json> <out.html>
64
+ ```
65
+
66
+ The page is self-contained, follows the host's theme variables when it is
67
+ embedded, and falls back to a dark surface. `assets/example-report.json` uses
68
+ every section type.
69
+
70
+ The data file declares `series` once, so each model keeps one color across
71
+ every chart in the report: up to three colored series with slots 0 to 2, and at
72
+ most one `"reference": true` series drawn dashed and neutral. The renderer
73
+ rejects a fourth colored series because no fourth hue passes the color-vision
74
+ check on the dark surface against the other three. Compare more models in a
75
+ second report file.
76
+
77
+ Section types:
78
+
79
+ - `curve`: points with `x`, `y`, `label`, `tip` per series; `xLog`, domains,
80
+ ticks and axis affixes. Use for score against cost across efforts.
81
+ - `ladder`: `categories` (usually efforts) and `values` per series, `null` for a
82
+ missing row, optional `notes` such as run counts.
83
+ - `bars`: `rows` of `label`, `key`, `value`, optional `display` and `tip`.
84
+ - `pair`: two sections side by side, such as token speed beside task time.
85
+ - `table`: `columns` and `rows`; a cell may be `{ "text", "verdict": "yes" | "maybe" | "no" }`;
86
+ `numeric` lists right-aligned column indexes.
87
+
88
+ End with one verdict table per model or role: role, key evidence with numbers,
89
+ verdict. Name the evidence date in the title, and apply any model or effort
90
+ limits the user has set before recommending a configuration.
@@ -0,0 +1,2 @@
1
+ interface:
2
+ display_name: "HR: Usage report"
@@ -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>