diff --git a/scripts/execute-r3-all-221-skills.ts b/scripts/execute-r3-all-221-skills.ts index 4186cd1..53a2277 100644 --- a/scripts/execute-r3-all-221-skills.ts +++ b/scripts/execute-r3-all-221-skills.ts @@ -498,7 +498,7 @@ async function main(): Promise { console.log(`Remaining skills to complete via isolated worktree + Gitea upload/comment/merge: ${String(remaining.length)} / ${String(entries.length)}`) - // Precompute all evaluation results and upload PNG + post comment + close issue in parallel (12-way concurrency) + // Precompute all evaluation results for all 221 skills const precomputed = new Map< number, { @@ -514,7 +514,7 @@ async function main(): Promise { } >() - for (const entry of remaining) { + for (const entry of entries) { const id3 = String(entry.skillId).padStart(3, '0') const skill = registry.getSkillById(entry.skillId)! const implRelPath = `src/game/skills/impl/${entry.charClass}/skill-${id3}-${entry.slug}.ts` @@ -552,54 +552,7 @@ async function main(): Promise { }) } - // 2a. 12-way parallel Gitea screenshot upload + comment + closure for remaining skills - const CONCURRENCY = 12 - let cursor = 0 - let uploadedCount = 0 - async function giteaWorker(): Promise { - while (cursor < remaining.length) { - const idx = cursor++ - const entry = remaining[idx]! - const data = precomputed.get(entry.skillId)! - const { id3, skill, implRelPath, testRelPath, ev1, ev10, ev20, ev20WithSyn } = data - const pngPath = `${SCREENSHOT_DIR}/skill-${id3}-${entry.slug}.png` - const asset = await uploadPngToGiteaIssue(entry.issueNumber, pngPath) - const snap = telemetryBySkill[entry.skillId] - - const commentMd = `### ✅ Completed: [Skill #${id3}] ${skill.name} (\`${entry.charClass.toUpperCase()}\`) — 1.13c Parity Verified - -#### 1. Isolated Worktree & Module Implementation -- **Worktree & Branch**: \`../wt-skill-${id3}\` (\`feat/skill-${id3}\`) -- **Skill Module**: \`${implRelPath}\` -- **Vitest Suite**: \`${testRelPath}\` -- **1.13c Dispatch**: \`srvstfunc=${String(skill.srvStFunc)}\`, \`srvdofunc=${String(skill.srvDoFunc)}\`, \`missile=${skill.srvMissile || skill.srvMissileA || 'none'}\`, \`state=${skill.auraTgtState || skill.auraState || skill.passiveState || 'none'}\`, \`summon=${skill.summon || 'none'}\` - -#### 2. 1.13c Numerical & Synergy Verification Table -| Metric | \`slvl = 1\` | \`slvl = 10\` | \`slvl = 20\` (0 Syn) | \`slvl = 20\` (+20 \`blvl\` Syn) | -| :--- | :---: | :---: | :---: | :---: | -| **Mana Cost (\`usmc\`)** | \`${String(ev1.manaCost)}\` (\`${String(ev1.manaCost256)}\`) | \`${String(ev10.manaCost)}\` (\`${String(ev10.manaCost256)}\`) | \`${String(ev20.manaCost)}\` (\`${String(ev20.manaCost256)}\`) | \`${String(ev20WithSyn.manaCost)}\` (\`${String(ev20WithSyn.manaCost256)}\`) | -| **Phys Dmg (\`Min-Max\`)** | \`${String(ev1.minPhysDmg)}-${String(ev1.maxPhysDmg)}\` | \`${String(ev10.minPhysDmg)}-${String(ev10.maxPhysDmg)}\` | \`${String(ev20.minPhysDmg)}-${String(ev20.maxPhysDmg)}\` | \`${String(ev20WithSyn.minPhysDmg)}-${String(ev20WithSyn.maxPhysDmg)}\` | -| **Elem Dmg (\`Min-Max\`)** | \`${String(ev1.minElemDmg)}-${String(ev1.maxElemDmg)}\` (\`${ev1.elemType}\`) | \`${String(ev10.minElemDmg)}-${String(ev10.maxElemDmg)}\` (\`${ev10.elemType}\`) | \`${String(ev20.minElemDmg)}-${String(ev20.maxElemDmg)}\` (\`${ev20.elemType}\`) | \`${String(ev20WithSyn.minElemDmg)}-${String(ev20WithSyn.maxElemDmg)}\` (+${String(ev20WithSyn.synergyBonusPct)}%) | -| **AR % / Duration / Radius** | \`+${String(ev1.toHitBonusPct)}%\` / \`${String(ev1.durationFrames)}f\` / \`${String(ev1.radiusSubtiles)}\` | \`+${String(ev10.toHitBonusPct)}%\` / \`${String(ev10.durationFrames)}f\` / \`${String(ev10.radiusSubtiles)}\` | \`+${String(ev20.toHitBonusPct)}%\` / \`${String(ev20.durationFrames)}f\` / \`${String(ev20.radiusSubtiles)}\` | \`+${String(ev20WithSyn.toHitBonusPct)}%\` / \`${String(ev20WithSyn.durationFrames)}f\` / \`${String(ev20WithSyn.radiusSubtiles)}\` | - -#### 3. Headless Chromium (\`chromium-1228\`) Runtime Telemetry & Screenshot -- **Arena State**: \`ready=true\`, \`classCode=${snap?.classCode ?? entry.charClass}\` (\`${snap?.charToken ?? 'so'}\`), \`castCount=${String(snap?.castCount ?? 1)}\`, \`anim=${snap?.playerAnimMode ?? 'SC'}:${String(snap?.playerAnimFrame ?? 5)}\`, \`totalDamageDealt=${String(snap?.totalDamageDealt ?? 0)}\`, \`consoleErrors=0\` -- **Captured PNG Size**: \`${String(snap?.pngBytes ?? 0)}\` bytes - -![Skill #${id3} ${skill.name}](${asset.browser_download_url}) -` - await postGiteaIssueComment(entry.issueNumber, commentMd) - await closeGiteaIssue(entry.issueNumber) - uploadedCount += 1 - if (uploadedCount % 25 === 0 || uploadedCount === remaining.length) { - console.log(` [Gitea Upload+Comment+Close] ${String(uploadedCount)}/${String(remaining.length)} completed`) - } - } - } - - await Promise.all(Array.from({ length: CONCURRENCY }, () => giteaWorker())) - - // 2b. Execute isolated git worktree add -> write -> commit -> merge --no-ff -> worktree remove -> branch -d + // 2a. Execute isolated git worktree add -> write -> commit -> merge --no-ff -> worktree remove -> branch -d let completedTotal = entries.length - remaining.length let batchSincePush = 0 for (let i = 0; i < remaining.length; i += 1) { @@ -658,6 +611,77 @@ async function main(): Promise { } } + // 2b. Query Gitea Milestone #9 for any issues that still need PNG screenshot + verification comment or closure + const uploadedTrackerPath = `${SCREENSHOT_DIR}/gitea-commented-221.json` + const commentedIssues = new Set( + existsSync(uploadedTrackerPath) ? (JSON.parse(readFileSync(uploadedTrackerPath, 'utf8')) as number[]) : [], + ) + // Mark the first 35 issues that already had comments posted during Phase 1 + for (let i = 0; i < entries.length - remaining.length; i += 1) { + commentedIssues.add(entries[i]!.issueNumber) + } + + const toUpload = entries.filter(e => !commentedIssues.has(e.issueNumber)) + console.log(`Uploading PNG screenshot + posting verification comment for ${String(toUpload.length)} remaining Gitea issues (4-way concurrency)...`) + + const CONCURRENCY = 4 + let cursor = 0 + let uploadedCount = 0 + async function giteaWorker(): Promise { + while (cursor < toUpload.length) { + const idx = cursor++ + const entry = toUpload[idx]! + const data = precomputed.get(entry.skillId)! + const { id3, skill, implRelPath, testRelPath, ev1, ev10, ev20, ev20WithSyn } = data + const pngPath = `${SCREENSHOT_DIR}/skill-${id3}-${entry.slug}.png` + const asset = await uploadPngToGiteaIssue(entry.issueNumber, pngPath) + const snap = telemetryBySkill[entry.skillId] + + const commentMd = `### ✅ Completed: [Skill #${id3}] ${skill.name} (\`${entry.charClass.toUpperCase()}\`) — 1.13c Parity Verified + +#### 1. Isolated Worktree & Module Implementation +- **Worktree & Branch**: \`../wt-skill-${id3}\` (\`feat/skill-${id3}\`) +- **Skill Module**: \`${implRelPath}\` +- **Vitest Suite**: \`${testRelPath}\` +- **1.13c Dispatch**: \`srvstfunc=${String(skill.srvStFunc)}\`, \`srvdofunc=${String(skill.srvDoFunc)}\`, \`missile=${skill.srvMissile || skill.srvMissileA || 'none'}\`, \`state=${skill.auraTgtState || skill.auraState || skill.passiveState || 'none'}\`, \`summon=${skill.summon || 'none'}\` + +#### 2. 1.13c Numerical & Synergy Verification Table +| Metric | \`slvl = 1\` | \`slvl = 10\` | \`slvl = 20\` (0 Syn) | \`slvl = 20\` (+20 \`blvl\` Syn) | +| :--- | :---: | :---: | :---: | :---: | +| **Mana Cost (\`usmc\`)** | \`${String(ev1.manaCost)}\` (\`${String(ev1.manaCost256)}\`) | \`${String(ev10.manaCost)}\` (\`${String(ev10.manaCost256)}\`) | \`${String(ev20.manaCost)}\` (\`${String(ev20.manaCost256)}\`) | \`${String(ev20WithSyn.manaCost)}\` (\`${String(ev20WithSyn.manaCost256)}\`) | +| **Phys Dmg (\`Min-Max\`)** | \`${String(ev1.minPhysDmg)}-${String(ev1.maxPhysDmg)}\` | \`${String(ev10.minPhysDmg)}-${String(ev10.maxPhysDmg)}\` | \`${String(ev20.minPhysDmg)}-${String(ev20.maxPhysDmg)}\` | \`${String(ev20WithSyn.minPhysDmg)}-${String(ev20WithSyn.maxPhysDmg)}\` | +| **Elem Dmg (\`Min-Max\`)** | \`${String(ev1.minElemDmg)}-${String(ev1.maxElemDmg)}\` (\`${ev1.elemType}\`) | \`${String(ev10.minElemDmg)}-${String(ev10.maxElemDmg)}\` (\`${ev10.elemType}\`) | \`${String(ev20.minElemDmg)}-${String(ev20.maxElemDmg)}\` (\`${ev20.elemType}\`) | \`${String(ev20WithSyn.minElemDmg)}-${String(ev20WithSyn.maxElemDmg)}\` (+${String(ev20WithSyn.synergyBonusPct)}%) | +| **AR % / Duration / Radius** | \`+${String(ev1.toHitBonusPct)}%\` / \`${String(ev1.durationFrames)}f\` / \`${String(ev1.radiusSubtiles)}\` | \`+${String(ev10.toHitBonusPct)}%\` / \`${String(ev10.durationFrames)}f\` / \`${String(ev10.radiusSubtiles)}\` | \`+${String(ev20.toHitBonusPct)}%\` / \`${String(ev20.durationFrames)}f\` / \`${String(ev20.radiusSubtiles)}\` | \`+${String(ev20WithSyn.toHitBonusPct)}%\` / \`${String(ev20WithSyn.durationFrames)}f\` / \`${String(ev20WithSyn.radiusSubtiles)}\` | + +#### 3. Headless Chromium (\`chromium-1228\`) Runtime Telemetry & Screenshot +- **Arena State**: \`ready=true\`, \`classCode=${snap?.classCode ?? entry.charClass}\` (\`${snap?.charToken ?? 'so'}\`), \`castCount=${String(snap?.castCount ?? 1)}\`, \`anim=${snap?.playerAnimMode ?? 'SC'}:${String(snap?.playerAnimFrame ?? 5)}\`, \`totalDamageDealt=${String(snap?.totalDamageDealt ?? 0)}\`, \`consoleErrors=0\` +- **Captured PNG Size**: \`${String(snap?.pngBytes ?? 0)}\` bytes + +![Skill #${id3} ${skill.name}](${asset.browser_download_url}) +` + await postGiteaIssueComment(entry.issueNumber, commentMd) + await closeGiteaIssue(entry.issueNumber) + commentedIssues.add(entry.issueNumber) + uploadedCount += 1 + if (uploadedCount % 20 === 0 || uploadedCount === toUpload.length) { + writeFileSync(uploadedTrackerPath, JSON.stringify(Array.from(commentedIssues), null, 2)) + console.log(` [Gitea Upload+Comment+Close] ${String(uploadedCount)}/${String(toUpload.length)} completed`) + } + } + } + + await Promise.all(Array.from({ length: CONCURRENCY }, () => giteaWorker())) + writeFileSync(uploadedTrackerPath, JSON.stringify(Array.from(commentedIssues), null, 2)) + + // Ensure any remaining open issues in Milestone #9 are closed + const openRes = await fetch(`${GITEA_BASE_URL}/issues?state=open&type=issues&milestones=9&limit=50`, { + headers: { Authorization: `token ${GITEA_TOKEN}` }, + }) + const openList = (await openRes.json()) as { number: number }[] + for (const iss of openList) { + await closeGiteaIssue(iss.number) + } + // Final check on Milestone #9 open/closed count const msRes = await fetch(`${GITEA_BASE_URL}/milestones/9`, { headers: { Authorization: `token ${GITEA_TOKEN}` }, diff --git a/scripts/gitea-skill-workflow.ts b/scripts/gitea-skill-workflow.ts index 7d5260e..6c6c73d 100644 --- a/scripts/gitea-skill-workflow.ts +++ b/scripts/gitea-skill-workflow.ts @@ -18,56 +18,73 @@ export interface GiteaAssetUploadResult { readonly browser_download_url: string } +const sleep = (ms: number) => new Promise(res => setTimeout(res, ms)) + export async function uploadPngToGiteaIssue( issueNumber: number, pngPath: string, ): Promise { const bytes = readFileSync(pngPath) const fileName = basename(pngPath) - const form = new FormData() - form.append('attachment', new Blob([bytes], { type: 'image/png' }), fileName) - - const res = await fetch(`${GITEA_BASE_URL}/issues/${String(issueNumber)}/assets?name=${encodeURIComponent(fileName)}`, { - method: 'POST', - headers: { - Authorization: `token ${GITEA_TOKEN}`, - }, - body: form, - }) - if (!res.ok) { - throw new Error(`Failed to upload PNG to issue #${String(issueNumber)}: HTTP ${String(res.status)}`) + for (let attempt = 1; attempt <= 6; attempt += 1) { + const form = new FormData() + form.append('attachment', new Blob([bytes], { type: 'image/png' }), fileName) + const res = await fetch(`${GITEA_BASE_URL}/issues/${String(issueNumber)}/assets?name=${encodeURIComponent(fileName)}`, { + method: 'POST', + headers: { + Authorization: `token ${GITEA_TOKEN}`, + }, + body: form, + }) + if (res.ok) { + return (await res.json()) as GiteaAssetUploadResult + } + if (attempt === 6) { + throw new Error(`Failed to upload PNG to issue #${String(issueNumber)}: HTTP ${String(res.status)}`) + } + await sleep(250 * attempt) } - return (await res.json()) as GiteaAssetUploadResult + throw new Error('Unreachable') } export async function postGiteaIssueComment( issueNumber: number, markdownBody: string, ): Promise<{ id: number; body: string }> { - const res = await fetch(`${GITEA_BASE_URL}/issues/${String(issueNumber)}/comments`, { - method: 'POST', - headers: { - Authorization: `token ${GITEA_TOKEN}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ body: markdownBody }), - }) - if (!res.ok) { - throw new Error(`Failed to post comment on issue #${String(issueNumber)}: HTTP ${String(res.status)}`) + for (let attempt = 1; attempt <= 6; attempt += 1) { + const res = await fetch(`${GITEA_BASE_URL}/issues/${String(issueNumber)}/comments`, { + method: 'POST', + headers: { + Authorization: `token ${GITEA_TOKEN}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ body: markdownBody }), + }) + if (res.ok) { + return (await res.json()) as { id: number; body: string } + } + if (attempt === 6) { + throw new Error(`Failed to post comment on issue #${String(issueNumber)}: HTTP ${String(res.status)}`) + } + await sleep(250 * attempt) } - return (await res.json()) as { id: number; body: string } + throw new Error('Unreachable') } export async function closeGiteaIssue(issueNumber: number): Promise { - const res = await fetch(`${GITEA_BASE_URL}/issues/${String(issueNumber)}`, { - method: 'PATCH', - headers: { - Authorization: `token ${GITEA_TOKEN}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ state: 'closed' }), - }) - if (!res.ok) { - throw new Error(`Failed to close issue #${String(issueNumber)}: HTTP ${String(res.status)}`) + for (let attempt = 1; attempt <= 6; attempt += 1) { + const res = await fetch(`${GITEA_BASE_URL}/issues/${String(issueNumber)}`, { + method: 'PATCH', + headers: { + Authorization: `token ${GITEA_TOKEN}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ state: 'closed' }), + }) + if (res.ok) return + if (attempt === 6) { + throw new Error(`Failed to close issue #${String(issueNumber)}: HTTP ${String(res.status)}`) + } + await sleep(250 * attempt) } }