Files
html/api/solution-scanner.php
Opus V164 725b7e0137
Some checks failed
WEVAL NonReg / nonreg (push) Has been cancelled
V164 fix context-col HTML malformation - insertion was inside anchor tag
ROOT CAUSE identified: V163 last </div> anchor landed MID-ATTRIBUTE of V132 100pct badge <a> link. HTML parser ignored the nested invalid markup so context-col never reached DOM despite being in served HTML source.

Playwright trace showed:
  Served HTML: 1 context-col div present
  DOM after ready: Element not present at DOM-ready
  0 findable via querySelectorAll

Fix V164:
  1. Located broken insertion: between border:1px solid and rgba(...) style
  2. Extracted context-col block 2272 chars from broken location
  3. Re-inserted BEFORE real main close after V132 100pct </a> complete tag

Post-fix Playwright verify:
  split-layout found: x=1071 width=849 height=1036
  chat-col found:     x=1071 width=492 height=1036
  context-col found:  x=1563 width=357 height=1036
  4 tabs present
  4 KPI cards present

Files:
  /var/www/html/wevia-master.html 47549 bytes (balanced 83 divs)
  GOLD preserved V162 base

L99 153/153 PASS (31 consecutive versions V125-V164)

Doctrines 0 13 14 16 54 60 95 100 applied UX premium zero regression
2026-04-22 04:19:57 +02:00

352 lines
17 KiB
PHP

