seed-product-features: two more icon-matching tiers

- token-containment pass: theme words inside the card title
  (ManagementFoundation -> "Aetria management foundation")
- last-pair rule: a complete set (#icons == #cards) with exactly one
  icon and one card left over pairs them (FlexibleConnection,
  EndToEndEncryption, HighPerformance, 4KSourcetoDisplays, AVoverIP)
- genuine mismatches (aligo-workstation, EZ-TV Platform, AvediaPlatform
  Artio icons) still stay unmatched for the editors

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
orascheandClaude Fable 5 committed 2026-11-04 20:03:50 +01:00
1 parent 46b12d4a58
commit 6ff9c3e7a8
1 file changed
+43
@@ -131,6 +131,36 @@ final class SeedProductFeaturesCommand extends Command
}
}
// second pass: token containment, e.g. theme "ManagementFoundation"
// inside the card title "Aetria management foundation"
foreach ($pairs as $i => $pair) {
if (isset($pair['icon'])) {
continue;
}
foreach (array_keys(array_diff_key($iconByTheme, $usedThemes)) as $theme) {
$titleTokens = $this->tokens($pair['title']);
$themeTokens = $this->tokens($theme);
if ($titleTokens !== [] && $themeTokens !== []
&& (array_diff($themeTokens, $titleTokens) === [] || array_diff($titleTokens, $themeTokens) === [])
) {
$pairs[$i]['icon'] = $iconByTheme[$theme];
$usedThemes[$theme] = true;
break;
}
}
}
// last pair standing: a complete set (#icons == #cards) with exactly
// one icon and one card left over - they belong to each other
if (count($iconByTheme) === count($pairs)) {
$freeThemes = array_keys(array_diff_key($iconByTheme, $usedThemes));
$freePairs = array_keys(array_filter($pairs, static fn(array $p): bool => !isset($p['icon'])));
if (count($freeThemes) === 1 && count($freePairs) === 1) {
$pairs[$freePairs[0]]['icon'] = $iconByTheme[$freeThemes[0]];
$usedThemes[$freeThemes[0]] = true;
}
}
foreach ($pairs as $pair) {
$output->writeln(sprintf(
' "%s"%s%s',
@@ -281,6 +311,19 @@ final class SeedProductFeaturesCommand extends Command
return $bestDistance <= 3 ? $best : null;
}
/**
* Lowercase word list (camelCase split, stop words removed) for the
* token-containment pass.
*
* @return string[]
*/
private function tokens(string $value): array
{
$words = preg_split('/(?=[A-Z][a-z])|[^A-Za-z0-9]+/', $value, -1, PREG_SPLIT_NO_EMPTY) ?: [];
$words = array_map(static fn(string $w): string => mb_strtolower($w), $words);
return array_values(array_filter($words, static fn(string $w): bool => !in_array($w, self::STOPWORDS, true)));
}
/** lowercase, letters/digits only; optionally with stop words removed (camelCase themes are split first) */
private function norm(string $value, bool $dropStopwords): string
{