edsger 0.36.2 → 0.37.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 (127) hide show
  1. package/.claude/settings.local.json +23 -3
  2. package/.env.local +12 -0
  3. package/dist/api/features/__tests__/regression-prevention.test.d.ts +5 -0
  4. package/dist/api/features/__tests__/regression-prevention.test.js +338 -0
  5. package/dist/api/features/__tests__/status-updater.integration.test.d.ts +5 -0
  6. package/dist/api/features/__tests__/status-updater.integration.test.js +497 -0
  7. package/dist/api/growth.d.ts +23 -1
  8. package/dist/api/growth.js +25 -0
  9. package/dist/commands/app-store/index.js +2 -6
  10. package/dist/commands/code-review/index.js +3 -3
  11. package/dist/commands/growth-analysis/index.js +2 -0
  12. package/dist/commands/init/index.js +3 -3
  13. package/dist/commands/workflow/pipeline-runner.d.ts +17 -0
  14. package/dist/commands/workflow/pipeline-runner.js +393 -0
  15. package/dist/commands/workflow/runner.d.ts +26 -0
  16. package/dist/commands/workflow/runner.js +119 -0
  17. package/dist/commands/workflow/workflow-runner.d.ts +26 -0
  18. package/dist/commands/workflow/workflow-runner.js +119 -0
  19. package/dist/index.js +4 -0
  20. package/dist/phases/app-store-generation/__tests__/screenshot-composer.test.js +452 -32
  21. package/dist/phases/app-store-generation/assets/inter-latin-ext.woff2 +0 -0
  22. package/dist/phases/app-store-generation/assets/inter-latin.woff2 +0 -0
  23. package/dist/phases/app-store-generation/inter-font.d.ts +20 -0
  24. package/dist/phases/app-store-generation/inter-font.js +49 -0
  25. package/dist/phases/app-store-generation/screenshot-composer.js +183 -19
  26. package/dist/phases/code-implementation/analyzer-helpers.d.ts +28 -0
  27. package/dist/phases/code-implementation/analyzer-helpers.js +177 -0
  28. package/dist/phases/code-implementation/analyzer.d.ts +32 -0
  29. package/dist/phases/code-implementation/analyzer.js +629 -0
  30. package/dist/phases/code-implementation/context-fetcher.d.ts +17 -0
  31. package/dist/phases/code-implementation/context-fetcher.js +86 -0
  32. package/dist/phases/code-implementation/mcp-server.d.ts +1 -0
  33. package/dist/phases/code-implementation/mcp-server.js +93 -0
  34. package/dist/phases/code-implementation/prompts-improvement.d.ts +5 -0
  35. package/dist/phases/code-implementation/prompts-improvement.js +108 -0
  36. package/dist/phases/code-implementation-verification/verifier.d.ts +31 -0
  37. package/dist/phases/code-implementation-verification/verifier.js +196 -0
  38. package/dist/phases/code-refine/analyzer.d.ts +41 -0
  39. package/dist/phases/code-refine/analyzer.js +561 -0
  40. package/dist/phases/code-refine/context-fetcher.d.ts +94 -0
  41. package/dist/phases/code-refine/context-fetcher.js +423 -0
  42. package/dist/phases/code-refine-verification/analysis/llm-analyzer.d.ts +22 -0
  43. package/dist/phases/code-refine-verification/analysis/llm-analyzer.js +134 -0
  44. package/dist/phases/code-refine-verification/verifier.d.ts +47 -0
  45. package/dist/phases/code-refine-verification/verifier.js +597 -0
  46. package/dist/phases/code-review/analyzer.d.ts +29 -0
  47. package/dist/phases/code-review/analyzer.js +363 -0
  48. package/dist/phases/code-review/context-fetcher.d.ts +92 -0
  49. package/dist/phases/code-review/context-fetcher.js +296 -0
  50. package/dist/phases/feature-analysis/analyzer-helpers.d.ts +10 -0
  51. package/dist/phases/feature-analysis/analyzer-helpers.js +47 -0
  52. package/dist/phases/feature-analysis/analyzer.d.ts +11 -0
  53. package/dist/phases/feature-analysis/analyzer.js +208 -0
  54. package/dist/phases/feature-analysis/context-fetcher.d.ts +26 -0
  55. package/dist/phases/feature-analysis/context-fetcher.js +134 -0
  56. package/dist/phases/feature-analysis/http-fallback.d.ts +20 -0
  57. package/dist/phases/feature-analysis/http-fallback.js +95 -0
  58. package/dist/phases/feature-analysis/mcp-server.d.ts +1 -0
  59. package/dist/phases/feature-analysis/mcp-server.js +144 -0
  60. package/dist/phases/feature-analysis/prompts-improvement.d.ts +8 -0
  61. package/dist/phases/feature-analysis/prompts-improvement.js +109 -0
  62. package/dist/phases/feature-analysis-verification/verifier.d.ts +37 -0
  63. package/dist/phases/feature-analysis-verification/verifier.js +147 -0
  64. package/dist/phases/growth-analysis/context.d.ts +2 -2
  65. package/dist/phases/growth-analysis/context.js +18 -4
  66. package/dist/phases/growth-analysis/index.d.ts +2 -0
  67. package/dist/phases/growth-analysis/index.js +21 -13
  68. package/dist/phases/technical-design/analyzer-helpers.d.ts +25 -0
  69. package/dist/phases/technical-design/analyzer-helpers.js +39 -0
  70. package/dist/phases/technical-design/analyzer.d.ts +21 -0
  71. package/dist/phases/technical-design/analyzer.js +461 -0
  72. package/dist/phases/technical-design/context-fetcher.d.ts +12 -0
  73. package/dist/phases/technical-design/context-fetcher.js +39 -0
  74. package/dist/phases/technical-design/http-fallback.d.ts +17 -0
  75. package/dist/phases/technical-design/http-fallback.js +151 -0
  76. package/dist/phases/technical-design/mcp-server.d.ts +1 -0
  77. package/dist/phases/technical-design/mcp-server.js +157 -0
  78. package/dist/phases/technical-design/prompts-improvement.d.ts +5 -0
  79. package/dist/phases/technical-design/prompts-improvement.js +93 -0
  80. package/dist/phases/technical-design-verification/verifier.d.ts +53 -0
  81. package/dist/phases/technical-design-verification/verifier.js +170 -0
  82. package/dist/services/feature-branches.d.ts +77 -0
  83. package/dist/services/feature-branches.js +205 -0
  84. package/dist/services/video/device-frames.d.ts +1 -1
  85. package/dist/services/video/device-frames.js +81 -3
  86. package/dist/services/video/index.d.ts +1 -1
  87. package/dist/services/video/index.js +12 -6
  88. package/dist/services/video/screenshot-generator.js +5 -8
  89. package/dist/services/video/video-assembler.js +2 -6
  90. package/dist/types/index.d.ts +2 -0
  91. package/dist/utils/validation.d.ts +11 -2
  92. package/dist/utils/validation.js +93 -6
  93. package/dist/workflow-runner/config/phase-configs.d.ts +5 -0
  94. package/dist/workflow-runner/config/phase-configs.js +120 -0
  95. package/dist/workflow-runner/core/feature-filter.d.ts +16 -0
  96. package/dist/workflow-runner/core/feature-filter.js +46 -0
  97. package/dist/workflow-runner/core/index.d.ts +8 -0
  98. package/dist/workflow-runner/core/index.js +12 -0
  99. package/dist/workflow-runner/core/pipeline-evaluator.d.ts +24 -0
  100. package/dist/workflow-runner/core/pipeline-evaluator.js +32 -0
  101. package/dist/workflow-runner/core/state-manager.d.ts +24 -0
  102. package/dist/workflow-runner/core/state-manager.js +42 -0
  103. package/dist/workflow-runner/core/workflow-logger.d.ts +20 -0
  104. package/dist/workflow-runner/core/workflow-logger.js +65 -0
  105. package/dist/workflow-runner/executors/phase-executor.d.ts +8 -0
  106. package/dist/workflow-runner/executors/phase-executor.js +248 -0
  107. package/dist/workflow-runner/feature-workflow-runner.d.ts +26 -0
  108. package/dist/workflow-runner/feature-workflow-runner.js +119 -0
  109. package/dist/workflow-runner/index.d.ts +2 -0
  110. package/dist/workflow-runner/index.js +2 -0
  111. package/dist/workflow-runner/pipeline-runner.d.ts +17 -0
  112. package/dist/workflow-runner/pipeline-runner.js +393 -0
  113. package/dist/workflow-runner/workflow-processor.d.ts +54 -0
  114. package/dist/workflow-runner/workflow-processor.js +170 -0
  115. package/package.json +2 -2
  116. package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.d.ts +0 -4
  117. package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.js +0 -133
  118. package/dist/services/lifecycle-agent/__tests__/transition-rules.test.d.ts +0 -4
  119. package/dist/services/lifecycle-agent/__tests__/transition-rules.test.js +0 -336
  120. package/dist/services/lifecycle-agent/index.d.ts +0 -24
  121. package/dist/services/lifecycle-agent/index.js +0 -25
  122. package/dist/services/lifecycle-agent/phase-criteria.d.ts +0 -57
  123. package/dist/services/lifecycle-agent/phase-criteria.js +0 -335
  124. package/dist/services/lifecycle-agent/transition-rules.d.ts +0 -60
  125. package/dist/services/lifecycle-agent/transition-rules.js +0 -184
  126. package/dist/services/lifecycle-agent/types.d.ts +0 -190
  127. package/dist/services/lifecycle-agent/types.js +0 -12
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Inter font (variable weight) embedded as base64 WOFF2.
3
+ * Latin + Latin-ext subsets cover Western European languages.
4
+ * Font files are loaded from assets/ at runtime and cached in memory.
5
+ *
6
+ * Source: Google Fonts Inter v20, weights 100-900 (variable)
7
+ * Total size: ~130KB raw WOFF2 (Latin 47KB + Latin-ext 83KB)
8
+ *
9
+ * CJK limitation: Inter does not include Chinese, Japanese, or Korean
10
+ * glyphs. Text in these scripts falls back to the system font stack
11
+ * (-apple-system, BlinkMacSystemFont, etc.). To add CJK coverage,
12
+ * embed Noto Sans CJK subsets (~300-500KB per script), which is not
13
+ * included here to keep the composition payload reasonable.
14
+ */
15
+ import { readFileSync } from 'fs';
16
+ import { join } from 'path';
17
+ const ASSETS_DIR = join(import.meta.dirname, 'assets');
18
+ let cachedStyle = null;
19
+ /**
20
+ * Returns a <style> block with @font-face declarations for Inter.
21
+ * Embed this in <head> to load Inter without any network requests.
22
+ * Font data is read from disk on first call and cached thereafter.
23
+ */
24
+ export function getInterFontStyle() {
25
+ if (cachedStyle) {
26
+ return cachedStyle;
27
+ }
28
+ const latinB64 = readFileSync(join(ASSETS_DIR, 'inter-latin.woff2')).toString('base64');
29
+ const latinExtB64 = readFileSync(join(ASSETS_DIR, 'inter-latin-ext.woff2')).toString('base64');
30
+ cachedStyle = `<style>
31
+ @font-face {
32
+ font-family: 'Inter';
33
+ font-style: normal;
34
+ font-weight: 100 900;
35
+ font-display: swap;
36
+ src: url(data:font/woff2;base64,${latinB64}) format('woff2');
37
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
38
+ }
39
+ @font-face {
40
+ font-family: 'Inter';
41
+ font-style: normal;
42
+ font-weight: 100 900;
43
+ font-display: swap;
44
+ src: url(data:font/woff2;base64,${latinExtB64}) format('woff2');
45
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
46
+ }
47
+ </style>`;
48
+ return cachedStyle;
49
+ }
@@ -11,6 +11,8 @@ import { tmpdir } from 'os';
11
11
  import { join } from 'path';
