diff --git a/.gitignore b/.gitignore index e451b87..b2c93ec 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,10 @@ __pycache__/ *.pyc analyses/ fix-snapshots/ +fix-queue.json +fix-approval-history.json +fix-webhook.json +test-results/ +test-results.json +junit.xml +playwright-report/ diff --git a/js/fix-review.js b/js/fix-review.js index bfc92ba..e7b6a55 100644 --- a/js/fix-review.js +++ b/js/fix-review.js @@ -1,6 +1,7 @@ const fixReview = { pollInterval: null, currentFixId: null, + _notifiedIds: new Set(), toggle() { const panel = document.getElementById('fix-review-panel'); @@ -19,31 +20,83 @@ const fixReview = { if (!fixes.length) { body.innerHTML = '
No fixes to review
'; document.getElementById('fix-review-badge').classList.remove('has-fixes'); + document.getElementById('fix-review-badge').textContent = 'Fix Review'; return; } document.getElementById('fix-review-badge').classList.add('has-fixes'); document.getElementById('fix-review-badge').textContent = `Fix Review (${fixes.length})`; + // Send desktop notifications for newly ready fixes + fixes.forEach(fix => { + if (fix.status === 'ready' && !this._notifiedIds.has(fix.fixId)) { + this._notifiedIds.add(fix.fixId); + this._notify(fix); + } + }); + + // Group fixes by issue class for batch actions + const byClass = {}; + fixes.forEach(fix => { + const cls = fix.issueClass || '__unknown__'; + (byClass[cls] = byClass[cls] || []).push(fix); + }); + let html = ''; + // Batch action bar when multiple fixes share a class + for (const [cls, group] of Object.entries(byClass)) { + if (group.length > 1 && cls !== '__unknown__') { + html += `
+ ${this._escHtml(cls)} + ${group.length} fixes + + +
`; + } + } + for (const fix of fixes) { - html += `
`; - html += `
${fix.description}
`; + html += `
`; + html += `
${this._escHtml(fix.description)}
`; + + // Issue class + confidence badge + if (fix.issueClass || fix.confidence !== undefined) { + html += `
`; + if (fix.issueClass) { + html += `${this._escHtml(fix.issueClass)}`; + } + const conf = typeof fix.confidence === 'number' ? fix.confidence : 0; + const confClass = conf >= 0.7 ? 'confidence-high' : 'confidence-low'; + const confLabel = conf >= 0.7 ? 'High' : 'Low'; + html += `${confLabel} ${(conf * 100).toFixed(0)}%`; + html += `
`; + } if (fix.status === 'running') { html += `
Session running... after screenshot pending
`; } else if (fix.hasBeforeAfter) { html += await this.renderDiff(fix.fixId); } else if (fix.status === 'ready') { - html += `
Screenshots captured (partial)
`; + html += `
Screenshots captured
`; } + // Code diff section (lazy-loaded) + html += `
+ + +
`; + html += `
`; html += ``; html += ``; html += ``; + html += ``; html += `
`; } + + // Graduation stats section + html += await this.renderGraduationStats(); + body.innerHTML = html; } catch (e) { console.warn('[fix-review] refresh error:', e); @@ -59,7 +112,7 @@ const fixReview = { const beforeFile = data.beforePath.split('/').pop(); const afterFile = data.afterPath.split('/').pop(); - return `
+ let html = `
Before @@ -69,15 +122,181 @@ const fixReview = { After
`; + + // Show graduation button if class qualifies + if (data.issueClass && data.canGraduate) { + html += `
+ 🎓 Class ${this._escHtml(data.issueClass)} is ready to graduate + (${(data.graduationRate * 100).toFixed(0)}% approval over ${data.graduationTotal} fixes) + +
`; + } + return html; } catch { return ''; } }, - resolve(fixId, outcome) { + async loadDiff(fixId) { + const wrapper = document.getElementById(`diff-${fixId}`); + const content = document.getElementById(`diff-content-${fixId}`); + if (!wrapper || !content) return; + + try { + const resp = await fetch(`/api/fix/diff?id=${fixId}`); + const data = await resp.json(); + const diffs = data.diffs || []; + if (!diffs.length) { + content.innerHTML = '
No code edits found in session
'; + } else { + content.innerHTML = diffs.map(d => ` +
+
${this._escHtml(d.file_path)} ${d.tool}
+
${this._renderUnifiedDiff(d.unified_diff)}
+
`).join(''); + } + content.classList.remove('hidden'); + wrapper.querySelector('.btn-show-diff').textContent = 'Hide Code Diff'; + wrapper.querySelector('.btn-show-diff').onclick = () => { + content.classList.toggle('hidden'); + wrapper.querySelector('.btn-show-diff').textContent = + content.classList.contains('hidden') ? 'Show Code Diff' : 'Hide Code Diff'; + }; + } catch (e) { + content.innerHTML = `
Error loading diff: ${e.message}
`; + content.classList.remove('hidden'); + } + }, + + _renderUnifiedDiff(diff) { + if (!diff) return ''; + return diff.split('\n').map(line => { + const esc = this._escHtml(line); + if (line.startsWith('+++') || line.startsWith('---')) return `${esc}`; + if (line.startsWith('@@')) return `${esc}`; + if (line.startsWith('+')) return `${esc}`; + if (line.startsWith('-')) return `${esc}`; + return `${esc}`; + }).join('\n'); + }, + + async renderGraduationStats() { + try { + const resp = await fetch('/api/fix/graduation-stats'); + const stats = await resp.json(); + const classes = Object.entries(stats); + if (!classes.length) return ''; + + let rows = classes.map(([cls, s]) => { + const badge = s.graduated + ? 'AUTO' + : s.canGraduate + ? `` + : ''; + return ` + ${this._escHtml(cls)} + ${s.approved}✓ ${s.partial}~ ${s.rejected}✗ + ${(s.rate * 100).toFixed(0)}% + ${badge} + `; + }).join(''); + + return `
+ Graduation Stats (${classes.length} classes) + + + ${rows} +
ClassOutcomesRateStatus
+
`; + } catch { return ''; } + }, + + async resolve(fixId, outcome) { + try { + await fetch('/api/fix/resolve', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ fixId, outcome }), + }); + } catch (e) { + console.warn('[fix-review] resolve error:', e); + } + // Show revert button on reject + if (outcome === 'failure') { + const btn = document.getElementById(`revert-${fixId}`); + if (btn) btn.classList.remove('hidden'); + } if (window.__claude && window.__claude.resolve) { window.__claude.resolve(fixId, outcome); } - // Remove from UI - this.refresh(); + // Delay refresh so the server has time to persist the resolved status + // before the poll reads it back (avoids the fix flickering back into view) + setTimeout(() => this.refresh(), 800); + }, + + async batchResolve(fixIds, outcome) { + await Promise.all(fixIds.map(id => this.resolve(id, outcome))); + }, + + async revert(fixId) { + try { + const resp = await fetch('/api/fix/revert', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ fixId }), + }); + const data = await resp.json(); + const btn = document.getElementById(`revert-${fixId}`); + if (btn) { + btn.textContent = data.reverted?.length + ? `Reverted ${data.reverted.length} file(s)` + : 'Nothing to revert'; + btn.disabled = true; + } + } catch (e) { + console.warn('[fix-review] revert error:', e); + } + }, + + async graduate(issueClass) { + try { + const resp = await fetch('/api/fix/graduate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ issueClass }), + }); + const data = await resp.json(); + if (data.ok) { + this.refresh(); + } else { + alert(`Cannot graduate: ${data.error}`); + } + } catch (e) { + console.warn('[fix-review] graduate error:', e); + } + }, + + _notify(fix) { + if (!('Notification' in window)) return; + const show = () => { + new Notification('Fix ready for review', { + body: fix.description || 'A fix is ready for your approval.', + icon: '/favicon.ico', + tag: fix.fixId, + }); + }; + if (Notification.permission === 'granted') { + show(); + } else if (Notification.permission !== 'denied') { + Notification.requestPermission().then(p => { if (p === 'granted') show(); }); + } + }, + + _escHtml(str) { + if (!str) return ''; + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); }, startPolling() { @@ -87,17 +306,26 @@ const fixReview = { .then(r => r.json()) .then(fixes => { const badge = document.getElementById('fix-review-badge'); - if (fixes.length > 0) { + const pending = fixes.filter(f => !['approved', 'rejected', 'reverted'].includes(f.status)); + if (pending.length > 0) { badge.classList.add('has-fixes'); - badge.textContent = `Fix Review (${fixes.length})`; + badge.textContent = `Fix Review (${pending.length})`; } else { badge.classList.remove('has-fixes'); + badge.textContent = 'Fix Review'; } // Auto-refresh if panel is open const panel = document.getElementById('fix-review-panel'); if (panel.classList.contains('visible')) { this.refresh(); } + // Notify for newly ready fixes + pending.forEach(fix => { + if (fix.status === 'ready' && !this._notifiedIds.has(fix.fixId)) { + this._notifiedIds.add(fix.fixId); + this._notify(fix); + } + }); }) .catch(() => {}); }, 5000); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c3c054b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1255 @@ +{ + "name": "tribbles", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tribbles", + "version": "1.0.0", + "devDependencies": { + "@cucumber/cucumber": "^9.5.1", + "@playwright/test": "^1.40.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cucumber/ci-environment": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@cucumber/ci-environment/-/ci-environment-9.2.0.tgz", + "integrity": "sha512-jLzRtVwdtNt+uAmTwvXwW9iGYLEOJFpDSmnx/dgoMGKXUWRx1UHT86Q696CLdgXO8kyTwsgJY0c6n5SW9VitAA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@cucumber/cucumber": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/@cucumber/cucumber/-/cucumber-9.6.0.tgz", + "integrity": "sha512-bCw2uJdGHHLg4B3RoZpLzx0RXyXURmPe+swtdK1cGoA8rs+vv+/6osifcNwvFM2sv0nQ91+gDACSrXK7AHCylg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cucumber/ci-environment": "9.2.0", + "@cucumber/cucumber-expressions": "16.1.2", + "@cucumber/gherkin": "26.2.0", + "@cucumber/gherkin-streams": "5.0.1", + "@cucumber/gherkin-utils": "8.0.2", + "@cucumber/html-formatter": "20.4.0", + "@cucumber/message-streams": "4.0.1", + "@cucumber/messages": "22.0.0", + "@cucumber/tag-expressions": "5.0.1", + "assertion-error-formatter": "^3.0.0", + "capital-case": "^1.0.4", + "chalk": "^4.1.2", + "cli-table3": "0.6.3", + "commander": "^10.0.0", + "debug": "^4.3.4", + "error-stack-parser": "^2.1.4", + "figures": "^3.2.0", + "glob": "^7.1.6", + "has-ansi": "^4.0.1", + "indent-string": "^4.0.0", + "is-installed-globally": "^0.4.0", + "is-stream": "^2.0.0", + "knuth-shuffle-seeded": "^1.0.6", + "lodash.merge": "^4.6.2", + "lodash.mergewith": "^4.6.2", + "luxon": "3.2.1", + "mkdirp": "^2.1.5", + "mz": "^2.7.0", + "progress": "^2.0.3", + "resolve-pkg": "^2.0.0", + "semver": "7.5.3", + "string-argv": "^0.3.1", + "strip-ansi": "6.0.1", + "supports-color": "^8.1.1", + "tmp": "^0.2.1", + "util-arity": "^1.1.0", + "verror": "^1.10.0", + "xmlbuilder": "^15.1.1", + "yaml": "^2.2.2", + "yup": "1.2.0" + }, + "bin": { + "cucumber-js": "bin/cucumber.js" + }, + "engines": { + "node": "14 || 16 || >=18" + } + }, + "node_modules/@cucumber/cucumber-expressions": { + "version": "16.1.2", + "resolved": "https://registry.npmjs.org/@cucumber/cucumber-expressions/-/cucumber-expressions-16.1.2.tgz", + "integrity": "sha512-CfHEbxJ5FqBwF6mJyLLz4B353gyHkoi6cCL4J0lfDZ+GorpcWw4n2OUAdxJmP7ZlREANWoTFlp4FhmkLKrCfUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regexp-match-indices": "1.0.2" + } + }, + "node_modules/@cucumber/gherkin": { + "version": "26.2.0", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-26.2.0.tgz", + "integrity": "sha512-iRSiK8YAIHAmLrn/mUfpAx7OXZ7LyNlh1zT89RoziSVCbqSVDxJS6ckEzW8loxs+EEXl0dKPQOXiDmbHV+C/fA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cucumber/messages": ">=19.1.4 <=22" + } + }, + "node_modules/@cucumber/gherkin-streams": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin-streams/-/gherkin-streams-5.0.1.tgz", + "integrity": "sha512-/7VkIE/ASxIP/jd4Crlp4JHXqdNFxPGQokqWqsaCCiqBiu5qHoKMxcWNlp9njVL/n9yN4S08OmY3ZR8uC5x74Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "9.1.0", + "source-map-support": "0.5.21" + }, + "bin": { + "gherkin-javascript": "bin/gherkin" + }, + "peerDependencies": { + "@cucumber/gherkin": ">=22.0.0", + "@cucumber/message-streams": ">=4.0.0", + "@cucumber/messages": ">=17.1.1" + } + }, + "node_modules/@cucumber/gherkin-streams/node_modules/commander": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.1.0.tgz", + "integrity": "sha512-i0/MaqBtdbnJ4XQs4Pmyb+oFQl+q0lsAmokVUH92SlSw4fkeAcG3bVon+Qt7hmtF+u3Het6o4VgrcY3qAoEB6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/@cucumber/gherkin-utils": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin-utils/-/gherkin-utils-8.0.2.tgz", + "integrity": "sha512-aQlziN3r3cTwprEDbLEcFoMRQajb9DTOu2OZZp5xkuNz6bjSTowSY90lHUD2pWT7jhEEckZRIREnk7MAwC2d1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cucumber/gherkin": "^25.0.0", + "@cucumber/messages": "^19.1.4", + "@teppeis/multimaps": "2.0.0", + "commander": "9.4.1", + "source-map-support": "^0.5.21" + }, + "bin": { + "gherkin-utils": "bin/gherkin-utils" + } + }, + "node_modules/@cucumber/gherkin-utils/node_modules/@cucumber/gherkin": { + "version": "25.0.2", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-25.0.2.tgz", + "integrity": "sha512-EdsrR33Y5GjuOoe2Kq5Y9DYwgNRtUD32H4y2hCrT6+AWo7ibUQu7H+oiWTgfVhwbkHsZmksxHSxXz/AwqqyCRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cucumber/messages": "^19.1.4" + } + }, + "node_modules/@cucumber/gherkin-utils/node_modules/@cucumber/messages": { + "version": "19.1.4", + "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-19.1.4.tgz", + "integrity": "sha512-Pksl0pnDz2l1+L5Ug85NlG6LWrrklN9qkMxN5Mv+1XZ3T6u580dnE6mVaxjJRdcOq4tR17Pc0RqIDZMyVY1FlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/uuid": "8.3.4", + "class-transformer": "0.5.1", + "reflect-metadata": "0.1.13", + "uuid": "9.0.0" + } + }, + "node_modules/@cucumber/gherkin-utils/node_modules/@types/uuid": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@cucumber/gherkin-utils/node_modules/commander": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.4.1.tgz", + "integrity": "sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/@cucumber/html-formatter": { + "version": "20.4.0", + "resolved": "https://registry.npmjs.org/@cucumber/html-formatter/-/html-formatter-20.4.0.tgz", + "integrity": "sha512-TnLSXC5eJd8AXHENo69f5z+SixEVtQIf7Q2dZuTpT/Y8AOkilGpGl1MQR1Vp59JIw+fF3EQSUKdf+DAThCxUNg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@cucumber/messages": ">=18" + } + }, + "node_modules/@cucumber/message-streams": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@cucumber/message-streams/-/message-streams-4.0.1.tgz", + "integrity": "sha512-Kxap9uP5jD8tHUZVjTWgzxemi/0uOsbGjd4LBOSxcJoOCRbESFwemUzilJuzNTB8pcTQUh8D5oudUyxfkJOKmA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@cucumber/messages": ">=17.1.1" + } + }, + "node_modules/@cucumber/messages": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-22.0.0.tgz", + "integrity": "sha512-EuaUtYte9ilkxcKmfqGF9pJsHRUU0jwie5ukuZ/1NPTuHS1LxHPsGEODK17RPRbZHOFhqybNzG2rHAwThxEymg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/uuid": "9.0.1", + "class-transformer": "0.5.1", + "reflect-metadata": "0.1.13", + "uuid": "9.0.0" + } + }, + "node_modules/@cucumber/tag-expressions": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@cucumber/tag-expressions/-/tag-expressions-5.0.1.tgz", + "integrity": "sha512-N43uWud8ZXuVjza423T9ZCIJsaZhFekmakt7S9bvogTxqdVGbRobjR663s0+uW0Rz9e+Pa8I6jUuWtoBLQD2Mw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@teppeis/multimaps": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@teppeis/multimaps/-/multimaps-2.0.0.tgz", + "integrity": "sha512-TL1adzq1HdxUf9WYduLcQ/DNGYiz71U31QRgbnr0Ef1cPyOUOsBojxHVWpFeOSUucB6Lrs0LxFRA14ntgtkc9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.17" + } + }, + "node_modules/@types/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/assertion-error-formatter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/assertion-error-formatter/-/assertion-error-formatter-3.0.0.tgz", + "integrity": "sha512-6YyAVLrEze0kQ7CmJfUgrLHb+Y7XghmL2Ie7ijVa2Y9ynP3LV+VDiwFk62Dn0qtqbmY0BT0ss6p1xxpiF2PYbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "diff": "^4.0.1", + "pad-right": "^0.2.2", + "repeat-string": "^1.6.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/capital-case": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", + "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-table3": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz", + "integrity": "sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-ansi": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-4.0.1.tgz", + "integrity": "sha512-Qr4RtTm30xvEdqUXbSBVWDu+PrTokJOwe/FU+VdfJPk+MXAPoeOzKpRyrDTnZIJwAkQ4oBLTU53nu0HrkF/Z2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/knuth-shuffle-seeded": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/knuth-shuffle-seeded/-/knuth-shuffle-seeded-1.0.6.tgz", + "integrity": "sha512-9pFH0SplrfyKyojCLxZfMcvkhf5hH0d+UwR9nTVJ/DDQJGuzcXjTwB7TP7sDfehSudlGGaOLblmEWqv04ERVWg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "seed-random": "~2.2.0" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/luxon": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.2.1.tgz", + "integrity": "sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mkdirp": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz", + "integrity": "sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pad-right": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/pad-right/-/pad-right-0.2.2.tgz", + "integrity": "sha512-4cy8M95ioIGolCoMmm2cMntGR1lPLEbOMzOKu8bzjuJP6JpzEMQcDHmh7hHLYGgob+nKe1YHFMaG4V59HQa89g==", + "dev": true, + "license": "MIT", + "dependencies": { + "repeat-string": "^1.5.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/regexp-match-indices": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regexp-match-indices/-/regexp-match-indices-1.0.2.tgz", + "integrity": "sha512-DwZuAkt8NF5mKwGGER1EGh2PRqyvhRhhLviH+R8y8dIuaQROlUfXjt4s9ZTXstIsSkptf06BSvwcEmmfheJJWQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "regexp-tree": "^0.1.11" + } + }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg/-/resolve-pkg-2.0.0.tgz", + "integrity": "sha512-+1lzwXehGCXSeryaISr6WujZzowloigEofRB+dj75y9RRa/obVcYgbHJd53tdYw8pvZj8GojXaaENws8Ktw/hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/seed-random": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/seed-random/-/seed-random-2.2.0.tgz", + "integrity": "sha512-34EQV6AAHQGhoc0tn/96a9Fsi6v2xdqe/dMUwljGRaFOzR3EgRmECvD0O8vi8X+/uQ50LGHfkNu/Eue5TPKZkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/upper-case-first": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", + "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/util-arity": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/util-arity/-/util-arity-1.1.0.tgz", + "integrity": "sha512-kkyIsXKwemfSy8ZEoaIz06ApApnWsk5hQO0vLjZS6UkBiGiW++Jsyb8vSBoc0WKlffGoGs5yYy/j5pp8zckrFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yup": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.2.0.tgz", + "integrity": "sha512-PPqYKSAXjpRCgLgLKVGPA33v5c/WgEx3wi6NFjIiegz90zSwyMpvTFp/uGcVnnbx6to28pgnzp/q8ih3QRjLMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } + } + } +} diff --git a/serve.py b/serve.py index 54308e6..9f102ba 100755 --- a/serve.py +++ b/serve.py @@ -30,6 +30,9 @@ LIVE_MODE = "--live" in sys.argv SCRIPT_DIR = Path(__file__).parent SNAPSHOTS_DIR = SCRIPT_DIR / "fix-snapshots" +QUEUE_FILE = SCRIPT_DIR / "fix-queue.json" +APPROVAL_HISTORY_FILE = SCRIPT_DIR / "fix-approval-history.json" +WEBHOOK_CONFIG_FILE = SCRIPT_DIR / "fix-webhook.json" ANALYSES_DIR = SCRIPT_DIR / "analyses" # Resolve the claude binary — prefer ~/.claude/local/claude over PATH @@ -681,7 +684,196 @@ def trigger_analysis_generation(): # ── Fix Snapshot Tracking ───────────────────────────────────── # Tracks active fix sessions so we can capture before/after screenshots -active_fixes = {} # fix_id -> { session_id, description, before_path, after_path, status } +# fix_id -> { session_id, description, issue_class, confidence, before_path, +# after_path, status, created_at, resolved_at, outcome, +# changed_files, diffs, auto_applied } +_fixes_lock = threading.Lock() + + +def _load_queue(): + """Load the persistent fix queue from disk.""" + if QUEUE_FILE.exists(): + try: + with open(QUEUE_FILE) as f: + return json.load(f) + except Exception: + pass + return {} + + +def _save_queue(): + """Persist the current fix queue to disk (call while holding _fixes_lock).""" + try: + with open(QUEUE_FILE, "w") as f: + json.dump(active_fixes, f, indent=2, default=str) + except Exception as e: + print(f"[queue] save error: {e}") + + +def _load_approval_history(): + """Load per-class approval history from disk.""" + if APPROVAL_HISTORY_FILE.exists(): + try: + with open(APPROVAL_HISTORY_FILE) as f: + return json.load(f) + except Exception: + pass + return {} + + +def _save_approval_history(): + """Persist approval history to disk (call while holding _fixes_lock).""" + try: + with open(APPROVAL_HISTORY_FILE, "w") as f: + json.dump(approval_history, f, indent=2, default=str) + except Exception as e: + print(f"[approval] save error: {e}") + + +def _load_webhook_config(): + """Load webhook configuration from disk.""" + if WEBHOOK_CONFIG_FILE.exists(): + try: + with open(WEBHOOK_CONFIG_FILE) as f: + return json.load(f) + except Exception: + pass + return {"url": None, "sound": False} + + +active_fixes = _load_queue() +approval_history = _load_approval_history() +webhook_config = _load_webhook_config() + +# Confidence threshold above which a fix can be auto-applied (configurable) +CONFIDENCE_AUTO_APPLY_THRESHOLD = 0.9 +# Minimum number of fixes needed before graduation is considered +GRADUATION_MIN_FIXES = 10 +# Approval rate required for graduation +GRADUATION_MIN_RATE = 0.95 + + +def compute_confidence(issue_class): + """Compute a confidence score (0–1) for a fix based on class history. + + - Classes with many approvals and high approval rate → high confidence + - Novel or frequently-rejected classes → low confidence + """ + if not issue_class: + return 0.3 # novel/unknown class — low confidence + stats = approval_history.get(issue_class) + if not stats or stats.get("total", 0) < 3: + return 0.4 # too few samples + total = stats["total"] + approved = stats.get("approved", 0) + partial = stats.get("partial", 0) + # Weight partial approvals at 0.5 + rate = (approved + 0.5 * partial) / total + # Scale: 0.4 at 0% approval → 1.0 at 100% approval + return round(0.4 + 0.6 * rate, 3) + + +def check_graduation(issue_class): + """Check if a class should be graduated to autonomous. + + Returns (should_graduate, current_rate, total) tuple. + """ + if not issue_class: + return False, 0.0, 0 + stats = approval_history.get(issue_class, {}) + total = stats.get("total", 0) + if total < GRADUATION_MIN_FIXES: + return False, 0.0, total + approved = stats.get("approved", 0) + partial = stats.get("partial", 0) + rate = (approved + 0.5 * partial) / total + return rate >= GRADUATION_MIN_RATE, round(rate, 3), total + + +def parse_session_diffs(session_id): + """Parse a Claude session JSONL for Edit and Write tool calls. + + Returns a list of diff entries: + [{ "tool": "Edit"|"Write", "file_path": str, "old_string": str, + "new_string": str, "unified_diff": str }, ...] + """ + import difflib + pattern = str(CLAUDE_DIR / "*" / f"{session_id}.jsonl") + matches = globmod.glob(pattern) + if not matches: + return [] + + diffs = [] + try: + with open(matches[0]) as f: + for raw in f: + raw = raw.strip() + if not raw: + continue + try: + entry = json.loads(raw) + except json.JSONDecodeError: + continue + if entry.get("type") != "assistant": + continue + for block in entry.get("message", {}).get("content", []): + if block.get("type") != "tool_use": + continue + name = block.get("name", "") + inp = block.get("input", {}) + if name == "Edit" and inp.get("file_path"): + old = inp.get("old_string", "") + new = inp.get("new_string", "") + udiff = "".join(difflib.unified_diff( + old.splitlines(keepends=True), + new.splitlines(keepends=True), + fromfile=f"a/{inp['file_path']}", + tofile=f"b/{inp['file_path']}", + )) + diffs.append({ + "tool": "Edit", + "file_path": inp["file_path"], + "old_string": old, + "new_string": new, + "unified_diff": udiff, + }) + elif name == "Write" and inp.get("file_path"): + content = inp.get("content", "") + diffs.append({ + "tool": "Write", + "file_path": inp["file_path"], + "new_string": content, + "unified_diff": f"--- /dev/null\n+++ b/{inp['file_path']}\n" + + "".join(f"+{l}" for l in content.splitlines(keepends=True)), + }) + except Exception as e: + print(f"[diff] parse error: {e}") + return diffs + + +def send_webhook_notification(fix_id, fix): + """POST a fix-ready notification to the configured webhook URL.""" + url = webhook_config.get("url") + if not url: + return + try: + import urllib.request + payload = json.dumps({ + "text": f"Fix ready for review: {fix.get('description', '')[:80]}", + "fixId": fix_id, + "issueClass": fix.get("issue_class"), + "confidence": fix.get("confidence"), + "status": fix.get("status"), + }).encode("utf-8") + req = urllib.request.Request( + url, data=payload, + headers={"Content-Type": "application/json"}, + method="POST", + ) + urllib.request.urlopen(req, timeout=5) + print(f"[webhook] notified for fix {fix_id}") + except Exception as e: + print(f"[webhook] notification error: {e}") def capture_screenshot(fix_id, label="before"): @@ -725,13 +917,12 @@ def capture_after_screenshot(fix_id): """Capture the 'after' screenshot for a completed fix. Called in a background thread that polls for session completion. + Also parses the session JSONL for code diffs and sends a webhook notification. """ fix = active_fixes.get(fix_id) if not fix: return - session_id = fix["session_id"] - # Poll for session completion (max 5 minutes) for _ in range(300): time.sleep(1) @@ -742,12 +933,30 @@ def capture_after_screenshot(fix_id): time.sleep(2) after_path = capture_screenshot(fix_id, "after") + with _fixes_lock: + fix = active_fixes.get(fix_id) + if not fix: + return + if after_path: + fix["after_path"] = after_path + fix["status"] = "ready" + print(f"[snapshot] After screenshot captured: {after_path}") + else: + fix["status"] = "after_failed" + # Parse code diffs from the session JSONL + session_id = fix.get("session_id", "") + if session_id: + fix["diffs"] = parse_session_diffs(session_id) + fix["changed_files"] = list({d["file_path"] for d in fix["diffs"]}) + _save_queue() + + # Send webhook notification now that the fix is ready if after_path: - fix["after_path"] = after_path - fix["status"] = "ready" - print(f"[snapshot] After screenshot captured: {after_path}") - else: - fix["status"] = "after_failed" + threading.Thread( + target=send_webhook_notification, + args=(fix_id, fix), + daemon=True, + ).start() def build_devtools_prompt(action, description, context): @@ -1091,27 +1300,82 @@ def do_GET(self): if not fix: self.send_json({"error": "Fix not found", "fixId": fix_id}) return + issue_class = fix.get("issue_class") + _, grad_rate, grad_total = check_graduation(issue_class) + stats = approval_history.get(issue_class, {}) if issue_class else {} self.send_json({ "fixId": fix_id, "status": fix["status"], "description": fix["description"], + "issueClass": issue_class, + "confidence": fix.get("confidence", 0.0), "beforePath": fix.get("before_path"), "afterPath": fix.get("after_path"), "hasBeforeAfter": bool(fix.get("before_path") and fix.get("after_path")), + "changedFiles": fix.get("changed_files", []), + "outcome": fix.get("outcome"), + "graduationRate": grad_rate, + "graduationTotal": grad_total, + "isGraduated": stats.get("graduated", False), }) elif parsed.path == "/api/fix/snapshots": # List all tracked fixes with snapshot status result = [] - for fid, fix in active_fixes.items(): + with _fixes_lock: + snapshot = dict(active_fixes) + for fid, fix in snapshot.items(): + if fix.get("status") in ("approved", "rejected", "reverted"): + continue # hide resolved fixes from the queue + issue_class = fix.get("issue_class") result.append({ "fixId": fid, "status": fix["status"], "description": fix["description"][:100], + "issueClass": issue_class, + "confidence": fix.get("confidence", 0.0), "hasBeforeAfter": bool(fix.get("before_path") and fix.get("after_path")), }) self.send_json(result) + elif parsed.path == "/api/fix/diff": + # Return parsed code diffs for a fix + params = parse_qs(parsed.query) + fix_id = params.get("id", [None])[0] + if not fix_id: + self.send_error(400, "Missing id parameter") + return + fix = active_fixes.get(fix_id) + if not fix: + self.send_json({"error": "Fix not found", "fixId": fix_id}) + return + diffs = fix.get("diffs") + if diffs is None: + session_id = fix.get("session_id", "") + diffs = parse_session_diffs(session_id) if session_id else [] + with _fixes_lock: + active_fixes.get(fix_id, {})["diffs"] = diffs + self.send_json({"fixId": fix_id, "diffs": diffs}) + + elif parsed.path == "/api/fix/graduation-stats": + # Return per-class approval rates and graduation status + result = {} + with _fixes_lock: + hist = dict(approval_history) + for cls, stats in hist.items(): + should_grad, rate, total = check_graduation(cls) + result[cls] = { + "approved": stats.get("approved", 0), + "partial": stats.get("partial", 0), + "rejected": stats.get("rejected", 0), + "total": total, + "rate": rate, + "graduated": stats.get("graduated", False), + "autoApply": stats.get("auto_apply", False), + "canGraduate": should_grad and not stats.get("graduated", False), + } + self.send_json(result) + elif parsed.path == "/api/themes": themes = list_vsc_themes() # Strip internal path from response @@ -1215,11 +1479,19 @@ def do_POST(self): description = data.get("description", "").strip() context = data.get("context", {}) fix_id = data.get("fixId") # from bridge telemetry + issue_class = data.get("issueClass", "").strip() or None if not description: self.send_error(400, "Missing description") return + # Compute confidence for this fix + confidence = compute_confidence(issue_class) + + # If class is graduated and confidence is high enough, skip HITL queue + cls_stats = approval_history.get(issue_class, {}) if issue_class else {} + auto_apply = cls_stats.get("auto_apply", False) and confidence >= CONFIDENCE_AUTO_APPLY_THRESHOLD + # Capture "before" screenshot (non-blocking, quick) before_path = None if action == "fix" and fix_id: @@ -1242,13 +1514,23 @@ def do_POST(self): # Track fix for after-capture if fix_id: - active_fixes[fix_id] = { - "session_id": result_id, - "description": description, - "before_path": before_path, - "after_path": None, - "status": "running", - } + with _fixes_lock: + active_fixes[fix_id] = { + "session_id": result_id, + "description": description, + "issue_class": issue_class, + "confidence": confidence, + "before_path": before_path, + "after_path": None, + "status": "running", + "created_at": datetime.now(timezone.utc).isoformat(), + "resolved_at": None, + "outcome": None, + "changed_files": [], + "diffs": None, + "auto_applied": auto_apply, + } + _save_queue() # Start background thread to capture "after" screenshot threading.Thread( target=capture_after_screenshot, @@ -1263,9 +1545,164 @@ def do_POST(self): "action": action, "description": description, "fixId": fix_id, + "issueClass": issue_class, + "confidence": confidence, + "autoApplied": auto_apply, "beforeScreenshot": before_path is not None, }) + elif parsed.path == "/api/fix/resolve": + # Approve / Partial / Reject a fix and update approval history + try: + data = json.loads(body) if body else {} + except json.JSONDecodeError: + self.send_error(400, "Invalid JSON") + return + fix_id = data.get("fixId") + outcome = data.get("outcome") # "success" | "partial" | "failure" + if not fix_id or outcome not in ("success", "partial", "failure"): + self.send_error(400, "Missing fixId or invalid outcome") + return + with _fixes_lock: + fix = active_fixes.get(fix_id) + if not fix: + self.send_json({"error": "Fix not found", "fixId": fix_id}) + return + fix["outcome"] = outcome + fix["resolved_at"] = datetime.now(timezone.utc).isoformat() + fix["status"] = "approved" if outcome == "success" else ( + "partial" if outcome == "partial" else "rejected" + ) + # Update per-class approval history + issue_class = fix.get("issue_class") + if issue_class: + if issue_class not in approval_history: + approval_history[issue_class] = { + "approved": 0, "partial": 0, "rejected": 0, + "total": 0, "graduated": False, "auto_apply": False, + } + cls = approval_history[issue_class] + cls["total"] += 1 + if outcome == "success": + cls["approved"] += 1 + elif outcome == "partial": + cls["partial"] += 1 + else: + cls["rejected"] += 1 + # Regression detection: revoke auto-apply if a graduated fix fails + if outcome == "failure" and cls.get("auto_apply"): + cls["auto_apply"] = False + cls["graduated"] = False + print(f"[graduation] Revoked auto-apply for class '{issue_class}' due to failure") + _save_approval_history() + _save_queue() + should_grad, rate, total = check_graduation(issue_class) if issue_class else (False, 0, 0) + self.send_json({ + "ok": True, + "fixId": fix_id, + "outcome": outcome, + "issueClass": issue_class, + "canGraduate": should_grad, + "graduationRate": rate, + "graduationTotal": total, + }) + + elif parsed.path == "/api/fix/revert": + # Revert changed files for a rejected fix via git checkout + try: + data = json.loads(body) if body else {} + except json.JSONDecodeError: + self.send_error(400, "Invalid JSON") + return + fix_id = data.get("fixId") + if not fix_id: + self.send_error(400, "Missing fixId") + return + fix = active_fixes.get(fix_id) + if not fix: + self.send_json({"error": "Fix not found", "fixId": fix_id}) + return + changed_files = fix.get("changed_files", []) + if not changed_files: + self.send_json({"ok": True, "reverted": [], "note": "no changed files tracked"}) + return + reverted = [] + errors = [] + for fp in changed_files: + try: + result = subprocess.run( + ["git", "checkout", "--", fp], + capture_output=True, text=True, timeout=10, + cwd=str(SCRIPT_DIR), + ) + if result.returncode == 0: + reverted.append(fp) + else: + errors.append({"file": fp, "error": result.stderr.strip()}) + except Exception as e: + errors.append({"file": fp, "error": str(e)}) + with _fixes_lock: + if fix_id in active_fixes: + active_fixes[fix_id]["status"] = "reverted" + _save_queue() + self.send_json({"ok": True, "reverted": reverted, "errors": errors}) + + elif parsed.path == "/api/fix/graduate": + # Graduate a class from HITL review to autonomous operation + try: + data = json.loads(body) if body else {} + except json.JSONDecodeError: + self.send_error(400, "Invalid JSON") + return + issue_class = data.get("issueClass", "").strip() + if not issue_class: + self.send_error(400, "Missing issueClass") + return + with _fixes_lock: + if issue_class not in approval_history: + self.send_json({"error": "No history for class", "issueClass": issue_class}) + return + should_grad, rate, total = check_graduation(issue_class) + if not should_grad: + self.send_json({ + "error": "Class does not meet graduation criteria", + "issueClass": issue_class, + "rate": rate, + "total": total, + "required_rate": GRADUATION_MIN_RATE, + "required_total": GRADUATION_MIN_FIXES, + }) + return + approval_history[issue_class]["graduated"] = True + approval_history[issue_class]["auto_apply"] = True + _save_approval_history() + print(f"[graduation] Class '{issue_class}' graduated (rate={rate}, total={total})") + self.send_json({ + "ok": True, + "issueClass": issue_class, + "rate": rate, + "total": total, + "graduated": True, + }) + + elif parsed.path == "/api/fix/webhook": + # Configure webhook URL for fix notifications + try: + data = json.loads(body) if body else {} + except json.JSONDecodeError: + self.send_error(400, "Invalid JSON") + return + url = data.get("url", "").strip() or None + sound = bool(data.get("sound", False)) + webhook_config["url"] = url + webhook_config["sound"] = sound + try: + with open(WEBHOOK_CONFIG_FILE, "w") as f: + json.dump(webhook_config, f, indent=2) + except Exception as e: + print(f"[webhook] config save error: {e}") + self.send_json({"ok": True, "url": url, "sound": sound}) + else: self.send_error(404, "Not found") diff --git a/style.css b/style.css index 4c1951e..4bd8417 100644 --- a/style.css +++ b/style.css @@ -1530,3 +1530,225 @@ body:has(#landing.sidebar) #sidebar-resize:hover, #zoom-group { opacity: 0.6; } } #fix-review-badge.has-fixes { display: block; } + +/* ── HITL: Fix Review Panel Enhancements ── */ +.fix-review-item { + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 1px solid var(--border); +} +.fix-review-meta { + display: flex; + align-items: center; + gap: 6px; + margin: 4px 0 8px; + flex-wrap: wrap; +} +.fix-class-badge { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 10px; + padding: 2px 8px; + font-size: 10px; + color: var(--text-dim); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} +.fix-confidence-badge { + border-radius: 10px; + padding: 2px 8px; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.3px; +} +.confidence-high { + background: #1a3a1a; + color: #4ade80; + border: 1px solid #2a5a2a; +} +.confidence-low { + background: #3a2a1a; + color: #fb923c; + border: 1px solid #5a3a1a; +} + +/* Batch action bar */ +.fix-batch-bar { + display: flex; + align-items: center; + gap: 8px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 6px; + padding: 6px 10px; + margin-bottom: 12px; + flex-wrap: wrap; +} +.fix-batch-class { + font-weight: 700; + color: var(--text-bright); + font-size: 11px; + text-transform: uppercase; +} +.fix-batch-count { + color: var(--text-dim); + font-size: 11px; +} +.btn-batch { + padding: 4px 10px !important; + font-size: 11px !important; + flex: 0 !important; +} + +/* Revert button */ +.btn-revert { + background: #2a1a3a; + color: #c084fc; + border: 1px solid #3a2a5a !important; + flex: 1; + padding: 8px; + border-radius: 4px; + cursor: pointer; + font-family: inherit; + font-size: 12px; + font-weight: 600; + transition: all 0.15s; +} +.btn-revert:hover { background: #3a2a5a; } +.btn-revert:disabled { opacity: 0.5; cursor: default; } + +/* Code diff section */ +.fix-code-diff-wrapper { + margin: 8px 0; +} +.btn-show-diff { + background: none; + border: 1px solid var(--border); + color: var(--text-dim); + border-radius: 4px; + padding: 4px 10px; + font-size: 11px; + font-family: inherit; + cursor: pointer; + transition: all 0.15s; +} +.btn-show-diff:hover { background: var(--bg-card); color: var(--text); } +.fix-code-diff { + margin-top: 6px; + max-height: 300px; + overflow: auto; + border: 1px solid var(--border); + border-radius: 4px; + background: #0d1117; +} +.fix-code-diff.hidden { display: none; } +.fix-diff-file { + border-bottom: 1px solid var(--border); +} +.fix-diff-file:last-child { border-bottom: none; } +.fix-diff-filename { + padding: 4px 8px; + background: var(--bg-card); + color: var(--text-dim); + font-size: 10px; + font-family: 'Menlo', 'Consolas', monospace; + border-bottom: 1px solid var(--border); + display: flex; + gap: 6px; + align-items: center; +} +.fix-diff-tool { + background: var(--bg-panel); + border: 1px solid var(--border); + border-radius: 3px; + padding: 0 4px; + font-size: 9px; + color: var(--text-dim); +} +.fix-diff-pre { + margin: 0; + padding: 6px 8px; + font-family: 'Menlo', 'Consolas', monospace; + font-size: 11px; + line-height: 1.5; + white-space: pre; + overflow-x: auto; +} +.diff-add { color: #4ade80; display: block; } +.diff-del { color: #f87171; display: block; } +.diff-hunk { color: #60a5fa; display: block; } +.diff-header { color: var(--text-dim); display: block; } +.diff-ctx { color: var(--text-dim); display: block; } + +/* Graduation bar and stats */ +.fix-graduation-bar { + display: flex; + align-items: center; + gap: 10px; + background: #1a2a1a; + border: 1px solid #2a5a2a; + border-radius: 6px; + padding: 8px 10px; + margin: 8px 0; + font-size: 11px; + color: #4ade80; + flex-wrap: wrap; +} +.fix-graduation-bar span { flex: 1; } +.btn-graduate { + background: #1a4a1a; + color: #4ade80; + border: 1px solid #2a7a2a !important; + border-radius: 4px; + padding: 6px 12px; + font-size: 12px; + font-weight: 700; + cursor: pointer; + font-family: inherit; + transition: all 0.15s; + white-space: nowrap; +} +.btn-graduate:hover { background: #2a6a2a; } +.btn-graduate-small { + padding: 2px 8px !important; + font-size: 10px !important; +} +.fix-grad-badge { + border-radius: 10px; + padding: 2px 8px; + font-size: 10px; + font-weight: 700; +} +.grad-auto { + background: #1a3a1a; + color: #4ade80; + border: 1px solid #2a5a2a; +} +.fix-graduation-stats { + margin-top: 16px; + border-top: 1px solid var(--border); + padding-top: 10px; +} +.fix-graduation-stats summary { + cursor: pointer; + color: var(--text-dim); + font-size: 11px; + padding: 4px 0; + user-select: none; +} +.fix-graduation-stats summary:hover { color: var(--text); } +.fix-stats-table { + width: 100%; + border-collapse: collapse; + margin-top: 8px; + font-size: 11px; +} +.fix-stats-table th, .fix-stats-table td { + padding: 4px 6px; + text-align: left; + border-bottom: 1px solid var(--border); + color: var(--text-dim); +} +.fix-stats-table th { color: var(--text); font-weight: 600; } +