diff --git a/RETRIEX_PATCH_57_SINGLE_GENRE_SOURCE_AUDIT_README.md b/RETRIEX_PATCH_57_SINGLE_GENRE_SOURCE_AUDIT_README.md new file mode 100644 index 0000000..4ec5217 --- /dev/null +++ b/RETRIEX_PATCH_57_SINGLE_GENRE_SOURCE_AUDIT_README.md @@ -0,0 +1,64 @@ +# RetrieX Patch p57 - Single-Genre Source Audit / Legacy Cleanup Guard + +## Ziel + +Dieser Patch ist der naechste kleine Schritt nach p56. Er loescht noch keine Legacy-YAML-Werte, sondern haertet die zentrale Single-Genre-Pflegeflaeche fuer den spaeteren Cleanup. + +Es bleibt strikt bei: eine Installation = ein Genre. Es gibt keine Multi-Genre-Umschaltung, keinen Tenant-Kontext und keinen Request-/Host-Resolver. + +## Warum dieser Schritt + +Nach p55/p56 sind viele fachliche Werte bevorzugt in `config/retriex/genre.yaml` gepflegt, waehrend die alten Pfade als Fallback bestehen bleiben. Damit diese Uebergangsphase kontrollierbar bleibt, muessen die in `genre.configuration_values.*.source_paths` dokumentierten Legacy-/Effective-Pfade selbst validierbar und auditierbar sein. + +p57 macht diese Source-Path-Metadaten maschinenlesbar und sichtbar. + +## Was geaendert wurde + +- `GenreConfig` erhaelt `getConfigurationValueSourcePaths()`. +- `RetriexEffectiveConfigProvider` validiert nun auch die `source_paths` unter `genre.configuration_values` gegen bekannte effektive bzw. Legacy-Config-Pfade. +- `ConfigSourceAuditProvider` sammelt die Single-Genre-Source-Path-Metadaten fuer `mto:agent:config:audit-source --json`. +- `ConfigSourceAuditCommand --details` zeigt zusaetzlich eine Tabelle der Single-Genre-Source-Pfade. + +## Nicht geaendert + +- Keine Fachlogik-Aenderung. +- Keine neuen fachlichen Listen. +- Keine neuen Defaults. +- Keine Entfernung von Legacy-Fallback-Werten. +- Keine Shopware-Kriterien-, Ranking-, Retrieval- oder LLM-Verhaltensaenderung. +- Keine Multi-Genre-/Tenant-Loesung. + +## Erwartetes Verhalten + +Das Antwortverhalten bleibt unveraendert. Der Unterschied ist nur, dass die zentrale Genre-Konfiguration besser validiert und auditierbar ist. + +Wenn spaeter ein falscher oder veralteter `source_paths`-Eintrag in `genre.yaml` entsteht, meldet `mto:agent:config:validate` eine Warnung statt den Fehler unbemerkt zu lassen. + +## Geaenderte Dateien + +- `src/Config/GenreConfig.php` +- `src/Config/RetriexEffectiveConfigProvider.php` +- `src/Config/ConfigSourceAuditProvider.php` +- `src/Command/ConfigSourceAuditCommand.php` +- `RETRIEX_PATCH_57_SINGLE_GENRE_SOURCE_AUDIT_README.md` + +## Lokale Checks + +```bash +php -l src/Config/GenreConfig.php +php -l src/Config/RetriexEffectiveConfigProvider.php +php -l src/Config/ConfigSourceAuditProvider.php +php -l src/Command/ConfigSourceAuditCommand.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 +``` diff --git a/src/Command/ConfigSourceAuditCommand.php b/src/Command/ConfigSourceAuditCommand.php index c28316f..ad25e67 100644 --- a/src/Command/ConfigSourceAuditCommand.php +++ b/src/Command/ConfigSourceAuditCommand.php @@ -63,7 +63,9 @@ final class ConfigSourceAuditCommand extends Command ['fallback_accessors_with_yaml' => (string) ($summary['fallback_accessors_with_yaml'] ?? 0)], ['fallback_accessors_missing_yaml' => (string) ($summary['fallback_accessors_missing_yaml'] ?? 0)], ['constructor_defaults' => (string) ($summary['constructor_defaults'] ?? 0)], - ['constructor_defaults_without_yaml_mapping' => (string) ($summary['constructor_defaults_without_yaml_mapping'] ?? 0)] + ['constructor_defaults_without_yaml_mapping' => (string) ($summary['constructor_defaults_without_yaml_mapping'] ?? 0)], + ['genre_value_paths_with_source_paths' => (string) ($summary['genre_value_paths_with_source_paths'] ?? 0)], + ['genre_declared_source_paths' => (string) ($summary['genre_declared_source_paths'] ?? 0)] ); $warnings = is_array($result['warnings'] ?? null) ? $result['warnings'] : []; @@ -98,5 +100,25 @@ final class ConfigSourceAuditCommand extends Command $io->section('Fallback accessors'); $io->table(['Class', 'Line', 'Key', 'YAML', 'Source'], $fallbackRows); } + + $genreSourceRows = []; + foreach (($result['genre_configuration_source_paths'] ?? []) as $valuePath => $sourcePaths) { + if (!is_string($valuePath) || !is_array($sourcePaths)) { + continue; + } + + foreach ($sourcePaths as $sourcePath) { + if (!is_string($sourcePath)) { + continue; + } + + $genreSourceRows[] = [$valuePath, $sourcePath]; + } + } + + if ($genreSourceRows !== []) { + $io->section('Single-genre configuration source paths'); + $io->table(['Genre value path', 'Legacy/effective source path'], $genreSourceRows); + } } } diff --git a/src/Config/ConfigSourceAuditProvider.php b/src/Config/ConfigSourceAuditProvider.php index 80a9773..7434c20 100644 --- a/src/Config/ConfigSourceAuditProvider.php +++ b/src/Config/ConfigSourceAuditProvider.php @@ -57,6 +57,7 @@ final readonly class ConfigSourceAuditProvider public function audit(): array { $yamlPaths = $this->collectYamlParameterPaths(); + $genreSourcePaths = $this->collectGenreConfigurationSourcePaths(); $fallbackAccessors = []; $constructorDefaults = []; $phpConstants = []; @@ -135,14 +136,30 @@ final readonly class ConfigSourceAuditProvider 'fallback_accessors_missing_yaml' => count($missingYamlFallbacks), 'constructor_defaults' => count($constructorDefaults), 'constructor_defaults_without_yaml_mapping' => count($constructorPhpDefaults), + 'genre_value_paths_with_source_paths' => count($genreSourcePaths), + 'genre_declared_source_paths' => $this->countGenreDeclaredSourcePaths($genreSourcePaths), ], 'warnings' => $this->buildWarnings($missingYamlFallbacks, $phpOnlyConstants, $constructorPhpDefaults), 'fallback_accessors' => $fallbackAccessors, 'constructor_defaults' => $constructorDefaults, 'php_constants' => $phpConstants, + 'genre_configuration_source_paths' => $genreSourcePaths, ]; } + /** + * @param array $sourcePaths + */ + private function countGenreDeclaredSourcePaths(array $sourcePaths): int + { + $count = 0; + foreach ($sourcePaths as $paths) { + $count += count($paths); + } + + return $count; + } + /** * @return list> */ @@ -158,6 +175,78 @@ final readonly class ConfigSourceAuditProvider return $files; } + + /** + * @return array + */ + private function collectGenreConfigurationSourcePaths(): array + { + $file = $this->projectDir . '/config/retriex/genre.yaml'; + if (!is_file($file)) { + return []; + } + + $parsed = Yaml::parseFile($file); + if (!is_array($parsed)) { + return []; + } + + $parameters = $parsed['parameters'] ?? []; + if (!is_array($parameters)) { + return []; + } + + $genreConfig = $parameters['retriex.genre.config'] ?? []; + if (!is_array($genreConfig)) { + return []; + } + + $configurationValues = $genreConfig['configuration_values'] ?? []; + if (!is_array($configurationValues)) { + return []; + } + + $out = []; + $this->collectGenreSourcePathsRecursive($configurationValues, '', $out); + + return $out; + } + + /** + * @param array $value + * @param array $out + */ + private function collectGenreSourcePathsRecursive(array $value, string $path, array &$out): void + { + $sourcePaths = $value['source_paths'] ?? null; + if (is_array($sourcePaths)) { + $clean = []; + foreach ($sourcePaths as $sourcePath) { + if (!is_string($sourcePath) || trim($sourcePath) === '') { + continue; + } + + $sourcePath = trim($sourcePath); + if (!in_array($sourcePath, $clean, true)) { + $clean[] = $sourcePath; + } + } + + if ($clean !== [] && $path !== '') { + $out[$path] = $clean; + } + } + + foreach ($value as $key => $child) { + if ($key === 'source_paths' || !is_string($key) || !is_array($child)) { + continue; + } + + $childPath = $path === '' ? $key : $path . '.' . $key; + $this->collectGenreSourcePathsRecursive($child, $childPath, $out); + } + } + /** * @return array */ diff --git a/src/Config/GenreConfig.php b/src/Config/GenreConfig.php index d3ff573..88a1886 100644 --- a/src/Config/GenreConfig.php +++ b/src/Config/GenreConfig.php @@ -64,6 +64,25 @@ final class GenreConfig return is_array($groupValues) ? $groupValues : []; } + + /** + * Returns all declared legacy/effective source paths from the single-genre value surface. + * + * The returned map uses the configuration_values-relative value path as key and the + * declared source paths as value. This is metadata only; runtime value lookup still + * happens through the typed getValue* accessors. + * + * @return array + */ + public function getConfigurationValueSourcePaths(): array + { + $values = $this->getConfigurationValues(); + $out = []; + $this->collectSourcePaths($values, '', $out); + + return $out; + } + /** * Returns a non-empty string list from the central single-genre value surface. * @@ -153,6 +172,31 @@ final class GenreConfig return $this->config; } + + /** + * @param array $value + * @param array $out + */ + private function collectSourcePaths(array $value, string $path, array &$out): void + { + $sourcePaths = $value['source_paths'] ?? null; + if (is_array($sourcePaths)) { + $clean = $this->uniqueStringList($sourcePaths); + if ($clean !== [] && $path !== '') { + $out[$path] = $clean; + } + } + + foreach ($value as $key => $child) { + if ($key === 'source_paths' || !is_string($key) || !is_array($child)) { + continue; + } + + $childPath = $path === '' ? $key : $path . '.' . $key; + $this->collectSourcePaths($child, $childPath, $out); + } + } + private function string(string $path, string $fallback): string { $value = $this->value($path, $fallback); diff --git a/src/Config/RetriexEffectiveConfigProvider.php b/src/Config/RetriexEffectiveConfigProvider.php index 6d2e323..317390c 100644 --- a/src/Config/RetriexEffectiveConfigProvider.php +++ b/src/Config/RetriexEffectiveConfigProvider.php @@ -1179,6 +1179,18 @@ final readonly class RetriexEffectiveConfigProvider } } + foreach ($this->collectGenreConfigurationValueSourcePaths($configurationValues) as $valuePath => $sourcePaths) { + foreach ($sourcePaths as $sourcePath) { + if (!isset($flattened[$sourcePath])) { + $warnings[] = sprintf( + 'genre.configuration_values.%s references unknown source path: %s.', + $valuePath, + $sourcePath + ); + } + } + } + foreach (array_keys($surface) as $group) { if (!is_string($group) || $group === '') { continue; @@ -1190,6 +1202,54 @@ final readonly class RetriexEffectiveConfigProvider } } + + /** + * @param array $configurationValues + * @return array + */ + private function collectGenreConfigurationValueSourcePaths(array $configurationValues): array + { + $out = []; + $this->collectGenreSourcePathsRecursive($configurationValues, '', $out); + + return $out; + } + + /** + * @param array $value + * @param array $out + */ + private function collectGenreSourcePathsRecursive(array $value, string $path, array &$out): void + { + $sourcePaths = $value['source_paths'] ?? null; + if (is_array($sourcePaths)) { + $clean = []; + foreach ($sourcePaths as $sourcePath) { + if (!is_string($sourcePath) || trim($sourcePath) === '') { + continue; + } + + $sourcePath = trim($sourcePath); + if (!in_array($sourcePath, $clean, true)) { + $clean[] = $sourcePath; + } + } + + if ($clean !== [] && $path !== '') { + $out[$path] = $clean; + } + } + + foreach ($value as $key => $child) { + if ($key === 'source_paths' || !is_string($key) || !is_array($child)) { + continue; + } + + $childPath = $path === '' ? $key : $path . '.' . $key; + $this->collectGenreSourcePathsRecursive($child, $childPath, $out); + } + } + /** * @param array $paths */