12
12
  import { generateDeviceFrameHtml, } from '../../services/video/device-frames.js';
13
13
  import { logError, logInfo, logWarning } from '../../utils/logger.js';
14
+ import { ensurePlaywright } from '../../utils/validation.js';
15
+ import { getInterFontStyle } from './inter-font.js';
14
16
  import { STORE_SCREENSHOT_SIZES } from './prompts.js';
15
17
  /**
16
18
  * Compose a single store screenshot for a specific device type
@@ -60,14 +62,18 @@ async function loadChromium() {
60
62
  return null;
61
63
  }
62
64
  }
65
+ async function isPlaywrightAvailable() {
66
+ return (await loadChromium()) !== null;
67
+ }
63
68
  /**
64
69
  * Map AI device_frame string to our DeviceFrameType
65
70
  */
66
71
  function mapDeviceFrame(frame) {
67
72
  switch (frame.toLowerCase()) {
68
73
  case 'iphone':
69
- case 'ipad':
70
74
  return 'iphone';
75
+ case 'ipad':
76
+ return 'ipad';
71
77
  case 'macbook':
72
78
  return 'macbook';
73
79
  case 'browser':
@@ -76,17 +82,119 @@ function mapDeviceFrame(frame) {
76
82
  return 'iphone';
77
83
  }
78
84
  }
85
+ /** Common named CSS colors likely in AI output. */
86
+ const NAMED_COLORS = {
87
+ white: [255, 255, 255],
88
+ black: [0, 0, 0],
89
+ red: [255, 0, 0],
90
+ green: [0, 128, 0],
91
+ blue: [0, 0, 255],
92
+ yellow: [255, 255, 0],
93
+ cyan: [0, 255, 255],
94
+ magenta: [255, 0, 255],
95
+ gray: [128, 128, 128],
96
+ grey: [128, 128, 128],
97
+ orange: [255, 165, 0],
98
+ purple: [128, 0, 128],
99
+ pink: [255, 192, 203],
100
+ };
101
+ /**
102
+ * Parse a CSS color string to [R, G, B] (0-255 each).
103
+ * Supports hex (#rgb, #rrggbb, #rrggbbaa), rgb(), rgba(), and named colors.
104
+ */
105
+ function parseColor(color) {
106
+ const s = color.trim().toLowerCase();
107
+ if (NAMED_COLORS[s]) {
108
+ return NAMED_COLORS[s];
109
+ }
110
+ const rgbMatch = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/.exec(s);
111
+ if (rgbMatch) {
112
+ return [parseInt(rgbMatch[1]), parseInt(rgbMatch[2]), parseInt(rgbMatch[3])];
113
+ }
114
+ const hexMatch = /^#?([0-9a-f]{3,8})$/i.exec(s);
115
+ if (!hexMatch) {
116
+ return null;
117
+ }
118
+ const h = hexMatch[1];
119
+ if (h.length === 3) {
120
+ return [
121
+ parseInt(h[0] + h[0], 16),
122
+ parseInt(h[1] + h[1], 16),
123
+ parseInt(h[2] + h[2], 16),
124
+ ];
125
+ }
126
+ if (h.length >= 6) {
127
+ return [
128
+ parseInt(h.slice(0, 2), 16),
129
+ parseInt(h.slice(2, 4), 16),
130
+ parseInt(h.slice(4, 6), 16),
131
+ ];
132
+ }
133
+ return null;
134
+ }
135
+ /**
136
+ * Relative luminance (0-1) per WCAG 2.x sRGB formula.
137
+ * Returns 1.0 for unparseable input — safe default that produces dark overlay.
138
+ */
139
+ function colorLuminance(color) {
140
+ const rgb = parseColor(color);
141
+ if (!rgb) {
142
+ return 1.0;
143
+ }
144
+ const toLinear = (c) => {
145
+ const s = c / 255;
146
+ return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
147
+ };
148
+ return (0.2126 * toLinear(rgb[0]) +
149
+ 0.7152 * toLinear(rgb[1]) +
150
+ 0.0722 * toLinear(rgb[2]));
151
+ }
152
+ /** Hermite interpolation for smooth 0→1 transition. */
153
+ function smoothstep(edge0, edge1, x) {
154
+ const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0)));
155
+ return t * t * (3 - 2 * t);
156
+ }
157
+ /**
158
+ * Returns overlay/shadow rgba() values that contrast with the given text color.
159
+ * Uses smooth interpolation — strong at extremes, reduced for mid-range text
160
+ * to avoid hurting contrast in ambiguous cases.
161
+ */
162
+ function contrastStyles(textColor) {
163
+ const lum = colorLuminance(textColor);
164
+ // Smooth 0→1: 0 = dark text, 1 = light text
165
+ const lightness = smoothstep(0.15, 0.65, lum);
166
+ // Overlay channel: black (0) for light text, white (255) for dark text
167
+ const ch = Math.round(255 * (1 - lightness));
168
+ // Certainty: 1 at extremes, 0 at the ambiguous middle
169
+ const certainty = Math.abs(lightness - 0.5) * 2;
170
+ // Base alphas — blend between dark-overlay and light-overlay presets
171
+ const baseOverlay = 0.25 * lightness + 0.2 * (1 - lightness);
172
+ const baseShadow = 0.35 * lightness + 0.4 * (1 - lightness);
173
+ const baseShadowLight = 0.25 * lightness + 0.3 * (1 - lightness);
174
+ // Scale by certainty — 50% minimum even at peak ambiguity
175
+ const scale = 0.5 + 0.5 * certainty;
176
+ const fmt = (alpha) => (alpha * scale).toFixed(2);
177
+ return {
178
+ overlayColor: `rgba(${ch},${ch},${ch},${fmt(baseOverlay)})`,
179
+ shadowColor: `rgba(${ch},${ch},${ch},${fmt(baseShadow)})`,
180
+ shadowColorLight: `rgba(${ch},${ch},${ch},${fmt(baseShadowLight)})`,
181
+ };
182
+ }
79
183
  /**
80
184
  * Create composition HTML: gradient background + text overlay + device-framed screenshot
81
185
  */
