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.
Files changed (41) hide show
  1. package/README.md +55 -0
  2. package/agents/fe-codebase-mapper.md +945 -0
  3. package/agents/fe-design-scanner.md +47 -0
  4. package/agents/fe-executor.md +221 -0
  5. package/agents/fe-fix-loop.md +310 -0
  6. package/agents/fe-fixer.md +153 -0
  7. package/agents/fe-project-scanner.md +95 -0
  8. package/agents/fe-reviewer.md +141 -0
  9. package/agents/fe-verifier.md +231 -0
  10. package/agents/fe-wave-runner.md +477 -0
  11. package/bin/install.js +292 -0
  12. package/commands/fe/complete.md +35 -0
  13. package/commands/fe/execute.md +46 -0
  14. package/commands/fe/help.md +17 -0
  15. package/commands/fe/map-codebase.md +60 -0
  16. package/commands/fe/plan.md +36 -0
  17. package/commands/fe/status.md +39 -0
  18. package/fe-harness/bin/browser.cjs +271 -0
  19. package/fe-harness/bin/fe-tools.cjs +317 -0
  20. package/fe-harness/bin/lib/__tests__/browser.test.cjs +422 -0
  21. package/fe-harness/bin/lib/__tests__/config.test.cjs +93 -0
  22. package/fe-harness/bin/lib/__tests__/core.test.cjs +127 -0
  23. package/fe-harness/bin/lib/__tests__/scoring.test.cjs +130 -0
  24. package/fe-harness/bin/lib/__tests__/tasks.test.cjs +698 -0
  25. package/fe-harness/bin/lib/browser-core.cjs +365 -0
  26. package/fe-harness/bin/lib/config.cjs +34 -0
  27. package/fe-harness/bin/lib/core.cjs +135 -0
  28. package/fe-harness/bin/lib/logger.cjs +93 -0
  29. package/fe-harness/bin/lib/scoring.cjs +219 -0
  30. package/fe-harness/bin/lib/tasks.cjs +632 -0
  31. package/fe-harness/references/model-profiles.md +44 -0
  32. package/fe-harness/templates/config.jsonc +31 -0
  33. package/fe-harness/vendor/.gitkeep +0 -0
  34. package/fe-harness/vendor/puppeteer-core.cjs +445 -0
  35. package/fe-harness/workflows/complete.md +143 -0
  36. package/fe-harness/workflows/execute.md +227 -0
  37. package/fe-harness/workflows/help.md +89 -0
  38. package/fe-harness/workflows/map-codebase.md +331 -0
  39. package/fe-harness/workflows/plan.md +244 -0
  40. package/package.json +35 -0
  41. 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
+ };