@thebattlearena/corner 0.10.1-beta.79 → 0.10.1-beta.80
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/README.md +13 -1
- package/package.json +1 -1
- package/ring/scripts/brief.mjs +28 -0
- package/ring/scripts/config.mjs +4 -0
- package/ring/scripts/judges-card.mjs +36 -4
- package/ring/scripts/review-blocks.mjs +232 -0
- package/ring/scripts/tech-plan-log.mjs +34 -2
- package/ring/skills/referee/SKILL.md +8 -0
package/README.md
CHANGED
|
@@ -374,7 +374,7 @@ rama. El contrato para el proyecto está en `ring/skills/sparring/references/hos
|
|
|
374
374
|
**Config del repo** (`.claude/ring.json`, opcional): `gates` (typecheck, tests por ruta con
|
|
375
375
|
`{paths}`, lint), `validateCommand`, `rulebook` (por defecto el genérico del plugin),
|
|
376
376
|
`protectedBranches`, `forbidAiTrailers` (por defecto `true`), `heavyCommands`, `delegation`,
|
|
377
|
-
`precedents`, `monitors` (ver «Monitores del repo»), `briefGuard` (`warn`, `block` u `off`), `layers`, `layersIgnore`, `layersRequired` y `findingsNeedJudge` (ver «Gate de arquitectura»), `relayEvery`, `engramUrl` y `engramProject` (ver «Memoria entre peleas»), y `agents` (modelo y esfuerzo por rol; por
|
|
377
|
+
`precedents`, `monitors` (ver «Monitores del repo»), `briefGuard` (`warn`, `block` u `off`), `layers`, `layersIgnore`, `layersRequired` y `findingsNeedJudge` (ver «Gate de arquitectura»), `relayEvery`, `humanReviewEvery` (ver «Revisión humana por bloques»), `engramUrl` y `engramProject` (ver «Memoria entre peleas»), y `agents` (modelo y esfuerzo por rol; por
|
|
378
378
|
defecto el luchador y el rudo en `sonnet` con esfuerzo `high` y el esquinero en `opus` con `high`,
|
|
379
379
|
p. ej. `"agents": { "wrestler": { "model": "opus" } }`). Sin el archivo, `ring` usa los gates que
|
|
380
380
|
detecta en `package.json`, `pyproject.toml`, `go.mod` y el `Makefile`.
|
|
@@ -437,6 +437,18 @@ compactarse. Y `judges-card.mjs close` no cierra mientras una propuesta de «For
|
|
|
437
437
|
precedents» (los `X0` incluidos) no tenga decisión: `judges-card.mjs proposal <pr> <n>
|
|
438
438
|
accept|reject --why "<lo que dijo el humano>"`.
|
|
439
439
|
|
|
440
|
+
**Revisión humana por bloques (PR Review Lab)**: cada punto del plan técnico es un bloque (`B<n>` es
|
|
441
|
+
el punto n). `judges-card.mjs plan <pr> estimate <n> --minutes <m> [--priority 1-4]` le pone estimado y
|
|
442
|
+
prioridad. Al pasar a «en curso» se anotan el inicio y el autor (el `user.email` de git del worktree),
|
|
443
|
+
y al pasar a «espera al juez» el fin y el rango (`--range A..B` lo fija a mano); todo va en
|
|
444
|
+
`plan.jsonl`, que es lo que lee el modo «Pelea del ring» de PR Review Lab. Cuando `humanReviewEvery`
|
|
445
|
+
bloques (2 por default, `null` lo apaga) esperan al juez, suena la campana en PR Review Lab y
|
|
446
|
+
`brief.mjs` no le arma instrucciones al siguiente luchador hasta que el humano revise ese corte en un
|
|
447
|
+
solo diff y firme un veredicto con Tape. PR Review Lab lo escribe en `<combate>/review-verdicts.jsonl`
|
|
448
|
+
y el ring lo lee en el siguiente brief o `summary`: si pasa, los bloques quedan en «hecho»; si no
|
|
449
|
+
pasa, vuelven a «en curso» con el feedback y el Tape como rebote, que el brief del siguiente luchador
|
|
450
|
+
trae al principio. Cada veredicto queda en `decisions.md` como decisión del juez (y en Engram).
|
|
451
|
+
|
|
440
452
|
**Memoria entre peleas (Engram, opcional)**: si en `engramUrl` (por default
|
|
441
453
|
`http://127.0.0.1:7437`) contesta un [Engram](https://github.com/Gentleman-Programming/engram)
|
|
442
454
|
(`engram serve`), los scripts le mandan lo que cada pelea aprendió: las faltas, las respuestas y
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thebattlearena/corner",
|
|
3
|
-
"version": "0.10.1-beta.
|
|
3
|
+
"version": "0.10.1-beta.80",
|
|
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": {
|
package/ring/scripts/brief.mjs
CHANGED
|
@@ -44,6 +44,8 @@ import {
|
|
|
44
44
|
} from './config.mjs';
|
|
45
45
|
import { recallSection } from './engram.mjs';
|
|
46
46
|
import { conceptsSinceRelay, relayDue } from './relay.mjs';
|
|
47
|
+
import { reviewTurn } from './review-blocks.mjs';
|
|
48
|
+
import { setPlanState } from './tech-plan-log.mjs';
|
|
47
49
|
import { loadRules } from './rule-ids.mjs';
|
|
48
50
|
|
|
49
51
|
const TEMPLATES = join(RING_ROOT, 'skills', 'referee', 'templates');
|
|
@@ -164,6 +166,20 @@ function boutVars({ dir, state, config }, values) {
|
|
|
164
166
|
};
|
|
165
167
|
}
|
|
166
168
|
|
|
169
|
+
/** The open rebounds the human's review sent back, as a section of the brief (none: nothing). */
|
|
170
|
+
function humanReview(dir) {
|
|
171
|
+
const path = join(dir, 'rebounds.md');
|
|
172
|
+
const open = (existsSync(path) ? readFileSync(path, 'utf8') : '')
|
|
173
|
+
.split('\n')
|
|
174
|
+
.filter((line) => /^- \[ \] .*Revisión humana/.test(line));
|
|
175
|
+
return open.length
|
|
176
|
+
? [
|
|
177
|
+
'## Sent back by the human review (PR Review Lab; fix these first, the Tape shows what failed)',
|
|
178
|
+
open.join('\n'),
|
|
179
|
+
]
|
|
180
|
+
: [];
|
|
181
|
+
}
|
|
182
|
+
|
|
167
183
|
const real = (path) => (existsSync(path) ? realpathSync(path) : path);
|
|
168
184
|
|
|
169
185
|
async function generate(kind, [pr, ...argv]) {
|
|
@@ -178,6 +194,17 @@ async function generate(kind, [pr, ...argv]) {
|
|
|
178
194
|
throw new Error(
|
|
179
195
|
`a relay is due (${conceptsSinceRelay(bout.state)} concepts since the last one): rewrite the card's «How to resume», run judges-card.mjs relay ${pr}, then brief the next wrestler`,
|
|
180
196
|
);
|
|
197
|
+
if (kind === 'wrestler') {
|
|
198
|
+
// The human's verdicts from PR Review Lab first: a fail comes back as a rebound for this brief.
|
|
199
|
+
const turn = reviewTurn(
|
|
200
|
+
bout.dir,
|
|
201
|
+
bout.config.humanReviewEvery,
|
|
202
|
+
(n, to, ref) => setPlanState(bout.dir, n, to, ref),
|
|
203
|
+
);
|
|
204
|
+
if (turn.applied.length)
|
|
205
|
+
console.error(`human review applied: ${turn.applied.join(' · ')}`);
|
|
206
|
+
if (turn.due) throw new Error(turn.due);
|
|
207
|
+
}
|
|
181
208
|
const own = values.vars ? JSON.parse(readFileSync(values.vars, 'utf8')) : {};
|
|
182
209
|
const roots = [
|
|
183
210
|
projectDir(),
|
|
@@ -220,6 +247,7 @@ async function generate(kind, [pr, ...argv]) {
|
|
|
220
247
|
'## Decisions already taken (excerpt: every foul, the ones on these files, the last 20)',
|
|
221
248
|
decisions,
|
|
222
249
|
`The whole ledger: \`${decisionsPath}\`.`,
|
|
250
|
+
...humanReview(bout.dir),
|
|
223
251
|
...(learned ? [learned] : []),
|
|
224
252
|
...(shape.rules ? ['---', rules.block] : []),
|
|
225
253
|
].join('\n\n');
|
package/ring/scripts/config.mjs
CHANGED
|
@@ -156,6 +156,10 @@ export const DEFAULTS = Object.freeze({
|
|
|
156
156
|
// Concepts after which the referee relays (rewrites the card's «How to resume» and runs
|
|
157
157
|
// judges-card.mjs relay) before brief.mjs briefs the next wrestler. null: never.
|
|
158
158
|
relayEvery: 4,
|
|
159
|
+
// The human review by blocks (review-blocks.mjs, PR Review Lab's bout mode): once this many
|
|
160
|
+
// blocks of the plan (points with a reviewed range) wait on the judge, brief.mjs briefs no
|
|
161
|
+
// wrestler until the human signs a verdict on that cut. null: no pause.
|
|
162
|
+
humanReviewEvery: 2,
|
|
159
163
|
// Engram (github.com/Gentleman-Programming/engram), the memory of what earlier bouts learned:
|
|
160
164
|
// fouls, the judge's verdicts, accepted precedents, X0 findings and each bout's status. Never the
|
|
161
165
|
// rules. The scripts sync and read it when something answers at this URL (null: off). The
|
|
@@ -115,7 +115,8 @@ import {
|
|
|
115
115
|
} from './preparation.mjs';
|
|
116
116
|
import { handOffWarning, withChronicleUrl } from './sparring-chronicle.mjs';
|
|
117
117
|
import { refereeRecall, syncAfter } from './engram.mjs';
|
|
118
|
-
import {
|
|
118
|
+
import { estimateFacts, reviewTurn } from './review-blocks.mjs';
|
|
119
|
+
import { parsePlan, PLAN, planItem } from './tech-plan.mjs';
|
|
119
120
|
import { addPlanItem, setPlanState } from './tech-plan-log.mjs';
|
|
120
121
|
import { logPageVerdicts } from './verdicts-log.mjs';
|
|
121
122
|
import { DEFAULT_VENUE } from './venues.mjs';
|
|
@@ -313,6 +314,22 @@ async function summary([pr]) {
|
|
|
313
314
|
contract: false,
|
|
314
315
|
venue: state.venue ?? DEFAULT_VENUE,
|
|
315
316
|
});
|
|
317
|
+
const review = reviewTurn(dir, config.humanReviewEvery, (n, to, ref) =>
|
|
318
|
+
setPlanState(dir, n, to, ref),
|
|
319
|
+
);
|
|
320
|
+
console.log(
|
|
321
|
+
[
|
|
322
|
+
'',
|
|
323
|
+
'## Human review by blocks (PR Review Lab, bout mode)',
|
|
324
|
+
review.applied.length
|
|
325
|
+
? `verdicts applied now: ${review.applied.join(' · ')}`
|
|
326
|
+
: 'no new verdicts',
|
|
327
|
+
review.waiting.length
|
|
328
|
+
? `waiting on the judge: ${review.waiting.map((item) => `B${item.n} «${item.title}»`).join(', ')}`
|
|
329
|
+
: 'no block waits on the judge',
|
|
330
|
+
review.due ?? '',
|
|
331
|
+
].join('\n'),
|
|
332
|
+
);
|
|
316
333
|
const learned = await refereeRecall(config, {
|
|
317
334
|
slug: repoSlug(projectDir()),
|
|
318
335
|
pr,
|
|
@@ -577,14 +594,29 @@ function plan([pr, target, ...rest]) {
|
|
|
577
594
|
: 'a point with that origin is already in the plan',
|
|
578
595
|
);
|
|
579
596
|
}
|
|
597
|
+
if (target === 'estimate') {
|
|
598
|
+
const [n] = rest;
|
|
599
|
+
const point = planItem(parsePlan(readFileSync(join(dir, PLAN), 'utf8')), n);
|
|
600
|
+
const estimated = setPlanState(dir, point.n, point.state, null, undefined, {
|
|
601
|
+
estimate: estimateFacts({
|
|
602
|
+
priority: value('--priority'),
|
|
603
|
+
minutes: value('--minutes'),
|
|
604
|
+
}),
|
|
605
|
+
});
|
|
606
|
+
return void console.log(
|
|
607
|
+
`B${estimated.n} «${estimated.title}»: priority ${estimated.facts.priority ?? '—'} · ${estimated.facts.estimateMin ?? '—'} min`,
|
|
608
|
+
);
|
|
609
|
+
}
|
|
580
610
|
const [state] = rest;
|
|
581
611
|
if (!target || !state)
|
|
582
612
|
throw new Error(
|
|
583
|
-
'usage: plan <pr> <n|title> <pendiente|espera al juez|en curso|hecho> [--ref <sha|round-bell>] | add … | list',
|
|
613
|
+
'usage: plan <pr> <n|title> <pendiente|espera al juez|en curso|hecho> [--ref <sha|round-bell>] [--range A..B] | estimate <n> --minutes <m> [--priority 1-4] | add … | list',
|
|
584
614
|
);
|
|
585
|
-
const changed = setPlanState(dir, target, state, value('--ref')
|
|
615
|
+
const changed = setPlanState(dir, target, state, value('--ref'), undefined, {
|
|
616
|
+
range: value('--range'),
|
|
617
|
+
});
|
|
586
618
|
console.log(
|
|
587
|
-
`${changed.n}. ${changed.title}: ${changed.from} → ${changed.to} at ${localStamp(new Date())}`,
|
|
619
|
+
`${changed.n}. ${changed.title}: ${changed.from} → ${changed.to} at ${localStamp(new Date())}${changed.facts.range && changed.to === 'espera al juez' ? ` · range ${changed.facts.range}` : ''}`,
|
|
588
620
|
);
|
|
589
621
|
}
|
|
590
622
|
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// The human review by blocks with PR Review Lab: each point of the technical plan is a block (B<n>
|
|
2
|
+
// is point n), with its priority, estimate, author and range in plan.jsonl; every `humanReviewEvery`
|
|
3
|
+
// blocks waiting on the judge, brief.mjs briefs no wrestler until the human reviews that cut in
|
|
4
|
+
// PR Review Lab and signs a verdict with a Tape. PR Review Lab appends the verdicts to
|
|
5
|
+
// `<bout dir>/review-verdicts.jsonl`; the ring reads them: a pass moves the blocks to «hecho», a
|
|
6
|
+
// fail sends them back with the feedback and the Tape as a rebound for the next wrestler.
|
|
7
|
+
// Zero-dependency.
|
|
8
|
+
import { execFileSync } from 'node:child_process';
|
|
9
|
+
import { appendFileSync, existsSync, readFileSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
|
|
12
|
+
import { readState, writeState } from './config.mjs';
|
|
13
|
+
import { parsePlan, parsePlanLog, PLAN, PLAN_LOG } from './tech-plan.mjs';
|
|
14
|
+
|
|
15
|
+
export const VERDICTS = 'review-verdicts.jsonl';
|
|
16
|
+
const WAITING = 'espera al juez';
|
|
17
|
+
const TAPE = /^https:\/\/(?:[\w-]+\.)?battlearena\.dev\/t\/[\w-]+$/;
|
|
18
|
+
|
|
19
|
+
/** What PR Review Lab reads of a block, as its last line of plan.jsonl carries it. */
|
|
20
|
+
const FACTS = [
|
|
21
|
+
'priority',
|
|
22
|
+
'estimateMin',
|
|
23
|
+
'email',
|
|
24
|
+
'range',
|
|
25
|
+
'startSha',
|
|
26
|
+
'startedAt',
|
|
27
|
+
'endedAt',
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/** The facts of point `n` so far: each field from the last plan.jsonl line that has it. PURE. */
|
|
31
|
+
export function blockFacts(log, n) {
|
|
32
|
+
return log
|
|
33
|
+
.filter((line) => line.n === n)
|
|
34
|
+
.reduce(
|
|
35
|
+
(facts, line) => ({
|
|
36
|
+
...facts,
|
|
37
|
+
...Object.fromEntries(
|
|
38
|
+
FACTS.filter((key) => line[key] != null).map((key) => [
|
|
39
|
+
key,
|
|
40
|
+
line[key],
|
|
41
|
+
]),
|
|
42
|
+
),
|
|
43
|
+
}),
|
|
44
|
+
{},
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The facts a state change adds: entering «en curso» stamps the start (the first time: a block sent
|
|
50
|
+
* back keeps its start, so its next review shows the whole block), and «espera al juez» the end and
|
|
51
|
+
* the range from that start to the worktree's head. PURE.
|
|
52
|
+
*/
|
|
53
|
+
export function factsForState(previous, to, { at, head, email, range }) {
|
|
54
|
+
if (to === 'en curso')
|
|
55
|
+
return {
|
|
56
|
+
startedAt: previous.startedAt ?? at,
|
|
57
|
+
startSha: previous.startSha ?? head,
|
|
58
|
+
email: previous.email ?? email,
|
|
59
|
+
// Back at work: no end and no range until it waits on the judge again.
|
|
60
|
+
endedAt: null,
|
|
61
|
+
range: null,
|
|
62
|
+
};
|
|
63
|
+
if (to === WAITING) {
|
|
64
|
+
const from = previous.startSha;
|
|
65
|
+
return {
|
|
66
|
+
endedAt: at,
|
|
67
|
+
range: range ?? (from && head ? `${from}..${head}` : null),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Checks an estimate: priority 1–4 and whole minutes. PURE. */
|
|
74
|
+
export function estimateFacts({ priority, minutes }) {
|
|
75
|
+
const facts = {};
|
|
76
|
+
if (priority != null) {
|
|
77
|
+
if (![1, 2, 3, 4].includes(Number(priority)))
|
|
78
|
+
throw new Error('--priority is 1 (first) to 4 (last)');
|
|
79
|
+
facts.priority = Number(priority);
|
|
80
|
+
}
|
|
81
|
+
if (minutes != null) {
|
|
82
|
+
if (!/^\d+$/.test(String(minutes)) || Number(minutes) < 1)
|
|
83
|
+
throw new Error('--minutes is a whole number of minutes');
|
|
84
|
+
facts.estimateMin = Number(minutes);
|
|
85
|
+
}
|
|
86
|
+
if (!Object.keys(facts).length)
|
|
87
|
+
throw new Error(
|
|
88
|
+
'usage: plan <pr> estimate <n> --minutes <m> [--priority 1-4]',
|
|
89
|
+
);
|
|
90
|
+
return facts;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The verdicts PR Review Lab signed, in order; a torn line is skipped. PURE. */
|
|
94
|
+
export function parseVerdicts(text) {
|
|
95
|
+
return parsePlanLog(text).filter(
|
|
96
|
+
(verdict) =>
|
|
97
|
+
['pass', 'fail'].includes(verdict.verdict) &&
|
|
98
|
+
Array.isArray(verdict.blocks) &&
|
|
99
|
+
verdict.at,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** `["B3", "B4"]` (or numbers) as point numbers. PURE. */
|
|
104
|
+
export const blockNumbers = (blocks) =>
|
|
105
|
+
blocks.map((block) => Number(/\d+/.exec(String(block))?.[0])).filter(Boolean);
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The blocks waiting for the human: in «espera al juez», with a range to review, and no verdict
|
|
109
|
+
* signed after the block's last change. PURE.
|
|
110
|
+
*/
|
|
111
|
+
export function blocksAwaitingReview(plan, log, verdicts) {
|
|
112
|
+
return parsePlan(plan)
|
|
113
|
+
.groups.flatMap((group) => group.items)
|
|
114
|
+
.filter((item) => item.state === WAITING)
|
|
115
|
+
.filter((item) => blockFacts(log, item.n).range)
|
|
116
|
+
.filter((item) => {
|
|
117
|
+
const last = log.filter((line) => line.n === item.n).at(-1)?.at ?? '';
|
|
118
|
+
return !verdicts.some(
|
|
119
|
+
(verdict) =>
|
|
120
|
+
blockNumbers(verdict.blocks).includes(item.n) && verdict.at > last,
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Why the next wrestler waits for the human's review, or null. PURE. */
|
|
126
|
+
export function reviewDue(waiting, every) {
|
|
127
|
+
if (!every || waiting.length < every) return null;
|
|
128
|
+
return `the human review is due: ${waiting.map((item) => `B${item.n} «${item.title}»`).join(', ')} wait on the judge (humanReviewEvery ${every}). The bell rang in PR Review Lab (bout mode): the human reviews that cut in one diff and signs a verdict with a Tape; brief.mjs reads it and goes on`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* What a verdict does to the bout: the plan state of each block, the decisions line (the judge's
|
|
133
|
+
* verdict, which Engram learns) and, for a fail, the rebound with the feedback and the Tape. PURE.
|
|
134
|
+
*/
|
|
135
|
+
export function verdictEffects(verdict) {
|
|
136
|
+
const blocks = blockNumbers(verdict.blocks);
|
|
137
|
+
const names = blocks.map((n) => `B${n}`).join(' + ');
|
|
138
|
+
const pass = verdict.verdict === 'pass';
|
|
139
|
+
if (!TAPE.test(String(verdict.tape ?? '')) || !verdict.feedback?.trim())
|
|
140
|
+
throw new Error(
|
|
141
|
+
`the verdict of ${names} at ${verdict.at} has no Tape or no feedback: PR Review Lab must not write it without both`,
|
|
142
|
+
);
|
|
143
|
+
const who = verdict.reviewer ? ` (${verdict.reviewer})` : '';
|
|
144
|
+
return {
|
|
145
|
+
blocks,
|
|
146
|
+
state: pass ? 'hecho' : 'en curso',
|
|
147
|
+
decision: `revisión humana de ${names}${who}: ${pass ? 'pasa' : 'no pasa'} · ${verdict.feedback.trim()} · Tape ${verdict.tape} · ${verdict.range ?? ''}`,
|
|
148
|
+
rebound: pass
|
|
149
|
+
? null
|
|
150
|
+
: `Revisión humana de ${names}${who} no pasa (Tape ${verdict.tape}, rango ${verdict.range ?? 'del corte'}): ${verdict.feedback.trim()}`,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const read = (path) => (existsSync(path) ? readFileSync(path, 'utf8') : '');
|
|
155
|
+
|
|
156
|
+
/** The author of a block: the git user of the worktree that writes it. */
|
|
157
|
+
export function authorEmail(worktree) {
|
|
158
|
+
try {
|
|
159
|
+
return (
|
|
160
|
+
execFileSync('git', ['-C', worktree, 'config', 'user.email'], {
|
|
161
|
+
encoding: 'utf8',
|
|
162
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
163
|
+
}).trim() || null
|
|
164
|
+
);
|
|
165
|
+
} catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** A bout's plan, its log and PR Review Lab's verdicts, as read now. */
|
|
171
|
+
export function reviewInputs(dir) {
|
|
172
|
+
return {
|
|
173
|
+
plan: read(join(dir, PLAN)),
|
|
174
|
+
log: parsePlanLog(read(join(dir, PLAN_LOG))),
|
|
175
|
+
verdicts: parseVerdicts(read(join(dir, VERDICTS))),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Applies the verdicts the bout has not handled yet (state.handledVerdicts holds their `at`):
|
|
181
|
+
* `setState(n, state, ref)` moves each block, and the decision and the rebound are appended.
|
|
182
|
+
* Returns the state to write and what was applied.
|
|
183
|
+
*/
|
|
184
|
+
export function applyVerdicts(dir, state, setState) {
|
|
185
|
+
const handled = new Set(state.handledVerdicts ?? []);
|
|
186
|
+
const fresh = reviewInputs(dir).verdicts.filter(
|
|
187
|
+
(verdict) => !handled.has(verdict.at),
|
|
188
|
+
);
|
|
189
|
+
const nextId = () =>
|
|
190
|
+
Math.max(
|
|
191
|
+
0,
|
|
192
|
+
...[...read(join(dir, 'decisions.md')).matchAll(/ · D(\d+) · /g)].map(
|
|
193
|
+
(match) => Number(match[1]),
|
|
194
|
+
),
|
|
195
|
+
) + 1;
|
|
196
|
+
const applied = fresh.map((verdict) => {
|
|
197
|
+
const effects = verdictEffects(verdict);
|
|
198
|
+
for (const n of effects.blocks)
|
|
199
|
+
setState(n, effects.state, `review ${verdict.tape}`);
|
|
200
|
+
appendFileSync(
|
|
201
|
+
join(dir, 'decisions.md'),
|
|
202
|
+
`- ${new Date().toISOString()} · D${nextId()} · judge · ${effects.decision}\n`,
|
|
203
|
+
);
|
|
204
|
+
if (effects.rebound)
|
|
205
|
+
appendFileSync(
|
|
206
|
+
join(dir, 'rebounds.md'),
|
|
207
|
+
`- [ ] ${new Date().toISOString()} · ${effects.rebound}\n`,
|
|
208
|
+
);
|
|
209
|
+
handled.add(verdict.at);
|
|
210
|
+
return `${verdict.verdict} ${effects.blocks.map((n) => `B${n}`).join('+')} (${verdict.at})`;
|
|
211
|
+
});
|
|
212
|
+
return {
|
|
213
|
+
state: applied.length ? { ...state, handledVerdicts: [...handled] } : state,
|
|
214
|
+
applied,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The review turn of a bout, before a wrestler is briefed or the referee resumes: applies the new
|
|
220
|
+
* verdicts, keeps `pauseEvery` in state.json for PR Review Lab, and says whether the human's review
|
|
221
|
+
* is due. `setState(n, state, ref)` moves a block of the plan.
|
|
222
|
+
*/
|
|
223
|
+
export function reviewTurn(dir, every, setState) {
|
|
224
|
+
const before = readState(dir);
|
|
225
|
+
const { state, applied } = applyVerdicts(dir, before, setState);
|
|
226
|
+
const next = { ...state, pauseEvery: every ?? null };
|
|
227
|
+
if (applied.length || before.pauseEvery !== next.pauseEvery)
|
|
228
|
+
writeState(dir, next);
|
|
229
|
+
const { plan, log, verdicts } = reviewInputs(dir);
|
|
230
|
+
const waiting = blocksAwaitingReview(plan, log, verdicts);
|
|
231
|
+
return { applied, waiting, due: reviewDue(waiting, every) };
|
|
232
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Reads and writes a bout's technical plan (tech-plan.mjs): `judges-card.mjs plan` changes a
|
|
2
2
|
// point's state or adds one, `page-verdicts` adds each judge's «se mejora», and `play-by-play.mjs
|
|
3
3
|
// embed --plan` puts the plan into the chronicle. Zero-dependency.
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
4
5
|
import {
|
|
5
6
|
appendFileSync,
|
|
6
7
|
existsSync,
|
|
@@ -11,6 +12,7 @@ import { join } from 'node:path';
|
|
|
11
12
|
|
|
12
13
|
import { readState } from './config.mjs';
|
|
13
14
|
import { scriptJson } from './contract-endpoints.mjs';
|
|
15
|
+
import { authorEmail, blockFacts, factsForState } from './review-blocks.mjs';
|
|
14
16
|
import {
|
|
15
17
|
parsePlan,
|
|
16
18
|
parsePlanLog,
|
|
@@ -32,18 +34,48 @@ export function setPlanState(
|
|
|
32
34
|
state,
|
|
33
35
|
ref = null,
|
|
34
36
|
at = new Date().toISOString(),
|
|
37
|
+
{ range = null, estimate = null } = {},
|
|
35
38
|
) {
|
|
36
39
|
const file = join(dir, PLAN);
|
|
37
40
|
const markdown = read(file);
|
|
38
41
|
if (markdown === null) throw new Error(`no ${PLAN} in ${dir}`);
|
|
39
42
|
const item = planItem(parsePlan(markdown), target);
|
|
40
43
|
const next = planState(state);
|
|
44
|
+
// Each line carries every fact of the block so far (PR Review Lab reads the last one): the
|
|
45
|
+
// priority and estimate, and the start, end, author and range a state change stamps.
|
|
46
|
+
const previous = blockFacts(parsePlanLog(read(join(dir, PLAN_LOG))), item.n);
|
|
47
|
+
const { worktree } = readState(dir);
|
|
48
|
+
const facts = {
|
|
49
|
+
...previous,
|
|
50
|
+
...(estimate ?? {}),
|
|
51
|
+
// Only a change of state stamps a start or an end: an estimate keeps the block's range.
|
|
52
|
+
...(next === item.state && !range
|
|
53
|
+
? {}
|
|
54
|
+
: factsForState(previous, next, {
|
|
55
|
+
at,
|
|
56
|
+
head: worktreeHead(worktree),
|
|
57
|
+
email: authorEmail(worktree),
|
|
58
|
+
range,
|
|
59
|
+
})),
|
|
60
|
+
};
|
|
41
61
|
writeFileSync(file, withPlanState(markdown, item, next));
|
|
42
62
|
appendFileSync(
|
|
43
63
|
join(dir, PLAN_LOG),
|
|
44
|
-
`${JSON.stringify({ at, n: item.n, title: item.title, from: item.state, to: next, ref })}\n`,
|
|
64
|
+
`${JSON.stringify({ at, n: item.n, title: item.title, from: item.state, to: next, ref, ...Object.fromEntries(Object.entries(facts).filter(([, value]) => value != null)) })}\n`,
|
|
45
65
|
);
|
|
46
|
-
return { ...item, from: item.state, to: next };
|
|
66
|
+
return { ...item, from: item.state, to: next, facts };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The worktree's head, or null when it is gone. */
|
|
70
|
+
function worktreeHead(worktree) {
|
|
71
|
+
try {
|
|
72
|
+
return execFileSync('git', ['-C', worktree, 'rev-parse', 'HEAD'], {
|
|
73
|
+
encoding: 'utf8',
|
|
74
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
75
|
+
}).trim();
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
47
79
|
}
|
|
48
80
|
|
|
49
81
|
/** Adds a point to the plan (creating the file when it does not exist); returns whether it was new. */
|
|
@@ -210,6 +210,14 @@ way out, and it refuses without a KO. Then the bout runs as always, with three i
|
|
|
210
210
|
concepts (4 by default) you relay first: rewrite the card's «How to resume» (where the bout is,
|
|
211
211
|
what is next, what waits on the human) and run `judges-card.mjs relay <pr>`; until then
|
|
212
212
|
`brief.mjs` briefs no wrestler. That is what a compaction or a new session resumes from.
|
|
213
|
+
**Human review by blocks**: each point of the technical plan is a block. Give each its estimate
|
|
214
|
+
and priority when the plan is made (`judges-card.mjs plan <pr> estimate <n> --minutes <m>
|
|
215
|
+
--priority <1-4>`), move it to «en curso» when its wrestler starts and to «espera al juez» when
|
|
216
|
+
its review passes (that stamps the author, the times and the range PR Review Lab reviews). Every
|
|
217
|
+
`humanReviewEvery` blocks waiting, brief.mjs stops: the human reviews that cut in PR Review Lab
|
|
218
|
+
(bout mode) and signs a verdict with a Tape. The next brief applies it: a pass closes the
|
|
219
|
+
blocks, a fail comes back as a rebound the next wrestler fixes first. Do not work around the
|
|
220
|
+
pause.
|
|
213
221
|
If the wrestler claimed something ("does not change counter X"), hand the claim to the
|
|
214
222
|
cornerman to verify against the base. The cornerman always checks the format of the range
|
|
215
223
|
(`format-gate.mjs check`); an unclean file is a finding like any other, fixed by the wrestler
|