From 772d8438171de37d5960ffdbe91f5a2d7a4a8c6d Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Wed, 5 Aug 2026 17:08:23 +0300 Subject: [PATCH] Auto-deploy: 2026-08-05 17:08:23 --- profile_data.js | 8 ++++++-- server/generate_cv.php | 29 ++++++++++++++++++++++++++++- server/profile.json | 20 ++++++++++---------- 3 files changed, 44 insertions(+), 13 deletions(-) diff --git a/profile_data.js b/profile_data.js index d346ca8..9167bed 100644 --- a/profile_data.js +++ b/profile_data.js @@ -167,8 +167,12 @@ const PROFILE_DATA = { bullets: [ { text: 'Delivered 30+ production-grade mobile applications across e-commerce, delivery, healthcare and news sectors, acting as technical decision-maker for early-stage clients.', tags: ['delivery', 'leadership', 'mobile', 'scale'] }, { text: 'Migrated legacy Android (Java) applications to Flutter, cutting maintenance complexity and delivery timelines by roughly 50%.', tags: ['flutter', 'migration', 'mobile'] }, - { text: 'Built custom Flutter plugins and tailored API layers to integrate with complex legacy backend systems.', tags: ['flutter', 'integration', 'architecture'] }, - { text: 'Owned App Store and Google Play release management end-to-end, from store listings and review submission through versioned production rollout.', tags: ['mobile', 'release', 'ios', 'android', 'delivery'] } + // Ordered by how often each is actually screened for in mobile roles. + // When the tailoring model returns fewer bullets than the page allows, + // the generator tops up in this order — so the more commonly required + // evidence fills the slot first. + { text: 'Owned App Store and Google Play release management end-to-end, from store listings and review submission through versioned production rollout.', tags: ['mobile', 'release', 'ios', 'android', 'delivery'] }, + { text: 'Built custom Flutter plugins and tailored API layers to integrate with complex legacy backend systems.', tags: ['flutter', 'integration', 'architecture'] } ] } ], diff --git a/server/generate_cv.php b/server/generate_cv.php index b397454..6a591fd 100644 --- a/server/generate_cv.php +++ b/server/generate_cv.php @@ -401,7 +401,8 @@ PROMPT; if ($b !== '') $bullets[] = $b; } } - if (empty($bullets)) { + $fromAi = !empty($bullets); + if (!$fromAi) { foreach ($role['bullets'] as $b) { $bullets[] = $b['text']; } @@ -413,6 +414,32 @@ PROMPT; $cap = $d['caps'][$ri] ?? 2; $bullets = array_slice($bullets, 0, $cap); + // The model also errs the other way, returning fewer bullets than + // the layout has room for. An empty slot is wasted page space, and + // worse, it can strand a skill in the Technical Skills line with no + // supporting evidence anywhere in the experience. Top up from the + // master bullets the model did not pick, keeping its ordering first. + // Which master bullets did the model already use? Match on the + // "roleIndex.bulletIndex" ids it returns, not on the text: it + // rewrites wording to carry the job's keywords, so comparing strings + // would treat a rewritten bullet as a new one and print the same + // fact twice. Without usable ids we cannot tell what was used, so we + // leave the role as-is rather than risk a duplicate. + $usedIdx = []; + foreach (($aiRoles[$ri]['bullet_ids'] ?? []) as $bid) { + $parts = explode('.', (string)$bid); + if (count($parts) === 2) $usedIdx[] = (int)$parts[1]; + } + + if ($fromAi && $usedIdx && count($bullets) < $cap) { + foreach ($role['bullets'] as $bi => $master) { + if (count($bullets) >= $cap) break; + if (!in_array($bi, $usedIdx, true)) { + $bullets[] = $master['text']; + } + } + } + $experienceBlock .= '