@progen-labs/spectra 0.4.0 → 0.4.1
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 +3 -2
- package/package.json +1 -1
- package/server/fix/harness.js +9 -0
- package/server/fix/test-fix.js +18 -2
package/README.md
CHANGED
|
@@ -235,12 +235,13 @@ The harness, not the agent, makes every decision:
|
|
|
235
235
|
- **Proof.** The change is kept only if `spectra compare` says *improved* with medium or high confidence and no new
|
|
236
236
|
critical finding; with low confidence it measures again.
|
|
237
237
|
- **A record.** Each kept commit carries its evidence; `.spectra/fix/<time>/report.md` summarises the session, and a
|
|
238
|
-
history skips findings that failed before.
|
|
238
|
+
history skips findings that failed before. A finding that an earlier kept fix already cleared is skipped too.
|
|
239
239
|
|
|
240
240
|
By default the agent is Claude Code in headless mode, allowed only to read and edit files (no shell, no web, no MCP),
|
|
241
241
|
with a spending cap per finding. A test bench (`npm run bench`) checks the harness's decisions in CI: a real fix is
|
|
242
242
|
kept, a "faster" change that breaks the page is caught, a cosmetic change doesn't count, and an edit to
|
|
243
|
-
`package.json` is refused.
|
|
243
|
+
`package.json` is refused. With Claude Code on the demo app, it kept two of three fixes and cut JavaScript CPU time
|
|
244
|
+
from 1.51 s to 82.5 ms (−95%) for $0.44. See [the AI fix harness](https://progen-labs.github.io/website/#spectra/docs/harness).
|
|
244
245
|
|
|
245
246
|
## In VS Code
|
|
246
247
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@progen-labs/spectra",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Spectra \u2014 a Chrome DevTools Protocol profiler that identifies the files, functions and components causing frontend slowness, across React, Vue, Svelte, Angular and Next.js builds.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/server/fix/harness.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Everything it touches is passed in (profile, compare, agent, workspace,
|
|
5
5
|
* checks), so the loop itself is tested without a browser or a model.
|
|
6
6
|
*/
|
|
7
|
+
import { findingKey } from '../../shared/compare.js';
|
|
7
8
|
import { fixPrompt, judge, rankFindings, scopeProblem } from './policy.js';
|
|
8
9
|
|
|
9
10
|
// agent errors no other finding would get past: stop instead of trying them all
|
|
@@ -34,6 +35,7 @@ export async function runFix(o) {
|
|
|
34
35
|
const baseline = await profile('baseline');
|
|
35
36
|
session.baseline = { file: baseline.file, summary: baseline.result.summary, url: baseline.result.url };
|
|
36
37
|
let current = [baseline.file];
|
|
38
|
+
let present = null; // finding keys the latest kept change still measures
|
|
37
39
|
|
|
38
40
|
const { candidates, skipped } = rankFindings(baseline.result.findings, { exists: ws.exists, history, retry, max: maxFixes });
|
|
39
41
|
session.skipped = skipped.map(s => ({ title: s.finding.title, reason: s.reason }));
|
|
@@ -46,6 +48,12 @@ export async function runFix(o) {
|
|
|
46
48
|
for (const [i, f] of candidates.entries()) {
|
|
47
49
|
if (signal?.aborted) break;
|
|
48
50
|
const n = `${i + 1}/${candidates.length}`;
|
|
51
|
+
if (present && !present.has(f.key)) {
|
|
52
|
+
// one fix often clears several findings (a memoised row also stops its hot file)
|
|
53
|
+
session.skipped.push({ title: f.title, reason: 'already resolved by an earlier kept change' });
|
|
54
|
+
log(`[${n}] – ${f.title}: already resolved by an earlier kept change.`);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
49
57
|
const attempt = { finding: { title: f.title, file: f.file, line: f.line, severity: f.severity, impactMs: f.impactMs, key: f.key }, outcome: null, reason: '', changes: [] };
|
|
50
58
|
session.attempts.push(attempt);
|
|
51
59
|
const discard = (outcome, reason) => {
|
|
@@ -127,6 +135,7 @@ export async function runFix(o) {
|
|
|
127
135
|
Object.assign(attempt, { outcome: 'kept', reason: verdict.reason });
|
|
128
136
|
session.kept++;
|
|
129
137
|
current = after.map(a => a.file);
|
|
138
|
+
present = new Set(after.flatMap(a => a.result.findings || []).map(findingKey));
|
|
130
139
|
log(`[${n}] ✓ kept (${attempt.commit}): ${verdict.reason}`);
|
|
131
140
|
}
|
|
132
141
|
|
package/server/fix/test-fix.js
CHANGED
|
@@ -70,7 +70,8 @@ test('tells the agent the finding and the rules', () => {
|
|
|
70
70
|
});
|
|
71
71
|
|
|
72
72
|
// --- the loop, with fakes
|
|
73
|
-
|
|
73
|
+
const bothFindings = () => [finding('row-render'), finding('heavy', { file: 'src/heavy.js', line: 2, culprits: [] })];
|
|
74
|
+
function fakes({ agentChanges, checks = [true, true, true, true], verdicts, measured = bothFindings }) {
|
|
74
75
|
const calls = { commits: [], undos: 0, profiles: 0, compares: [] };
|
|
75
76
|
let pending = [];
|
|
76
77
|
let checkRun = 0;
|
|
@@ -91,7 +92,7 @@ function fakes({ agentChanges, checks = [true, true, true, true], verdicts }) {
|
|
|
91
92
|
workspace,
|
|
92
93
|
checks: [{ name: 'npm test' }],
|
|
93
94
|
runChecks: async () => [{ name: 'npm test', ok: checks[checkRun++] ?? true, tail: 'expected 120 rows' }],
|
|
94
|
-
profile: async label => ((calls.profiles++), { file: `${label}.json`, result: { url: 'http://localhost:5199', summary: {}, findings:
|
|
95
|
+
profile: async label => ((calls.profiles++), { file: `${label}.json`, result: { url: 'http://localhost:5199', summary: {}, findings: measured(label) } }),
|
|
95
96
|
compare: async (before, after) => (calls.compares.push([before, after]), verdicts[verdictRun++] || comparison('neutral')),
|
|
96
97
|
},
|
|
97
98
|
};
|
|
@@ -112,6 +113,21 @@ test('keeps a change that passes the checks and measures faster, and builds on i
|
|
|
112
113
|
assert.match(reportMarkdown({ ...s, runs: 3, branch: 'spectra/fix-x' }), /1 of 2 findings fixed/);
|
|
113
114
|
});
|
|
114
115
|
|
|
116
|
+
test('skips a finding that an earlier kept change already resolved', async () => {
|
|
117
|
+
// the memoised row also stops the hot file: after the first fix, neither is measured
|
|
118
|
+
const measured = label => (label === 'baseline' ? bothFindings() : []);
|
|
119
|
+
const { calls, options } = fakes({ agentChanges: [edit, edit], verdicts: [comparison('improved'), comparison('improved')], measured });
|
|
120
|
+
let asked = 0;
|
|
121
|
+
const run = options.agent.run;
|
|
122
|
+
options.agent.run = (...a) => (asked++, run(...a));
|
|
123
|
+
const s = await runFix(options);
|
|
124
|
+
assert.equal(asked, 1, 'the agent is not asked about the resolved finding');
|
|
125
|
+
assert.deepEqual(s.attempts.map(a => a.outcome), ['kept']);
|
|
126
|
+
assert.deepEqual(s.skipped, [{ title: 'heavy is slow', reason: 'already resolved by an earlier kept change' }]);
|
|
127
|
+
assert.equal(calls.profiles, 2, 'baseline and one measurement');
|
|
128
|
+
assert.match(reportMarkdown({ ...s, runs: 3, branch: 'spectra/fix-x' }), /heavy is slow: already resolved by an earlier kept change/);
|
|
129
|
+
});
|
|
130
|
+
|
|
115
131
|
test('undoes a change whose checks fail, without measuring it', async () => {
|
|
116
132
|
const { calls, options } = fakes({ agentChanges: [edit], checks: [true, false], verdicts: [] });
|
|
117
133
|
options.workspace.exists = f => f === 'src/Row.jsx';
|