@thebattlearena/corner 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/ring/agents/cornerman.md +20 -3
- package/ring/agents/wrestler.md +11 -0
- package/ring/hooks/hooks.json +20 -0
- package/ring/scripts/bout-stats-render.mjs +366 -0
- package/ring/scripts/bout-stats.mjs +360 -0
- package/ring/scripts/config.mjs +1 -1
- package/ring/scripts/contract-endpoints.mjs +163 -0
- package/ring/scripts/contract-preview.mjs +34 -14
- package/ring/scripts/fight.mjs +27 -0
- package/ring/scripts/findings.mjs +3 -1
- package/ring/scripts/format-gate.mjs +723 -0
- package/ring/scripts/format-worker.mjs +120 -0
- package/ring/scripts/front-routes.mjs +345 -0
- package/ring/scripts/judges-card.mjs +324 -3
- package/ring/scripts/lookout.mjs +22 -4
- package/ring/scripts/mask-guard.mjs +34 -1
- package/ring/scripts/page-embed.mjs +112 -0
- package/ring/scripts/play-by-play.mjs +155 -7
- package/ring/scripts/preparation-collect.mjs +177 -0
- package/ring/scripts/preparation.mjs +547 -0
- package/ring/scripts/round-bell.mjs +9 -2
- package/ring/scripts/tech-plan-log.mjs +81 -0
- package/ring/scripts/tech-plan.mjs +191 -0
- package/ring/scripts/verdicts-log.mjs +75 -0
- package/ring/scripts/verdicts.mjs +205 -0
- package/ring/skills/play-by-play/SKILL.md +235 -33
- package/ring/skills/play-by-play/templates/blocks.css +196 -0
- package/ring/skills/play-by-play/templates/chronicle.html +358 -55
- package/ring/skills/play-by-play/templates/endpoints-view.js +1153 -0
- package/ring/skills/play-by-play/templates/interview-form.js +516 -0
- package/ring/skills/play-by-play/templates/now-panel.js +719 -0
- package/ring/skills/referee/SKILL.md +136 -42
- package/ring/skills/referee/references/api-diagram.md +56 -0
- package/ring/skills/referee/references/precedents.md +11 -0
- package/ring/skills/referee/references/rulebook.md +65 -4
- package/ring/skills/referee/templates/api-diagram.html +115 -0
- package/ring/skills/referee/templates/escalation.md +7 -1
- package/ring/skills/referee/templates/round-bell-body.md +4 -0
- package/ring/skills/referee/templates/wrestler-brief.md +3 -0
- package/ring/skills/sparring/SKILL.md +10 -2
- package/ring/skills/sparring/templates/contrato.md +29 -0
- package/ring/skills/sparring/templates/rudo-brief.md +7 -0
- package/ring/skills/weigh-in/SKILL.md +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thebattlearena/corner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Corner: tu esquina de Battle Arena en Claude Code (el plugin `battlearena`). Hooks de tracking, objetivo del turno, plan de batalla /battlearena:conquer, Ringside (el panel de tu día), el ring y la radio. Zero-dependency.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "battlearena",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Corner, tu esquina de Battle Arena en Claude Code: tus sesiones al colector, el objetivo del turno, el plan de batalla /battlearena:conquer y Ringside, el panel de tu día junto al chat.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Battle Arena"
|
package/ring/agents/cornerman.md
CHANGED
|
@@ -38,6 +38,15 @@ Per commit of the range (`git -C <wt> show <sha>`). Output, at most **15 lines**
|
|
|
38
38
|
|
|
39
39
|
BLOCKERS first. A commit that complies gets "<sha>: no findings".
|
|
40
40
|
|
|
41
|
+
**Format, in every mode:** `node "<ring>/scripts/format-gate.mjs" check <wt> --range <A>..<B>`
|
|
42
|
+
(read-only: it formats a copy of each file of the range as it is at B and compares; never
|
|
43
|
+
`sort-imports --check`, which reports false positives). Each file in `unclean` is one finding:
|
|
44
|
+
`<file> — format — not as sort-imports + prettier leave it — the wrestler runs format-gate fix
|
|
45
|
+
--range <A>..<B> and commits the result`. **Exit 2 (`verified: false`) is NOT VERIFIED, never
|
|
46
|
+
clean:** sort-imports or prettier is not installed (`missing`), so the files in `files` were not
|
|
47
|
+
checked. Report it as its own line, `format — NO VERIFICADO — falta <missing> — <files>`, never as
|
|
48
|
+
"no findings".
|
|
49
|
+
|
|
41
50
|
## Mode: concept
|
|
42
51
|
|
|
43
52
|
A deep review when a concept closes. The brief lists the commits with what each claims to do and a
|
|
@@ -71,12 +80,20 @@ bout's venue). For a `backend` bout they are these 8:
|
|
|
71
80
|
|
|
72
81
|
1. Layer and dependency direction.
|
|
73
82
|
2. Coupling.
|
|
74
|
-
3. Duplication, looking at both places
|
|
83
|
+
3. Duplication, looking at both places: the same condition in several files is a finding even
|
|
84
|
+
when each piece works (R11); repeated conditionals by type, state or provider come with the
|
|
85
|
+
pattern that removes them or the reason none is simpler (R22).
|
|
75
86
|
4. Single responsibility and the rest of SOLID.
|
|
76
|
-
5. Domain model (rich or anemic, value objects, invariants where they belong)
|
|
87
|
+
5. Domain model (rich or anemic, value objects, invariants where they belong): every new file
|
|
88
|
+
with its business sentence (R15), the backend shaped by the business and not by a screen
|
|
89
|
+
(R16), every optional with its reason (R17), aggregates born valid (R18), no primitives
|
|
90
|
+
crossing commands, events or ports (R19), no new endpoint an existing one covers (R20), names
|
|
91
|
+
that do not collide with another context (R21). A deviation from a pattern the host's rules
|
|
92
|
+
fix is a BLOCKER (P14).
|
|
77
93
|
6. Errors: does it fail open or closed, who catches what.
|
|
78
94
|
7. Expressiveness: names, more than 3 parameters, control booleans.
|
|
79
|
-
8. Production risk: concurrency, idempotency, volume, reversibility
|
|
95
|
+
8. Production risk: concurrency, idempotency, volume, reversibility, and a new infrastructure
|
|
96
|
+
resource counted against the provider's quota over the base (F13).
|
|
80
97
|
|
|
81
98
|
Return per file the findings with rule and precedent ids, then the **open design questions**,
|
|
82
99
|
each with two options, their cost and your recommendation, numbered so they can be answered as
|
package/ring/agents/wrestler.md
CHANGED
|
@@ -80,6 +80,17 @@ edit after approval needs a new one.
|
|
|
80
80
|
staged, run the gates on a copy of the index under `<bout dir>/tools/`
|
|
81
81
|
(`git checkout-index -a --prefix=<bout dir>/tools/staged/`), never by stashing the rest: the
|
|
82
82
|
stash is shared by every worktree on the machine, and `stash@{0}` may be another session's.
|
|
83
|
+
**Then the format gate, always, last before staging:**
|
|
84
|
+
`node "<ring>/scripts/format-gate.mjs" fix <worktree>` (sort-imports with the repo's tsconfig,
|
|
85
|
+
then prettier, on the changed files; the repo's own `fix:changed` when it has one). Ring also
|
|
86
|
+
formats every file you edit or write from the shell while the bout is active, but a commit
|
|
87
|
+
never relies on that: run `fix` and stage what it left. If it says the binaries are not
|
|
88
|
+
installed, install the dependencies in your worktree or tell the referee; never commit without
|
|
89
|
+
it.
|
|
90
|
+
When a rebound says files already committed are not formatted, in this order: run
|
|
91
|
+
`format-gate.mjs fix <worktree> --range <base>..HEAD`, commit what it changed, and only then
|
|
92
|
+
`format-gate.mjs check <worktree> --range <base>..HEAD`. A check before that commit still
|
|
93
|
+
exits 1: it reads the range as committed, not the worktree.
|
|
83
94
|
5. Commits: stage by explicit paths (never `git add -A` or `.`), a conventional message that says
|
|
84
95
|
the business reason, in plain words, in the language of the branch's previous commits (read
|
|
85
96
|
`git log` once before the first commit and keep it for the whole bout): never bout ids (D12, P7,
|
package/ring/hooks/hooks.json
CHANGED
|
@@ -22,6 +22,26 @@
|
|
|
22
22
|
}
|
|
23
23
|
],
|
|
24
24
|
"PostToolUse": [
|
|
25
|
+
{
|
|
26
|
+
"matcher": "Edit|Write|MultiEdit",
|
|
27
|
+
"hooks": [
|
|
28
|
+
{
|
|
29
|
+
"type": "command",
|
|
30
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/format-gate.mjs\" edit-hook",
|
|
31
|
+
"timeout": 30
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"matcher": "Bash",
|
|
37
|
+
"hooks": [
|
|
38
|
+
{
|
|
39
|
+
"type": "command",
|
|
40
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/format-gate.mjs\" shell-hook",
|
|
41
|
+
"timeout": 60
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
},
|
|
25
45
|
{
|
|
26
46
|
"matcher": "Agent|Task|AskUserQuestion|Bash",
|
|
27
47
|
"hooks": [
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
// Renders the numbers of bout-stats.mjs as the "El cierre en números" block of the chronicle, in
|
|
2
|
+
// the Battle Arena voice: a weigh-in (code against tests), round by round between the ropes, the
|
|
3
|
+
// fight clock (24 h), a CompuBox of punch types, the referee's scorecards and the highlight reel.
|
|
4
|
+
// Inline SVG and HTML only, colored through the template's tokens (classes cn-*), so light and dark
|
|
5
|
+
// both work and nothing loads from outside. PURE and zero-dependency.
|
|
6
|
+
|
|
7
|
+
const LABELS = {
|
|
8
|
+
es: {
|
|
9
|
+
title: 'El cierre en números',
|
|
10
|
+
tiles: {
|
|
11
|
+
duration: 'de pelea, del primer registro al último',
|
|
12
|
+
commits: 'commits del luchador',
|
|
13
|
+
rounds: 'rounds con su campana',
|
|
14
|
+
lines: 'líneas agregadas',
|
|
15
|
+
},
|
|
16
|
+
weighIn: 'La báscula',
|
|
17
|
+
weighInNote: (ratio) =>
|
|
18
|
+
`${ratio} líneas de prueba por cada línea de código.`,
|
|
19
|
+
red: 'Esquina roja',
|
|
20
|
+
blue: 'Esquina azul',
|
|
21
|
+
code: 'Código',
|
|
22
|
+
tests: 'Pruebas',
|
|
23
|
+
lockfile: 'Lockfile',
|
|
24
|
+
other: 'Config, scripts y assets',
|
|
25
|
+
deadWeight: 'Peso muerto',
|
|
26
|
+
linesUnit: 'líneas',
|
|
27
|
+
rounds: 'Round por round',
|
|
28
|
+
roundsNote: 'Commits por día, por fecha del commit.',
|
|
29
|
+
star: 'Round estelar',
|
|
30
|
+
clock: 'El reloj de la pelea',
|
|
31
|
+
clockNote: 'Commits por hora del día. La franja sombreada es la madrugada.',
|
|
32
|
+
peak: 'hora pico',
|
|
33
|
+
night: 'de madrugada (00–06)',
|
|
34
|
+
bands: ['Madrugada 00–06', 'Mañana 06–12', 'Tarde 12–18', 'Noche 18–24'],
|
|
35
|
+
compubox: 'CompuBox',
|
|
36
|
+
compuboxNote: 'Cada commit, por su tipo de golpe.',
|
|
37
|
+
types: {
|
|
38
|
+
feat: 'Golpes de poder',
|
|
39
|
+
fix: 'Jabs',
|
|
40
|
+
refactor: 'Juego de piernas',
|
|
41
|
+
test: 'Guardia',
|
|
42
|
+
docs: 'Esquina',
|
|
43
|
+
perf: 'Velocidad',
|
|
44
|
+
style: 'Estilo',
|
|
45
|
+
build: 'Calentamiento',
|
|
46
|
+
ci: 'Campana',
|
|
47
|
+
chore: 'Vendajes',
|
|
48
|
+
other: 'Otros',
|
|
49
|
+
},
|
|
50
|
+
card: 'La tarjeta del árbitro',
|
|
51
|
+
decisions: 'decisiones registradas',
|
|
52
|
+
judge: (n) => `${n} del juez`,
|
|
53
|
+
contracts: 'contratos firmados por el juez',
|
|
54
|
+
reboundsLabel: 'puntos de rebote',
|
|
55
|
+
reboundsSub: (closed, open) => `${closed} cerrados · ${open} abiertos`,
|
|
56
|
+
findings: 'hallazgos de esquinero',
|
|
57
|
+
findingsSub: (f) =>
|
|
58
|
+
`${f.applied} aplicados · ${f.discarded} descartados · ${f.ticket} a ticket`,
|
|
59
|
+
fouls: 'faltas que el árbitro se marcó',
|
|
60
|
+
reel: 'Mejores momentos',
|
|
61
|
+
commitsWord: 'commits',
|
|
62
|
+
tags: {
|
|
63
|
+
reclamo: 'Reclamo',
|
|
64
|
+
dado: 'Golpe dado',
|
|
65
|
+
recibido: 'Golpe recibido',
|
|
66
|
+
falta: 'Falta',
|
|
67
|
+
juez: 'Juez',
|
|
68
|
+
arbitro: 'Árbitro',
|
|
69
|
+
bateria: 'Batería',
|
|
70
|
+
campana: 'Campana',
|
|
71
|
+
},
|
|
72
|
+
weekday: 'es-MX',
|
|
73
|
+
},
|
|
74
|
+
en: {
|
|
75
|
+
title: 'The bout in numbers',
|
|
76
|
+
tiles: {
|
|
77
|
+
duration: 'of fighting, first record to last',
|
|
78
|
+
commits: "the wrestler's commits",
|
|
79
|
+
rounds: 'rounds with their bell',
|
|
80
|
+
lines: 'lines added',
|
|
81
|
+
},
|
|
82
|
+
weighIn: 'The weigh-in',
|
|
83
|
+
weighInNote: (ratio) => `${ratio} lines of tests for every line of code.`,
|
|
84
|
+
red: 'Red corner',
|
|
85
|
+
blue: 'Blue corner',
|
|
86
|
+
code: 'Code',
|
|
87
|
+
tests: 'Tests',
|
|
88
|
+
lockfile: 'Lockfile',
|
|
89
|
+
other: 'Config, scripts and assets',
|
|
90
|
+
deadWeight: 'Dead weight',
|
|
91
|
+
linesUnit: 'lines',
|
|
92
|
+
rounds: 'Round by round',
|
|
93
|
+
roundsNote: 'Commits per day, by commit date.',
|
|
94
|
+
star: 'Main event',
|
|
95
|
+
clock: 'The fight clock',
|
|
96
|
+
clockNote:
|
|
97
|
+
'Commits per hour of the day. The shaded band is the small hours.',
|
|
98
|
+
peak: 'peak hour',
|
|
99
|
+
night: 'in the small hours (00–06)',
|
|
100
|
+
bands: [
|
|
101
|
+
'Small hours 00–06',
|
|
102
|
+
'Morning 06–12',
|
|
103
|
+
'Afternoon 12–18',
|
|
104
|
+
'Night 18–24',
|
|
105
|
+
],
|
|
106
|
+
compubox: 'CompuBox',
|
|
107
|
+
compuboxNote: 'Every commit, by its punch type.',
|
|
108
|
+
types: {
|
|
109
|
+
feat: 'Power punches',
|
|
110
|
+
fix: 'Jabs',
|
|
111
|
+
refactor: 'Footwork',
|
|
112
|
+
test: 'Guard',
|
|
113
|
+
docs: 'Corner',
|
|
114
|
+
perf: 'Speed',
|
|
115
|
+
style: 'Style',
|
|
116
|
+
build: 'Warm-up',
|
|
117
|
+
ci: 'Bell',
|
|
118
|
+
chore: 'Hand wraps',
|
|
119
|
+
other: 'Other',
|
|
120
|
+
},
|
|
121
|
+
card: "The referee's card",
|
|
122
|
+
decisions: 'decisions logged',
|
|
123
|
+
judge: (n) => `${n} by the judge`,
|
|
124
|
+
contracts: 'contracts the judge signed',
|
|
125
|
+
reboundsLabel: 'rebound points',
|
|
126
|
+
reboundsSub: (closed, open) => `${closed} closed · ${open} open`,
|
|
127
|
+
findings: 'cornerman findings',
|
|
128
|
+
findingsSub: (f) =>
|
|
129
|
+
`${f.applied} applied · ${f.discarded} discarded · ${f.ticket} ticketed`,
|
|
130
|
+
fouls: 'fouls the referee called on itself',
|
|
131
|
+
reel: 'Highlight reel',
|
|
132
|
+
commitsWord: 'commits',
|
|
133
|
+
tags: {
|
|
134
|
+
reclamo: 'Claim',
|
|
135
|
+
dado: 'Blow landed',
|
|
136
|
+
recibido: 'Blow taken',
|
|
137
|
+
falta: 'Foul',
|
|
138
|
+
juez: 'Judge',
|
|
139
|
+
arbitro: 'Referee',
|
|
140
|
+
bateria: 'Battery',
|
|
141
|
+
campana: 'Bell',
|
|
142
|
+
},
|
|
143
|
+
weekday: 'en-US',
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const esc = (text) =>
|
|
148
|
+
String(text)
|
|
149
|
+
.replaceAll('&', '&')
|
|
150
|
+
.replaceAll('<', '<')
|
|
151
|
+
.replaceAll('>', '>')
|
|
152
|
+
.replaceAll('"', '"');
|
|
153
|
+
const fmt = (n, locale) => new Intl.NumberFormat(locale).format(n);
|
|
154
|
+
const r1 = (n) => Math.round(n * 10) / 10;
|
|
155
|
+
|
|
156
|
+
function tiles(stats, t, locale) {
|
|
157
|
+
const tile = (value, label) =>
|
|
158
|
+
value === null || value === undefined
|
|
159
|
+
? ''
|
|
160
|
+
: `<div><b>${esc(value)}</b><span>${esc(label)}</span></div>`;
|
|
161
|
+
return `<div class="cn-tiles cn-cards">${[
|
|
162
|
+
tile(stats.duration.label, t.tiles.duration),
|
|
163
|
+
tile(stats.rhythm && fmt(stats.rhythm.total, locale), t.tiles.commits),
|
|
164
|
+
tile(stats.book.roundBells, t.tiles.rounds),
|
|
165
|
+
tile(stats.lines && fmt(stats.lines.added, locale), t.tiles.lines),
|
|
166
|
+
].join('')}</div>`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function weighIn({ buckets }, t, locale) {
|
|
170
|
+
const code = buckets.code.added;
|
|
171
|
+
const tests = buckets.tests.added;
|
|
172
|
+
const both = code + tests || 1;
|
|
173
|
+
const ratio = code
|
|
174
|
+
? new Intl.NumberFormat(locale, { maximumFractionDigits: 2 }).format(
|
|
175
|
+
tests / code,
|
|
176
|
+
)
|
|
177
|
+
: '0';
|
|
178
|
+
const dead = [
|
|
179
|
+
[t.lockfile, buckets.lockfile.added],
|
|
180
|
+
[t.other, buckets.other.added],
|
|
181
|
+
].filter(([, n]) => n);
|
|
182
|
+
const corner = (side, label, name, n) =>
|
|
183
|
+
`<div class="cn-corner cn-${side}"><span class="cn-corner-label">${esc(label)}</span><b>${fmt(n, locale)}</b><span>${esc(name)} · ${esc(t.linesUnit)}</span></div>`;
|
|
184
|
+
return `<h4 class="cn-h">${esc(t.weighIn)}</h4><p class="cn-note">${esc(t.weighInNote(ratio))}</p>
|
|
185
|
+
<div class="cn-weigh">
|
|
186
|
+
<div class="cn-weigh-top">${corner('red', t.red, t.code, code)}<div class="cn-vs">VS</div>${corner('blue', t.blue, t.tests, tests)}</div>
|
|
187
|
+
<div class="cn-tape" role="img" aria-label="${esc(`${t.code} ${code}, ${t.tests} ${tests}`)}"><i class="cn-tape-red" style="width:${r1((code * 100) / both)}%" title="${esc(`${t.code}: ${fmt(code, locale)}`)}"></i><i class="cn-tape-blue" style="width:${r1((tests * 100) / both)}%" title="${esc(`${t.tests}: ${fmt(tests, locale)}`)}"></i></div>
|
|
188
|
+
${dead.length ? `<div class="cn-dead"><span>${esc(t.deadWeight)}</span>${dead.map(([name, n]) => `<span>${esc(name)} <b>${fmt(n, locale)}</b></span>`).join('')}</div>` : ''}
|
|
189
|
+
</div>`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function roundByRound(byDay, t, locale) {
|
|
193
|
+
const W = 640;
|
|
194
|
+
const H = 262;
|
|
195
|
+
const top = 72;
|
|
196
|
+
const base = 218;
|
|
197
|
+
const max = Math.max(1, ...byDay.map((d) => d.n));
|
|
198
|
+
const slot = (W - 60) / byDay.length;
|
|
199
|
+
const bar = Math.min(64, slot * 0.6);
|
|
200
|
+
const star = byDay.findIndex((d) => d.n === max);
|
|
201
|
+
const ropes = [0.25, 0.5, 0.75]
|
|
202
|
+
.map(
|
|
203
|
+
(f, i) =>
|
|
204
|
+
`<line class="cn-rope cn-rope-${i}" x1="22" x2="${W - 22}" y1="${r1(base - f * (base - top))}" y2="${r1(base - f * (base - top))}"/>`,
|
|
205
|
+
)
|
|
206
|
+
.join('');
|
|
207
|
+
const label = (day) =>
|
|
208
|
+
new Intl.DateTimeFormat(t.weekday, {
|
|
209
|
+
weekday: 'short',
|
|
210
|
+
day: 'numeric',
|
|
211
|
+
timeZone: 'UTC',
|
|
212
|
+
}).format(new Date(`${day}T12:00:00Z`));
|
|
213
|
+
const bars = byDay
|
|
214
|
+
.map((d, i) => {
|
|
215
|
+
const h = (d.n / max) * (base - top);
|
|
216
|
+
const x = 30 + slot * i + (slot - bar) / 2;
|
|
217
|
+
const y = base - h;
|
|
218
|
+
const cls = `cn-bar cn-fill-${i % 5}${i === star ? ' cn-bar-star' : ''}`;
|
|
219
|
+
return `<g><title>${esc(`${label(d.day)}: ${fmt(d.n, locale)} ${t.commitsWord}`)}</title><path class="${cls}" d="M${r1(x)},${base} V${r1(y + Math.min(4, h))} q0,-4 4,-4 h${r1(bar - 8)} q4,0 4,4 V${base} Z"/>${
|
|
220
|
+
d.n
|
|
221
|
+
? `<text class="cn-val" x="${r1(x + bar / 2)}" y="${r1(y - 8)}">${fmt(d.n, locale)}</text>`
|
|
222
|
+
: ''
|
|
223
|
+
}<text class="cn-axis" x="${r1(x + bar / 2)}" y="${base + 22}">${esc(label(d.day))}</text>${
|
|
224
|
+
i === star
|
|
225
|
+
? `<text class="cn-star" x="${r1(x + bar / 2)}" y="${r1(y - 40)}">${esc(t.star)}</text>`
|
|
226
|
+
: ''
|
|
227
|
+
}</g>`;
|
|
228
|
+
})
|
|
229
|
+
.join('');
|
|
230
|
+
return `<h4 class="cn-h">${esc(t.rounds)}</h4><p class="cn-note">${esc(t.roundsNote)}</p>
|
|
231
|
+
<div class="cn-chart"><svg viewBox="0 0 ${W} ${H}" role="img" aria-label="${esc(t.rounds)}">${ropes}<rect class="cn-post" x="14" y="${top - 6}" width="8" height="${base - top + 6}" rx="2"/><rect class="cn-post" x="${W - 22}" y="${top - 6}" width="8" height="${base - top + 6}" rx="2"/><line class="cn-canvas-line" x1="14" x2="${W - 14}" y1="${base}" y2="${base}"/>${bars}</svg></div>`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function fightClock(byHour, t, locale) {
|
|
235
|
+
const C = 170;
|
|
236
|
+
const r0 = 62;
|
|
237
|
+
const len = 92;
|
|
238
|
+
const max = Math.max(1, ...byHour);
|
|
239
|
+
const peak = byHour.indexOf(max);
|
|
240
|
+
const point = (r, deg) => {
|
|
241
|
+
const a = ((deg - 90) * Math.PI) / 180;
|
|
242
|
+
return [r1(C + r * Math.cos(a)), r1(C + r * Math.sin(a))];
|
|
243
|
+
};
|
|
244
|
+
const wedge = (ri, ro, from, to) => {
|
|
245
|
+
const [x1, y1] = point(ri, from);
|
|
246
|
+
const [x2, y2] = point(ro, from);
|
|
247
|
+
const [x3, y3] = point(ro, to);
|
|
248
|
+
const [x4, y4] = point(ri, to);
|
|
249
|
+
const large = to - from > 180 ? 1 : 0;
|
|
250
|
+
return `M${x1},${y1} L${x2},${y2} A${ro},${ro} 0 ${large} 1 ${x3},${y3} L${x4},${y4} A${ri},${ri} 0 ${large} 0 ${x1},${y1} Z`;
|
|
251
|
+
};
|
|
252
|
+
const night = `<path class="cn-night" d="${wedge(r0, r0 + len + 4, 0, 90)}"/>`;
|
|
253
|
+
const hours = byHour
|
|
254
|
+
.map((n, h) => {
|
|
255
|
+
const ro = r0 + 3 + (n / max) * len;
|
|
256
|
+
const cls = `cn-hour cn-band-${Math.floor(h / 6)}${h === peak ? ' cn-hour-peak' : ''}`;
|
|
257
|
+
return n
|
|
258
|
+
? `<path class="${cls}" d="${wedge(r0 + 3, ro, h * 15 + 2, h * 15 + 13)}"><title>${String(h).padStart(2, '0')}:00 · ${fmt(n, locale)} ${t.commitsWord}</title></path>`
|
|
259
|
+
: '';
|
|
260
|
+
})
|
|
261
|
+
.join('');
|
|
262
|
+
const ticks = [0, 6, 12, 18]
|
|
263
|
+
.map((h) => {
|
|
264
|
+
const [x, y] = point(r0 + len + 18, h * 15 + 7.5);
|
|
265
|
+
return `<text class="cn-axis" x="${x}" y="${y + 4}">${String(h).padStart(2, '0')}</text>`;
|
|
266
|
+
})
|
|
267
|
+
.join('');
|
|
268
|
+
const bandCounts = [0, 1, 2, 3].map((band) =>
|
|
269
|
+
byHour.slice(band * 6, band * 6 + 6).reduce((a, b) => a + b, 0),
|
|
270
|
+
);
|
|
271
|
+
const nightCount = bandCounts[0];
|
|
272
|
+
const legend = bandCounts
|
|
273
|
+
.map(
|
|
274
|
+
(n, band) =>
|
|
275
|
+
`<li><i class="cn-band-${band}"></i>${esc(t.bands[band])}<b>${fmt(n, locale)}</b></li>`,
|
|
276
|
+
)
|
|
277
|
+
.join('');
|
|
278
|
+
return `<h4 class="cn-h">${esc(t.clock)}</h4><p class="cn-note">${esc(t.clockNote)}</p>
|
|
279
|
+
<div class="cn-chart cn-clock-wrap"><svg class="cn-clock" viewBox="-24 -24 388 388" role="img" aria-label="${esc(t.clock)}"><circle class="cn-face" cx="${C}" cy="${C}" r="${r0 + len + 4}"/>${night}<circle class="cn-hub" cx="${C}" cy="${C}" r="${r0}"/>${hours}${ticks}<text class="cn-clock-big" x="${C}" y="${C + 6}">${String(peak).padStart(2, '0')}:00</text><text class="cn-clock-sub" x="${C}" y="${C + 28}">${esc(t.peak)}</text></svg>
|
|
280
|
+
<div class="cn-clock-facts"><div><b>${fmt(max, locale)}</b><span>${esc(t.commitsWord)} · ${String(peak).padStart(2, '0')}:00, ${esc(t.peak)}</span></div><div><b>${fmt(nightCount, locale)}</b><span>${esc(t.commitsWord)} ${esc(t.night)}</span></div><ul class="cn-bands">${legend}</ul></div></div>`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function compuBox(byType, total, t, locale) {
|
|
284
|
+
const max = Math.max(1, ...byType.map((x) => x.n));
|
|
285
|
+
const rows = byType
|
|
286
|
+
.map(({ type, n }) => {
|
|
287
|
+
const cls =
|
|
288
|
+
{
|
|
289
|
+
feat: 'cn-fill-0',
|
|
290
|
+
fix: 'cn-fill-1',
|
|
291
|
+
refactor: 'cn-fill-2',
|
|
292
|
+
test: 'cn-fill-3',
|
|
293
|
+
docs: 'cn-fill-4',
|
|
294
|
+
}[type] ?? 'cn-ink';
|
|
295
|
+
const pct = total ? Math.round((n * 100) / total) : 0;
|
|
296
|
+
return `<div class="cn-box-row" title="${esc(`${t.types[type]} (${type}): ${fmt(n, locale)}`)}"><span class="cn-box-name">${esc(t.types[type])}<code>${esc(type)}</code></span><span class="cn-box-track"><i class="${cls}" style="width:${r1((n * 100) / max)}%"></i></span><b>${fmt(n, locale)}</b><span class="cn-box-pct">${pct}%</span></div>`;
|
|
297
|
+
})
|
|
298
|
+
.join('');
|
|
299
|
+
return `<h4 class="cn-h">${esc(t.compubox)}</h4><p class="cn-note">${esc(t.compuboxNote)}</p><div class="cn-chart cn-box">${rows}</div>`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function refereeCard(book, t, locale) {
|
|
303
|
+
const f = book.findings;
|
|
304
|
+
let next = 0;
|
|
305
|
+
const card = (n, label, sub, cls = `cn-head-${next++ % 5}`) =>
|
|
306
|
+
`<div class="cn-score ${cls}"><span class="cn-score-head">${esc(label)}</span><b>${fmt(n, locale)}</b>${sub ? `<span>${esc(sub)}</span>` : ''}</div>`;
|
|
307
|
+
return `<h4 class="cn-h">${esc(t.card)}</h4><div class="cn-scores">${[
|
|
308
|
+
card(book.decisions, t.decisions, t.judge(book.judgeDecisions)),
|
|
309
|
+
card(
|
|
310
|
+
book.rebounds.total,
|
|
311
|
+
t.reboundsLabel,
|
|
312
|
+
t.reboundsSub(
|
|
313
|
+
book.rebounds.closed,
|
|
314
|
+
book.rebounds.total - book.rebounds.closed,
|
|
315
|
+
),
|
|
316
|
+
),
|
|
317
|
+
f.total ? card(f.total, t.findings, t.findingsSub(f)) : '',
|
|
318
|
+
book.contracts ? card(book.contracts, t.contracts, '') : '',
|
|
319
|
+
card(book.fouls, t.fouls, '', 'cn-score-foul'),
|
|
320
|
+
].join('')}</div>`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function reel(moments, timeZone, t) {
|
|
324
|
+
if (!moments.length) return '';
|
|
325
|
+
const first = Date.parse(moments[0].at);
|
|
326
|
+
const span = Math.max(1, Date.parse(moments.at(-1).at) - first);
|
|
327
|
+
const when = (at) =>
|
|
328
|
+
new Intl.DateTimeFormat(t.weekday, {
|
|
329
|
+
timeZone,
|
|
330
|
+
day: 'numeric',
|
|
331
|
+
hour: '2-digit',
|
|
332
|
+
minute: '2-digit',
|
|
333
|
+
hour12: false,
|
|
334
|
+
}).format(new Date(at));
|
|
335
|
+
const marks = moments
|
|
336
|
+
.map(
|
|
337
|
+
(m, i) =>
|
|
338
|
+
`<span class="cn-bell cn-fill-${i % 5}" style="left:${r1(3 + ((Date.parse(m.at) - first) * 94) / span)}%" title="${esc(`${when(m.at)} · ${m.title}`)}">${i + 1}</span>`,
|
|
339
|
+
)
|
|
340
|
+
.join('');
|
|
341
|
+
const items = moments
|
|
342
|
+
.map(
|
|
343
|
+
(m, i) =>
|
|
344
|
+
`<li><span class="cn-reel-n">${i + 1}</span><time>${esc(when(m.at))}</time><p><span class="tag t-${esc(m.tag)}">${esc(t.tags[m.tag] ?? m.tag)}</span>${esc(m.title)}</p></li>`,
|
|
345
|
+
)
|
|
346
|
+
.join('');
|
|
347
|
+
return `<h4 class="cn-h">${esc(t.reel)}</h4><div class="cn-reel"><div class="cn-reel-line">${marks}</div><ol>${items}</ol></div>`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** The whole block. `stats` is collectStats()'s output; moments come from readMoments(). PURE. */
|
|
351
|
+
export function renderStats(stats, { lang = 'es', moments = [] } = {}) {
|
|
352
|
+
const t = LABELS[lang] ?? LABELS.es;
|
|
353
|
+
const locale = lang === 'en' ? 'en-US' : 'es-MX';
|
|
354
|
+
return `<section class="round cn">
|
|
355
|
+
<div class="cn-picado" aria-hidden="true"></div>
|
|
356
|
+
<h2>${esc(t.title)}</h2>
|
|
357
|
+
${tiles(stats, t, locale)}
|
|
358
|
+
${stats.lines ? weighIn(stats.lines, t, locale) : ''}
|
|
359
|
+
${stats.rhythm?.byDay.length ? roundByRound(stats.rhythm.byDay, t, locale) : ''}
|
|
360
|
+
${stats.rhythm?.total ? fightClock(stats.rhythm.byHour, t, locale) : ''}
|
|
361
|
+
${stats.rhythm?.total ? compuBox(stats.rhythm.byType, stats.rhythm.total, t, locale) : ''}
|
|
362
|
+
${refereeCard(stats.book, t, locale)}
|
|
363
|
+
${reel(moments, stats.timezone, t)}
|
|
364
|
+
<!-- CHANGELOG: the skill writes <h4 class="cn-h"> and <ul class="cn-changelog"> here -->
|
|
365
|
+
</section>`;
|
|
366
|
+
}
|