@surlymochan/evalid 0.0.0-stage → 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.
Files changed (192) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/LICENSE +21 -0
  3. package/README.md +315 -2
  4. package/RELEASE_NOTES.md +124 -0
  5. package/atoms/registry.json +758 -0
  6. package/bin/xflow.js +7261 -0
  7. package/docs/adoption/README.md +128 -0
  8. package/docs/adoption/openflow-release-hardening.md +61 -0
  9. package/docs/benchmark-openflow-vs-gstack-superpowers-openspec-2026-05-25.md +81 -0
  10. package/docs/compatibility.md +52 -0
  11. package/docs/competitive-benchmark.md +108 -0
  12. package/docs/corps-operator-guide.md +75 -0
  13. package/docs/demo-proof.md +126 -0
  14. package/docs/examples-gallery.md +115 -0
  15. package/docs/fixtures/tracker-item.json +7 -0
  16. package/docs/goal-vs-codex.md +129 -0
  17. package/docs/gstack-comparison.md +88 -0
  18. package/docs/install-upgrade.md +133 -0
  19. package/docs/integrations.md +49 -0
  20. package/docs/launch-demo.md +123 -0
  21. package/docs/launch-dossier.md +180 -0
  22. package/docs/methodology.md +149 -0
  23. package/docs/npm-publish-handoff.md +96 -0
  24. package/docs/openspec-migration.md +106 -0
  25. package/docs/public-benchmark.md +68 -0
  26. package/docs/public-release.md +145 -0
  27. package/docs/quality-assessment.md +139 -0
  28. package/docs/quickstart.md +156 -0
  29. package/docs/reviewer-guide.md +43 -0
  30. package/docs/spec-kit-benchmark.md +73 -0
  31. package/docs/super-assistant-comparison.md +106 -0
  32. package/docs/superpowers-comparison.md +82 -0
  33. package/docs/team-adoption.md +123 -0
  34. package/docs/tooling-matrix.md +91 -0
  35. package/docs/walkthrough.md +76 -0
  36. package/docs/workflow-manual.md +1130 -0
  37. package/package.json +83 -4
  38. package/prompts/pencil-draft.md +46 -0
  39. package/prompts/pencil-refine.md +46 -0
  40. package/prompts/phase-artifact.md +24 -0
  41. package/prompts/phase-execution.md +32 -0
  42. package/schemas/competitor-reconstruction-contract.schema.json +91 -0
  43. package/schemas/llm-design-review-aggregate.schema.json +83 -0
  44. package/schemas/visual-benchmark.schema.json +978 -0
  45. package/schemas/workflow.schema.json +100 -0
  46. package/scripts/run-tests.mjs +23 -0
  47. package/scripts/takein_digest.py +139 -0
  48. package/specs/README.md +118 -0
  49. package/specs/architecture.md +36 -0
  50. package/specs/product.md +33 -0
  51. package/specs/workflow.md +349 -0
  52. package/src/agent_team/atoms/codex-app-bridge.js +8 -0
  53. package/src/agent_team/atoms/design/competitor-reconstruction-review.js +44 -0
  54. package/src/agent_team/atoms/design/contract-freeze.js +8 -0
  55. package/src/agent_team/atoms/design/generation-contract.js +442 -0
  56. package/src/agent_team/atoms/design/reconstruction-pack.js +246 -0
  57. package/src/agent_team/atoms/design/reference-surface-lock.js +49 -0
  58. package/src/agent_team/atoms/patch-challenge.js +19 -0
  59. package/src/agent_team/atoms/pencil/accept.js +9 -0
  60. package/src/agent_team/atoms/pencil/draft.js +365 -0
  61. package/src/agent_team/atoms/pencil/refine.js +229 -0
  62. package/src/agent_team/atoms/phase-run-dispatch.js +8 -0
  63. package/src/agent_team/atoms/team-run.js +380 -0
  64. package/src/agent_team/atoms/visual/aesthetic-review.js +231 -0
  65. package/src/agent_team/atoms/visual/benchmark-repair-plan.js +291 -0
  66. package/src/agent_team/atoms/visual/benchmark.js +1652 -0
  67. package/src/agent_team/atoms/visual/design-selection.js +23 -0
  68. package/src/agent_team/atoms/visual/direction-synthesis.js +124 -0
  69. package/src/agent_team/atoms/visual/dom-rect-export.js +129 -0
  70. package/src/agent_team/atoms/visual/image-diff.js +186 -0
  71. package/src/agent_team/atoms/visual/layout-competition.js +8 -0
  72. package/src/agent_team/atoms/visual/page-evidence-capture.js +806 -0
  73. package/src/agent_team/atoms/visual/playwright-runtime.js +8 -0
  74. package/src/agent_team/atoms/visual/review-aggregate.js +87 -0
  75. package/src/agent_team/atoms/visual/review.js +77 -0
  76. package/src/agent_team/atoms/visual/visual-report.js +197 -0
  77. package/src/agent_team/atoms/visual/visual-token-export.js +417 -0
  78. package/src/agent_team/common.js +311 -0
  79. package/src/core/atoms/agent-invoke.js +684 -0
  80. package/src/core/atoms/event/ingest.js +30 -0
  81. package/src/core/atoms/experience/digest.js +55 -0
  82. package/src/core/atoms/gate/artifact-verify.js +83 -0
  83. package/src/core/atoms/gate/competitor-contract-validate.js +119 -0
  84. package/src/core/atoms/gate/evidence-collect.js +64 -0
  85. package/src/core/atoms/gate/payload-from-phase-run.js +78 -0
  86. package/src/core/atoms/gate/visual-benchmark-validate.js +183 -0
  87. package/src/core/atoms/gate/workbench.js +79 -0
  88. package/src/core/atoms/intake/classify.js +15 -0
  89. package/src/core/atoms/intake/design-skill-select.js +16 -0
  90. package/src/core/atoms/intake/preview-or-refine.js +19 -0
  91. package/src/core/atoms/intake/recommend.js +20 -0
  92. package/src/core/atoms/intake/research-same-category.js +19 -0
  93. package/src/core/atoms/intervention/create.js +30 -0
  94. package/src/core/atoms/memory/manage.js +27 -0
  95. package/src/core/atoms/memory/mount.js +13 -0
  96. package/src/core/atoms/mission/create.js +19 -0
  97. package/src/core/atoms/mission/list.js +14 -0
  98. package/src/core/atoms/mission/show.js +26 -0
  99. package/src/core/atoms/notify/checkpoint.js +46 -0
  100. package/src/core/atoms/phase-run/complete.js +42 -0
  101. package/src/core/atoms/phase-run/fail.js +40 -0
  102. package/src/core/atoms/phase-run/persist.js +39 -0
  103. package/src/core/atoms/phase-run/start.js +30 -0
  104. package/src/core/atoms/plugin/run.js +76 -0
  105. package/src/core/atoms/policy/gate.js +90 -0
  106. package/src/core/atoms/policy/resolve.js +11 -0
  107. package/src/core/atoms/storage/sync.js +57 -0
  108. package/src/core/atoms/workflow/advance.js +25 -0
  109. package/src/core/atoms/workflow/guard.js +127 -0
  110. package/src/core/atoms/workflow/state-load.js +37 -0
  111. package/src/core/bindings.js +37 -0
  112. package/src/core/change-artifacts.js +134 -0
  113. package/src/core/competitive-score.js +265 -0
  114. package/src/core/concurrency-policy.js +35 -0
  115. package/src/core/execution-log.js +108 -0
  116. package/src/core/intake-recommendation.js +134 -0
  117. package/src/core/memory-state.js +62 -0
  118. package/src/core/mission-state.js +780 -0
  119. package/src/core/pending-gates.js +66 -0
  120. package/src/core/phase-cost.js +116 -0
  121. package/src/core/phase-run-read.js +27 -0
  122. package/src/core/policy-overlay.js +180 -0
  123. package/src/core/state-store.js +201 -0
  124. package/src/core/workflow-executor.js +1014 -0
  125. package/src/core/workflow-integrity.js +111 -0
  126. package/src/core/workflow-loader.js +163 -0
  127. package/src/core/workflow-rerun.js +84 -0
  128. package/src/core/workflow-state.js +119 -0
  129. package/src/server.js +1307 -0
  130. package/workflows/corps.yaml +481 -0
  131. package/workflows/yolo.yaml +241 -0
  132. package/xflow/README.md +76 -0
  133. package/xflow/aha/SKILL.md +82 -0
  134. package/xflow/aha/scripts/append_aha.py +50 -0
  135. package/xflow/archive/scripts/archive_change.py +338 -0
  136. package/xflow/archive/templates/merge-design.md +54 -0
  137. package/xflow/archive/templates/merge-workflow.md +26 -0
  138. package/xflow/atoms/__init__.py +6 -0
  139. package/xflow/atoms/a1_issue_create_or_patch.py +70 -0
  140. package/xflow/atoms/a2_branch_create_and_link.py +42 -0
  141. package/xflow/atoms/a3_repo_doctor.py +11 -0
  142. package/xflow/atoms/a4_project_set_status.py +11 -0
  143. package/xflow/atoms/a5_archive_commit_push_close.py +11 -0
  144. package/xflow/atoms/a6_pr_create.py +72 -0
  145. package/xflow/atoms/b1_change_scaffold.py +11 -0
  146. package/xflow/atoms/b2_status_read.py +16 -0
  147. package/xflow/atoms/b2b_status_write.py +20 -0
  148. package/xflow/atoms/b3_status_transition.py +27 -0
  149. package/xflow/atoms/b4_handoff_scaffold_or_refresh.py +11 -0
  150. package/xflow/atoms/b5_aha_append.py +11 -0
  151. package/xflow/atoms/b6_takein_digest.py +11 -0
  152. package/xflow/atoms/b6b_lookback_digest.py +11 -0
  153. package/xflow/atoms/b7_change_mission_bind.py +43 -0
  154. package/xflow/atoms/c1_mem.py +54 -0
  155. package/xflow/atoms/e6_gate_local_precheck.py +23 -0
  156. package/xflow/atoms/h1_design_lite_gate.py +74 -0
  157. package/xflow/atoms/i6a_tdd_run.py +107 -0
  158. package/xflow/atoms/i6b_tdd_proof_validate.py +184 -0
  159. package/xflow/atoms/i6c_tdd_quality_review.py +372 -0
  160. package/xflow/atoms/j1_tests_run.py +129 -0
  161. package/xflow/atoms/j4a_spec_consistency_structural.py +105 -0
  162. package/xflow/atoms/j4b_spec_delta_review.py +316 -0
  163. package/xflow/atoms/j4c_openspec_migration_map.py +121 -0
  164. package/xflow/atoms/j4d_spec_clarify_gate.py +373 -0
  165. package/xflow/atoms/j6_cr_review.py +443 -0
  166. package/xflow/atoms/k1_artifacts_complete_check.py +39 -0
  167. package/xflow/atoms/k2_merge_snippets_apply.py +11 -0
  168. package/xflow/atoms/k3_handoff_refresh.py +14 -0
  169. package/xflow/atoms/k4_mem_lesson_persist.py +65 -0
  170. package/xflow/atoms/k5_archive_publish.py +21 -0
  171. package/xflow/atoms/k6_aha_merge.py +42 -0
  172. package/xflow/atoms/tdd_lib.py +956 -0
  173. package/xflow/corps/SKILL.md +109 -0
  174. package/xflow/exec/scripts/set_issue_in_progress.py +159 -0
  175. package/xflow/goal/SKILL.md +150 -0
  176. package/xflow/handoff/SKILL.md +95 -0
  177. package/xflow/handoff/scripts/scaffold_handoff.py +304 -0
  178. package/xflow/lookback/SKILL.md +66 -0
  179. package/xflow/lookback/scripts/lookback_digest.py +46 -0
  180. package/xflow/openissue/scripts/open_issue_flow.py +438 -0
  181. package/xflow/plan/SKILL.md +47 -0
  182. package/xflow/ralph/SKILL.md +123 -0
  183. package/xflow/scripts/check_installed_xflow_skill_sync.sh +44 -0
  184. package/xflow/scripts/common.py +483 -0
  185. package/xflow/scripts/doctor.py +278 -0
  186. package/xflow/scripts/sync_installed_xflow_skill.sh +134 -0
  187. package/xflow/takein/SKILL.md +81 -0
  188. package/xflow/takein/scripts/takein_digest.py +88 -0
  189. package/xflow/yolo/SKILL.md +75 -0
  190. package/xflow/yolo/scripts/scaffold_specs_change.py +673 -0
  191. package/xflow/yolo/scripts/yolo_gate.py +312 -0
  192. package/xflow/yolo/templates/DESIGN.md +179 -0