82
186
  function createCompositionHtml(framedScreenshotDataUrl, spec, targetWidth, targetHeight) {
83
187
  const isPortrait = targetHeight > targetWidth;
84
188
  const textColor = spec.text_color || '#ffffff';
189
+ const fontStyle = getInterFontStyle();
190
+ const fontFamily = `'Inter', -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', sans-serif`;
191
+ const { overlayColor, shadowColor, shadowColorLight } = contrastStyles(textColor);
85
192
  if (isPortrait) {
86
193
  // Portrait: text at top, device below
87
194
  return `<!DOCTYPE html>
88
195
  <html>
89
196
  <head>
197
+ ${fontStyle}
90
198
  <style>
91
199
  * { margin: 0; padding: 0; box-sizing: border-box; }
92
200
  body {
@@ -97,35 +205,60 @@ function createCompositionHtml(framedScreenshotDataUrl, spec, targetWidth, targe
97
205
  flex-direction: column;
98
206
  align-items: center;
99
207
  justify-content: flex-start;
100
- font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', sans-serif;
208
+ font-family: ${fontFamily};
101
209
  overflow: hidden;
102
210
  }
103
211
  .text-area {
104
- padding: ${Math.round(targetHeight * 0.06)}px ${Math.round(targetWidth * 0.08)}px;
212
+ padding: ${Math.round(targetHeight * 0.05)}px ${Math.round(targetWidth * 0.08)}px;
105
213
  text-align: center;
106
214
  flex-shrink: 0;
215
+ max-height: 28%;
216
+ position: relative;
217
+ }
218
+ .text-area::after {
219
+ content: '';
220
+ position: absolute;
221
+ inset: 0;
222
+ background: linear-gradient(to bottom, ${overlayColor} 0%, transparent 100%);
223
+ border-radius: inherit;
224
+ pointer-events: none;
107
225
  }
108
226
  .headline {
109
- font-size: ${Math.round(targetWidth * 0.07)}px;
227
+ font-size: ${Math.round(targetWidth * 0.1)}px;
110
228
  font-weight: 800;
111
229
  color: ${textColor};
112
230
  line-height: 1.15;
113
231
  letter-spacing: -0.02em;
114
232
  margin-bottom: ${Math.round(targetHeight * 0.01)}px;
233
+ text-shadow: 0 2px 12px ${shadowColor};
234
+ position: relative;
235
+ z-index: 1;
236
+ display: -webkit-box;
237
+ -webkit-line-clamp: 3;
238
+ -webkit-box-orient: vertical;
239
+ overflow: hidden;
115
240
  }
116
241
  .subheadline {
117
- font-size: ${Math.round(targetWidth * 0.035)}px;
242
+ font-size: ${Math.round(targetWidth * 0.05)}px;
118
243
  font-weight: 400;
119
244
  color: ${textColor};
120
245
  opacity: 0.85;
121
246
  line-height: 1.3;
247
+ text-shadow: 0 1px 8px ${shadowColorLight};
248
+ position: relative;
249
+ z-index: 1;
250
+ display: -webkit-box;
251
+ -webkit-line-clamp: 2;
252
+ -webkit-box-orient: vertical;
253
+ overflow: hidden;
122
254
  }
123
255
  .device-area {
124
256
  flex: 1;
125
257
  display: flex;
126
- align-items: flex-start;
258
+ align-items: center;
127
259
  justify-content: center;
128
260
  overflow: hidden;
261
+ min-height: 0;
129
262
  padding: 0 ${Math.round(targetWidth * 0.02)}px;
130
263
  }
131
264
  .device-area img {
@@ -148,6 +281,7 @@ function createCompositionHtml(framedScreenshotDataUrl, spec, targetWidth, targe
148
281
  return `<!DOCTYPE html>
149
282
  <html>
150
283
  <head>
284
+ ${fontStyle}
151
285
  <style>
152
286
  * { margin: 0; padding: 0; box-sizing: border-box; }
153
287
  body {
@@ -156,31 +290,60 @@ function createCompositionHtml(framedScreenshotDataUrl, spec, targetWidth, targe
156
290
  background: ${spec.background_gradient};
157
291
  display: flex;
158
292
  align-items: center;
159
- font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', sans-serif;
293
+ font-family: ${fontFamily};
160
294
  overflow: hidden;
161
295
  }
162
296
  .text-area {
163
- width: 40%;
297
+ flex: 0 1 auto;
298
+ min-width: 28%;
299
+ max-width: 44%;
164
300
  padding: 0 ${Math.round(targetWidth * 0.04)}px;
301
+ display: flex;
302
+ flex-direction: column;
303
+ justify-content: center;
165
304
  text-align: left;
305
+ position: relative;
306
+ }
307
+ .text-area::after {
308
+ content: '';
309
+ position: absolute;
310
+ inset: 0;
311
+ background: linear-gradient(to right, ${overlayColor} 0%, transparent 100%);
312
+ border-radius: inherit;
313
+ pointer-events: none;
166
314
  }
167
315
  .headline {
168
- font-size: ${Math.round(targetWidth * 0.04)}px;
316
+ font-size: ${Math.round(targetWidth * 0.055)}px;
169
317
  font-weight: 800;
170
318
  color: ${textColor};
171
319
  line-height: 1.15;
172
320
  letter-spacing: -0.02em;
173
321
  margin-bottom: ${Math.round(targetHeight * 0.02)}px;
322
+ text-shadow: 0 2px 12px ${shadowColor};
323
+ position: relative;
324
+ z-index: 1;
325
+ display: -webkit-box;
326
+ -webkit-line-clamp: 3;
327
+ -webkit-box-orient: vertical;
328
+ overflow: hidden;
174
329
  }
175
330
  .subheadline {
176
- font-size: ${Math.round(targetWidth * 0.02)}px;
331
+ font-size: ${Math.round(targetWidth * 0.028)}px;
177
332
  font-weight: 400;
178
333
  color: ${textColor};
179
334
  opacity: 0.85;
180
335
  line-height: 1.4;
336
+ text-shadow: 0 1px 8px ${shadowColorLight};
337
+ position: relative;
338
+ z-index: 1;
339
+ display: -webkit-box;
340
+ -webkit-line-clamp: 2;
341
+ -webkit-box-orient: vertical;
342
+ overflow: hidden;
181
343
  }
182
344
  .device-area {
183
- width: 60%;
345
+ flex: 1 1 0;
346
+ min-width: 0;
184
347
  height: 100%;
185
348
  display: flex;
186
349
  align-items: center;
@@ -222,10 +385,8 @@ function escapeHtml(str) {
222
385
  * 3. Compose framed PNG + text overlay + gradient background at exact store dimensions → final PNG
223
386
  */
224
387
  export async function generateStoreScreenshots(specs, options) {
225
- const chromium = await loadChromium();
226
- if (!chromium) {
227
- throw new Error('Playwright is not installed. Install it with: npm install playwright && npx playwright install chromium');
228
- }
388
+ await ensurePlaywright(isPlaywrightAvailable);
389
+ const chromium = (await loadChromium());
229
390
  const { productId, targetStore, locale: _locale, verbose } = options;
230
391
  const outputDir = join(tmpdir(), 'edsger-app-store-assets', productId);
231
392
  await mkdir(outputDir, { recursive: true });
@@ -242,10 +403,13 @@ export async function generateStoreScreenshots(specs, options) {
242
403
  logInfo(`Generating screenshot ${specIdx + 1}/${specs.length}: "${spec.headline}"`);
243
404
  // Step 1: Render the AI HTML template at a base viewport to get raw app mock
244
405
  const deviceFrame = mapDeviceFrame(spec.device_frame);
245
- const isPhone = deviceFrame === 'iphone' || spec.device_frame === 'ipad';
406
+ const isPhone = deviceFrame === 'iphone';
407
+ const isTablet = deviceFrame === 'ipad';
246
408
  const baseViewport = isPhone
247
409
  ? { width: 390, height: 844 }
248
- : { width: 1280, height: 720 };
410
+ : isTablet
411
+ ? { width: 1024, height: 1366 }
412
+ : { width: 1280, height: 720 };
249
413
  let rawScreenshotBuffer;
250
414
  try {
251
415
  const rawContext = await browser.newContext({
@@ -271,8 +435,8 @@ export async function generateStoreScreenshots(specs, options) {
271
435
  // Use tight canvas dimensions so the device fills most of the frame image.
272
436
  // This prevents excess padding that would make the device appear small in composition.
273
437
  const rawDataUrl = `data:image/png;base64,${rawScreenshotBuffer.toString('base64')}`;
274
- const frameWidth = deviceFrame === 'iphone' ? 500 : 1600;
275
- const frameHeight = deviceFrame === 'iphone' ? 1050 : 1000;
438
+ const frameWidth = deviceFrame === 'iphone' ? 500 : deviceFrame === 'ipad' ? 900 : 1600;
439
+ const frameHeight = deviceFrame === 'iphone' ? 1050 : deviceFrame === 'ipad' ? 1200 : 1000;
276
440
  const frameHtml = generateDeviceFrameHtml(rawDataUrl, deviceFrame, {
277
441
  background: 'transparent',
278
442
  shadow: true,
@@ -0,0 +1,28 @@
1
+ import { EdsgerConfig } from '../../types/index.js';
2
+ import { ChecklistPhaseContext } from '../../services/checklist.js';
3
+ import { ChecklistVerificationResult } from '../code-implementation-verification/agent.js';
4
+ import { CodeImplementationResult } from './index.js';
5
+ export interface VerificationCycleResult {
6
+ passed: boolean;
7
+ verificationResult: ChecklistVerificationResult | null;
8
+ nextPrompt: string | null;
9
+ }
10
+ /**
11
+ * Perform a complete verification cycle:
12
+ * 1. Verify code implementation against checklist
13
+ * 2. Log results to audit logs
14
+ * 3. Create improvement prompt if verification failed
15
+ */
16
+ export declare function performVerificationCycle(branchName: string, baseBranch: string, checklistContext: ChecklistPhaseContext | null, featureId: string, featureName: string, featureDescription: string | undefined, config: EdsgerConfig, currentIteration: number, maxIterations: number, verbose?: boolean): Promise<VerificationCycleResult>;
17
+ /**
18
+ * Build successful implementation result
19
+ */
20
+ export declare function buildImplementationResult(featureId: string, branchName: string, implementationSummary: string, filesModified: string[], commitHash: string, iterations: number, checklistResults?: any[], checklistItemResults?: any[]): CodeImplementationResult;
21
+ /**
22
+ * Build verification failure result
23
+ */
24
+ export declare function buildVerificationFailureResult(featureId: string, branchName: string, implementationSummary: string, filesModified: string[], commitHash: string, verificationResult: ChecklistVerificationResult, iterations: number): CodeImplementationResult;
25
+ /**
26
+ * Build error result for no implementation
27
+ */
28
+ export declare function buildNoResultsError(featureId: string, branchName: string): CodeImplementationResult;
@@ -0,0 +1,177 @@
1
+ import { logInfo, logError } from '../../utils/logger.js';
2
+ import { logFeatureVerificationEvent } from '../../services/audit-logs.js';
3
+ import { formatChecklistsForContext, processChecklistItemResultsFromResponse, } from '../../services/checklist.js';
4
+ import { verifyCodeImplementationCompliance, } from '../code-implementation-verification/agent.js';
5
+ import { createCodeImplementationImprovementPrompt } from './prompts.js';
6
+ /**
7
+ * Perform a complete verification cycle:
8
+ * 1. Verify code implementation against checklist
9
+ * 2. Log results to audit logs
10
+ * 3. Create improvement prompt if verification failed
11
+ */
12
+ export async function performVerificationCycle(branchName, baseBranch, checklistContext, featureId, featureName, featureDescription, config, currentIteration, maxIterations, verbose) {
13
+ // If no checklist, verification passes automatically
14
+ if (!checklistContext || checklistContext.checklists.length === 0) {
15
+ if (verbose) {
16
+ logInfo('ā„¹ļø No checklists to verify - skipping verification');
17
+ }
18
+ return {
19
+ passed: true,
20
+ verificationResult: null,
21
+ nextPrompt: null,
22
+ };
23
+ }
24
+ if (verbose) {
25
+ logInfo(`\nšŸ” Verifying code implementation (iteration ${currentIteration}/${maxIterations})...`);
26
+ }
27
+ // Format checklist context for verification
28
+ const checklistInfo = formatChecklistsForContext(checklistContext);
29
+ // Perform verification
30
+ const verificationResult = await verifyCodeImplementationCompliance({
31
+ featureId,
32
+ branchName,
33
+ baseBranch,
34
+ featureName,
35
+ featureDescription,
36
+ checklistContext: checklistInfo,
37
+ verbose,
38
+ }, config);
39
+ // Determine if verification passed
40
+ const verificationPassed = verificationResult.rejected_count === 0;
41
+ // Log verification result
42
+ if (verificationPassed) {
43
+ if (verbose) {
44
+ logInfo(`āœ… Code verification passed: ${verificationResult.confirmed_count} confirmed, ${verificationResult.uncertain_count} uncertain`);
45
+ }
46
+ await logFeatureVerificationEvent({
47
+ featureId,
48
+ phase: 'code_implementation',
49
+ iteration: currentIteration,
50
+ result: 'success',
51
+ verificationData: {
52
+ confirmed_count: verificationResult.confirmed_count,
53
+ rejected_count: verificationResult.rejected_count,
54
+ uncertain_count: verificationResult.uncertain_count,
55
+ summary: verificationResult.summary,
56
+ },
57
+ }, verbose);
58
+ // Create checklist results based on verification result
59
+ const checklistItemResults = verificationResult.item_verifications.map((item) => ({
60
+ checklist_item_id: item.checklist_item_id,
61
+ is_passed: item.is_satisfied && item.verification_status === 'confirmed',
62
+ value: item.verification_status,
63
+ notes: item.verification_reason || undefined,
64
+ }));
65
+ await processChecklistItemResultsFromResponse({ featureId }, 'code_implementation', checklistItemResults, verbose);
66
+ }
67
+ else {
68
+ logError(`āŒ Iteration ${currentIteration}: Code verification FAILED - ${verificationResult.rejected_count} items rejected, ${verificationResult.uncertain_count} uncertain`);
69
+ await logFeatureVerificationEvent({
70
+ featureId,
71
+ phase: 'code_implementation',
72
+ iteration: currentIteration,
73
+ result: 'error',
74
+ verificationData: {
75
+ confirmed_count: verificationResult.confirmed_count,
76
+ rejected_count: verificationResult.rejected_count,
77
+ uncertain_count: verificationResult.uncertain_count,
78
+ rejected_items: verificationResult.item_verifications
79
+ .filter((item) => item.verification_status === 'rejected')
80
+ .map((item) => ({
81
+ checklist_item_id: item.checklist_item_id,
82
+ reason: item.verification_reason || 'No reason provided',
83
+ concerns: item.concerns || [],
84
+ improvement_suggestions: item.improvement_suggestions || [],
85
+ })),
86
+ uncertain_items: verificationResult.item_verifications
87
+ .filter((item) => item.verification_status === 'uncertain')
88
+ .map((item) => ({
89
+ checklist_item_id: item.checklist_item_id,
90
+ reason: item.verification_reason || 'No reason provided',
91
+ concerns: item.concerns || [],
92
+ improvement_suggestions: item.improvement_suggestions || [],
93
+ })),
94
+ summary: verificationResult.summary,
95
+ overall_suggestions: verificationResult.overall_suggestions || [],
96
+ },
97
+ }, verbose);
98
+ }
99
+ // If verification failed and we have iterations left, create improvement prompt
100
+ let nextPrompt = null;
101
+ if (!verificationPassed && currentIteration < maxIterations) {
102
+ nextPrompt = createCodeImplementationImprovementPrompt(verificationResult, branchName, baseBranch);
103
+ if (verbose) {
104
+ logInfo(`\nšŸ“ Created improvement prompt for next iteration (${maxIterations - currentIteration} attempts remaining)`);
105
+ }
106
+ }
107
+ else if (!verificationPassed) {
108
+ // Max iterations reached - create checklist results based on final verification
109
+ if (verbose) {
110
+ logInfo(`\nšŸ“‹ Creating checklist results for final iteration (max iterations reached)`);
111
+ }
112
+ // Create checklist results for all items (both passed and failed)
113
+ const checklistItemResults = verificationResult.item_verifications.map((item) => ({
114
+ checklist_item_id: item.checklist_item_id,
115
+ is_passed: item.is_satisfied && item.verification_status === 'confirmed',
116
+ value: item.verification_status,
117
+ notes: item.verification_reason || undefined,
118
+ }));
119
+ await processChecklistItemResultsFromResponse({ featureId }, 'code_implementation', checklistItemResults, verbose);
120
+ }
121
+ return {
122
+ passed: verificationPassed,
123
+ verificationResult,
124
+ nextPrompt,
125
+ };
126
+ }
127
+ /**
128
+ * Build successful implementation result
129
+ */
130
+ export function buildImplementationResult(featureId, branchName, implementationSummary, filesModified, commitHash, iterations, checklistResults, checklistItemResults) {
131
+ return {
132
+ featureId,
133
+ branchName,
134
+ implementationSummary,
135
+ status: 'success',
136
+ message: 'Code implementation completed successfully',
137
+ filesModified,
138
+ commitHash,
139
+ data: {
140
+ checklist_results: checklistResults,
141
+ checklist_item_results: checklistItemResults,
142
+ },
143
+ };
144
+ }
145
+ /**
146
+ * Build verification failure result
147
+ */
148
+ export function buildVerificationFailureResult(featureId, branchName, implementationSummary, filesModified, commitHash, verificationResult, iterations) {
149
+ return {
150
+ featureId,
151
+ branchName,
152
+ implementationSummary,
153
+ status: 'error',
154
+ message: `Code implementation verification failed after ${iterations} iterations. Code committed for manual review.`,
155
+ filesModified,
156
+ commitHash,
157
+ data: {
158
+ verification_failed: true,
159
+ verification_summary: verificationResult.summary,
160
+ rejected_count: verificationResult.rejected_count,
161
+ uncertain_count: verificationResult.uncertain_count,
162
+ improvement_suggestions: verificationResult.overall_suggestions || [],
163
+ },
164
+ };
165
+ }
166
+ /**
167
+ * Build error result for no implementation
168
+ */
169
+ export function buildNoResultsError(featureId, branchName) {
170
+ return {
171
+ featureId,
172
+ branchName,
173
+ implementationSummary: null,
174
+ status: 'error',
175
+ message: 'Code implementation failed - no results produced',
176
+ };
177
+ }
@@ -0,0 +1,32 @@
1
+ import { EdsgerConfig } from '../../types/index.js';
2
+ import { ChecklistPhaseContext } from '../../services/checklist.js';
3
+ export interface CodeImplementationOptions {
4
+ featureId: string;
5
+ verbose?: boolean;
6
+ baseBranch?: string;
7
+ maxVerificationIterations?: number;
8
+ }
9
+ export interface CodeImplementationResult {
10
+ featureId: string;
11
+ branchName: string;
12
+ implementationSummary: string | null;
13
+ status: 'success' | 'error';
14
+ message: string;
15
+ filesModified?: string[];
16
+ commitHash?: string;
17
+ data?: {
18
+ checklist_results?: Array<{
19
+ checklist_id: string;
20
+ satisfied: boolean;
21
+ notes?: string;
22
+ }>;
23
+ checklist_item_results?: Array<{
24
+ checklist_item_id: string;
25
+ is_passed: boolean;
26
+ value?: any;
27
+ notes?: string;
28
+ }>;
29
+ [key: string]: any;
30
+ };
31
+ }
32
+ export declare const implementFeatureCode: (options: CodeImplementationOptions, config: EdsgerConfig, checklistContext?: ChecklistPhaseContext | null) => Promise<CodeImplementationResult>;