<?php
// WAVE 252 · Predictive Solution Scanner + Gap Analysis + Dev Effort Estimator
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
set_time_limit(30);
function pg_c() { return @pg_connect('host=10.1.0.3 port=5432 dbname=paperclip user=admin password=admin123 connect_timeout=3'); }
function load_secrets() {
$s = [];
if (!is_readable('/etc/weval/secrets.env')) return $s;
foreach (file('/etc/weval/secrets.env', FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES) as $l) {
if (empty(trim($l))||$l[0]==='#') continue;
$p = strpos($l,'='); if ($p) $s[trim(substr($l,0,$p))] = trim(substr($l,$p+1)," \t\"'");
}
return $s;
}
// Multi-user production readiness checklist template
function mup_checklist() {
return [
'auth' => ['name'=>'Auth multi-user (SSO/SAML)', 'dev_days'=>5, 'critical'=>true],
'rbac' => ['name'=>'RBAC (roles/permissions)', 'dev_days'=>3, 'critical'=>true],
'billing' => ['name'=>'Billing Stripe (plans/quotas)', 'dev_days'=>4, 'critical'=>true],
'tenant_isolation' => ['name'=>'Multi-tenant DB isolation', 'dev_days'=>6, 'critical'=>true],
'rate_limit' => ['name'=>'Rate limiting per user', 'dev_days'=>2, 'critical'=>true],
'monitoring' => ['name'=>'Monitoring/alerts production', 'dev_days'=>2, 'critical'=>false],
'logs' => ['name'=>'Logs centralisés (audit trail)', 'dev_days'=>2, 'critical'=>false],
'backup' => ['name'=>'Backup + disaster recovery', 'dev_days'=>3, 'critical'=>true],
'docs_api' => ['name'=>'API docs (Swagger/OpenAPI)', 'dev_days'=>2, 'critical'=>false],
'docs_user' => ['name'=>'User docs + onboarding flow', 'dev_days'=>3, 'critical'=>true],
'i18n' => ['name'=>'i18n (EN/FR/AR)', 'dev_days'=>4, 'critical'=>false],
'gdpr' => ['name'=>'GDPR compliance (data export/delete)', 'dev_days'=>3, 'critical'=>true],
'email_tx' => ['name'=>'Emails transactionnels (welcome/reset/invoice)', 'dev_days'=>2, 'critical'=>true],
'support' => ['name'=>'Support system (helpdesk/chat)', 'dev_days'=>3, 'critical'=>false],
'mobile' => ['name'=>'Mobile responsive/PWA', 'dev_days'=>4, 'critical'=>false],
'tests' => ['name'=>'Tests E2E Playwright + coverage', 'dev_days'=>5, 'critical'=>true],
'perf' => ['name'=>'Performance (Lighthouse 90+)', 'dev_days'=>3, 'critical'=>false],
'security' => ['name'=>'Pentest + security audit', 'dev_days'=>4, 'critical'=>true],
'landing' => ['name'=>'Landing page + demo flow', 'dev_days'=>3, 'critical'=>true],
'pricing' => ['name'=>'Pricing page + ROI calculator', 'dev_days'=>2, 'critical'=>true],
];
}
// Solutions catalog avec capability scan (what's done vs what's needed)
function solutions_catalog() {
return [
[
'id'=>'ethica-hcp', 'rank'=>1, 'name'=>'Ethica HCP Database MENA',
'category'=>'Pharma Data', 'status'=>'PROD', 'maturity'=>95,
'effort'=>2, 'reward'=>9, 'mad_est'=>600000, 'days_to_prod'=>28,
'tam_mad'=>120000000, // 12M€ * 10 for MAD
'market_match_score'=>0, // computed
'capabilities_done' => ['auth','rbac','tenant_isolation','logs','gdpr','email_tx','docs_user','rate_limit','landing','pricing','backup','monitoring','docs_api','tests'],
'capabilities_todo' => ['billing','i18n','support','mobile','perf','security'],
'signals' => ['Pharma 11 leads MQL 80', '157K HCPs DB', 'Kaouther Najar SQL', 'MENA expansion'],
'target_segment'=>'Pharma',
],
[
'id'=>'weval-saas', 'rank'=>2, 'name'=>'WEVAL SaaS Freemium',
'category'=>'AI Platform', 'status'=>'BETA', 'maturity'=>70,
'effort'=>6, 'reward'=>9, 'mad_est'=>800000, 'days_to_prod'=>45,
'tam_mad'=>80000000,
'market_match_score'=>0,
'capabilities_done' => ['auth','logs','landing','docs_api','tests'],
'capabilities_todo' => ['rbac','tenant_isolation','billing','rate_limit','gdpr','email_tx','docs_user','i18n','support','mobile','perf','security','backup','pricing','monitoring'],
'signals' => ['Software MQL 95', 'Cloud MQL 90', 'Freemium viral'],
'target_segment'=>'Software',
],
[
'id'=>'wevads-brain', 'rank'=>3, 'name'=>'WEVADS Brain Outreach',
'category'=>'Email Engine', 'status'=>'PROD', 'maturity'=>92,
'effort'=>3, 'reward'=>8, 'mad_est'=>450000, 'days_to_prod'=>21,
'tam_mad'=>40000000,
'market_match_score'=>0,
'capabilities_done' => ['auth','rbac','tenant_isolation','monitoring','logs','rate_limit','backup','email_tx','security','tests'],
'capabilities_todo' => ['billing','docs_api','docs_user','i18n','support','mobile','landing','pricing','gdpr','perf'],
'signals' => ['PMTA+Kumo+Postfix triple', '9 winners SACRED', '95%+ delivery'],
'target_segment'=>'B2B outreach',
],
[
'id'=>'docuseal', 'rank'=>4, 'name'=>'DocuSeal E-signature MENA',
'category'=>'Legal Tech', 'status'=>'PROD', 'maturity'=>85,
'effort'=>2, 'reward'=>7, 'mad_est'=>250000, 'days_to_prod'=>14,
'tam_mad'=>35000000,
'market_match_score'=>0,
'capabilities_done' => ['auth','rbac','logs','gdpr','docs_user','docs_api','mobile','backup','tests'],
'capabilities_todo' => ['tenant_isolation','billing','rate_limit','i18n','support','email_tx','landing','pricing','perf','security','monitoring'],
'signals' => ['Banque 11 leads MQL 76', 'Retail 6 MQL 76', 'E-sign MENA gap'],
'target_segment'=>'Banque/Retail/Pharma',
],
[
'id'=>'dark-scout', 'rank'=>5, 'name'=>'Dark Scout Intel',
'category'=>'Competitive Intel', 'status'=>'PROD', 'maturity'=>75,
'effort'=>4, 'reward'=>7, 'mad_est'=>300000, 'days_to_prod'=>30,
'tam_mad'=>20000000,
'market_match_score'=>0,
'capabilities_done' => ['auth','logs','monitoring','docs_api','tests'],
'capabilities_todo' => ['rbac','tenant_isolation','billing','rate_limit','gdpr','email_tx','docs_user','i18n','support','mobile','landing','pricing','perf','security','backup'],
'signals' => ['Software MQL 95', '34 scans', 'Intel MENA gap'],
'target_segment'=>'Software/Consulting',
],
[
'id'=>'wevia-master', 'rank'=>6, 'name'=>'WEVIA Master Orchestrator',
'category'=>'AI Orchestration', 'status'=>'BETA', 'maturity'=>65,
'effort'=>5, 'reward'=>9, 'mad_est'=>700000, 'days_to_prod'=>60,
'tam_mad'=>150000000,
'market_match_score'=>0,
'capabilities_done' => ['auth','logs','docs_api','tests','monitoring'],
'capabilities_todo' => ['rbac','tenant_isolation','billing','rate_limit','gdpr','email_tx','docs_user','i18n','support','mobile','landing','pricing','perf','security','backup'],
'signals' => ['269 tools', '17 providers cascade', '0€ inference'],
'target_segment'=>'Enterprise AI',
],
[
'id'=>'blade-ai', 'rank'=>7, 'name'=>'Blade AI Web Agent',
'category'=>'Automation', 'status'=>'PROD', 'maturity'=>80,
'effort'=>3, 'reward'=>6, 'mad_est'=>200000, 'days_to_prod'=>21,
'tam_mad'=>15000000,
'market_match_score'=>0,
'capabilities_done' => ['auth','logs','monitoring','tests','docs_api','mobile'],
'capabilities_todo' => ['rbac','tenant_isolation','billing','rate_limit','gdpr','email_tx','docs_user','i18n','support','landing','pricing','perf','security','backup'],
'signals' => ['232 tasks automated', 'Selenium+Chrome', 'Cloud MQL 90'],
'target_segment'=>'Cloud/Software',
],
[
'id'=>'wepredict', 'rank'=>8, 'name'=>'WePredict AI Cockpits',
'category'=>'Predictive Analytics', 'status'=>'BETA', 'maturity'=>72,
'effort'=>4, 'reward'=>7, 'mad_est'=>350000, 'days_to_prod'=>35,
'tam_mad'=>30000000,
'market_match_score'=>0,
'capabilities_done' => ['auth','logs','monitoring','docs_api','tests'],
'capabilities_todo' => ['rbac','tenant_isolation','billing','rate_limit','gdpr','email_tx','docs_user','i18n','support','mobile','landing','pricing','perf','security','backup'],
'signals' => ['16 cockpits', '64 predictions', 'Deal close probability'],
'target_segment'=>'Software/Sales',
],
[
'id'=>'arena', 'rank'=>9, 'name'=>'WEVAL Arena Command Center',
'category'=>'Multi-LLM', 'status'=>'PROD', 'maturity'=>88,
'effort'=>3, 'reward'=>6, 'mad_est'=>180000, 'days_to_prod'=>14,
'tam_mad'=>10000000,
'market_match_score'=>0,
'capabilities_done' => ['auth','logs','monitoring','docs_api','tests','mobile','perf'],
'capabilities_todo' => ['rbac','tenant_isolation','billing','rate_limit','gdpr','email_tx','docs_user','i18n','support','landing','pricing','security','backup'],
'signals' => ['409 options', '715 agents', '17 providers'],
'target_segment'=>'Developers',
],
[
'id'=>'paperclip', 'rank'=>10, 'name'=>'Paperclip PM + CRM',
'category'=>'Project Mgmt', 'status'=>'BETA', 'maturity'=>60,
'effort'=>6, 'reward'=>5, 'mad_est'=>150000, 'days_to_prod'=>45,
'tam_mad'=>8000000,
'market_match_score'=>0,
'capabilities_done' => ['auth','logs','docs_api'],
'capabilities_todo' => ['rbac','tenant_isolation','billing','rate_limit','gdpr','email_tx','docs_user','i18n','support','mobile','landing','pricing','perf','security','backup','tests','monitoring'],
'signals' => ['48 leads tracked', 'Self-host'],
'target_segment'=>'SMB',
],
];
}
// WePredict-style regression prediction: market_match_score
// Formula: weighted market signals (industry MQL + lead count + SQL qualification rate)
function predict_market_score($solution) {
$pg = pg_c();
if (!$pg) return 50;
// Map solution target to industries
$target_map = [
'Pharma' => ['Pharma'],
'Banque/Retail/Pharma' => ['Banque','Retail','Pharma'],
'Software' => ['Software','Cloud'],
'Software/Consulting' => ['Software','Cloud','Streaming'],
'Software/Sales' => ['Software','Cloud'],
'Cloud/Software' => ['Cloud','Software'],
'Enterprise AI' => ['Pharma','Banque','Software','Cloud','Retail'],
'Developers' => ['Software','Cloud'],
'SMB' => ['Retail','Telecom','Mining'],
'B2B outreach' => ['Banque','Retail','Pharma','Software','Telecom'],
];
$industries = $target_map[$solution['target_segment']] ?? ['Pharma','Banque'];
$in_list = "'" . implode("','", array_map(function($i){return pg_escape_string($i);}, $industries)) . "'";
$sql = "SELECT COUNT(*) AS n, ROUND(AVG(mql_score)) AS avg_mql,
SUM(CASE WHEN sql_qualified THEN 1 ELSE 0 END) AS sql_q,
ROUND(SUM(CASE WHEN sql_qualified THEN 1 ELSE 0 END)::numeric / NULLIF(COUNT(*),0) * 100) AS sql_pct
FROM weval_leads WHERE industry IN ($in_list)";
$r = @pg_query($pg, $sql);
$stats = $r ? pg_fetch_assoc($r) : [];
pg_close($pg);
$leads = (int)($stats['n'] ?? 0);
$avg_mql = (int)($stats['avg_mql'] ?? 70);
$sql_q = (int)($stats['sql_q'] ?? 0);
$sql_pct = (int)($stats['sql_pct'] ?? 0);
// Predictive score:
// - Lead density (normalized 0-100): leads/48 * 100
// - Avg MQL (0-100 already)
// - SQL qualification rate (0-100)
// - Maturity (product readiness, 0-100)
$lead_density = min(100, ($leads / 48) * 100);
$maturity = (int)$solution['maturity'];
$score = round(
$lead_density * 0.25 +
$avg_mql * 0.30 +
$sql_pct * 0.15 +
$maturity * 0.30
);
return [
'score' => $score,
'breakdown' => [
'lead_density' => round($lead_density, 1),
'avg_mql' => $avg_mql,
'sql_pct' => $sql_pct,
'maturity' => $maturity,
'leads_in_target' => $leads,
'sql_qualified_in_target' => $sql_q,
],
'recommendation' => $score >= 75 ? 'LAUNCH NOW' : ($score >= 60 ? 'ACCELERATE' : ($score >= 45 ? 'NURTURE' : 'PIVOT')),
];
}
// Dev effort estimation for multi-user production
function dev_effort_to_production($solution) {
$checklist = mup_checklist();
$todo = $solution['capabilities_todo'] ?? [];
$total_days = 0;
$critical_days = 0;
$done_days = 0;
$nice_days = 0;
foreach ($todo as $cap) {
if (!isset($checklist[$cap])) continue;
$d = $checklist[$cap]['dev_days'];
$total_days += $d;
if ($checklist[$cap]['critical']) $critical_days += $d;
else $nice_days += $d;
}
// Done
$done = $solution['capabilities_done'] ?? [];
foreach ($done as $cap) {
if (isset($checklist[$cap])) $done_days += $checklist[$cap]['dev_days'];
}
// Dev cost estimate: 1 senior dev = 2500 MAD/day, 1 junior = 1200 MAD/day
// Assume 1 senior + 1 junior in parallel = 3700 MAD/day but -30% parallel overhead
$dev_cost_per_day = 3700 * 0.7; // 2590 MAD/day effective
$dev_cost_mad = round($total_days * $dev_cost_per_day);
// Parallelizable? Yes, roughly 60% of tasks can overlap
$calendar_days = round($total_days * 0.65);
return [
'total_dev_days' => $total_days,
'critical_path_days' => $critical_days,
'nice_to_have_days' => $nice_days,
'calendar_days_est' => $calendar_days,
'dev_cost_mad' => $dev_cost_mad,
'breakeven_customers' => $solution['mad_est'] > 0 ? round($dev_cost_mad / $solution['mad_est'] * 10, 1) / 10 : null,
'completion_pct' => count($done) > 0 ? round(count($done) / (count($done) + count($todo)) * 100) : 0,
];
}
// Main action routing
$action = $_GET['action'] ?? 'full_analysis';
if ($action === 'full_analysis') {
$solutions = solutions_catalog();
$checklist = mup_checklist();
foreach ($solutions as &$s) {
$prediction = predict_market_score($s);
$s['market_prediction'] = $prediction;
$s['dev_effort'] = dev_effort_to_production($s);
// Winning formula: combine market prediction + ICE + maturity
$ice_raw = ($s['reward'] * ($s['mad_est'] / 10000)) / max(1, $s['effort']);
$s['ice_score'] = round($ice_raw, 1);
// FINAL WINNING SCORE: combines all predictors
$s['winning_score'] = round(
$prediction['score'] * 0.40 +
min(100, $ice_raw / 3) * 0.25 +
$s['maturity'] * 0.25 +
(100 - min(100, $s['dev_effort']['calendar_days_est'])) * 0.10
);
// Decision: SHIP IT vs DEV vs PIVOT
if ($s['winning_score'] >= 78) $s['decision'] = 'SHIP_IT';
elseif ($s['winning_score'] >= 68) $s['decision'] = 'ACCELERATE';
elseif ($s['winning_score'] >= 55) $s['decision'] = 'DEV_SPRINT';
elseif ($s['winning_score'] >= 40) $s['decision'] = 'NURTURE';
else $s['decision'] = 'PIVOT_OR_PARK';
}
unset($s);
// Sort by winning_score desc
usort($solutions, function($a,$b){return $b['winning_score'] - $a['winning_score'];});
// Overall GAP analysis
$all_missing = [];
foreach ($solutions as $s) {
foreach ($s['capabilities_todo'] as $cap) {
$all_missing[$cap] = ($all_missing[$cap] ?? 0) + 1;
}
}
arsort($all_missing);
$top_gaps = [];
foreach ($all_missing as $cap => $n) {
if (isset($checklist[$cap])) {
$top_gaps[] = [
'capability' => $cap,
'name' => $checklist[$cap]['name'],
'dev_days' => $checklist[$cap]['dev_days'],
'critical' => $checklist[$cap]['critical'],
'missing_in_solutions' => $n,
];
}
}
echo json_encode([
'ok' => true,
'wave' => 252,
'ts' => date('c'),
'solutions_count' => count($solutions),
'solutions' => $solutions,
'top_gaps' => array_slice($top_gaps, 0, 15),
'mup_checklist' => $checklist,
'summary' => [
'ship_it' => count(array_filter($solutions, function($s){return $s['decision']==='SHIP_IT';})),
'accelerate' => count(array_filter($solutions, function($s){return $s['decision']==='ACCELERATE';})),
'dev_sprint' => count(array_filter($solutions, function($s){return $s['decision']==='DEV_SPRINT';})),
'nurture' => count(array_filter($solutions, function($s){return $s['decision']==='NURTURE';})),
'pivot' => count(array_filter($solutions, function($s){return $s['decision']==='PIVOT_OR_PARK';})),
'total_mad_pipeline' => array_sum(array_column($solutions, 'mad_est')),
'total_dev_days' => array_sum(array_map(function($s){return $s['dev_effort']['total_dev_days'];}, $solutions)),
'total_dev_cost_mad' => array_sum(array_map(function($s){return $s['dev_effort']['dev_cost_mad'];}, $solutions)),
],
], JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT);
exit;
}
if ($action === 'checklist') {
echo json_encode(['ok'=>true, 'wave'=>252, 'checklist'=>mup_checklist()], JSON_PRETTY_PRINT);
exit;
}
http_response_code(400);
echo json_encode(['error'=>'unknown action']);