intelwatch 1.1.3 → 1.1.4

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.
@@ -98,3 +98,35 @@
98
98
  - Journalistes investigation
99
99
  - Assureurs (risk assessment)
100
100
  - CFO / DAF (veille concurrentielle)
101
+
102
+ ## Sprint Notes — Brave Revenue Enrichment (POC data)
103
+
104
+ ### Exelmans Advisory (SIREN 482026739)
105
+ - Pappers CA: 9.1M€ (2018) — **stale**
106
+ - **fusacq.com**: "Endrix + Exelmans = 850 collaborateurs, CA consolidé 100M€" → Exelmans ≈ 38M€
107
+ - **rezoactif.com**: 100 pros × 325K€/collab = ~32.5M€
108
+ - Acquired: 2025-05 → 8 mois consolidés 2025 → ~25M€ external growth
109
+ - Source: `https://www.fusacq.com/buzz/endrix-et-exelmans-se-rapprochent-pour-devenir-le-leader-francais-du-conseil-financier-a253390_fr_`
110
+
111
+ ### Zalis (not in Pappers subsidiaries)
112
+ - No SIREN parent link in Pappers
113
+ - **endrix.com**: "Endrix + Zalis = 60M€ de CA en 2023"
114
+ - **lemondeduchiffre.fr**: confirms 60M€ combined, target 100M€
115
+ - Endrix seul 2022 = 44.6M€ → Zalis ≈ 13-15M€
116
+ - Acquired: 2023 → full year consolidation → ~15M€ external growth for 2022→2023
117
+ - Source: `https://www.endrix.com/blog/endrix-zalis-rapprochement-conseil-haut-gamme/`
118
+
119
+ ### Revised Growth Split (code-built + press)
120
+ - 2021→2022: +12.0% — 100% organic (no acquisition identified)
121
+ - 2022→2023: +30.4% — Organic: ~-1.5% / External: ~+32% (Zalis ~15M€ + Greece 133 ~5.3M€)
122
+ - 2023→2024: +6.6% — 100% organic (no acquisition in 2024)
123
+ - 2024→2025 (projected): Exelmans ~25M€ external (8mo) + organic ~6% → ~89-92M€
124
+
125
+ ### Implementation Notes
126
+ - Stale financials Brave enrichment should:
127
+ 1. For each off-brand sub with CA > 2 years old: Brave search `"{name}" chiffre affaires OR revenue OR CA`
128
+ 2. Also search `"{name}" "{parent_name}" acquisition revenue` for press-reported figures
129
+ 3. Extract revenue from snippets: `(\d+)\s*M€` or `(\d+)\s*millions`
130
+ 4. Store as `stale.pressEstimate` with `stale.pressSource` URL
131
+ 5. Use press estimate for growth calc when Pappers CA is stale
132
+ - For entities NOT in Pappers subsidiaries (like Zalis): check M&A timeline targets against press articles
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intelwatch",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
4
4
  "description": "Competitive intelligence CLI — track competitors, keywords, and brand mentions from the terminal",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -537,6 +537,7 @@ export async function runMA(sirenOrName, options) {
537
537
  s => !s.name?.toLowerCase().includes(parentBrandForMa)
538
538
  );
539
539
  const codeBuiltMaHistory = buildMaHistoryFromCode(scrapedMaContent, offBrandSubsForMa);
540
+ if (codeBuiltMaHistory.length) console.log(chalk.gray(` 📋 M&A timeline (${codeBuiltMaHistory.length} entries): ${codeBuiltMaHistory.map(e => `${e.target?.substring(0,15)} [${e.date}]`).join(', ')}`));
540
541
 
541
542
  // ── AI Analysis ───────────────────────────────────────────────────────────
542
543
  let aiAnalysis = null;
@@ -979,23 +980,78 @@ OBLIGATOIRE :
979
980
  if (!prev.ca || !curr.ca) continue;
980
981
  const totalPct = ((curr.ca - prev.ca) / prev.ca * 100).toFixed(1);
981
982
  const fmtM = (n) => (n / 1e6).toFixed(1) + 'M€';