@@ -0,0 +1,291 @@
1
+ import { readJsonFile, resolveChangePath, writeJsonFile } from '../../../core/change-artifacts.js';
2
+
3
+ function summarizeHotspot(hotspot = {}) {
4
+ if (!hotspot || typeof hotspot !== 'object') return 'unknown hotspot';
5
+ const row = Number.isFinite(hotspot.row) ? hotspot.row : '?';
6
+ const col = Number.isFinite(hotspot.col) ? hotspot.col : '?';
7
+ const ratio = typeof hotspot.changed_ratio === 'number' ? hotspot.changed_ratio.toFixed(4) : 'n/a';
8
+ return `grid(${row},${col}) changed_ratio=${ratio}`;
9
+ }
10
+
11
+ function unique(values = []) {
12
+ return [...new Set((Array.isArray(values) ? values : []).filter(Boolean))];
13
+ }
14
+
15
+ function componentBlueprintForChange(projectRoot, changeId) {
16
+ const generationArtifact = readJsonFile(resolveChangePath(projectRoot, changeId, 'generation_contract.json'), null);
17
+ return generationArtifact?.generation_contract?.component_constraints?.component_blueprint || [];
18
+ }
19
+
20
+ function stagePlanForChange(projectRoot, changeId) {
21
+ const generationArtifact = readJsonFile(resolveChangePath(projectRoot, changeId, 'generation_contract.json'), null);
22
+ return generationArtifact?.generation_contract?.component_constraints?.staged_generation?.stages || [];
23
+ }
24
+
25
+ function primitiveFeedbackForChange(projectRoot, changeId) {
26
+ const feedbackArtifact = readJsonFile(resolveChangePath(projectRoot, changeId, 'primitive_feedback_history.json'), null);
27
+ const entries = Array.isArray(feedbackArtifact?.primitive_feedback_history?.entries)
28
+ ? feedbackArtifact.primitive_feedback_history.entries
29
+ : [];
30
+ return new Map(entries.map((entry) => [`${entry.module}::${entry.primitive_role}`, entry]));
31
+ }
32
+
33
+ function areaOf(rect = {}) {
34
+ return Math.max(0, Number(rect.width) || 0) * Math.max(0, Number(rect.height) || 0);
35
+ }
36
+
37
+ function overlapArea(a = {}, b = {}) {
38
+ const left = Math.max(Number(a.x ?? a.left) || 0, Number(b.x ?? b.left) || 0);
39
+ const top = Math.max(Number(a.y ?? a.top) || 0, Number(b.y ?? b.top) || 0);
40
+ const right = Math.min(
41
+ (Number(a.x ?? a.left) || 0) + (Number(a.width) || 0),
42
+ (Number(b.x ?? b.left) || 0) + (Number(b.width) || 0),
43
+ );
44
+ const bottom = Math.min(
45
+ (Number(a.y ?? a.top) || 0) + (Number(a.height) || 0),
46
+ (Number(b.y ?? b.top) || 0) + (Number(b.height) || 0),
47
+ );
48
+ return Math.max(0, right - left) * Math.max(0, bottom - top);
49
+ }
50
+
51
+ function impactedPanelsForScenario(scenario = {}) {
52
+ const hotspots = Array.isArray(scenario.diff_metrics?.hotspots) ? scenario.diff_metrics.hotspots : [];
53
+ const rects = Array.isArray(scenario.layout_observations?.dom_rects) ? scenario.layout_observations.dom_rects : [];
54
+ if (hotspots.length === 0 || rects.length === 0) return [];
55
+
56
+ const totals = new Map();
57
+ for (const hotspot of hotspots) {
58
+ for (const rect of rects) {
59
+ const overlap = overlapArea(hotspot, rect);
60
+ if (overlap <= 0) continue;
61
+ const key = rect.panel_id || rect.id || rect.selector;
62
+ totals.set(key, (totals.get(key) || 0) + overlap);
63
+ }
64
+ }
65
+ return [...totals.entries()]
66
+ .sort((a, b) => b[1] - a[1])
67
+ .map(([panelId]) => panelId)
68
+ .filter(Boolean);
69
+ }
70
+
71
+ function pickBlueprint(blueprint = [], predicate) {
72
+ const matches = blueprint.filter(predicate);
73
+ return matches.length > 0 ? matches : blueprint;
74
+ }
75
+
76
+ function scoreComponentForRepair(component = {}, feedbackMap = new Map()) {
77
+ const feedback = feedbackMap.get(`${component.module}::${component.primitive_role}`) || {};
78
+ return {
79
+ attention_score: Number(feedback.attention_score) || 0,
80
+ current_issue_score: Number(feedback.current_issue_score) || 0,
81
+ improvement_score: Number(feedback.improvement_score) || 0,
82
+ regression_score: Number(feedback.regression_score) || 0,
83
+ stabilization_score: Number(feedback.stabilization_score) || 0,
84
+ };
85
+ }
86
+
87
+ function pickRepairComponents(components = [], feedbackMap = new Map()) {
88
+ const scored = components.map((component) => ({
89
+ component,
90
+ feedback: scoreComponentForRepair(component, feedbackMap),
91
+ }));
92
+ const actionable = scored.filter(({ feedback }) => feedback.attention_score > 0 || feedback.current_issue_score > 0 || feedback.regression_score > 0);
93
+ const candidateSet = actionable.length > 0 ? actionable : scored;
94
+ const filtered = actionable.length > 0
95
+ ? candidateSet.filter(({ feedback }) => !(feedback.current_issue_score === 0 && feedback.stabilization_score > 0))
96
+ : candidateSet;
97
+ const finalSet = filtered.length > 0 ? filtered : candidateSet;
98
+ return finalSet
99
+ .sort((a, b) => {
100
+ const attentionDelta = b.feedback.attention_score - a.feedback.attention_score;
101
+ if (attentionDelta !== 0) return attentionDelta;
102
+ const regressionDelta = b.feedback.regression_score - a.feedback.regression_score;
103
+ if (regressionDelta !== 0) return regressionDelta;
104
+ const improvementDelta = a.feedback.improvement_score - b.feedback.improvement_score;
105
+ if (improvementDelta !== 0) return improvementDelta;
106
+ return a.component.id.localeCompare(b.component.id);
107
+ })
108
+ .map(({ component }) => component);
109
+ }
110
+
111
+ function buildRepairTarget({ id, priority, reason, components, focusDimensions, instruction, hotspots, impactedPanels = [], stagePlan = [], feedbackMap = new Map() }) {
112
+ const repairComponents = pickRepairComponents(components, feedbackMap);
113
+ const targetComponentIds = unique(repairComponents.map((component) => component.id));
114
+ const targetStageIds = stagePlan
115
+ .filter((stage) => (stage.target_component_ids || []).some((componentId) => targetComponentIds.includes(componentId)))
116
+ .map((stage) => stage.id);
117
+ return {
118
+ id,
119
+ priority,
120
+ reason,
121
+ target_component_ids: unique(repairComponents.map((component) => component.id)),
122
+ target_modules: unique(repairComponents.map((component) => component.module)),
123
+ target_primitive_roles: unique(repairComponents.map((component) => component.primitive_role)),
124
+ target_stage_ids: targetStageIds,
125
+ impacted_panels: impactedPanels,
126
+ focus_dimensions: focusDimensions,
127
+ preserve_non_target_components: true,
128
+ evidence_hotspots: hotspots,
129
+ target_feedback_summary: repairComponents.map((component) => {
130
+ const feedback = scoreComponentForRepair(component, feedbackMap);
131
+ return {
132
+ component_id: component.id,
133
+ module: component.module,
134
+ primitive_role: component.primitive_role,
135
+ attention_score: feedback.attention_score,
136
+ current_issue_score: feedback.current_issue_score,
137
+ improvement_score: feedback.improvement_score,
138
+ regression_score: feedback.regression_score,
139
+ stabilization_score: feedback.stabilization_score,
140
+ };
141
+ }),
142
+ instruction,
143
+ };
144
+ }
145
+
146
+ function buildScenarioRepairs(projectRoot, changeId, scenario = {}) {
147
+ const blockers = Array.isArray(scenario.blockers) ? scenario.blockers : [];
148
+ const blueprint = componentBlueprintForChange(projectRoot, changeId);
149
+ const stagePlan = stagePlanForChange(projectRoot, changeId);
150
+ const impactedPanels = impactedPanelsForScenario(scenario);
151
+ const primitiveFeedback = primitiveFeedbackForChange(projectRoot, changeId);
152
+ const panelScopedBlueprint = impactedPanels.length > 0
153
+ ? pickBlueprint(blueprint, (component) => impactedPanels.includes(component.module))
154
+ : blueprint;
155
+ const hotspots = Array.isArray(scenario.diff_metrics?.hotspots)
156
+ ? scenario.diff_metrics.hotspots.slice(0, 5).map((hotspot) => summarizeHotspot(hotspot))
157
+ : [];
158
+ const repairs = [];
159
+ if (blockers.includes('layout_shift_score_above_threshold')) {
160
+ const shellTargets = pickBlueprint(panelScopedBlueprint, (component) => ['toolbar_group', 'grid_header', 'grid_cell', 'list_row'].includes(component.primitive_role));
161
+ repairs.push(buildRepairTarget({
162
+ id: 'layout_ratio_repair',
163
+ priority: 'high',
164
+ reason: 'layout_shift_score_above_threshold',
165
+ components: shellTargets,
166
+ focusDimensions: ['panel_ratio', 'row_rhythm', 'anchor_alignment'],
167
+ instruction: 'Realign the shell and repeated primary primitives to the locked geometry before touching decorative tokens.',
168
+ hotspots,
169
+ impactedPanels,
170
+ stagePlan,
171
+ feedbackMap: primitiveFeedback,
172
+ }));
173
+ }
174
+ if (blockers.includes('structural_similarity_below_threshold')) {
175
+ const structureTargets = pickBlueprint(panelScopedBlueprint, (component) => component.hierarchy_role === 'primary');
176
+ repairs.push(buildRepairTarget({
177
+ id: 'structure_repair',
178
+ priority: 'high',
179
+ reason: 'structural_similarity_below_threshold',
180
+ components: structureTargets,
181
+ focusDimensions: ['primitive_presence', 'component_order', 'visual_hierarchy'],
182
+ instruction: 'Restore the reference primitive hierarchy component by component before widening to secondary polish.',
183
+ hotspots,
184
+ impactedPanels,
185
+ stagePlan,
186
+ feedbackMap: primitiveFeedback,
187
+ }));
188
+ }
189
+ if (blockers.includes('pixel_diff_ratio_above_threshold')) {
190
+ const tokenTargets = pickBlueprint(panelScopedBlueprint, (component) => ['status_chip', 'selection_badge', 'meta_line', 'input_block'].includes(component.primitive_role));
191
+ repairs.push(buildRepairTarget({
192
+ id: 'token_repair',
193
+ priority: 'medium',
194
+ reason: 'pixel_diff_ratio_above_threshold',
195
+ components: tokenTargets,
196
+ focusDimensions: ['spacing', 'typography', 'surface_emphasis', 'color_roles'],
197
+ instruction: 'Tighten token deltas only on the affected primitive families; preserve shell geometry while doing it.',
198
+ hotspots,
199
+ impactedPanels,
200
+ stagePlan,
201
+ feedbackMap: primitiveFeedback,
202
+ }));
203
+ }
204
+ if (!Array.isArray(scenario.structure_checks) || scenario.structure_checks.length === 0) {
205
+ const scaffoldTargets = pickBlueprint(panelScopedBlueprint, (component) => component.stage_bucket === 'shell');
206
+ repairs.push(buildRepairTarget({
207
+ id: 'missing_structure_contract',
208
+ priority: 'high',
209
+ reason: 'structure_checks_missing',
210
+ components: scaffoldTargets,
211
+ focusDimensions: ['layout_contract', 'region_anchors'],
212
+ instruction: 'Backfill structure checks and shell anchors from the generation contract before the next recheck.',
213
+ hotspots,
214
+ impactedPanels,
215
+ stagePlan,
216
+ feedbackMap: primitiveFeedback,
217
+ }));
218
+ }
219
+ return {
220
+ repairs,
221
+ repair_sequence: stagePlan.map((stage) => ({
222
+ stage_id: stage.id,
223
+ target_component_ids: stage.target_component_ids || [],
224
+ reason: repairs.some((repair) => (repair.target_component_ids || []).some((id) => (stage.target_component_ids || []).includes(id)))
225
+ ? 'has_repair_targets'
226
+ : 'preserve_if_unaffected',
227
+ })),
228
+ preserved_component_ids: unique(
229
+ blueprint
230
+ .map((component) => component.id)
231
+ .filter((id) => !repairs.some((repair) => (repair.target_component_ids || []).includes(id))),
232
+ ),
233
+ };
234
+ }
235
+
236
+ export async function benchmarkRepairPlan(input = {}, context = {}) {
237
+ const projectRoot = context.projectRoot || process.cwd();
238
+ const changeId = context.changeId || input.change_id || null;
239
+ if (!changeId) throw new Error('change_id is required for H6c.visual.benchmark_repair_plan');
240
+
241
+ const benchmark = readJsonFile(resolveChangePath(projectRoot, changeId, 'visual_benchmark.json'), { scenarios: [] });
242
+ const repairPath = resolveChangePath(projectRoot, changeId, 'benchmark_repair_plan.json');
243
+ const scenarios = Array.isArray(benchmark.scenarios) ? benchmark.scenarios : [];
244
+ const scenarioRepairs = scenarios.map((scenario) => {
245
+ const componentRepairs = buildScenarioRepairs(projectRoot, changeId, scenario);
246
+ return {
247
+ id: scenario.id || null,
248
+ status: scenario.status || 'unknown',
249
+ hotspots: Array.isArray(scenario.diff_metrics?.hotspots)
250
+ ? scenario.diff_metrics.hotspots.slice(0, 5).map((hotspot) => summarizeHotspot(hotspot))
251
+ : [],
252
+ repairs: componentRepairs.repairs,
253
+ repair_sequence: componentRepairs.repair_sequence,
254
+ preserved_component_ids: componentRepairs.preserved_component_ids,
255
+ };
256
+ });
257
+ const unresolved = scenarioRepairs.filter((scenario) => scenario.status !== 'pass');
258
+
259
+ const payload = {
260
+ version: 1,
261
+ phase: 'benchmark_repair_plan',
262
+ change_id: changeId,
263
+ generated_at: new Date().toISOString(),
264
+ status: unresolved.length === 0 ? 'not_needed' : 'ready',
265
+ summary: unresolved.length === 0
266
+ ? 'Benchmark already passed; no repair loop is needed.'
267
+ : `Prepared targeted repair actions for ${unresolved.length} unresolved benchmark scenario(s).`,
268
+ benchmark_repair_plan: {
269
+ status: unresolved.length === 0 ? 'not_needed' : 'ready',
270
+ unresolved_scenario_count: unresolved.length,
271
+ scenario_repairs: scenarioRepairs,
272
+ convergence_rules: [
273
+ 'Repair only the targeted component primitives before considering wider restyling.',
274
+ 'Preserve non-target components unless a repair target explicitly names them.',
275
+ 'Repair the reference skeleton first, then tune tokens.',
276
+ 'Do not widen scope beyond the unresolved benchmark hotspots.',
277
+ 'Preserve the locked primary reference surface during refinement.',
278
+ ],
279
+ },
280
+ };
281
+
282
+ writeJsonFile(repairPath, payload);
283
+ return {
284
+ ok: true,
285
+ status: payload.status,
286
+ output_file: repairPath,
287
+ payload,
288
+ };
289
+ }
290
+
291
+ export default benchmarkRepairPlan;