@thebattlearena/corner 0.9.1-beta.70 → 0.10.1-beta.78

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 (58) hide show
  1. package/README.md +46 -26
  2. package/package.json +1 -1
  3. package/plugin/.claude-plugin/plugin.json +1 -1
  4. package/ring/agents/cornerman.md +32 -6
  5. package/ring/agents/rudo.md +2 -1
  6. package/ring/agents/wrestler.md +19 -4
  7. package/ring/hooks/hooks.json +42 -0
  8. package/ring/scripts/bout-stats-render.mjs +366 -0
  9. package/ring/scripts/bout-stats.mjs +360 -0
  10. package/ring/scripts/brief-build.mjs +130 -0
  11. package/ring/scripts/brief-guard.mjs +154 -0
  12. package/ring/scripts/brief.mjs +272 -0
  13. package/ring/scripts/compact-hook.mjs +110 -0
  14. package/ring/scripts/config.mjs +25 -1
  15. package/ring/scripts/contract-endpoints.mjs +163 -0
  16. package/ring/scripts/contract-preview.mjs +34 -14
  17. package/ring/scripts/fight.mjs +27 -0
  18. package/ring/scripts/findings.mjs +82 -3
  19. package/ring/scripts/format-gate.mjs +723 -0
  20. package/ring/scripts/format-worker.mjs +120 -0
  21. package/ring/scripts/front-routes.mjs +345 -0
  22. package/ring/scripts/judges-card.mjs +324 -3
  23. package/ring/scripts/lookout.mjs +22 -4
  24. package/ring/scripts/mask-guard.mjs +34 -1
  25. package/ring/scripts/page-embed.mjs +112 -0
  26. package/ring/scripts/play-by-play.mjs +155 -7
  27. package/ring/scripts/preparation-collect.mjs +177 -0
  28. package/ring/scripts/preparation.mjs +547 -0
  29. package/ring/scripts/round-bell.mjs +9 -2
  30. package/ring/scripts/rule-ids.mjs +124 -0
  31. package/ring/scripts/tech-plan-log.mjs +81 -0
  32. package/ring/scripts/tech-plan.mjs +191 -0
  33. package/ring/scripts/verdicts-log.mjs +75 -0
  34. package/ring/scripts/verdicts.mjs +205 -0
  35. package/ring/skills/play-by-play/SKILL.md +235 -33
  36. package/ring/skills/play-by-play/templates/blocks.css +196 -0
  37. package/ring/skills/play-by-play/templates/chronicle.html +358 -55
  38. package/ring/skills/play-by-play/templates/endpoints-view.js +1153 -0
  39. package/ring/skills/play-by-play/templates/interview-form.js +516 -0
  40. package/ring/skills/play-by-play/templates/now-panel.js +719 -0
  41. package/ring/skills/referee/SKILL.md +181 -56
  42. package/ring/skills/referee/references/api-diagram.md +56 -0
  43. package/ring/skills/referee/references/precedents.md +11 -0
  44. package/ring/skills/referee/references/rulebook.md +97 -6
  45. package/ring/skills/referee/templates/api-diagram.html +115 -0
  46. package/ring/skills/referee/templates/cornerman-batch.md +17 -11
  47. package/ring/skills/referee/templates/cornerman-concept.md +28 -30
  48. package/ring/skills/referee/templates/cornerman-range.md +16 -11
  49. package/ring/skills/referee/templates/escalation.md +7 -1
  50. package/ring/skills/referee/templates/round-bell-body.md +4 -0
  51. package/ring/skills/referee/templates/wrestler-brief.md +28 -41
  52. package/ring/skills/referee/templates/wrestler-cutoff.md +4 -0
  53. package/ring/skills/referee/templates/wrestler-resume.md +4 -0
  54. package/ring/skills/sparring/SKILL.md +13 -4
  55. package/ring/skills/sparring/templates/contrato.md +29 -0
  56. package/ring/skills/sparring/templates/rudo-brief.md +7 -0
  57. package/ring/skills/title-defense/SKILL.md +2 -2
  58. package/ring/skills/weigh-in/SKILL.md +6 -0
@@ -17,6 +17,8 @@
17
17
  // When a database model changes, the referee draws the change as an entity-relationship diagram