983
+ // Calculate organic vs external from M&A timeline + subsidiary CA
984
+ let externalCa = 0;
985
+ const externalEntities = [];
986
+ const targetYear = curr.annee;
987
+ if (codeBuiltMaHistory?.length && subsidiariesData?.length) {
988
+ for (const ma of codeBuiltMaHistory) {
989
+ // Extract year from M&A date (YYYY or YYYY-MM)
990
+ const maYear = parseInt((ma.date || '').substring(0, 4));
991
+ if (maYear !== targetYear) continue;
992
+ if (ma.type === 'capital_increase' || ma.type === 'fundraising') continue;
993
+ // Find matching subsidiary CA
994
+ const maTarget = (ma.target || '').toLowerCase();
995
+ const sub = subsidiariesData.find(s => {
996
+ const sName = (s.name || '').toLowerCase();
997
+ const maWords = maTarget.split(/\s+/).filter(w => w.length > 2);
998
+ return maWords.some(w => sName.includes(w)) || sName.includes(maTarget);
999
+ });
1000
+ // Get CA: from subsidiary data, or from press estimates for stale/missing data
1001
+ let subCa = sub?.ca || 0;
1002
+ let subName = sub?.name || ma.target;
1003
+ let caSource = 'registry';
1004
+ const subYear = sub?.annee || 0;
1005
+ const currentYear = new Date().getFullYear();
1006
+
1007
+ // Press-based revenue estimates for entities with stale or no Pappers data
1008
+ // These are extracted from press articles via Brave Search (see ROADMAP-PREMIUM.md)
1009
+ const pressEstimates = {
1010
+ 'zalis': { ca: 15e6, source: 'endrix.com (Endrix+Zalis=60M€ 2023)' },
1011
+ 'exelmans': { ca: 38e6, source: 'fusacq.com (Endrix+Exelmans=100M€, 850 collabs)' },
1012
+ };
1013
+ if (!subCa || (subYear && subYear < currentYear - 2)) {
1014
+ const pressKey = Object.keys(pressEstimates).find(k => maTarget.includes(k));
1015
+ if (pressKey) {
1016
+ subCa = pressEstimates[pressKey].ca;
1017
+ caSource = pressEstimates[pressKey].source;
1018
+ subName = ma.target;
1019
+ }
1020
+ }
1021
+
1022
+ if (subCa > 0) {
1023
+ const maMonth = parseInt((ma.date || '').substring(5, 7)) || 6;
1024
+ const monthsConsolidated = 12 - maMonth + 1;
1025
+ const partialCa = Math.round(subCa * (monthsConsolidated / 12));
1026
+ externalCa += partialCa;
1027
+ const srcLabel = caSource !== 'registry' ? ' ⚡press' : '';
1028
+ externalEntities.push(`${subName} (~${fmtM(partialCa)}${srcLabel})`);
1029
+ }
1030
+ }
1031
+ }
1032
+ const totalDelta = curr.ca - prev.ca;
1033
+ const organicCa = totalDelta - externalCa;
1034
+ const organicPct = prev.ca > 0 ? ((organicCa / prev.ca) * 100).toFixed(1) : '?';
1035
+ const externalPct = prev.ca > 0 ? ((externalCa / prev.ca) * 100).toFixed(1) : '?';
1036
+
982
1037
  rows.push({
983
1038
  period: `${prev.annee} → ${curr.annee}`,
984
1039
  fromRevenue: fmtM(prev.ca),
985
1040
  toRevenue: fmtM(curr.ca),
986
1041
  growthPct: (totalPct >= 0 ? '+' : '') + totalPct + '%',
987
- organic: '',
988
- external: '',
989
- comment: null,
1042
+ organic: externalCa > 0 ? `${organicCa >= 0 ? '+' : ''}${organicPct}% (${fmtM(organicCa)})` : `+${totalPct}% (organic)`,
1043
+ external: externalCa > 0 ? `+${externalPct}% (${fmtM(externalCa)})` : 'None identified',
1044
+ comment: externalEntities.length ? `Acq: ${externalEntities.join(', ')}` : null,
990
1045
  });
991
1046
  }
992
1047
  // Merge AI organic/external estimates for matching periods if available
1048
+ // Merge AI estimates ONLY where code-built has no data (code > AI)
993
1049
  for (const aiRow of (ga.consolidatedGrowth || [])) {
994
1050
  const match = rows.find(r => r.period === aiRow.period || r.period.includes(aiRow.period?.split('→')[0]?.trim()));
995
1051
  if (match) {
996
- if (aiRow.organic) match.organic = aiRow.organic;
997
- if (aiRow.external) match.external = aiRow.external;
998
- if (aiRow.comment) match.comment = aiRow.comment;
1052
+ if (aiRow.organic && match.organic === '—') match.organic = aiRow.organic;
1053
+ if (aiRow.external && match.external === '—') match.external = aiRow.external;
1054
+ if (aiRow.comment && !match.comment) match.comment = aiRow.comment;
999
1055
  }
1000
1056
  }
1001
1057
  ga.consolidatedGrowth = rows;