fe-harness 1.0.0
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 +55 -0
- package/agents/fe-codebase-mapper.md +945 -0
- package/agents/fe-design-scanner.md +47 -0
- package/agents/fe-executor.md +221 -0
- package/agents/fe-fix-loop.md +310 -0
- package/agents/fe-fixer.md +153 -0
- package/agents/fe-project-scanner.md +95 -0
- package/agents/fe-reviewer.md +141 -0
- package/agents/fe-verifier.md +231 -0
- package/agents/fe-wave-runner.md +477 -0
- package/bin/install.js +292 -0
- package/commands/fe/complete.md +35 -0
- package/commands/fe/execute.md +46 -0
- package/commands/fe/help.md +17 -0
- package/commands/fe/map-codebase.md +60 -0
- package/commands/fe/plan.md +36 -0
- package/commands/fe/status.md +39 -0
- package/fe-harness/bin/browser.cjs +271 -0
- package/fe-harness/bin/fe-tools.cjs +317 -0
- package/fe-harness/bin/lib/__tests__/browser.test.cjs +422 -0
- package/fe-harness/bin/lib/__tests__/config.test.cjs +93 -0
- package/fe-harness/bin/lib/__tests__/core.test.cjs +127 -0
- package/fe-harness/bin/lib/__tests__/scoring.test.cjs +130 -0
- package/fe-harness/bin/lib/__tests__/tasks.test.cjs +698 -0
- package/fe-harness/bin/lib/browser-core.cjs +365 -0
- package/fe-harness/bin/lib/config.cjs +34 -0
- package/fe-harness/bin/lib/core.cjs +135 -0
- package/fe-harness/bin/lib/logger.cjs +93 -0
- package/fe-harness/bin/lib/scoring.cjs +219 -0
- package/fe-harness/bin/lib/tasks.cjs +632 -0
- package/fe-harness/references/model-profiles.md +44 -0
- package/fe-harness/templates/config.jsonc +31 -0
- package/fe-harness/vendor/.gitkeep +0 -0
- package/fe-harness/vendor/puppeteer-core.cjs +445 -0
- package/fe-harness/workflows/complete.md +143 -0
- package/fe-harness/workflows/execute.md +227 -0
- package/fe-harness/workflows/help.md +89 -0
- package/fe-harness/workflows/map-codebase.md +331 -0
- package/fe-harness/workflows/plan.md +244 -0
- package/package.json +35 -0
- package/scripts/bundle-puppeteer.js +38 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Design task scoring dimensions and weights
|
|
4
|
+
const DESIGN_WEIGHTS = {
|
|
5
|
+
layout: 2.0,
|
|
6
|
+
spacing: 1.5,
|
|
7
|
+
colors: 1.5,
|
|
8
|
+
typography: 1.0,
|
|
9
|
+
borders: 0.5,
|
|
10
|
+
shadows: 0.5,
|
|
11
|
+
icons_images: 1.0,
|
|
12
|
+
completeness: 2.0,
|
|
13
|
+
};
|
|
14
|
+
const DESIGN_WEIGHT_SUM = Object.values(DESIGN_WEIGHTS).reduce((a, b) => a + b, 0);
|
|
15
|
+
|
|
16
|
+
const LOGIC_WEIGHTS = {
|
|
17
|
+
correctness: 2.5,
|
|
18
|
+
completeness: 2.0,
|
|
19
|
+
error_handling: 1.5,
|
|
20
|
+
code_quality: 1.5,
|
|
21
|
+
type_safety: 1.0,
|
|
22
|
+
integration: 1.5,
|
|
23
|
+
};
|
|
24
|
+
const LOGIC_WEIGHT_SUM = Object.values(LOGIC_WEIGHTS).reduce((a, b) => a + b, 0);
|
|
25
|
+
|
|
26
|
+
// Common key aliases: verifier agents sometimes output wrong key names.
|
|
27
|
+
// Map them to the canonical keys defined in DESIGN_WEIGHTS / LOGIC_WEIGHTS.
|
|
28
|
+
const KEY_ALIASES = {
|
|
29
|
+
color: 'colors',
|
|
30
|
+
colour: 'colors',
|
|
31
|
+
colour_s: 'colors',
|
|
32
|
+
border: 'borders',
|
|
33
|
+
shadow: 'shadows',
|
|
34
|
+
icons: 'icons_images',
|
|
35
|
+
images: 'icons_images',
|
|
36
|
+
interaction: null, // not a valid design dimension — drop it
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Normalise score keys to match the canonical weight definitions.
|
|
41
|
+
* - Remap known aliases (e.g. "color" → "colors")
|
|
42
|
+
* - Strip unknown keys with a warning
|
|
43
|
+
* - Report missing dimensions so callers know something is off
|
|
44
|
+
* @returns {{ normalised, warnings }}
|
|
45
|
+
*/
|
|
46
|
+
function normaliseScoreKeys(scores, type) {
|
|
47
|
+
const weights = type === 'design' ? DESIGN_WEIGHTS : LOGIC_WEIGHTS;
|
|
48
|
+
const validKeys = new Set(Object.keys(weights));
|
|
49
|
+
const normalised = {};
|
|
50
|
+
const warnings = [];
|
|
51
|
+
|
|
52
|
+
for (const [key, value] of Object.entries(scores)) {
|
|
53
|
+
if (validKeys.has(key)) {
|
|
54
|
+
normalised[key] = value;
|
|
55
|
+
} else if (key in KEY_ALIASES) {
|
|
56
|
+
const mapped = KEY_ALIASES[key];
|
|
57
|
+
if (mapped && validKeys.has(mapped)) {
|
|
58
|
+
normalised[mapped] = value;
|
|
59
|
+
warnings.push(`key "${key}" remapped to "${mapped}"`);
|
|
60
|
+
} else {
|
|
61
|
+
warnings.push(`key "${key}" dropped (not a valid ${type} dimension)`);
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
warnings.push(`unknown key "${key}" dropped (not a valid ${type} dimension)`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Check for missing dimensions — these will default to 0
|
|
69
|
+
for (const dim of validKeys) {
|
|
70
|
+
if (normalised[dim] == null) {
|
|
71
|
+
warnings.push(`missing dimension "${dim}" — defaults to 0`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { normalised, warnings };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Calculate weighted score from dimension scores.
|
|
80
|
+
* @param {Object} scores - { dimension: score(0-10) }
|
|
81
|
+
* @param {'design'|'logic'} type
|
|
82
|
+
* @param {Object} thresholds - { verifyThreshold, reviewThreshold, dimensionThreshold }
|
|
83
|
+
* @returns {{ total_score, passed, failed_dimensions, weighted_scores, warnings }}
|
|
84
|
+
*/
|
|
85
|
+
function calculateScore(scores, type, thresholds) {
|
|
86
|
+
const weights = type === 'design' ? DESIGN_WEIGHTS : LOGIC_WEIGHTS;
|
|
87
|
+
const weightSum = type === 'design' ? DESIGN_WEIGHT_SUM : LOGIC_WEIGHT_SUM;
|
|
88
|
+
const threshold = type === 'design' ? thresholds.verifyThreshold : thresholds.reviewThreshold;
|
|
89
|
+
const dimThreshold = thresholds.dimensionThreshold;
|
|
90
|
+
|
|
91
|
+
// Normalise keys before calculating
|
|
92
|
+
const { normalised, warnings } = normaliseScoreKeys(scores, type);
|
|
93
|
+
|
|
94
|
+
if (warnings.length > 0) {
|
|
95
|
+
process.stderr.write(`[scoring] warnings for ${type} scores:\n`);
|
|
96
|
+
for (const w of warnings) {
|
|
97
|
+
process.stderr.write(` - ${w}\n`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let weightedSum = 0;
|
|
102
|
+
const weightedScores = {};
|
|
103
|
+
const failedDimensions = [];
|
|
104
|
+
|
|
105
|
+
for (const [dim, weight] of Object.entries(weights)) {
|
|
106
|
+
const score = normalised[dim] != null ? Number(normalised[dim]) : 0;
|
|
107
|
+
weightedScores[dim] = score * weight;
|
|
108
|
+
weightedSum += score * weight;
|
|
109
|
+
|
|
110
|
+
if (score < dimThreshold) {
|
|
111
|
+
failedDimensions.push(dim);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const totalScore = Math.round((weightedSum / (weightSum * 10)) * 100);
|
|
116
|
+
const passed = totalScore >= threshold && failedDimensions.length === 0;
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
total_score: totalScore,
|
|
120
|
+
passed,
|
|
121
|
+
failed_dimensions: failedDimensions,
|
|
122
|
+
weighted_scores: weightedScores,
|
|
123
|
+
warnings,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Check for score regression.
|
|
129
|
+
* @param {Object} current - { total_score, scores: { dim: score } }
|
|
130
|
+
* @param {Object} best - { total_score, scores: { dim: score } }
|
|
131
|
+
* @param {Object} thresholds - { scoreDropTolerance, dimensionThreshold }
|
|
132
|
+
* @returns {{ regressed, reason, action }}
|
|
133
|
+
*/
|
|
134
|
+
function checkRegression(current, best, thresholds) {
|
|
135
|
+
// Auto-calculate total_score from scores if not provided (defensive)
|
|
136
|
+
const currentTotal = current.total_score != null
|
|
137
|
+
? current.total_score
|
|
138
|
+
: (current.scores ? autoCalcTotal(current.scores) : 0);
|
|
139
|
+
const bestTotal = best.total_score != null
|
|
140
|
+
? best.total_score
|
|
141
|
+
: (best.scores ? autoCalcTotal(best.scores) : 0);
|
|
142
|
+
|
|
143
|
+
const scoreDrop = bestTotal - currentTotal;
|
|
144
|
+
const tolerance = thresholds.scoreDropTolerance;
|
|
145
|
+
const dimThreshold = thresholds.dimensionThreshold;
|
|
146
|
+
|
|
147
|
+
// Check total score drop
|
|
148
|
+
if (scoreDrop > tolerance) {
|
|
149
|
+
return {
|
|
150
|
+
regressed: true,
|
|
151
|
+
reason: `Total score dropped ${scoreDrop} points (${bestTotal} → ${currentTotal}), exceeds tolerance ${tolerance}`,
|
|
152
|
+
action: 'rollback',
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Check dimension regression: previously-passed dimension now fails
|
|
157
|
+
if (current.scores && best.scores) {
|
|
158
|
+
for (const [dim, bestScore] of Object.entries(best.scores)) {
|
|
159
|
+
if (bestScore >= dimThreshold && current.scores[dim] < dimThreshold) {
|
|
160
|
+
return {
|
|
161
|
+
regressed: true,
|
|
162
|
+
reason: `Dimension "${dim}" regressed from ${bestScore} to ${current.scores[dim]} (below threshold ${dimThreshold})`,
|
|
163
|
+
action: 'rollback',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return { regressed: false };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Auto-calculate a rough total_score from raw dimension scores.
|
|
174
|
+
* Used as fallback when total_score is not provided.
|
|
175
|
+
*/
|
|
176
|
+
function autoCalcTotal(scores) {
|
|
177
|
+
const scoreKeys = Object.keys(scores);
|
|
178
|
+
|
|
179
|
+
// Use unique keys to determine type:
|
|
180
|
+
// - Design-only keys: layout, spacing, colors, typography, borders, shadows, icons_images
|
|
181
|
+
// - Logic-only keys: correctness, error_handling, code_quality, type_safety, integration
|
|
182
|
+
// - Shared key: completeness (exists in both)
|
|
183
|
+
const designOnlyKeys = ['layout', 'spacing', 'colors', 'typography', 'borders', 'shadows', 'icons_images'];
|
|
184
|
+
const logicOnlyKeys = ['correctness', 'error_handling', 'code_quality', 'type_safety', 'integration'];
|
|
185
|
+
|
|
186
|
+
const hasDesignKey = designOnlyKeys.some(k => scoreKeys.includes(k));
|
|
187
|
+
const hasLogicKey = logicOnlyKeys.some(k => scoreKeys.includes(k));
|
|
188
|
+
|
|
189
|
+
// If both or neither unique keys found, count matches to break tie
|
|
190
|
+
let isDesign;
|
|
191
|
+
if (hasDesignKey && !hasLogicKey) {
|
|
192
|
+
isDesign = true;
|
|
193
|
+
} else if (hasLogicKey && !hasDesignKey) {
|
|
194
|
+
isDesign = false;
|
|
195
|
+
} else {
|
|
196
|
+
// Ambiguous — count how many keys match each type
|
|
197
|
+
const designMatches = Object.keys(DESIGN_WEIGHTS).filter(k => scoreKeys.includes(k)).length;
|
|
198
|
+
const logicMatches = Object.keys(LOGIC_WEIGHTS).filter(k => scoreKeys.includes(k)).length;
|
|
199
|
+
isDesign = designMatches >= logicMatches;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const weights = isDesign ? DESIGN_WEIGHTS : LOGIC_WEIGHTS;
|
|
203
|
+
const weightSum = isDesign ? DESIGN_WEIGHT_SUM : LOGIC_WEIGHT_SUM;
|
|
204
|
+
|
|
205
|
+
let weightedSum = 0;
|
|
206
|
+
for (const [dim, weight] of Object.entries(weights)) {
|
|
207
|
+
const score = scores[dim] != null ? Number(scores[dim]) : 0;
|
|
208
|
+
weightedSum += score * weight;
|
|
209
|
+
}
|
|
210
|
+
return Math.round((weightedSum / (weightSum * 10)) * 100);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = {
|
|
214
|
+
DESIGN_WEIGHTS,
|
|
215
|
+
LOGIC_WEIGHTS,
|
|
216
|
+
calculateScore,
|
|
217
|
+
checkRegression,
|
|
218
|
+
normaliseScoreKeys,
|
|
219
|
+
};
|