18
18
  // (references/db-diagram.md) in <bout dir>/db-diagram.html; the page shows it above the model
19
19
  // diff, which folds into a "Code diff" disclosure, and warns when a model file changed after it.
20
+ // The endpoints get theirs the same way (references/api-diagram.md, <bout dir>/api-diagram.html):
21
+ // shown above "What changes", whose diff folds under it, and stale when a spec file changed after.
20
22
  //
21
23
  // The diff runs in two places from one module (spec-diff.mjs): the page parses both specs (YAML
22
24
  // via js-yaml) and computes operations, schemas and error responses; the server computes the same
@@ -129,7 +131,7 @@ function worktreeFiles(worktree) {
129
131
  }
130
132
 
131
133
  /** The spec roots the config names: its plain paths, and the OpenAPI roots its globs match. */
132
- function specRoots(worktree, config) {
134
+ export function specRoots(worktree, config) {
133
135
  const files = worktreeFiles(worktree);
134
136
  const head = (path) => {
135
137
  try {
@@ -241,6 +243,9 @@ function dbDiff(worktree, ref, config) {
241
243
  /** The referee's diagram of the model change, drawn per references/db-diagram.md. */
242
244
  export const DIAGRAM_FILE = 'db-diagram.html';
243
245
 
246
+ /** The referee's diagram of the endpoints, drawn per references/api-diagram.md. */
247
+ export const API_DIAGRAM_FILE = 'api-diagram.html';
248
+
244
249
  /**
245
250
  * When a changed model file was last written, in ms: its mtime, or for a deleted file the time of
246
251
  * the commit that deleted it. A deletion not committed yet has no date: `Infinity`, so it always
@@ -263,12 +268,12 @@ function writtenAt(worktree, path) {
263
268
  }
264
269
 
265
270
  /**
266
- * The diagram of the model change, or `null` when the referee has not drawn one: `stale` lists the
267
- * changed model files written (or deleted) after it, so the judge knows it may not match the diff
268
- * below.
271
+ * A diagram of the change (the models by default, or `API_DIAGRAM_FILE`), or `null` when the
272
+ * referee has not drawn it: `stale` lists the changed files written (or deleted) after it, so the
273
+ * judge knows it may not match the diff below.
269
274
  */
270
- export function diagramState(dir, worktree, files) {
271
- const diagram = join(dir, DIAGRAM_FILE);
275
+ export function diagramState(dir, worktree, files, file = DIAGRAM_FILE) {
276
+ const diagram = join(dir, file);
272
277
  if (!existsSync(diagram)) return null;
273
278
  const drawnAt = statSync(diagram).mtimeMs;
274
279
  const stale = files
@@ -319,7 +324,7 @@ export function page(specs, first = 0) {
319
324
  pre { overflow-x: auto; border: 1px solid var(--line); border-radius: 6px; padding: 8px; font-size: 12px; }
320
325
  pre span { display: block; white-space: pre; }
321
326
  #swagger { background: #fff; border-radius: 6px; }
322
- #diagram { width: 100%; min-height: 320px; border: 1px solid var(--line); border-radius: 6px; background: #fff; }
327
+ .diagram { width: 100%; min-height: 320px; border: 1px solid var(--line); border-radius: 6px; background: #fff; }
323
328
  .warn { background: var(--chg); border-radius: 6px; padding: 8px; }
324
329
  details { margin-top: 12px; } summary { cursor: pointer; }
325
330
  </style></head>
@@ -364,7 +369,14 @@ export function page(specs, first = 0) {
364
369
  html += list('Spec files changed', specFiles.files.map((f) => f.status + ' ' + f.path), '') +
365
370
  (specFiles.diff ? '<details><summary>Spec text diff</summary><pre>' + specLines + '</pre></details>' : '');
366
371
  }
367
- document.getElementById('changes').innerHTML = html || 'No API changes against the base.';
372
+ const apiStale = data.apiDiagram && data.apiDiagram.stale.length
373
+ ? '<p class="warn">Changed after the diagram was drawn: ' + esc(data.apiDiagram.stale.join(', ')) + '. Ask the referee to redraw it.</p>'
374
+ : '';
375
+ const changes = html || 'No API changes against the base.';
376
+ document.getElementById('changes').innerHTML = data.apiDiagram
377
+ ? apiStale + '<iframe class="diagram" src="/api-diagram" sandbox="allow-same-origin" title="Endpoints of the contract"></iframe>' +
378
+ '<details><summary>Code diff</summary>' + changes + '</details>'
379
+ : changes;
368
380
  const db = data.db;
369
381
  const lines = db.diff.split('\\n').map((l) => '<span class="' + (l.startsWith('+') ? 'add' : l.startsWith('-') ? 'del' : '') + '">' + esc(l) + '</span>').join('');
370
382
  const code = list('Files', db.files.map((f) => f.status + ' ' + f.path), '') +
@@ -374,13 +386,13 @@ export function page(specs, first = 0) {
374
386
  ? '<p class="warn">Changed after the diagram was drawn: ' + esc(db.diagram.stale.join(', ')) + '. Ask the referee to redraw it.</p>'
375
387
  : '';
376
388
  const drawn = db.diagram
377
- ? stale + '<iframe id="diagram" src="/db-diagram" sandbox="allow-same-origin" title="Database model change"></iframe>' +
389
+ ? stale + '<iframe class="diagram" src="/db-diagram" sandbox="allow-same-origin" title="Database model change"></iframe>' +
378
390
  '<details><summary>Code diff</summary>' + code + '</details>'
379
391
  : code;
380
392
  const dbBox = document.getElementById('db');
381
393
  dbBox.innerHTML = db.files.length ? drawn : 'No database-model changes against the base.';
382
- const frame = document.getElementById('diagram');
383
- if (frame) frame.addEventListener('load', () => { frame.style.height = frame.contentDocument.documentElement.scrollHeight + 'px'; });
394
+ for (const frame of document.querySelectorAll('iframe.diagram'))
395
+ frame.addEventListener('load', () => { frame.style.height = frame.contentDocument.documentElement.scrollHeight + 'px'; });
384
396
  </script>
385
397
  </body></html>`;
386
398
  }
@@ -447,8 +459,11 @@ export function handler({ dir, worktree, base, config }) {
447
459
  : send(200, contentType(path), text);
448
460
  }
449
461
  if (url.pathname === '/favicon.ico') return send(204, 'text/plain', '');
450
- if (url.pathname === '/db-diagram') {
451
- const diagram = join(dir, DIAGRAM_FILE);
462
+ if (url.pathname === '/db-diagram' || url.pathname === '/api-diagram') {
463
+ const diagram = join(
464
+ dir,
465
+ url.pathname === '/db-diagram' ? DIAGRAM_FILE : API_DIAGRAM_FILE,
466
+ );
452
467
  return existsSync(diagram)
453
468
  ? send(200, 'text/html; charset=utf-8', readFileSync(diagram))
454
469
  : send(404, 'text/plain', 'not found');
@@ -467,6 +482,7 @@ export function handler({ dir, worktree, base, config }) {
467
482
  : send(200, contentType(spec), text);
468
483
  }
469
484
  if (url.pathname === '/diff.json') {
485
+ const specFiles = specFilesDiff(worktree, ref, config);
470
486
  const body = {
471
487
  spec,
472
488
  specs,
@@ -476,7 +492,11 @@ export function handler({ dir, worktree, base, config }) {
476
492
  ? serverSpecDiff(spec, atBase(spec), proposed(spec))
477
493
  : null,
478
494
  db: withDiagram(dbDiff(worktree, ref, config)),
479
- specFiles: specFilesDiff(worktree, ref, config),
495
+ specFiles,
496
+ // Like the database's: no diagram without a spec change, so an old one never shows.
497
+ apiDiagram: specFiles.files.length
498
+ ? diagramState(dir, worktree, specFiles.files, API_DIAGRAM_FILE)
499
+ : null,
480
500
  };
481
501
  return send(
482
502
  200,
@@ -11,6 +11,7 @@ import { join } from 'node:path';
11
11
  import { commitEvents, localDay, localTime, score } from './chronicle.mjs';
12
12
  import { repoBouts, slugFromRemote } from './config.mjs';
13
13
  import { LIVE_LOG, SPARRING_FILE, sparringsHome } from './live.mjs';
14
+ import { sparringFold } from './preparation.mjs';
14
15
  import {
15
16
  dedupe,
16
17
  feedEvents,
@@ -86,6 +87,29 @@ function option(argv, name) {
86
87
  return index === -1 ? null : argv[index + 1];
87
88
  }
88
89
 
90
+ /**
91
+ * The bout's weigh-in with what `sparring.mjs promote` made for its repo: the deliverable PR
92
+ * (mainevent → release) and the sparring's draft and worktree it came from. PURE.
93
+ */
94
+ export function withPromotion(weighIn, state, repo) {
95
+ const entry = (state.repos ?? []).find((item) => item.repo === repo);
96
+ if (!entry) return weighIn;
97
+ return {
98
+ ...weighIn,
99
+ promotion: {
100
+ ...weighIn.promotion,
101
+ repo,
102
+ mainevent: entry.mainevent ?? null,
103
+ sparring: {
104
+ branch: entry.branch,
105
+ pr: entry.pr ?? null,
106
+ url: entry.url ?? null,
107
+ worktree: entry.worktree,
108
+ },
109
+ },
110
+ };
111
+ }
112
+
89
113
  /** Every bout opened `--from-sparring` this sparring, in the sparring's repos. */
90
114
  export function boutsOfSparring(state, env = process.env) {
91
115
  return (state.repos ?? []).flatMap((entry) =>
@@ -220,12 +244,15 @@ export function collectFight(slug, argv, { collectBout, readExtra }) {
220
244
  proof: state.proof,
221
245
  verdict: state.verdict ?? null,
222
246
  repos: (state.repos ?? []).map(({ repo, pr, url }) => ({ repo, pr, url })),
247
+ fold: sparringFold(state, bouts.length),
223
248
  bouts: bouts.map((bout) => ({
224
249
  repo: bout.repo,
225
250
  pr: bout.pr,
226
251
  boutDir: bout.boutDir,
227
252
  opened: bout.opened,
228
253
  copilotOnHead: bout.copilotOnHead,
254
+ weighIn: withPromotion(bout.weighIn, state, bout.repo),
255
+ interview: bout.interview,
229
256
  })),
230
257
  falls: sliced.falls,
231
258
  rounds: sliced.rounds,
@@ -1,8 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  // The ledger of every cornerman finding of a bout, and its audit. Zero-dependency.
3
3
  //
4
- // node findings.mjs add <pr> --source range|concept|batch|final|copilot|human [--range <from>..<to>]
4
+ // node findings.mjs add <pr> --source range|concept|batch|final|copilot|human|contract [--range <from>..<to>]
5
+ // (contract: the judge's «se mejora» on an endpoint, from judges-card page-verdicts)
5
6
  // (<text> | --file <report>) one finding, or one per line of a report
7
+ // [--rule <id>] the referee's own rule for the lines that cite none (logged as theirs)
8
+ // A reviewer's finding (range, concept, batch, final, human) cites a rule of the venue
9
+ // in its second field (`<file:line> — R11 — …`); X0 when none covers it.
6
10
  // node findings.mjs clean <pr> --range <from>..<to> the cornerman found nothing in that range
7
11
  // node findings.mjs applied <pr> F<n>[,F<m>…] <sha> the wrestler applied it in that commit
8
12
  // node findings.mjs discard <pr> F<n>[,…] --id D<k> --reason "<why>"
@@ -38,8 +42,16 @@ import {
38
42
  repoSlug,
39
43
  } from './config.mjs';
40
44
  import { record } from './live.mjs';
45
+ import {
46
+ citedRules,
47
+ loadRules,
48
+ NO_RULE,
49
+ uncitedFindings,
50
+ } from './rule-ids.mjs';
41
51
 
42
52
  export const LEDGER = 'findings.json';
53
+ /** Sources whose findings come from a review against the rulebook, so each one cites a rule. */
54
+ export const CITED_SOURCES = ['range', 'concept', 'batch', 'final', 'human'];
43
55
  export const SOURCES = [
44
56
  'range',
45
57
  'concept',
@@ -47,6 +59,7 @@ export const SOURCES = [
47
59
  'final',
48
60
  'copilot',
49
61
  'human',
62
+ 'contract',
50
63
  ];
51
64
  /** States that need nothing more from the referee before a round-bell or a push. */
52
65
  const SETTLED = ['applied', 'discarded', 'ticket', 'escalated'];
@@ -377,19 +390,85 @@ function scorecard(dir, ledger, label) {
377
390
  });
378
391
  }
379
392
 
393
+ /**
394
+ * The rule each finding line answers to: the one it cites, or the referee's `--rule` for a line
395
+ * that cites none. Throws listing every line without a valid one. PURE.
396
+ */
397
+ export function ruledLines(texts, ids, rule) {
398
+ if (rule && rule !== NO_RULE && !ids.has(rule))
399
+ throw new Error(`--rule ${rule} is not a rule of this venue`);
400
+ const lines = texts.map((text) => ({
401
+ text,
402
+ by: citedRules(text) || !rule ? null : 'referee',
403
+ }));
404
+ const pending = lines.filter((line) => !line.by).map((line) => line.text);
405
+ const wrong = uncitedFindings(pending, ids);
406
+ if (wrong.length)
407
+ throw new Error(
408
+ `every finding cites a rule of the venue in its second field (\`<file:line> — R11 — …\`), or X0 when none covers it; rebound the report to the cornerman, or take the line as yours with --rule <id>:\n${wrong.map(({ line, reason }) => ` ${line} (${reason})`).join('\n')}`,
409
+ );
410
+ return lines.map(({ text, by }) => (by ? { text, by, rule } : { text }));
411
+ }
412
+
413
+ /** The findings no rule covers, as proposals for the rulebook in the judges card. */
414
+ function proposeRules(dir, findings) {
415
+ const card = join(dir, 'judges-card.md');
416
+ const uncovered = findings.filter(
417
+ (finding) =>
418
+ finding.rule === NO_RULE || citedRules(finding.text)?.includes(NO_RULE),
419
+ );
420
+ if (!uncovered.length || !existsSync(card)) return;
421
+ const text = uncovered.reduce(
422
+ (current, finding) =>
423
+ withSectionLine(
424
+ current,
425
+ 'For the rulebook / precedents',
426
+ `- ${finding.id} (X0, no rule covers it): ${finding.text} — which rule is missing?`,
427
+ ),
428
+ readFileSync(card, 'utf8'),
429
+ );
430
+ writeFileSync(card, text);
431
+ }
432
+
380
433
  function add([pr, ...argv]) {
381
434
  const { values, rest } = flags(argv);
382
435
  const { dir, state } = bout(pr);
383
436
  const texts = values.file
384
437
  ? reportLines(readFileSync(values.file, 'utf8'))
385
438
  : [rest.join(' ').trim()].filter(Boolean);
439
+ const cited = CITED_SOURCES.includes(values.source);
440
+ const ids = cited
441
+ ? loadRules(
442
+ loadConfig(projectDir(), {
443
+ contract: false,
444
+ venue: state.venue ?? 'backend',
445
+ }).config,
446
+ ).ids
447
+ : null;
448
+ const lines = cited
449
+ ? ruledLines(texts, ids, values.rule)
450
+ : texts.map((text) => ({ text }));
386
451
  const { ledger, added } = withFindings(readLedger(dir), {
387
452
  source: values.source,
388
453
  ...rangeOf(state, values.range),
389
- texts,
454
+ texts: lines.map((line) => line.text),
390
455
  at: now(),
391
456
  });
392
- scorecard(dir, writeLedger(dir, ledger), 'registró hallazgos');
457
+ const authored = new Map(added.map((finding, i) => [finding.id, lines[i]]));
458
+ const findings = ledger.findings.map((finding) =>
459
+ authored.get(finding.id)?.by
460
+ ? { ...finding, by: 'referee', rule: authored.get(finding.id).rule }
461
+ : finding,
462
+ );
463
+ scorecard(
464
+ dir,
465
+ writeLedger(dir, { ...ledger, findings }),
466
+ 'registró hallazgos',
467
+ );
468
+ proposeRules(
469
+ dir,
470
+ findings.filter((finding) => authored.has(finding.id)),
471
+ );
393
472
  for (const finding of added) console.log(`${finding.id} · ${finding.text}`);
394
473
  }
395
474