This commit is contained in:
team 1
2026-05-06 17:23:57 +02:00
parent 81ae3c3902
commit e9c464c057
8 changed files with 399 additions and 40 deletions

View File

@@ -0,0 +1,62 @@
# RetrieX Patch p56 - Single-Genre Config Wiring
## Ziel
Dieser Patch macht die in p55 eingefuehrte `config/retriex/genre.yaml` fuer eine erste, risikoarme Auswahl fachlicher Runtime-Parameter zur bevorzugten Quelle.
Es bleibt strikt bei: eine Installation = ein Genre. Es gibt keine Multi-Genre-Umschaltung, keinen Tenant-Kontext und keinen Request-/Host-Resolver.
## Was geaendert wurde
- `GenreConfig` erhaelt typisierte Value-Getter fuer Listen, Maps, Strings, Booleans und Integer unter `configuration_values`.
- `DomainVocabularyConfig` liest bevorzugt passende Genre-Werte fuer zentrale Produktrollen, Shop-/Prompt-Views, Search-Repair-Views und relevante Vocabulary-Maps.
- `CommerceQueryParserConfig` liest bevorzugt Genre-Werte fuer `known_brands` und `search_token_canonical_map`.
- `QueryEnricherConfig` liest bevorzugt Genre-Werte fuer Query-Enrichment-Regeln.
- `CommerceIntentConfig` liest bevorzugt Genre-Werte fuer Commerce-Signale, Advisory-Patterns, Farben und Groessen.
- `AgentRunnerConfig` liest bevorzugt Genre-Werte fuer ausgewaehlte Shop-Runtime-Listen, Kontextanker, Direct-Answer-Texte und Laengen-Constraint-Patterns.
- `config/services.yaml` verdrahtet `GenreConfig` in diese Config-Fassaden.
## Nicht geaendert
- Keine neue Profil-/Layer-Architektur.
- Keine Multi-Domain-/Tenant-Loesung.
- Keine neuen fachlichen Listen.
- Keine Shopware-Kriterien-, Ranking-, Retrieval- oder LLM-Verhaltensaenderung.
- Legacy-YAML-Pfade bleiben als Fallback erhalten.
## Erwartetes Verhalten
Da p55 die aktuellen Werte bereits identisch gespiegelt hat, sollte sich das Laufzeitverhalten nicht aendern. Der Unterschied ist nur die bevorzugte Pflegequelle fuer die verdrahteten Parameter.
## Geaenderte Dateien
- `config/services.yaml`
- `src/Config/AgentRunnerConfig.php`
- `src/Config/CommerceIntentConfig.php`
- `src/Config/CommerceQueryParserConfig.php`
- `src/Config/DomainVocabularyConfig.php`
- `src/Config/GenreConfig.php`
- `src/Config/QueryEnricherConfig.php`
## Lokale Checks
```bash
php -l src/Config/AgentRunnerConfig.php
php -l src/Config/CommerceIntentConfig.php
php -l src/Config/CommerceQueryParserConfig.php
php -l src/Config/DomainVocabularyConfig.php
php -l src/Config/GenreConfig.php
php -l src/Config/QueryEnricherConfig.php
```
YAML-Parsing fuer alle Dateien unter `config/retriex/*.yaml`.
Projektchecks nach dem Einspielen:
```bash
bin/console cache:clear
bin/console mto:agent:config:validate
bin/console mto:agent:regression:test
bin/console mto:agent:config:audit-source --details
bin/console mto:agent:config:audit-patterns --details
```

View File

@@ -124,6 +124,7 @@ services:
App\Config\DomainVocabularyConfig:
arguments:
$config: '%retriex.vocabulary.config%'
$genreConfig: '@App\Config\GenreConfig'
App\Config\ContextServiceConfig:
arguments:
@@ -142,6 +143,7 @@ services:
arguments:
$config: '%retriex.agent.config%'
$vocabulary: '@App\Config\DomainVocabularyConfig'
$genreConfig: '@App\Config\GenreConfig'
App\Config\NdjsonHybridRetrieverConfig:
arguments:
@@ -159,6 +161,7 @@ services:
App\Config\QueryEnricherConfig:
arguments:
$config: '%retriex.query_enrichment.config%'
$genreConfig: '@App\Config\GenreConfig'
App\Config\GovernanceConfig:
arguments:
@@ -211,11 +214,13 @@ services:
App\Config\CommerceIntentConfig:
arguments:
$config: '%retriex.intent.commerce.config%'
$genreConfig: '@App\Config\GenreConfig'
App\Config\CommerceQueryParserConfig:
arguments:
$config: '%retriex.commerce_query.config%'
$vocabulary: '@App\Config\DomainVocabularyConfig'
$genreConfig: '@App\Config\GenreConfig'
App\Config\CommerceReferenceResolverConfig:
arguments:

View File

@@ -12,6 +12,7 @@ final class AgentRunnerConfig
public function __construct(
private readonly array $config = [],
private readonly ?DomainVocabularyConfig $vocabulary = null,
private readonly ?GenreConfig $genreConfig = null,
) {
}
@@ -290,6 +291,27 @@ final class AgentRunnerConfig
);
}
/** @return string[] */
private function genreStringList(string $path): array
{
return $this->genreConfig?->getValueStringList($path) ?? [];
}
private function genreString(string $path): string
{
return $this->genreConfig?->getValueString($path) ?? '';
}
private function genreBool(string $path): ?bool
{
return $this->genreConfig?->getValueBool($path);
}
private function genreInt(string $path): ?int
{
return $this->genreConfig?->getValueInt($path);
}
private function getRequiredInt(string $key): int
{
$value = $this->requiredValue($key);
@@ -749,37 +771,44 @@ final class AgentRunnerConfig
public function isDirectShopResultAnswerEnabled(): bool
{
return $this->getRequiredBool('shop_runtime.direct_answer.enabled');
return $this->genreBool('shop_query_runtime.direct_answer.enabled')
?? $this->getRequiredBool('shop_runtime.direct_answer.enabled');
}
public function getDirectShopResultAnswerMaxResults(): int
{
return $this->getRequiredInt('shop_runtime.direct_answer.max_results');
return $this->genreInt('shop_query_runtime.direct_answer.max_results')
?? $this->getRequiredInt('shop_runtime.direct_answer.max_results');
}
public function getDirectShopResultAnswerIntro(): string
{
return $this->getRequiredString('shop_runtime.direct_answer.intro');
return $this->genreString('shop_query_runtime.direct_answer.intro')
?: $this->getRequiredString('shop_runtime.direct_answer.intro');
}
public function getDirectShopResultAnswerNoResultsMessage(): string
{
return $this->getRequiredString('shop_runtime.direct_answer.no_results');
return $this->genreString('shop_query_runtime.direct_answer.no_results')
?: $this->getRequiredString('shop_runtime.direct_answer.no_results');
}
public function getDirectShopResultAnswerSortedByLengthNote(): string
{
return $this->getRequiredString('shop_runtime.direct_answer.sorted_by_length_note');
return $this->genreString('shop_query_runtime.direct_answer.sorted_by_length_note')
?: $this->getRequiredString('shop_runtime.direct_answer.sorted_by_length_note');
}
public function getDirectShopResultAnswerMinLengthFilterNote(): string
{
return $this->getRequiredString('shop_runtime.direct_answer.min_length_filter_note');
return $this->genreString('shop_query_runtime.direct_answer.min_length_filter_note')
?: $this->getRequiredString('shop_runtime.direct_answer.min_length_filter_note');
}
public function getDirectShopResultAnswerMaxLengthFilterNote(): string
{
return $this->getRequiredString('shop_runtime.direct_answer.max_length_filter_note');
return $this->genreString('shop_query_runtime.direct_answer.max_length_filter_note')
?: $this->getRequiredString('shop_runtime.direct_answer.max_length_filter_note');
}
public function getNoLlmFallbackMaxShopResults(): int
@@ -1073,7 +1102,8 @@ final class AgentRunnerConfig
*/
public function getShopQueryContextUsageReferentialTerms(): array
{
return $this->getRequiredStringList('shop_runtime.context_resolution.context_usage.referential_terms');
return $this->genreStringList('context_resolution.referential_terms.terms')
?: $this->getRequiredStringList('shop_runtime.context_resolution.context_usage.referential_terms');
}
public function isShopQueryCurrentInputPreservationEnabled(): bool
@@ -1129,7 +1159,8 @@ final class AgentRunnerConfig
*/
public function getShopQueryProductAttributeCleanupComparativeConstraintPatterns(): array
{
return $this->getRequiredStringList('shop_runtime.attribute_cleanup.comparative_constraint_patterns');
return $this->genreStringList('product_attributes.direct_attribute_cleanup.comparative_constraint_patterns')
?: $this->getRequiredStringList('shop_runtime.attribute_cleanup.comparative_constraint_patterns');
}
public function isShopQueryStopwordCleanupEnabled(): bool
@@ -1147,7 +1178,8 @@ final class AgentRunnerConfig
*/
public function getShopQueryStopwordCleanupTerms(): array
{
return $this->getRequiredStringList('shop_runtime.query_cleanup.stopword_cleanup.terms');
return $this->genreStringList('shop_query_runtime.stopword_cleanup.terms')
?: $this->getRequiredStringList('shop_runtime.query_cleanup.stopword_cleanup.terms');
}
public function isDirectShopResultGuardEnabled(): bool
@@ -1170,7 +1202,8 @@ final class AgentRunnerConfig
*/
public function getDirectShopResultGuardCompoundPrefixTerms(): array
{
return $this->getOptionalStringList('shop_runtime.result_identity.compound_prefix_match.terms');
return $this->genreStringList('shop_query_runtime.compound_prefix_match.terms')
?: $this->getOptionalStringList('shop_runtime.result_identity.compound_prefix_match.terms');
}
public function isDirectShopResultGuardPrimaryIdentityRepairEnabled(): bool
@@ -1188,7 +1221,8 @@ final class AgentRunnerConfig
*/
public function getDirectShopResultGuardPrimaryIdentityRepairStopTerms(): array
{
return $this->getOptionalStringList('shop_runtime.result_identity.primary_identity_repair.stop_terms');
return $this->genreStringList('shop_query_runtime.primary_identity_repair.stop_terms')
?: $this->getOptionalStringList('shop_runtime.result_identity.primary_identity_repair.stop_terms');
}
public function isShopResultLengthSortEnabled(): bool
@@ -1201,7 +1235,8 @@ final class AgentRunnerConfig
*/
public function getShopResultLengthSortTriggerPatterns(): array
{
return $this->getRequiredStringList('shop_runtime.answer_constraints.length_sort.trigger_patterns');
return $this->genreStringList('product_attributes.numeric_length_constraints.length_sort.trigger_patterns')
?: $this->getRequiredStringList('shop_runtime.answer_constraints.length_sort.trigger_patterns');
}
/**
@@ -1209,7 +1244,8 @@ final class AgentRunnerConfig
*/
public function getShopResultLengthSortValuePatterns(): array
{
return $this->getRequiredStringList('shop_runtime.answer_constraints.length_sort.value_patterns');
return $this->genreStringList('product_attributes.numeric_length_constraints.length_sort.value_patterns')
?: $this->getRequiredStringList('shop_runtime.answer_constraints.length_sort.value_patterns');
}
public function isShopResultLengthFilterEnabled(): bool
@@ -1222,7 +1258,8 @@ final class AgentRunnerConfig
*/
public function getShopResultMinLengthFilterPatterns(): array
{
return $this->getRequiredStringList('shop_runtime.answer_constraints.length_filter.min_patterns');
return $this->genreStringList('product_attributes.numeric_length_constraints.length_filter.min_patterns')
?: $this->getRequiredStringList('shop_runtime.answer_constraints.length_filter.min_patterns');
}
/**
@@ -1230,7 +1267,8 @@ final class AgentRunnerConfig
*/
public function getShopResultMaxLengthFilterPatterns(): array
{
return $this->getRequiredStringList('shop_runtime.answer_constraints.length_filter.max_patterns');
return $this->genreStringList('product_attributes.numeric_length_constraints.length_filter.max_patterns')
?: $this->getRequiredStringList('shop_runtime.answer_constraints.length_filter.max_patterns');
}
public function getShopPromptIntro(): string
@@ -1323,7 +1361,8 @@ final class AgentRunnerConfig
*/
public function getShopQueryMetaOnlyTerms(): array
{
return $this->getRequiredStringList('shop_runtime.context_resolution.meta_query_guard.meta_only_terms');
return $this->genreStringList('context_resolution.meta_query_guard.meta_only_terms')
?: $this->getRequiredStringList('shop_runtime.context_resolution.meta_query_guard.meta_only_terms');
}
public function isShopQueryContextFallbackEnabled(): bool
@@ -1356,7 +1395,8 @@ final class AgentRunnerConfig
*/
public function getShopQueryContextFallbackFilterTerms(): array
{
return $this->getRequiredStringList('shop_runtime.context_resolution.meta_query_guard.context_fallback_filter_terms');
return $this->genreStringList('context_resolution.meta_query_guard.context_fallback_filter_terms')
?: $this->getRequiredStringList('shop_runtime.context_resolution.meta_query_guard.context_fallback_filter_terms');
}
public function isShopQueryContextAnchorEnrichmentEnabled(): bool
@@ -1385,12 +1425,14 @@ final class AgentRunnerConfig
*/
public function getShopQueryContextAnchorEnrichmentPatterns(): array
{
return $this->getRequiredStringList('shop_runtime.context_resolution.history_anchor_enrichment.anchor_patterns');
return $this->genreStringList('context_resolution.history_anchor_enrichment.anchor_patterns')
?: $this->getRequiredStringList('shop_runtime.context_resolution.history_anchor_enrichment.anchor_patterns');
}
public function getShopQueryContextAnchorEnrichmentTemplate(): string
{
return $this->getRequiredString('shop_runtime.context_resolution.history_anchor_enrichment.template');
return $this->genreString('context_resolution.history_anchor_enrichment.template')
?: $this->getRequiredString('shop_runtime.context_resolution.history_anchor_enrichment.template');
}
public function isShopQueryRagAnchorEnrichmentEnabled(): bool
{
@@ -1437,7 +1479,8 @@ final class AgentRunnerConfig
*/
public function getShopQueryRagAnchorEnrichmentNumericFocusPatterns(): array
{
return $this->getRequiredStringList('shop_runtime.context_resolution.rag_anchor_enrichment.numeric_focus_patterns');
return $this->genreStringList('context_resolution.rag_anchor_enrichment.numeric_focus_patterns')
?: $this->getRequiredStringList('shop_runtime.context_resolution.rag_anchor_enrichment.numeric_focus_patterns');
}
/**
@@ -1445,7 +1488,8 @@ final class AgentRunnerConfig
*/
public function getShopQueryRagAnchorEnrichmentProductTitlePatterns(): array
{
return $this->getRequiredStringList('shop_runtime.context_resolution.rag_anchor_enrichment.product_title_patterns');
return $this->genreStringList('context_resolution.rag_anchor_enrichment.product_title_patterns')
?: $this->getRequiredStringList('shop_runtime.context_resolution.rag_anchor_enrichment.product_title_patterns');
}
/**
@@ -1453,7 +1497,8 @@ final class AgentRunnerConfig
*/
public function getShopQueryRagAnchorEnrichmentAnchorBonusPatterns(): array
{
return $this->getRequiredStringList('shop_runtime.context_resolution.rag_anchor_enrichment.anchor_bonus_patterns');
return $this->genreStringList('context_resolution.rag_anchor_enrichment.anchor_bonus_patterns')
?: $this->getRequiredStringList('shop_runtime.context_resolution.rag_anchor_enrichment.anchor_bonus_patterns');
}
/**
@@ -1461,7 +1506,8 @@ final class AgentRunnerConfig
*/
public function getShopQueryRagAnchorEnrichmentSubjectTerms(): array
{
return $this->getRequiredStringList('shop_runtime.context_resolution.rag_anchor_enrichment.subject_terms');
return $this->genreStringList('context_resolution.rag_anchor_enrichment.subject_terms')
?: $this->getRequiredStringList('shop_runtime.context_resolution.rag_anchor_enrichment.subject_terms');
}
public function getShopQueryTranslationReplacements(string $language): array

View File

@@ -9,26 +9,31 @@ final class CommerceIntentConfig
/**
* @param array<string, mixed> $config
*/
public function __construct(private readonly array $config = [])
{
public function __construct(
private readonly array $config = [],
private readonly ?GenreConfig $genreConfig = null,
) {
}
/** @return string[] */
public function getStrongSignalsList(): array
{
return $this->requiredStringList('strong_signals');
return $this->genreStringList('intent_and_routing.commerce_intent.strong_signals')
?: $this->requiredStringList('strong_signals');
}
/** @return string[] */
public function getAdvisorySignals(): array
{
return $this->requiredStringList('advisory_signals');
return $this->genreStringList('intent_and_routing.commerce_intent.advisory_signals')
?: $this->requiredStringList('advisory_signals');
}
/** @return string[] */
public function getAdvisoryProductSelectionPatterns(): array
{
return $this->requiredStringList('advisory_product_selection_patterns');
return $this->genreStringList('intent_and_routing.commerce_intent.advisory_product_selection_patterns')
?: $this->requiredStringList('advisory_product_selection_patterns');
}
/** @return string[] */
@@ -54,6 +59,12 @@ final class CommerceIntentConfig
return $this->requiredStringList('technical_factual_knowledge.fact_patterns');
}
/** @return string[] */
private function genreStringList(string $path): array
{
return $this->genreConfig?->getValueStringList($path) ?? [];
}
/** @return string[] */
public function getPriceTerms(): array
{
@@ -68,7 +79,8 @@ final class CommerceIntentConfig
/** @return string[] */
public function getColorTerms(): array
{
return $this->requiredStringList('color_terms');
return $this->genreStringList('product_attributes.size_and_color_terms.color_terms')
?: $this->requiredStringList('color_terms');
}
public function getColorPattern(): string
@@ -79,7 +91,8 @@ final class CommerceIntentConfig
/** @return string[] */
public function getSizeTokenTerms(): array
{
return $this->requiredStringList('size_token_terms');
return $this->genreStringList('product_attributes.size_and_color_terms.size_token_terms')
?: $this->requiredStringList('size_token_terms');
}
public function getSizeTokenPattern(): string
@@ -90,7 +103,8 @@ final class CommerceIntentConfig
/** @return string[] */
public function getSizeTerms(): array
{
return $this->requiredStringList('size_terms');
return $this->genreStringList('product_attributes.size_and_color_terms.size_terms')
?: $this->requiredStringList('size_terms');
}
public function getSizePattern(): string
@@ -114,7 +128,8 @@ final class CommerceIntentConfig
/** @return string[] */
public function getExplicitCommerceIntentPatterns(): array
{
return $this->requiredStringList('explicit_commerce_intent_patterns');
return $this->genreStringList('intent_and_routing.commerce_intent.explicit_commerce_intent_patterns')
?: $this->requiredStringList('explicit_commerce_intent_patterns');
}
public function getSkuLikePattern(): string

View File

@@ -14,6 +14,7 @@ final class CommerceQueryParserConfig
public function __construct(
private readonly array $config = [],
private readonly ?DomainVocabularyConfig $vocabulary = null,
private readonly ?GenreConfig $genreConfig = null,
) {
}
@@ -26,7 +27,8 @@ final class CommerceQueryParserConfig
/** @return string[] */
public function getKnownBrands(): array
{
return $this->stringList('known_brands');
return $this->genreStringList('brands_and_canonical_terms.known_brands.terms')
?: $this->stringList('known_brands');
}
/** @return string[] */
@@ -61,6 +63,18 @@ final class CommerceQueryParserConfig
return $this->stringList('search_control_tokens');
}
/** @return string[] */
private function genreStringList(string $path): array
{
return $this->genreConfig?->getValueStringList($path) ?? [];
}
/** @return array<string, string> */
private function genreStringMap(string $path): array
{
return $this->genreConfig?->getValueStringMap($path) ?? [];
}
/** @return string[] */
private function whitespacePreservingStringList(string $path): array
{
@@ -99,7 +113,8 @@ final class CommerceQueryParserConfig
/** @return array<string, string> */
public function getSearchTokenCanonicalMap(): array
{
return $this->stringMap('search_token_canonical_map');
return $this->genreStringMap('brands_and_canonical_terms.canonical_terms.map')
?: $this->stringMap('search_token_canonical_map');
}
/**

View File

@@ -6,13 +6,57 @@ namespace App\Config;
final class DomainVocabularyConfig
{
public function __construct(private readonly array $config = [])
{
private const CLASS_GENRE_VALUE_PATHS = [
'device' => 'product_roles.primary_product_terms.terms',
'accessory' => 'product_roles.accessory_product_terms.terms',
'requested_accessory_code_terms' => 'product_roles.requested_accessory_code_terms.terms',
'direct_product_attribute_stop_terms' => 'product_attributes.direct_attribute_cleanup.stop_terms',
'input_normalization_fuzzy_routing_terms' => 'intent_and_routing.fuzzy_routing_terms.terms',
'agent_no_llm_main_device_request_keywords' => 'product_roles.no_llm_fallback_terms.main_device_request_keywords',
'agent_no_llm_accessory_product_keywords' => 'product_roles.no_llm_fallback_terms.accessory_product_keywords',
'agent_shop_current_input_preservation_terms' => 'shop_query_runtime.current_input_preservation_terms.terms',
'agent_shop_context_anchor_trigger_terms' => 'context_resolution.history_anchor_enrichment.trigger_terms',
];
private const VIEW_GENRE_VALUE_PATHS = [
'shop.device_query' => 'product_roles.shop_views.device_query_terms',
'shop.accessory_query' => 'product_roles.shop_views.accessory_query_terms',
'shop.device_product' => 'product_roles.shop_views.device_product_terms',
'shop.accessory_product' => 'product_roles.shop_views.accessory_product_terms',
'shop.device_focus' => 'product_roles.shop_views.device_focus_terms',
'shop.accessory_focus' => 'product_roles.shop_views.accessory_focus_terms',
'prompt.main_device_request_keywords' => 'product_roles.prompt_views.main_device_request_keywords',
'prompt.accessory_request_keywords' => 'product_roles.prompt_views.accessory_request_keywords',
'prompt.main_device_product_keywords' => 'product_roles.prompt_views.main_device_product_keywords',
'prompt.accessory_product_keywords' => 'product_roles.prompt_views.accessory_product_keywords',
'search_repair.direct_product_type_terms' => 'product_attributes.direct_attribute_cleanup.product_type_terms',
'search_repair.direct_product_attribute_stop_terms' => 'product_attributes.direct_attribute_cleanup.stop_terms',
'search_repair.requested_accessory_code_terms' => 'product_roles.requested_accessory_code_terms.terms',
'agent.rag_evidence_guard.accessory_lookup_guard_terms' => 'result_identity_and_answer_policy.measurement_evidence_guard_terms.accessory_lookup_guard_terms',
'agent.rag_evidence_guard.accessory_lookup_passthrough_terms' => 'result_identity_and_answer_policy.measurement_evidence_guard_terms.accessory_lookup_passthrough_terms',
'agent.rag_evidence_guard.generic_positive_context_terms' => 'result_identity_and_answer_policy.measurement_evidence_guard_terms.generic_positive_context_terms',
'agent.rag_evidence_guard.generic_negative_context_terms' => 'result_identity_and_answer_policy.measurement_evidence_guard_terms.generic_negative_context_terms',
];
private const MAP_GENRE_VALUE_PATHS = [
'shop.accessory_focus_variants' => 'brands_and_canonical_terms.accessory_focus_variants.map',
'agent.rag_evidence_guard.synonyms' => 'brands_and_canonical_terms.rag_evidence_synonyms.map',
];
public function __construct(
private readonly array $config = [],
private readonly ?GenreConfig $genreConfig = null,
) {
}
/** @return string[] */
public function view(string $path, array $fallback = []): array
{
$genreTerms = $this->genreStringListForView($path);
if ($genreTerms !== []) {
return $genreTerms;
}
$definition = $this->value('views.' . $path, null);
if (!is_array($definition)) {
return $this->uniqueStringList($fallback);
@@ -35,12 +79,22 @@ final class DomainVocabularyConfig
/** @return string[] */
public function domainClass(string $name): array
{
$genreTerms = $this->genreStringListForClass($name);
if ($genreTerms !== []) {
return $genreTerms;
}
return $this->stringList('classes.' . $name, []);
}
/** @return array<string, string[]> */
public function map(string $path, array $fallback = []): array
{
$genreMap = $this->genreStringListMapForMap($path);
if ($genreMap !== []) {
return $genreMap;
}
$value = $this->value('maps.' . $path, null);
if (!is_array($value)) {
return $this->uniqueStringListMap($fallback);
@@ -114,6 +168,41 @@ final class DomainVocabularyConfig
return $this->config;
}
/** @return string[] */
private function genreStringListForClass(string $name): array
{
if ($this->genreConfig === null || !isset(self::CLASS_GENRE_VALUE_PATHS[$name])) {
return [];
}
return $this->genreConfig->getValueStringList(self::CLASS_GENRE_VALUE_PATHS[$name]);
}
/** @return string[] */
private function genreStringListForView(string $path): array
{
if ($this->genreConfig === null || !isset(self::VIEW_GENRE_VALUE_PATHS[$path])) {
return [];
}
return $this->genreConfig->getValueStringList(self::VIEW_GENRE_VALUE_PATHS[$path]);
}
/** @return array<string, string[]> */
private function genreStringListMapForMap(string $path): array
{
if ($this->genreConfig === null || !isset(self::MAP_GENRE_VALUE_PATHS[$path])) {
return [];
}
$value = $this->genreConfig->getValueArray(self::MAP_GENRE_VALUE_PATHS[$path]);
if ($value === []) {
return [];
}
return $this->stringListMapWithAliases($value);
}
/** @return string[] */
private function stringList(string $path, array $fallback): array
{

View File

@@ -64,6 +64,87 @@ final class GenreConfig
return is_array($groupValues) ? $groupValues : [];
}
/**
* Returns a non-empty string list from the central single-genre value surface.
*
* The path is relative to `configuration_values`, for example
* `product_roles.primary_product_terms.terms`.
*
* @return string[]
*/
public function getValueStringList(string $path): array
{
return $this->uniqueStringList($this->value('configuration_values.' . $path, []));
}
/**
* Returns a non-empty scalar string map from the central single-genre value surface.
*
* @return array<string, string>
*/
public function getValueStringMap(string $path): array
{
$value = $this->value('configuration_values.' . $path, []);
return is_array($value) ? $this->uniqueStringMap($value) : [];
}
/**
* Returns a raw map/list value from the central single-genre value surface.
*
* @return array<int|string, mixed>
*/
public function getValueArray(string $path): array
{
$value = $this->value('configuration_values.' . $path, []);
return is_array($value) ? $value : [];
}
public function getValueString(string $path): string
{
$value = $this->value('configuration_values.' . $path, null);
if (!is_scalar($value)) {
return '';
}
return trim((string) $value);
}
public function getValueBool(string $path): ?bool
{
$value = $this->value('configuration_values.' . $path, null);
if (is_bool($value)) {
return $value;
}
if (is_scalar($value)) {
$normalized = strtolower(trim((string) $value));
if (in_array($normalized, ['1', 'true', 'yes', 'on'], true)) {
return true;
}
if (in_array($normalized, ['0', 'false', 'no', 'off'], true)) {
return false;
}
}
return null;
}
public function getValueInt(string $path): ?int
{
$value = $this->value('configuration_values.' . $path, null);
if (is_int($value)) {
return $value;
}
if (is_string($value) && preg_match('/^-?\d+$/', trim($value)) === 1) {
return (int) trim($value);
}
return null;
}
/**
* @return array<string, mixed>
*/
@@ -82,6 +163,47 @@ final class GenreConfig
return trim((string) $value);
}
/** @return string[] */
private function uniqueStringList(mixed $value): array
{
if (!is_array($value)) {
return [];
}
$out = [];
foreach ($value as $item) {
if (!is_scalar($item)) {
continue;
}
$item = trim((string) $item);
if ($item !== '' && !in_array($item, $out, true)) {
$out[] = $item;
}
}
return $out;
}
/** @return array<string, string> */
private function uniqueStringMap(array $value): array
{
$out = [];
foreach ($value as $key => $mappedValue) {
if (!is_scalar($key) || !is_scalar($mappedValue)) {
continue;
}
$cleanKey = trim((string) $key);
$cleanValue = trim((string) $mappedValue);
if ($cleanKey !== '' && $cleanValue !== '') {
$out[$cleanKey] = $cleanValue;
}
}
return $out;
}
private function value(string $path, mixed $fallback): mixed
{
$current = $this->config;

View File

@@ -15,8 +15,10 @@ final readonly class QueryEnricherConfig
/**
* @param array<string, mixed> $config
*/
public function __construct(private array $config)
{
public function __construct(
private array $config,
private readonly ?GenreConfig $genreConfig = null,
) {
}
/**
@@ -39,7 +41,10 @@ final readonly class QueryEnricherConfig
public function getEnrichQueryList(): array
{
$normalized = [];
$rules = $this->genreConfig?->getValueArray('brands_and_canonical_terms.query_enrichment_rules.rules') ?? [];
if ($rules === []) {
$rules = $this->requiredArray('rules');
}
foreach ($rules as $key => $value) {
if (is_array($value)) {