From f0c1bdacfa29e1121672a512bb437be7f264bc4e Mon Sep 17 00:00:00 2001 From: team3 Date: Tue, 7 Jul 2026 00:25:23 +0200 Subject: [PATCH] update --- AUFTRAG-inventar-fix.md | 1146 +++++++++++++++-- backend/block_calls.py | 153 +-- backend/board_artefacts.py | 16 +- backend/board_inventory.py | 29 +- backend/fake_agents.py | 67 +- backend/tests/test_block_calls.py | 139 +- backend/tests/test_board_inventory.py | 9 +- backend/tests/test_e2e_fake.py | 47 +- frontend/src/components/GenerationView.vue | 2 +- frontend/src/components/TopicDetail.vue | 11 +- frontend/src/levels.js | 4 + .../Prompt/Blocks-Gruppierung-Completion.md | 17 +- templates/Prompt/Blocks-Gruppierung.md | 38 +- templates/Prompt/Subblock-Anreichern.md | 32 + templates/Prompt/Subblock-Verify.md | 20 +- 15 files changed, 1363 insertions(+), 367 deletions(-) create mode 100644 templates/Prompt/Subblock-Anreichern.md diff --git a/AUFTRAG-inventar-fix.md b/AUFTRAG-inventar-fix.md index cabbe9f..b8a2ce1 100644 --- a/AUFTRAG-inventar-fix.md +++ b/AUFTRAG-inventar-fix.md @@ -1,98 +1,1092 @@ -# Auftrag: Inventar-Pipeline reparieren — zu viele, fehlende und schlecht benannte Bausteine +QUELLE: https://hub.shopware.com/learn/unit/debug-your-code-within-shopware -## Kontext +Power-up your learning experience now +Benefit from progress tracking, gamification, individualized suggestions and community discussions. +Great, we are making progress in this learning course. You have learned how to create a plugin, how to add custom data to a product, and how to create a custom controller. +But what if some parts in your code don’t work as expected? How can you find the root cause of the issue? This is where debugging comes into play. +In this learning unit, we will focus on how to debug your code within Shopware. You will learn how to use debugging tools, set breakpoints, inspect variables, trace the execution flow of your code, and use logging to identify issues. Additionally, we will cover common debugging techniques and best practices. +There are two different template sources. One being the Shopware repository that our community commits to and a production-optimized template that is a symfony flex recipe. +If you are using the Symfony Flex template: +composer create-project shopware/production You will need to add the Symfony Webprofiler Bundle to your dev dependencies. +composer require --dev symfony/web-profiler-bundleDebugging should only be done in a safe environment, never in production. +Let’s check our .env file for the APP_ENV variable. It should be set to dev. +APP_ENV=devNow we refresh our homepage and should see a Symfony Profiler toolbar. If not, you might have to clear the cache with the following command: +bin/console cache:clearThe Symfony Profiler provides many helpful features and overviews, including a performance timeline, memory usage and detailed request insights. It can help you uncover critical issues in your codebase – for example, when rules are not active, or when there are too many cache tags. +Some of them are Symfony standard, like: +Others are Shopware specific: +Twig dump() is a function that can be used to dump variables in your templates. +{{ dump(variable) }}If you don’t pass a variable, it will dump all variables in the current context. This can be very helpful to see what data is available in your template and how it is structured. +Xdebug is a PHP extension that helps you debug your code. It provides a lot of valuable information such as stack traces, profiling, and code coverage. +Especially when working on complex data structures, setting breakpoints and inspecting variables can be very helpful. +Depending on your setup, you can install or activate Xdebug as follows: +If you are using the Shopware Devenv, you can simply enable Xdebug through the built-in configuration. +First, install the PHP extension with your package manager: +sudo apt update +sudo apt install php-xdebugThen configure your php.ini file: +sudo nano /etc/php/8.x/fpm/php.ini +zend_extension=xdebug.so +xdebug.mode=debug +xdebug.start_with_request=yes +xdebug.client_host=127.0.0.1 +xdebug.client_port=9003After that, enable the extension: +sudo phpenmod xdebugFinally, restart your webserver: +sudo systemctl restart apache2If you are using Docker, follow this official Shopware guide to enable Xdebug. +Docker will be the recommended standard setup for Shopware in the future. +If you are using Dockware, it is recommended to use the #dev image, where you can simply enable it with a flag. +Dockware is planned to be deprecated in the future. It is recommended to migrate to a Docker-based setup for long-term compatibility. +If you are using PhpStorm, you can configure Xdebug in the IDE settings. Make sure to set the correct path mappings (important to set the public folder here), port, and server. +You can find a detailed guide in the official PhpStorm documentation. +You will find your logs in the var/log directory of your Shopware root directory. There are different log files for different purposes. +If your environment is set to dev, you will see a lot of logs in your dev.log file. +Shopware uses the monolog-bundle from Symfony, so you can configure your logging in the config/packages/dev/monolog.yaml file. +Find out more about logging in Shopware. +Finding performance bottlenecks can be a challenging task, especially in complex production setups. +Shopware provides a profiler interface that can help you identify performance issues. +The profilers differ in the way they are typically used. +Datadog and Tideways are two popular profiler backends that gather traces and performance data and have predefined dashboards. +OpenTelemetry is open-source and provides a lot of flexibility. You can use it to trace your code and send the data to different backends like Jaeger, Zipkin, or Prometheus. +We mentioned the Symfony Profiler already. It provides detailed information about the performance of your code but is only used locally in development environments. +The Frosh Development Helper is a Shopware plugin that provides a lot of useful tools for developers. +Some features are: +The Meteor Shopware 6 Toolkit is a Chrome extension that helps you jump to the right place in the Shopware 6 administration when being on a product detail page or category page. +Helpful when debugging or developing new features. +In this learning unit, you have learned: +dump function.With these tools and best practices, you can confidently trace, analyze, and fix issues within your Shopware plugins. +Inspect discussions, ask questions and give feedback. +Sign up now -Das Thema `aak` (Uni-Quelle `uni/aak`: Skript + Serien + Klausuren + Hausaufgaben) erzeugte **108 Bausteine**. Soll-Abgleich mit dem Skript ergibt: sinnvoll wären **~60–70** (davon ~40–45 Skript-Kern). Der letzte QA-Lauf (`storage/qa/aak/20260705-201016.json`) zeigt 36 % Dubletten-Verdacht bei Note 10.0 und Lücken-Quote 0.0 — obwohl Kernkonzepte fehlen. +QUELLE: https://hub.shopware.com/learn/unit/add-data-to-your-product -Zahlenfluss des Laufs (aus `storage/topics/aak/arbeit/inventar-*.json`): -~1150 Roh-Nennungen (24 Chunks × 2 Research-Pässe) → 203 Kandidaten → Filter degradiert 69, Recheck holt 17 zurück → 134 → Dedup prüft 183 Paare, merged nur 17 (9 %) → 117 → Gruppierung: 5 Umbrellas, 17 Umbrella-Merges per Type-Gate verworfen → Supplement +4 → **108**. +Power-up your learning experience now +Benefit from progress tracking, gamification, individualized suggestions and community discussions. +The product is at the front and center of every Shopware shop. It is the most important entity and can be extended with custom data. +In this learning unit, we will take a look at ways in which we can programmatically add custom data to a product. +Before we start, we need to decide how we want to add custom data to our product. There are three main ways to do this: +Product properties: The easiest way to add custom data to a product. You can add properties like color, size, or any other custom property you need. +Custom fields: Custom fields are more flexible than product properties and can be used to store any kind of data. +Custom entities: If more complex data structures are required, you can create custom entities in Shopware. Custom entities are custom database tables, which can be used to store any kind of data. +| Feature | Product Properties | Custom Fields | Entity Extensions | +|---|---|---|---| +| CRUD via Admin | Yes | Yes | Not by default | +| Category filtering | Yes | No | No | +| Variant generation | Yes | No | No | +| Hide in Admin | No | Yes | Yes | +| Migration needed | No | No | Yes | +| Association possible | No | No | Yes | +| Custom Entities | No | No | Yes | +As shown, each method has its own advantages and disadvantages. Let’s break down the use cases: +Use product properties when you need to filter products in your category listing or generate variants based on the property. +Example: color: red, blue, green: Generates variants for each color. Needed for filtering and variant generation. +Use custom fields when you need to store additional, simple information that is not part of the standard attributes and does not require filtering or variant generation. They can be added to almost all entities (e.g., product entity, customer entity, order entity). +Example: dangerous-goods: true: Displays a warning on the product page if set to true. Not necessarily needed for filtering or variant generation. +In this learning unit, we will focus on adding custom fields to a product. Custom fields are a good compromise between flexibility and ease of use. +Use custom entities if you need to store complex data structures that are not related to the product itself. +Example: customize-product: COMPLEX DATABASE STRUCTURE: Stores complex data structures in separate database tables. For example a ring product configurator (material, size, engraving, etc.). +The Custom Products extension is a good example of the implementation of custom entities. +In our case, we want to add a custom field to our product that stores a “cargo” flag. If the flag is set to true, a warning should be displayed on the product page. +This information is not needed for filtering or variant generation, so we will use a custom field for this. It will also be used in the email notification for the warehouse. +To add a custom field to a product, we need to create a new custom fieldset and add a custom field to it. We can do this via the Shopware administration, or we can do it programmatically. +As of Shopware 6.7.0.0, custom field names and field set names must be valid Twig variable names. This means hyphens (-) and dots (.) are no longer allowed. Existing custom fields will continue to work. The validation is only enforced when creating new custom fields. +In our case, it makes sense to define custom fields programmatically because we want to automate the process of adding the custom field to our products. This plugin will be rolled out to all shops in the network, and to avoid human error on creation, we will automate the process. +If you feel exploratory, you can create a very basic plugin with the following command: +bin/console plugin:create AcademyDemoProductCustomFieldIn the interactive setup, you can choose the example custom fieldset option if you want Shopware to generate the basic file structure for you. In this learning unit, you will replace the generated example content before installing the plugin. +Do you want to create an example custom fieldset? (yes/no)The generated Shopware example custom fieldset uses generic technical names such as swag_example_set and swag_example_size. Custom field names must be unique in the database. If the generated example was installed before, installing another plugin with the same names can cause a duplicate-entry error. +Before you install the plugin, replace the generated example content with the code shown below. This learning unit uses its own custom field name, academy_demo_product_cargo. If you compare your code with the Academy reference plugin, remember that the reference plugin uses different names, such as academy_product_set and academy_product_cargo. +Keep this command for later. After you have added the code in this learning unit, install and activate the plugin with: +bin/console plugin:refresh +bin/console plugin:install AcademyDemoProductCustomField --activateOr you can use our AcademyProductCustomField reference plugin. The reference plugin follows the same pattern, but it uses the AcademyProductCustomField namespace and academy_* technical names. +The custom fields integration in this example is a combination of different parts: +| Part | Description | +|---|---| +| The CustomFieldsInstaller | A PHP class that creates and removes the field set, relation, and custom field | +| The plugin lifecycle | Calls the installer during install and uninstall | +| The template | A Twig file that renders the custom field | +In our case, we do not need to create a migration to add the custom field set, because we will use an entity (product) that already exists. +If you are more versed and have a custom entity, please take a look at the custom entity documentation. +The custom fieldset is a collection of custom fields that can be added to a product. In this example, the installer creates the field set, links it to the product entity, and adds the custom field in one nested write. +Replace the content of your generated CustomFieldsInstaller.php file with the following content: +customFieldSetRepository->upsert([ + [ + 'id' => Uuid::fromStringToHex(self::CUSTOM_FIELD_SET_NAME), + 'name' => self::CUSTOM_FIELD_TECHNICAL_NAME, + 'position' => 0, + 'config' => [ + 'label' => [ + 'en-GB' => self::CUSTOM_FIELD_SET_NAME, + 'de-DE' => self::CUSTOM_FIELD_SET_NAME, + ], + ], + 'relations' => [ + [ + 'id' => Uuid::fromStringToHex(self::CUSTOM_FIELD_SET_NAME . '_product_relation'), + 'entityName' => ProductDefinition::ENTITY_NAME, + ], + ], + 'customFields' => [ + [ + 'id' => Uuid::fromStringToHex(self::CUSTOM_FIELD_NAME), + 'name' => self::CUSTOM_FIELD_NAME, + 'type' => CustomFieldTypes::BOOL, + 'config' => [ + 'componentName' => 'mt-switch', + 'type' => 'checkbox', + 'label' => [ + Defaults::LANGUAGE_SYSTEM => 'Cargo Flag', + 'en-GB' => 'Cargo Flag', + 'de-DE' => 'Cargo Flag', + ], + ], + ], + ], + ], + ], $context); + } + public function uninstall(Context $context): void + { + $this->customFieldSetRepository->delete([ + [ + 'id' => Uuid::fromStringToHex(self::CUSTOM_FIELD_SET_NAME), + ], + ], $context); + } +}You do not need a separate services.xml registration for this example. The plugin lifecycle can create the installer and pass the custom_field_set.repository service directly. +Now we can call the installer from our plugin class: +installCustomFields($installContext->getContext()); + } + public function uninstall(UninstallContext $uninstallContext): void + { + parent::uninstall($uninstallContext); + if ($uninstallContext->keepUserData()) { + return; + } + $this->uninstallCustomFields($uninstallContext->getContext()); + } + private function installCustomFields(Context $context): void + { + $customFieldsInstaller = new CustomFieldsInstaller( + $this->container->get('custom_field_set.repository') + ); + $customFieldsInstaller->install($context); + } + private function uninstallCustomFields(Context $context): void + { + $customFieldsInstaller = new CustomFieldsInstaller( + $this->container->get('custom_field_set.repository') + ); + $customFieldsInstaller->uninstall($context); + } +}Plugin lifecycle methods are commonly used to create or clean up plugin-owned setup data, such as custom field sets. +In this example, the plugin creates the custom field during installation and removes it during uninstallation only when the user does not keep plugin data. +Our class CustomFieldsInstaller +Uuid::fromStringToHex() to create stable IDs from these values.install() method creates or updates the custom field set, product relation, and academy_demo_product_cargo custom field in one nested write.uninstall() method removes the custom field set again when plugin data should be deleted.The plugin base class (AcademyDemoProductCustomField) +install() method calls installCustomFields(). bin/console plugin:install .uninstall() method calls uninstallCustomFields() only when the user does not keep plugin data. bin/console plugin:uninstall .Flow: When the plugin is installed, the custom field set is created, linked to the product entity, and filled with the academy_demo_product_cargo custom field. After that, the custom field is available in the product settings in the administration. +To display the custom field on the product detail page, we need to extend the buy-widget template and include our custom template. +In this example, we extend the original buy-widget and include a new Twig file that renders the custom field: +{% sw_extends '@Storefront/storefront/component/buy-widget/buy-widget.html.twig' %} +{% block buy_widget_ordernumber_container %} + {{ parent() }} + {% include '@AcademyDemoProductCustomField/storefront/component/academy-demo-product-cargo.html.twig' %} +{% endblock %}Here, we use sw_extends and parent to inherit the buy-widget template and extend it by using include to load our custom template. +In this learning unit, we use the standard Twig include syntax for simplicity. In advanced use cases, consider using Shopware’s sw_include tag, which supports multi inheritance. +It is always good practice to use includes in Twig. This way you can keep your code clean and modular. +Also, remember to wrap your custom field in a block, so you can override it in your theme. +{% block academy_demo_product_cargo %} + {% if product.translated.customFields.academy_demo_product_cargo %} +
+ Cargo shipping +
+ {% endif %} +{% endblock %}As some of you may be aware, you can add media to the custom field via CustomFieldTypes::MEDIA. Nonetheless, choosing to define our custom field in this way gives full control to the developer / plugin - a new release of our Plugin can contain a new image, and the rollout will change it in all shops. In contrast, the media field cannot be updated in this way. +So let’s add an image to the file system. This is done via the asset structure in Shopware. +# PluginRoot +├── composer.json +└── src + ├── Resources + │ └── public + │ └── images + │ └── cargo.png <-- Asset file here + └── AcademyDemoProductCustomField.phpIf your image does not show up in the storefront, you can run the bin/console assets:install command to copy the assets to the public (PROJECT_ROOT/public/bundles) folder. +You can assign the custom field to a product via the Shopware administration. Go to the product detail page and click on the “Specifications” tab. +Here you can add the custom field to the product. In our case, we toggle the “Cargo flag” to true. +You can also assign the custom field to a product via the Admin API. You can use the POST /api/product/{productId} endpoint to update the product with the custom field. +PATCH http://YOUR_SHOP_URL/api/product/YOUR_PRODUCT_UUID +Content-Type: application/json +Authorization: Bearer YOUR_ACCESS_TOKENWith the JSON body: +{ + "customFields": { + "academy_demo_product_cargo": true + } +}Learn more about the Admin API in our Stoplight documentation. +The result of our work is a product that has a custom field “Cargo flag” that can be toggled in the Shopware administration. If the flag is set to true, an image of a truck is displayed on the product page, indicating that our XXL washing machine is shipped via cargo. +If you did everything correctly but don’t see the cargo flag on the product page, run the following commands in your shop’s root directory: +bin/console cache:clear: Clears old cache and reindex compiled Twig templates.bin/console assets:install: Copies your plugin’s assets into the public bundles folder ([shop_root]/public/bundles). At this point, you may have already done it.bin/console theme:compile: Rebuilds your storefront theme so that the changes (theme configurations, assets, SCSS changes) become visible.In this learning unit, you have learned: +CustomFieldsInstaller and hook it into the plugin lifecycle (install, uninstall).With this, you have a solid foundation to extend products safely and consistently, from data modeling to storefront rendering and API updates. +Inspect discussions, ask questions and give feedback. +Sign up now -Die Pipeline-Stages stehen in `inventory_stages` (`backend/board_inventory.py:1614`). Alle Zeilennummern unten beziehen sich auf den Branch `refactor` inkl. der offenen (uncommitteten) Diffs — siehe „Leitplanken". +QUELLE: https://hub.shopware.com/learn/unit/getting-started-with-shopware-backend-development -## Befund: sechs Root Causes +Power-up your learning experience now +Benefit from progress tracking, gamification, individualized suggestions and community discussions. +In this learning unit, you will implement a simple storefront controller and use it to display a modal window in the storefront. +Before we start implementing, you need to know where development begins. In Shopware, we have Plugins. A plugin is a server-side extension implemented as a Symfony bundle, like browser-addons for the browser. + Plugins are the entry point of development without breaking the core logic of Shopware. You can implement controllers, services, event subscribers, database entities, CLI commands, scheduled tasks, and storefront templates and more. + Classes from your plugin are registered in Symfony’s Dependency Injection (DI) container (via services.xml) and injected where needed. +To get a quick picture, check the screenshots below. They show you a minimal plugin structure, the entry points for controller and subscribers, and how services are registered with the DI container. +Feel free to dive deeper by exploring the official guide: Plugin base structure. +Every person is different and unique, so is every Shopware project. In many projects, however, you want to show something specific in a modal window to not interrupt the shopping experience. +This can be a newsletter subscription, a login form, or a product detail view. To achieve this, we will fetch some data from an external API and show it in a modal window. +We cover the topic in our official documentation on adding a custom controller. +We will walk through creating a storefront controller step by step, but we recommend reading the documentation first to get an overview. +If you feel like exploring, you can create a very basic plugin with the following command and in the interactive setup, choose the Storefront option to generate a controller that you can use in the storefront. For all other options, you can choose “no” to keep it clean and simple. +Your generated controller is called ExampleController and would need to be renamed if you want to follow this guide step by step. +bin/console plugin:create AcademyStorefrontController + Do you want to create an example storefront controller? (yes/no) [yes]:The plugin:create command is a powerful command to create plugins with all necessary files and directories. Especially if you want to create entities, a JavaScript plugin, or an administration module. +Or you can use our example plugin, which already contains all necessary files. +# Clone the repository into your custom/plugins directory of your Shopware project +git clone git@github.com:ShopwareAcademy/AcademyStorefrontController.git custom/plugins/AcademyStorefrontControllerAfter creating or cloning the plugin, first refresh the plugin list so Shopware can detect the new plugin: +bin/console plugin:refreshThen install and activate the plugin: +bin/console plugin:install AcademyStorefrontController --activateOptionally, you can clear the cache to ensure your changes take effect: +bin/console cache:clearAlternatively, you can clear the cache automatically after the installation by using the --clearCache option: +bin/console plugin:install AcademyStorefrontController --activate --clearCachecomposer.json File (Optional)If you used the generator to create the plugin, it will have a composer.json file in the plugin directory. For your controller to work as expected, make sure your namespace autoloading is correctly configured. Add the following code snippet to the composer.json file of your generated plugin: +"autoload": { + "psr-4": { + "ShopwareAcademy\\StorefrontController\\": "src/" + } +}, +"autoload-dev": { + "psr-4": { + "ShopwareAcademy\\StorefrontController\\Tests\\": "tests/" + } +}Then, in the root directory of your shop, rebuild the autoloader to make sure your changes take effect: +composer dump-autoloadYou can also check out our demo plugin, which already contains the correct autoload configuration. +If you used the generator to create the controller, it creates a controller called ExampleController. In this learning unit, we use ImageController. To rename it safely: +ImageController (e.g., src/Storefront/Controller/ImageController.php and class ImageController extends StorefrontController).ShopwareAcademy\StorefrontController\Storefront\Controller).services.xml is service id="ShopwareAcademy\StorefrontController\Storefront\Controller\ImageController" public="trueFinally, clear the cache so that your changes take effect by running the following command in your shop’s root directory: +bin/console cache:clearThe controller feature consists of four parts: +| Part | Description | +|---|---| +| The controller | A PHP class | +| The route import | Part of the routes.xmlfile | +| The service definition | Part of the services.xmlfile | +| The template | A Twig file | +If you are a beginner, it is highly recommended to use the generator to create the controller. Often the little things like the correct namespace or the correct path can be tricky and lead to frustration. +First, you need to create the controller itself. This is a simple PHP class that extends the StorefrontController class. + [StorefrontRouteScope::ID], 'XmlHttpRequest' => true])] +class ImageController extends StorefrontController +{ + public function __construct( + private readonly HttpClientInterface $client, + private readonly SystemConfigService $systemConfigService + ) { + } + #[Route( + path: '/image', + name: 'frontend.image.show', + methods: ['GET'] + )] + public function showImage(SalesChannelContext $context): Response + { + $apiAccessKey = $this->systemConfigService->get('AcademyStorefrontController.config.apiAccessKey', $context->getSalesChannelId()); + $apiProvider = $this->systemConfigService->get('AcademyStorefrontController.config.apiProvider', $context->getSalesChannelId()); + $response = $this->client->request('GET', 'https://api.'.$apiProvider.'.com/v1/images/search', [ + 'headers' => [ + 'x-api-key' => $apiAccessKey + ] + ]); + $data = $response->toArray(); + $imageUrl = $data[0]['url']; + return $this->renderStorefront('@AcademyStorefrontController/storefront/page/image.html.twig', [ + 'imageUrl' => $imageUrl + ]); + } +} +ImageController extends the StorefrontController which gives you helper functions like renderStorefront().ImageController is set to storefront and the route is marked as AJAX-only (XmlHttpRequest: true).HttpClientInterface and SystemConfigService from the DI Container.showImage() is bound to the GET request with the path /image, The route name is frontend.image.show, where you can call it from JS or Twig.SystemConfigService gets the apiProvider and the API key from the plugin config.imageUrl as a parameter to show the view.In the following, you get more detailed information of the parts. +Attributes are a new feature in PHP 8. They are used to define metadata for classes, methods, and properties. In this case, we define the route for the showImage method. +#[Route(defaults: [PlatformRequest::ATTRIBUTE_ROUTE_SCOPE => [StorefrontRouteScope::ID], 'XmlHttpRequest' => true])]Shopware comes with four route scopes: +storefront -> StorefrontRouteScope::IDstore-api -> StoreApiRouteScope::IDapi -> ApiRouteScope::IDadministration -> AdministrationRouteScope::IDSince we are creating a storefront controller, we should use the storefront route scope. The xmlHttpRequest attribute is set to true, which means that the route is only available for AJAX requests. The xmlHttpRequest is set to true as we want to open the image in a modal window. +You should always use the constant for the route scope, as it is more readable and less error-prone than using a string. +The method showImage also has a route attribute. This is the route that will be called when the link is clicked in the storefront. +It defines a readable path, a name, and the HTTP method. The path is /image, the name is frontend.image.show, and the method is GET. +#[Route( + path: '/image', + name: 'frontend.image.show', + methods: ['GET'] + )]SystemConfigServiceThe controller uses the SystemConfigService to fetch the optional API access key and provider from the plugin configuration. Plugin configurations are stored in the database and can be accessed via the SystemConfigService. +We are using two configuration input fields, the important one is the apiProvider field. It is a single-select field with two options: thecatapi and thedogapi. +So you can choose if you are a cat or dog person, based on that you will get a random dog or cat image. + + + + API settings + + apiProvider + + + + + + thedogapi + + + apiAccessKey + + You don't need an API key to get a single image. This input is optional for > 10 images. https://developers.thecatapi.com + + +If you want to learn more about plugin configurations, check out our plugin configuration unit, which is part of this learning path. +HttpClientInterfaceWe are using the HttpClientInterface to fetch the image from the API. The HttpClientInterface is a Symfony component that allows you to send HTTP requests. +The route import is part of the routes.xml file. This file is located in the src/Resources/config directory of your plugin. + + + +This tells the system where to find the controller. The type="attribute" is important here, as it tells Shopware that any routes declared in the controller class are defined as attributes. +If you don’t know what attributes are, don’t worry. It is a new feature in PHP 8 and used to define metadata for classes, methods, and properties. Check out the Symfony Docs for more information. +The service definition is part of the services.xml file. This file is located in the src/Resources/config directory of your plugin. +It is one of the key files in your plugin, as it tells the system which classes are services and how they should be instantiated. + + + + + + + + + + + +In this file, we define the ImageController as a service. We pass the HttpClientInterface and the SystemConfigService as arguments to the constructor. +We also call the setContainer method to define the container service respectively. +Normally, in the StorefrontController, the setTwig method is also included beneath the setContainer method. However, since Shopware 6.7.0.0, the setTwig method is removed from the StrorefrontController. +If you want to debug a route, type bin/console debug:router YOUR_ROUTE_NAME in your console. It will validate first if the route exists and then show you the route configuration. Note that your plugin must be installed one time, otherwise your custom route will not be recognized from the system. ++--------------+----------------------------------------------------------------------------------+ +| Property | Value | ++--------------+----------------------------------------------------------------------------------+ +| Route Name | frontend.image.show | +| Path | /image | +| Path Regex | {^/image$}sDu | +| Host | ANY | +| Host Regex | | +| Scheme | ANY | +| Method | GET | +| Requirements | NO CUSTOM | +| Class | Symfony\Component\Routing\Route | +| Defaults | XmlHttpRequest: true | +| | _controller: StorefrontPlugin\Storefront\Controller\ImageController::showImage() | +| | _routeScope: array (0 => 'storefront',) | +| Options | compiler_class: Symfony\Component\Routing\RouteCompiler | +| | utf8: true | ++--------------+----------------------------------------------------------------------------------+ +The template is a twig file used to render the output of the controller. It is located in the src/Resources/views/storefront/page directory of your plugin. +The path to that file is defined via the controller class method renderStorefront as seen in the controller example above (src/Resources/views/storefront/page/image.html.twig). +{% block base_content %} +

Some random image

+ test +{% endblock %}To display the output of your custom controller in the storefront, you need to create a link to its route in a Twig template. +In Shopware, you override or extend a template by mirroring their folder structure and file name inside your plugin. +This means, if the original file (located under vendor/shopware) is at storefront/Resources/views/storefront/component/buy-widget/buy-widget.html.twig, you must create a file with the same name and path in your plugin: src/Resources/views/storefront/component/buy-widget/buy-widget.html.twig. +So the full path is: [shop_root]/custom/plugins/[your_plugin]/src/Resources/views/storefront/component/buy-widget/buy-widget.html.twig. +{# File is located in src/Resources/views/storefront/component/buy-widget/buy-widget.html.twig #} +{% sw_extends '@Storefront/storefront/component/buy-widget/buy-widget.html.twig' %} +{% block buy_widget_ordernumber_container %} + {{ parent() }} + View Image +{% endblock %}Since Shopware uses plugins and themes that you can sort in a hierarchy, it has its own way to extend templates. You have to use sw_extends instead of extends tag to extend a template from a plugin or theme. +This will create a link in the buy widget that opens a modal window. +The data-ajax-modal="true" attribute tells the system to open the link in a modal window. The data-url attribute tells the system which route to call when the link is clicked. +In this learning unit, you have learned: +services.xml file.routes.xml file.SystemConfigService and HttpClientInterface within a controller.With this, you have a practical foundation to start developing your own backend features and extend the storefront with custom controllers. +Inspect discussions, ask questions and give feedback. +Sign up now -### R1 — Extraktion erlaubt Beweisschritt-Granularität (Hauptursache „zu viele") -- `templates/Prompt/Blocks-Research.md` erklärt „named theorems with their own statement" und jede „standalone REDUCTION" zum eigenen Baustein. `templates/Prompt/Blocks-Source-Uni.md` nennt „formulas and theorems" als Zielkategorie, ohne Granularitätsgrenze. -- Folge im aak-Lauf: 14 Beweisschritt-/Beweis-Objekt-Bausteine (z. B. `Type-I`, `Schedule D*`, `Gewichtsfunktion d2`, `Zick-Zack-Tour`, `Optimaler Rundweg OPT((Kn,d2)) = n`, `3-SAT ≤ K-COLOR: G=(V,E) Konstruktion`) und 8 Bausteine für EIN Konzept („untere Schranken via ETH": `Lower Bound via k/n/|E|/|T|` + 4 weitere). -- Verstärker: `_is_named_statement` (`backend/blocks.py:1355`) schützt alles mit Reduktions-Syntax vor Demotion — auch Konstruktionsdetails wie `…: G=(V,E) Konstruktion`. +QUELLE: https://hub.shopware.com/learn/unit/app-configurations -### R2 — Quellen-Nummern als Titel, Reparaturstufe greift nicht (Hauptursache „Benennung") -- Research-Pässe übernehmen Skript-Nummerierungen als Titel („Satz 7.13", „Bemerkung 7.22", „Definition 6.19"). 6 finale Bausteine tragen reine Satz-/Bemerkungs-Nummern. -- `_REFERENCE_RE` (`backend/blocks.py:1022`) matcht nur nackte Referenzen („Bemerkung 7.22"), NICHT „Satz 7.13 (Christofides)" oder „Satz 7.6: Kriterium für Eulerschen Kreis" — die laufen als „gute" Titel durch. -- Rename ist Best-Effort: In `clarify` wird nur umbenannt, wenn ein Judge freiwillig `rename` liefert (`backend/board_inventory.py:663`). Beispiel Cluster `08c52a84`: Judge j1 schlug für „Bemerkung 7.22" den korrekten Konzeptnamen vor („Untere Schranke für FPTAS …"), die Aggregation verwarf ihn. -- Singletons überspringen Naming komplett: `_choose_title` läuft nur bei `len(rows) > 1` (`backend/board_inventory.py:725` und `:802`). +Power-up your learning experience now +Benefit from progress tracking, gamification, individualized suggestions and community discussions. +In this learning unit, you will learn how to add app configurations to your Shopware App. App configurations allow you to predefine values or load default data during installation. This can save time and reduce manual setup steps. +Imagine you are developing a shipping provider app. Adding the shipping methods with a plugin can be cumbersome – you need to add the shipping methods manually in the Shopware administration. To make this process easier, you can add a configuration file to your App that contains the shipping methods. This way, the user can easily select the predefined shipping methods directly in the Shopware administration. +manifest.xml File to Your AppEvery Shopware App starts with a manifest.xml file. This file defines the app’s metadata, permissions, and configuration setup. It is located in the root directory of your App. +The basic manifest.xml looks like this: + + + + ShippingApp + + This app adds 2 new shipping methods + shopware AG + (c) shopware AG + 1.0.0 + MIT + +For an in-depth explanation of the manifest.xml file, please refer to the official Shopware documentation. +In this learning unit, we will not cover the entire manifest.xml file, but focus specifically on the configuration section. +Shopware Apps support predefined XML tags such as shipping-methods and shipping-method. These tags allow you to define new shipping methods directly in the App’s manifest.xml file without writing additional code. +This is particularly useful for shipping provider apps, as they often need to register custom shipping methods during installation. +Here is how you can add shipping methods to the app configuration: + + + + FastShippingMethod + Fast shipping method + + + c8864e36a4d84bd4a16cc31b5953431b + From 1 to 2 days + 1 + 2 + day + + + + + CargoShippingMethod + Cargo shipping method + + + 46177c36a84b418b8ae1a22028aeb1c5 + From 1 to 2 weeks + 1 + 2 + week + + +Avoid changing the identifier after the App is released. If you do so, Shopware will treat it as a new shipping method and create a duplicate entry, which you will have to remove manually. +Great, we added two shipping methods that can be selected in the Shopware administration. This will make the process of adding shipping methods easier for the user. +For a complete list of all available tags within shipping methods, please refer to the official Shopware documentation. +Besides shipping methods, you can also define payment providers and tax providers via the manifest.xml file. +After adding or changing configurations in your App, you need to refresh the App, so Shopware can re-read the updated manifest.xml file. It is done in two steps: +Step 1: You have to increase the version number manually in the manifest.xml file. Only then Shopware detects the change and re-reads the file. For example, update the version number from 1.0.0 to 1.0.1. +Before: +1.0.0After: +1.0.1Step 2: Now refresh your App using the following command in your shop’s directory: +bin/console app:refreshThis command updates the App’s configuration and re-registers any changes defined in the manifest.xml file in the database. +Here is a list of all App-related CLI commands available in Shopware: +Available commands for the "app" namespace: + app:activate Activates an app + app:create Creates an app skeleton + app:deactivate Deactivates an app + app:install Installs an app + app:refresh [app:update] Refreshes an app + app:uninstall Uninstalls an app + app:update Refreshes an app + app:url-change:resolve Resolves app url changes + app:validate Validates an appBefore refreshing your App, you can run the bin/console app:validate command to check if your manifest.xml file is valid. This helps you catch syntax or schema errors early. +In this learning unit, you have learned: +manifest.xml file contains the configuration section.With this knowledge, you have a solid overview of how to add App configurations which fundamentally differ from plugins. +Inspect discussions, ask questions and give feedback. +Sign up now -### R3 — Titel-Split zerschneidet Namen (abgeschnittene Titel) -- `_title = entry.split(" — ")[0]` (`backend/textkit.py:35`) + aggressives `_norm_dash` (`backend/textkit.py:77`): ein en-Dash MIT einseitigem Space **innerhalb** eines Titels wird zum Separator → Split mitten im Titel. -- Es gibt keine Klammer-Balance-Prüfung und keinen Repair. Ergebnis: `Aε-Algorithmus (Güte 1+ε, Laufzeit O(n3` und `N P via nicht-deterministische Turingmaschine (Definition 6.` liefen wortwörtlich bis `done`. -- (Die 60-Zeichen-Kürzung in `backend/board_artefacts.py:81` betrifft nur Ordnernamen, nicht Titel — kein Fix nötig.) +QUELLE: https://hub.shopware.com/learn/course/configuration-and-settings -### R4 — Dedup-Dämme brechen (36 % Verdacht überleben) -- Merge braucht Einstimmigkeit zweier Judges (`if ja1 and ja2`, `backend/board_inventory.py:1200`); „uneinig" → kein Merge, kein Tiebreak. -- Parameter-Familien werden als Paare je einzeln mit „nein" beschieden (`Lower Bound via k`↔`n`↔`|E|`↔`|T|`, alle QA-Jaccard 1.0). -- Kandidaten-Generierung verpasst Komposita: `_canonical_key` (`backend/blocks.py:1118`) trennt „Cliquenproblem" nicht in „Clique"+Stopwort → „Clique" vs. „Cliquenproblem (k-Clique)" wird ggf. nie Judge-Kandidat. -- Type-Gate der Gruppierung verwarf 17 Umbrella-Merges (`grund: "type-gate"` in `inventar-gruppierung.json`) — exakt die späteren QA-Dubletten (Approximations-Grundbegriffe, Type-I/II+Schedule-Objekte, NP-Definitionen). -- Filter-Recheck holte 17 korrekte Degradierungen zurück (`ueberstimmt`/`floor_veto` in `inventar-filter-535eb84c.json`) — darunter die komplette Lower-Bound-Familie. +manifest.xml file and how they differ from plugins.Configuring your Shopware instance is a crucial part of the development process. This course will guide you through how to manage configuration and settings of your Shopware instance from a developer’s perspective. +You will explore how to configure your Shopware instance to suit your needs, adapt its behavior, and manage settings across different environments. +Everything starts with the environment variables. They are used to store sensitive information such as database credentials, API keys, and other configuration values. The .env file is not committed to the repository. +System configurations define how your Shopware instance behaves at runtime, for example, whether certain features are enabled or disabled. They can be managed via the administration, Admin API, or static configuration files. +Plugins and Apps can provide their own configuration interfaces. These allow developers and merchants to tailor functionality directly within the administration panel, once the extension is installed and activated. +Learn how to configure, override, and manage environment variables in Shopware for different environments. +Learn how to configure and manage system configurations in Shopware using the Admin API or static configuration files. +Learn how to add configuration fields to your Shopware plugin to make it more flexible, reusable, and customizable. +Learn how to add configuration fields to your Shopware App to make it more flexible, reusable, and customizable. +Learn how to manage configurations in Shopware, from environment variables to system settings and extension configuration. +Sign up to enroll and track your progress on this course +Sign up nowInspect discussions, ask questions and give feedback. +Sign up now -### R5 — Echte Lücken trotz Quote 0.0 (Ursache „fehlende") -- Fehlend im aak-Inventar: **SAT/Satz von Cook-Levin** (zentralster Satz), **MGA (Güte 2)**, **Sahni-PTAS Ak**, **Satz von Lawler**, **Satz 6.16 (P=NP-Kriterium)**, Lastfunktion (Def 7.26, per Supplement teilweise nachgeholt). -- QA-Lücken sind token-basiert (`backend/qa.py:163`): 108 übergranulare Bausteine decken jede Sektion irgendwie ab → Konzeptlücken unsichtbar, Supplement (`_proc_gap_check`) ergänzt zu wenig. -- Verlust-Vektor upstream: `clarify` verlangt für Einzel-Reader-Funde Einstimmigkeit aller Judges (`accept = v >= len(outs)`, `backend/board_inventory.py:657`) — ein einziges „nein" verwirft einen real existierenden Fund als `failed-quorum`. +QUELLE: https://hub.shopware.com/learn/unit/system-configurations -### R6 — QA-Note ist blind für das Problem -- `NOTE_GEWICHTE` (`backend/qa.py:40`) enthält keine Dubletten. 36 % Verdacht + Verschlechterung zum Vorlauf (+0.06) ergeben trotzdem Note 10.0. +Power-up your learning experience now +Benefit from progress tracking, gamification, individualized suggestions and community discussions. +System configurations in Shopware define how the system behaves and how it interacts with the user. They are used to configure various aspects of the system, such as the appearance of the storefront, the behavior of the administration, and the performance of the system without changing the code. +In practice, system configurations act as a bridge between flexibility and control. You can change them dynamically through the administration or Admin-API, or define them statically to ensure consistent behavior across environments. As a developer, understanding system configurations allows you to adapt Shopware’s behavior quickly without touching the codebase. +To explore all system configurations directly, run the following query in your database: +SELECT * FROM system_config;This table includes both core settings and plugin configurations – for example, the SwagPaypal flag (SwagPayPal.settings.sandbox). +Imagine you have a Shopware shop, and you want to disable the “Buy” button in the product listing since your products are quite complex, and you want the customer to check out the product detail page. +You can do this by setting the system configuration core.listing.allowBuyInListing to false. This way, the “Buy” button will not be displayed in the product listing, and customers will have to click on the product to see the details and buy it. +You can find the setting in the administration panel under Settings > Shop > Products (https://YOUR_SHOP_DOMAIN/admin#/sw/settings/listing/index). +It is a simple toggle and can be true or false. Per default, it is set to true. +You can also configure system settings via the Admin API. In practice, you can use two different approaches. +Use this variant when you already know the exact configuration key that you want to update. +POST /api/_action/system-config +Content-Type: application/json +{ + "core.listing.allowBuyInListing": false +}You can also update a system configuration via PATCH if you already know the ID of the matching system_config entity (in this example, the ID of the system config for core.listing.allowBuyInListing). +This variant is a bit more technical because it works with the entity ID instead of the configuration key: Update system configuration +PATCH /api/system-config/ +Content-Type: application/json +{ + "configurationValue": false +}If you want to use the PATCH variant, you first need the ID of the matching system_config entity. You can get it from the database in the format expected by the API with this query: +SELECT LOWER(HEX(id)) AS id +FROM system_config +WHERE configuration_key = 'core.listing.allowBuyInListing';This endpoint is different from the POST endpoint /api/_action/system-config. The POST example above updates a configuration by key, while this PATCH example updates an existing system_config entity by ID. +If you do not need to work with a specific system_config entity, the POST variant is usually the simpler option. +.env or Config YAMLThis feature is available since Shopware 6.6.4.0. +You can also set the core.listing.allowBuyInListing setting statically via the .env file or the config/packages/shopware.yaml file. Static configuration is ideal when: +shopware: + system_config: + default: + core.listing.allowBuyInListing: true + # Disable it for the specific sales channel + 0188da12724970b9b4a708298259b171: + core.listing.allowBuyInListing: falseStatic configuration has a higher priority over changes made via the administration. If a value is defined in shopware.yaml, it cannot be changed from the administration. +For all possible options and more information, check out the official documentation. +You may notice examples in the documentation where system configuration values are referenced from .env variables. +Shopware does not automatically derive environment variable names from the configuration key. +When using .env values inside config/packages/shopware.yaml, you are simply using Symfony’s standard env() processor, for example: +shopware: + system_config: + default: + core.listing.allowBuyInListing: '%env(bool:ALLOW_BUY_IN_LISTING)%'In this example: +ALLOW_BUY_IN_LISTING is not generated by Shopware.core.listing.allowBuyInListing.You can define any environment variable name you want. What matters is: +core.listing.allowBuyInListing..env file.Example: +shopware: + system_config: + default: + core.listing.allowBuyInListing: '%env(bool:DISABLE_BUY_IN_LISTING)%'Then your .env file must contain: +DISABLE_BUY_IN_LISTING=trueThis means: +config/packages/shopware.yaml..env variables are optional and only used when you explicitly reference them using %env()%.In this learning unit, you have learned: +system_config table)..env file or the config/packages/shopware.yaml file.core.listing.allowBuyInListing setting controls the visibility of the “Buy” button in the product listings.With this knowledge, you can confidently manage system behavior in Shopware; either dynamically via API or statically for stable, environment-specific setups. +Inspect discussions, ask questions and give feedback. +Sign up now -## Maßnahmen (priorisiert; M1–M3 = Ursachen, M4–M5 = Dämme, M6 = Messung) +QUELLE: https://hub.shopware.com/learn/unit/introduction-to-the-basic-architectural-pattern -### M1 — Titel-Hygiene, deterministisch -1. `_norm_dash`/Titel-Split klammer-bewusst machen: Dash innerhalb offener Klammer ist KEIN Separator. Nach dem Split: unbalancierte Klammern → Titel reparieren (bevorzugt: Split rückgängig / nächsten Separator nehmen). -2. `_REFERENCE_RE` erweitern: auch `Satz 7.13 (Christofides)`, `Satz 7.6: Kriterium …`, `… (Definition 6.19)` erkennen. Deterministischer Strip: Referenz-Präfix/Suffix entfernen, Konzeptrest behalten („Satz 7.6: Kriterium für Eulerschen Kreis" → „Kriterium für Eulerschen Kreis"). Reine Nummern ohne Konzeptrest („Bemerkung 7.22") → Rename-PFLICHT (Judge muss Konzeptnamen liefern, sonst bleibt die Karte vor `done` hängen bzw. wird saniert), statt Best-Effort. -3. Naming/Sanierung auch für Singletons erzwingen, wenn Titel `_is_reference`, klammer-unbalanciert oder >80 Zeichen ist (`board_inventory.py:725`, `:802`, `_sanierung_noetig` `:492`). +Power-up your learning experience now +Benefit from progress tracking, gamification, individualized suggestions and community discussions. +Before we start, it’s important to know that Shopware is built on Symfony and therefore follows its architectural patterns. In this learning unit, you will explore the key components that form the backbone of Shopware’s architecture. + Keep in mind that this is not the complete picture, but a simplified overview to help you get oriented. +Let’s start with a short overview of the architectural pattern behind Shopware. The following components are essential building blocks: +In Symfony/Shopware, services, controllers, and subscribers are registered in the Dependency Injection Container (via services.xml or autowiring) and then injected where needed. +Services are plain PHP classes that contain your business logic in methods. These are reusable helpers that can be called from controllers, event subscribers, and other services. + Using services, you can centralize your business logic in one place. This makes your project easier to maintain and avoids code duplication by reusing the same logic in multiple places. +Controllers are one possible entry-point to trigger your business logic via an HTTP-Request. They usually handle incoming HTTP requests, delegate logic to services, and return a response. +An event subscriber is a plain PHP class, where you can hook into predefined Shopware or Symfony events by implementing your own methods. Event subscribers are also entry points to add your business logic at the right point in the workflow without losing the core logic. + For example, if you want to add logic to the product detail page, you can listen to the ProductPageLoadedEvent. +Before DI, you had to specify what you need and how to build it in every new call inside each PHP class. This led to poor readability, maintainability, and testability. +Every time you needed a service, you had to build the what and how manually. +This meant that every place using the OrderService (what) needed a definition of how to build Mailer and Logger. The result was duplicated code and a codebase that was hard to maintain. +In PHP, you now only write what you need. The how is defined in the DI-Container, Symfony/Shopware will inject the dependencies automatically into your constructor. + Dependency Injection is managed via services.xml. In this file, you register each PHP class you create (e.g., subscriber classes, controller classes, service classes, etc.) and define which constructor arguments they require. +orderRepository = $orderRepository; + }For now, focus on understanding the concept. We will cover later how to implement and use dependency injection in code. +In this learning unit, you have learned: +With this, you have a solid foundation to understand how Shopware’s backend is structured and how its components work together behind the scenes. +Inspect discussions, ask questions and give feedback. +Sign up now -### M2 — Granularitätsgrenze in den Prompts -1. `Blocks-Research.md` + `Blocks-Source-Uni.md` (und Geschwister `Blocks-Source-*.md`): Negativ-Liste ergänzen — KEINE eigenen Bausteine sind: einzelne Beweisschritte, Hilfskonstruktionen/Beweis-Objekte (Hilfs-Schedules, Gewichtsfunktionen, konstruierte Touren), berechnete Einzel-Instanzen, Parameter-Instanzen desselben Arguments (dieselbe Technik „via k / via n / via |E|" = EIN Baustein). Solche Inhalte gehören als Aspekt in den Eltern-Baustein. -2. Titel-Regel in beide Naming-Prompts (`Blocks-Naming.md`, `Blocks-Klaerung.md`, `Blocks-Sanierung.md`): Titel ist der KONZEPTNAME; Quellen-Nummerierungen („Satz 7.13") sind als Titel verboten, dürfen aber in der Beschreibung stehen. -3. Sätze bleiben erlaubt als Baustein, wenn sie ein eigenständig prüfbares Ergebnis sind (Cook-Levin, Christofides) — benannt nach dem Ergebnis, nicht der Nummer. +QUELLE: https://hub.shopware.com/learn/unit/environment-variables -### M3 — Fragment-Filter nachschärfen -1. `_is_named_statement` (`blocks.py:1355`) verengen: Reduktions-Syntax mit Konstruktions-Suffix (Doppelpunkt/Zusatz NACH der Relation, z. B. `…≤ K-COLOR: G=(V,E) Konstruktion`) ist NICHT geschützt, sondern fragment-verdächtig. -2. Recheck-Overturn erschweren: `ueberstimmt` nur bei einstimmigem Recheck-Panel statt Mehrheit (`board_inventory.py:1031`). Beleg: 15 von 17 zurückgeholten Karten waren spätere QA-Dubletten. -3. Parameter-Varianten als Verdacht markieren: Titel-Paare, die sich nur durch ein Parameter-Token unterscheiden (`via k`/`via n`/`via |E|`), in `_filter_suspect` bzw. als eigene Familien-Heuristik aufnehmen → Filter-/Dedup-Kandidat. +Power-up your learning experience now +Benefit from progress tracking, gamification, individualized suggestions and community discussions. +.env files.Before writing any code, it’s important to understand why environment variables exist and how to use them. In Shopware and also in any Symfony project, environment variables are the foundation for flexible and secure configuration. They allow you to: +This helps you ensure that your project remains both secure and portable +Every Symfony project, including Shopware, uses .env files to store environment variables. Environment variables are key-value pairs that define configuration such as database credentials, API keys, or URLs outside your codebase. You can create multiple .env files for different environments: +| File Name | Purpose | +|---|---| +| .env | Default file. Contains the default values for the environment variables. Used as template for others. | +| .env.local | Developer-specific overrides (e.g., local DB credentials). | +| .env.prod.local | Overrides for production configuration. Contains the values for the environment variables that are specific to the production environment. | +| .env.test.local | Overrides for testing configuration. Used in environments running tests (unit tests, integration tests, application-tests). | +In modern Symfony/Shopware projects, the .env file is usually committed to version control and acts as a baseline with non-sensitive defaults. Do not put real secrets into .env. Instead, store secrets and machine/environment-specific overrides in .env.local / .env.*.local (which are typically excluded via .gitignore) or provide them via your hosting environment / secret management solution. +When another developer clones the repository, they can copy the .env file to .env.local and define their own credentials and secrets without exposing sensitive data in version control. +You can find more information about environment variables in the official Symfony documentation. +Never commit files containing credentials or secrets (like the .env.local file) to version control. Always keep sensitive data out of repositories. +A common change within the .env file is the MAILER_DSN variable. Let’s say your shop is running in production, and you want to send emails to your customers. You can set the MAILER_DSN variable to use the SMTP mailer to send emails. This way, you can send emails to your customers without any additional setup. +You can set the MAILER_DSN variable in the .env.prod.local file: Open the .env.prod.local file and add or adjust the following line: +MAILER_DSN=smtp://username:password@smtp.gmail.com:587?encryption=tls&auth_mode=loginAnd clear the cache by running the following command in your shop’s root directory: +bin/console cache:clearNow your Shopware shop will use Gmail’s SMTP server to send emails. +Even though Shopware allows email settings via the Admin Panel (Settings -> Email Settings), it’s recommended to set the MAILER_DSN in the .env file, especially for production environments, to ensure consistency and to prevent accidental overwrites. +Now that you have seen a practical example, let’s take a closer look at the most common environment variables in Shopware. +Below you will find the common environment variables. +| Variable | Explanation | +|---|---| +| APP_ENV | Specifies the current environment, e.g., dev,prod,test. Affects caching, logging, error pages, etc. | +| APP_URL | The accessible URL of the shop. Important for generated links and redirects. | +| APP_SECRET | Used internally for hashes, tokens, etc. Should not be changed unless intentional, otherwise sessions and other feature may break. | +| INSTANCE_ID | Unique ID of the Shopware installation. Used for the shop and plugin verification. | +| DATABASE_URL | Connection URL for the MySQL or MariaDB database. Format mysql://user:password@host:port/databasename. | +| MAILER_DSN | Configuration for sending emails via Symfony Mailer. Default null://localhost. | +| SHOPWARE_ES_ENABLED | Enables/Disables OpenSearch/Elasticsearch. Default 0 | +| SHOPWARE_HTTP_CACHE_ENABLED | Enable/Disable the HTTP cache. Default 1. | +| BLUE_GREEN_DEPLOYMENT | Enables special mechanisms for deployments in production environments. Default 0. | +If you want to see the full list of the environment variables, go to this official document. +The Symfony Mailer component is used to send emails from the application. It can be configured to use different mailers for sending emails. The default mailer is the null mailer, which does not send any emails. This is a good choice for development environments, as it does not require any additional setup. +# Default mailer +MAILER_DSN=null://nullThe most common mailer is the smtp mailer, which sends emails using an SMTP server. This is a good choice for production environments, as it is fast and reliable. +Here is an example of a free and easy to set up SMTP server with GMAIL: +MAILER_DSN=smtp://username:password@smtp.gmail.com:587?encryption=tls&auth_mode=loginUsing a private email server is not recommended in production. It is better to use a third party email provider. +If you want to check out the other environment variables, you can find them in the official Developer documentation. +In this learning unit, you have learned: +.env files.MAILER_DSN.With this knowledge, you can confidently configure and manage your Shopware environments for development, staging, and production. +Inspect discussions, ask questions and give feedback. +Sign up now -### M4 — Dedup-Recall erhöhen (ohne Fehlmerge-Risiko aufzugeben) -1. Die drei offenen Diffs (Richtungs-Guard `_direction_conflict`, Judge-Kanal-Guard, Dedup-Prompt-Zeile) BEHALTEN — sie sind Teil der Lösung. -2. Tiebreak statt Veto: bei 1-1-Judge-Split (`uneinig`, `board_inventory.py:1200`) einen dritten Judge entscheiden lassen. -3. Familien-Kollaps: Cluster von ≥3 Titeln, die sich nur im Parameter-Token unterscheiden, als EIN Merge-Fall (ein Judge-Call für die Familie) statt n² Einzelpaare. -4. `_canonical_key` (`blocks.py:1118`): Kompositum-Suffix „…problem" auch verklebt abtrennen („Cliquenproblem" → Stamm + Stopwort), damit solche Paare Kandidaten werden. Generisch halten (Suffix-Regel, keine Wortliste pro Domäne). -5. Type-Gate der Gruppierung (`_GROUP_STANDALONE`-Pfad) lockern: Umbrella-Merge nicht allein wegen gemischter Content-Typen (Definition+Satz+Algorithmus) verwerfen, wenn das Judge-Panel die Gruppe bestätigt hat. Die 17 verworfenen Fälle aus `inventar-gruppierung.json` sind der Prüfmaßstab. -6. NICHT anfassen: die Varianten-Regel „k-X ≠ X / Max-X ≠ X" im Dedup-Prompt und das 2-Judge-Panel für klare Fälle. Bisher 0 Fehlmerges — das muss so bleiben. +QUELLE: https://hub.shopware.com/learn/course/testing-and-quality-assurance -### M5 — Lücken schließen -1. `clarify`-Quorum für Einzel-Reader-Funde von Einstimmigkeit auf Mehrheit senken (`board_inventory.py:657`) ODER abgelehnte Funde als Fragment aufbewahren statt `rejected`, damit `gap_check`/Supplement sie wiederfinden kann. -2. Gap-Check konzeptbasiert ergänzen: benannte Ergebnisse des Korpus (Sätze/Definitionen mit eigener Aussage, Abschnittsüberschriften) gegen das Inventar prüfen, nicht nur Token-Abdeckung. Ein benanntes Kernresultat ohne zugeordneten Baustein = Lücke → Supplement-Kandidat. -3. Prüffälle für aak (müssen nach einem Neulauf existieren): SAT/Cook-Levin, MGA, Sahni-PTAS Ak, Satz von Lawler, P=NP-Kriterium (Satz 6.16). +Testing and Quality Assurance (QA) are crucial parts of the development process. They ensure that your projects remain stable, maintainable, and ready for future updates. +This course will guide you through the testing and quality assurance of your Shopware instance from a developer’s perspective. You will learn how to test your Shopware instance to ensure that it works as expected and how to ensure the quality of your code. +Learn why testing matters in Shopware, how unit tests and end-to-end tests differ, and when to use PHPUnit, Jest, and Playwright. +Learn how to ensure high code quality in Shopware through testing, reviews, tools, and continuous improvement. +Learn how to set up PHPUnit for a Shopware plugin, test a console command, and run the tests locally and in CI. +Learn how to set up, write, and run tests in Shopware to ensure code stability and long-term quality. +Sign up to enroll and track your progress on this course +Sign up nowInspect discussions, ask questions and give feedback. +Sign up now -### M6 — QA ehrlich machen -1. `dubletten_verdacht` in `NOTE_GEWICHTE` aufnehmen (`qa.py:40`), damit die Note das Nutzerproblem abbildet. -2. Optional: konzeptbasierte Lücken (aus M5.2) als eigene QA-Quote ausweisen. +QUELLE: https://hub.shopware.com/learn/course/basic-plugin-development -## Leitplanken +In this course, you will learn the basics of Shopware plugin development. You will learn how to create a plugin, understand the plugin structure, and how to manipulate the shop instance programmatically. +Get an overview of the basic architectural pattern used in Shopware. +In this course, you will learn how to get started with Shopware backend development based on a storefront controller implemented as a service. +Learn how to extend products with custom data programmatically using custom fields and understand when to use properties or custom entities. +Learn how to debug your code within Shopware, trace issues effectively, and apply best practices for troubleshooting. +Learn the fundamentals of Shopware plugin development, including architecture, controllers, data extension, and debugging techniques. +Sign up to enroll and track your progress on this course +Sign up nowInspect discussions, ask questions and give feedback. +Sign up now -- **Generisch bleiben**: keine aak-/Komplexitätstheorie-Sonderregeln in Pipeline oder Prompts. aak ist nur der Prüffall. Alle Heuristiken müssen für beliebige Themen tragen. -- **YAGNI**: kleine, gezielte Diffs; bestehende Muster (Judge-Panels, Schema-Parser, Kanban-Stages) wiederverwenden; nichts Neues bauen, was eine Prompt-Zeile löst. -- **Offene Diffs behalten**: `backend/blocks.py`, `backend/board_inventory.py`, `templates/Prompt/Blocks-Dedup.md` enthalten uncommittete Änderungen (Richtungs-Guard). Nicht verwerfen, darauf aufbauen. -- **Niemals `git commit` oder `git push`** — der Nutzer committet selbst. -- **Fehlmerge-Bilanz**: Recall-Erhöhungen (M4) dürfen keine falschen Merges einführen. Jede Lockerung braucht einen Judge-Beleg-Pfad; deterministische Auto-Merges bleiben streng. -- **Keine Echtläufe ohne Freigabe**: `make qa TOPIC=aak` mit LLM-Judges und komplette Neuläufe kosten Tokens — nur nach Rückfrage beim Nutzer starten. +QUELLE: https://hub.shopware.com/learn/path/shopware-backend-development-essentials -## Validierung (in dieser Reihenfolge) +This is the right learning path for you if you want to start developing powerful backend extensions with Shopware! +It introduces you to the core architecture, development concepts, and quality practices that form the foundation of Shopware backend development. You will learn: +Tailored for developers eager to dive into Shopware backend development, this learning path will equip you with the skills to build robust, maintainable, and customized solutions for Shopware projects. Note, whenever we mention “Shopware,” we are referring to Shopware 6. +Before diving into this learning path, make sure you have: +Learn the fundamentals of Shopware plugin development, including architecture, controllers, data extension, and debugging techniques. +Learn how to manage configurations in Shopware, from environment variables to system settings and extension configuration. +Learn how to set up, write, and run tests in Shopware to ensure code stability and long-term quality. +Sign up to enroll and track your progress on this learning path +Sign up nowInspect discussions, ask questions and give feedback. +Sign up now -1. `pytest` in `backend/` — bestehende Tests dürfen nicht brechen; für M1 (Titel-Split, Referenz-Regex) und M4.4 (`_canonical_key`) gezielte Unit-Tests ergänzen (Fixtures: die realen Schadensfälle aus diesem Auftrag). -2. `make test-e2e` — Fake-E2E (`backend/fake_agents.py`) muss grün bleiben; Störfall-Matrix ggf. um Referenz-Titel und unbalancierte Klammern erweitern. -3. QA-Fehler-Injektion (Muster siehe QA-Modul-Tests): injizierte Satz-Nummern-Titel, Parameter-Familien und Konzeptlücken müssen von den geänderten Checks erkannt werden. -4. Erst nach Nutzer-Freigabe: Neulauf aak + `make qa TOPIC=aak`. Erfolgskriterien: - - Baustein-Anzahl ~60–70 (statt 108), - - `dubletten_verdacht` < 0.10 (statt 0.361), - - die 5 Prüffall-Konzepte aus M5.3 existieren, - - kein Titel matcht die erweiterte Referenz-Erkennung, keine unbalancierten Klammern, - - Lücken-/Fremd-Quote nicht schlechter als der Referenzlauf `20260705-201016.json`. +QUELLE: https://hub.shopware.com/learn/unit/intro-testing-shopware + +Power-up your learning experience now +Benefit from progress tracking, gamification, individualized suggestions and community discussions. +Testing is not only a quality topic. In Shopware projects, it directly affects how safely you can change code, ship new features, and keep extensions stable over time. +If your plugin or App grows, manual checks quickly stop being enough. A small change in business logic, storefront behavior, or the administration can break something that used to work. +In this learning unit, you learn why testing matters, how the main test types differ, and which tools Shopware uses for unit and end-to-end testing. +Imagine your plugin or App is feature-complete and works well in your local environment. The next release changes a service, updates a template, or adds a new storefront interaction. Without tests, you often notice problems only after manual QA or after release. +Tests reduce that risk. They help you verify expected behavior early and give you more confidence when the codebase changes. +That way, testing becomes part of reliable delivery, not just a final check before launch. +Testing often looks expensive at first because it adds work early in the project. In practice, it usually saves time later by reducing regressions, manual retesting, and risky releases. +If testing is not part of your development process yet, prepare a short pitch with numbers and facts: +You do not need to present testing as a perfect solution. A more realistic message is enough: Testing lowers risk, improves feedback, and helps teams scale development more safely. +Some developers follow a Test-Driven Development (TDD) approach, where tests are written before the actual implementation. +However, in Shopware projects and plugin development, it is often more practical to start testing once the architecture and key components are stable enough to test deliberately. +The next question is not which framework to install first. The more useful question is: What exactly do I want to verify? +This distinction keeps testing efficient. Small logic problems should not require a full browser test, and user journeys cannot be proven by isolated unit tests alone. +Unit tests are used to test individual units of code, such as functions or classes. They run in isolation from the rest of the codebase and should be fast to execute. Their main goal is to ensure that small building blocks or your application behave as expected. +Use them when you want fast feedback on business logic, service behavior, or small frontend units. +Shopware uses PHPUnit for backend unit tests. If you created your plugin using the bin/console plugin:create command, the necessary PHPUnit configuration is already set up for you. +This is the right tool for testing PHP classes, services, and backend logic in your extension. +You will find all necessary information about writing and running unit tests, setting up integration tests, and mocking services in the official documentation. +If you want to dive deeper into PHP unit testing (test structure, best practices, and more examples), continue with the successor intermediate learning path. +A good next reference point is the Unit Tests learning unit. +You can also write unit tests using Jest for frontend development (storefront and administration). +Jest is useful when you want to test Vue components, custom JavaScript functions, and storefront plugins in isolation. It also provides features such as mocking and snapshot testing. +You can find more information in the official documentation: +End-to-end tests are used to test the entire application flow or a specific feature. They simulate real user interactions with the application. +For example, you might verify that a customer can open a product detail page, click a button, complete a checkout step, or see the expected result in the UI. +Use this test type when the question is no longer “Does this function return the right value?” but “Does this feature work from the user’s point of view?” +Shopware uses Playwright for end-to-end testing. Playwright allows you to automate browser actions, simulate user journeys, and verify UI behavior across different pages and browsers. +Shopware provides an official acceptance test suite, which includes preconfigured tests and utilities to kickstart your testing setup. +This is the right choice when you want to test real browser behavior across the storefront or administration. +For detailed installation and usage instructions, check the official developer documentation and the dedicated Playwright learning unit. They show how to install the test suite and run the first tests. +Older Shopware materials may still mention Cypress, but that setup is now legacy. For new work, use Playwright as the default end-to-end testing framework. +If your project does not have tests yet, do not try to automate everything at once. +A good starting point is usually: +That way, you build coverage where it gives the highest value first. +In this learning unit, you have learned: +With this knowledge, you can now choose the right test type more deliberately and build a testing strategy that fits your Shopware project. +Inspect discussions, ask questions and give feedback. +Sign up now + +QUELLE: https://hub.shopware.com/learn/unit/plugin-configuration + +Power-up your learning experience now +Benefit from progress tracking, gamification, individualized suggestions and community discussions. +In this learning unit, you will learn how to create configuration fields for your Shopware plugin. By using configuration fields, your plugin becomes more flexible, reusable, and customizable for different projects and environments. +Imagine you are developing a plugin that should display a banner on the homepage on a specific date, for example, Black Friday. To make this feature configurable, you can add a date input field to your plugin configuration. This allows the merchant to set the date directly in the administration panel without touching any code. +config.xml File to Your PluginTo add configurations to your plugin, you need to create a config.xml file in the src/Resources/config directory of your plugin. This file will contain all the configurations for your plugin. +└── plugins + └── EventPlugin + ├── src + │ ├── Resources + │ │ └── config + │ │ └── config.xml + │ └── EventPlugin.php + └── composer.jsonIn the beginning, the config.xml file should look like this: + + +date FieldA date field is a common input field type that allows the user to select a date from a calendar. To add a date field to your plugin, you need to add the following code to your config.xml file: + + specialEventDate + + 2025-11-29T00:00:00 +Your config.xml file should now look like this: + + + + Minimal configuration + + specialEventDate + + 2025-09-29T00:00:00 + + +Great, we can now check in our PHP code for this value, and if the date is today, we can show the banner. However, this would be very static. +How about adding a translatable text field to the configuration, so the user can set the banner text in multiple languages? +A snippet is a translatable piece of text. It allows you to manage and display text content in multiple languages, making your plugin multilingual and user-friendly – a must-have for international shops. They are often used for labels, messages, and other text elements in the Shopware Frontend and Backend. +To add a snippet field to your plugin, use the sw-snippet-field component: + + bannerText + + eventPlugin.banner.text +At this stage, your config.xml file should look like this: + + + + Minimal configuration + + specialEventDate + + 2025-09-29T00:00:00 + + + bannerText + + eventPlugin.banner.text + + +Components are custom input fields that can be used to create complex configurations. In this case, we are using the sw-snippet-field component to create a snippet field for the banner text across different languages. +Perfect, and what about the colors? Let’s add a color picker to the configuration so we can also use the banner in different colors (for example, red for Valentine’s Day). +To add a color picker field to your plugin, you need to add the following code to your config.xml file: + + bannerBackgroundColor + + #000000 + + + bannerTextColor + + #FFFFFF +At this stage, your config.xml file should look like this: + + + + Minimal configuration + + specialEventDate + + 2025-09-29T00:00:00 + + + bannerText + + eventPlugin.banner.text + + + bannerBackgroundColor + + #000000 + + + bannerTextColor + + #FFFFFF + + + +With this, the merchant can customize both the background color and the text color for the banner directly from the administration panel. +You can add more complex fields such as: +For example, you could create a multi-select-field to show the banner only on desktop and tablet devices. +If you want to explore all options, please visit the official developer documentation. +Whenever a plugin configuration is created or its value has been updated, Shopware saves the values in the system_config table. +You can find your configuration entries by searching for your plugin prefix (usually the technical plugin name) in the configuration_key column. +In this learning unit, you have learned: +config.xml) to your plugin.With this knowledge, you can now create plugins that are easily configurable and adaptable to various business requirements. +Inspect discussions, ask questions and give feedback. +Sign up now + +QUELLE: https://hub.shopware.com/learn/unit/setting-up-and-running-phpunit-tests + +Power-up your learning experience now +Benefit from progress tracking, gamification, individualized suggestions and community discussions. +TestBootstrapper and autoload-dev.phpunit.xml file.Great to see you have made it this far! We know that writing tests is not the most exciting part of development, but it is a crucial step for maintaining code quality, stability, and confidence in your project. +In this learning unit, you will learn how to write and run PHPUnit tests for your Shopware plugin, both locally and in a CI environment. +To not bore you with too much theory, we will focus on a real-world example to show you how to write a PHPUnit test for a Shopware plugin. If you want to dive deeper into the topic, check out the official PHPUnit documentation. +Imagine you have a custom command in your plugin that should return a specific value or trigger a certain workflow. You can write a PHPUnit test to verify that the command returns the expected value. This way, you can ensure that your command works as expected even after future changes. +To write tests in Shopware, you first need a plugin that contains a testable component, for example, a command. When working with PHPUnit, each plugin requires: +composer.json file, so PHPUnit knows where to find the tests.You can either create your own plugin from scratch or use the existing example to follow along. +Run the following command in your shop’s root directory to create a new plugin: +bin/console plugin:create MyPhpUnitPluginWhen prompted, select the Command option to create a testable command class out of the box. +If your generated plugin does not contain a command yet, create a small example command first. Add the file src/Command/ExampleCommand.php in your plugin: +writeln('It works!'); + // Exit code 0 for success + return self::SUCCESS; + } +}If your plugin has a different namespace, replace MyPhpUnitPlugin with your plugin namespace. The test in this unit needs a real class to test. If the command class is missing, the test cannot run successfully. +If you prefer to start with a working example, clone the following repository into the plugins folder ([shop_root]/custom/plugins): +git clone git@github.com:ShopwareAcademy/AcademyPhpUnit.git custom/plugins/AcademyPhpUnitThis example plugin already includes: +ExampleCommand).ExampleCommandTest).TestBootstrapper class and configuration.You can find a detailed explanation of PHPUnit setup and configuration in the official documentation. +Remember to install and activate the plugin independently of the option you chose by running the following commands in your shop’s root directory. +If you created your own plugin, use: +bin/console plugin:refresh +bin/console plugin:install MyPhpUnitPlugin --activate --clearCacheIf you cloned the example plugin, use: +bin/console plugin:refresh +bin/console plugin:install AcademyPhpUnit --activate --clearCacheNow you are ready to start writing your first PHPUnit test. +The examples below use AcademyPhpUnit because that is the name of the reference plugin. If you created MyPhpUnitPlugin, replace AcademyPhpUnit with MyPhpUnitPlugin in every related place: namespace, addActivePlugins, autoload-dev, test imports, and the phpunit.xml path. +The PHPUnit setup consists of four essential parts. Each part has its own purpose in ensuring that your tests run smoothly and can properly interact with your plugin: +| Part | Description | +|---|---| +| The TestBootstrapper | A PHP class that prepares the Shopware test environment. | +| The tests | PHP classes that contain your actual test cases and assertions. | +| autoload-dev | A section in the composer.jsonfile that tells PHPUnit where to find your test classes. | +| phpunit.xml | The PHPUnit configuration file that points to the bootstrap file and test directory. | +Let’s take a closer look at each part. +The TestBootstrapper class is responsible for setting up the Shopware test environment. It ensures that all required services, plugins, and autoloading rules are available when running your tests. +Add a file named TestBootstrap.php to the tests directory of your plugin ([shop_root]/custom/plugins/[Your_Plugin]/tests). +addCallingPlugin() + ->addActivePlugins('AcademyPhpUnit') // Replace it with your plugin name if different + ->setForceInstallPlugins(true) + ->bootstrap() + ->getClassLoader(); +} catch (Exception $exception) { + throw new RuntimeException( + 'Could not bootstrap the PHPUnit test environment.', + 0, + $exception + ); +} +$loader->addPsr4('AcademyPhpUnit\\Tests\\', __DIR__);The name inside addActivePlugins must match your plugin’s name exactly. +If you cloned the example repository, keep it as AcademyPhpUnit. +If you created your own plugin with plugin:create MyPhpUnitPlugin, use 'MyPhpUnitPlugin' instead. +If you created your own plugin, the same replacement is needed for the test namespace registration: +$loader->addPsr4('MyPhpUnitPlugin\\Tests\\', __DIR__);Explanation: +addCallingPlugin method registers your plugin as the one being tested.addActivePlugins method registers the plugins that should be available during testing (e.g., the plugin that contains the command you want to test).setForceInstallPlugins method ensures that all required plugins are automatically installed before running the tests.bootstrap method prepares the Shopware testing environment.getClassLoader method returns the autoloader used to load all required classes.addPsr4 method adds the AcademyPhpUnit\Tests namespace to the autoloader.try-catch block wraps bootstrap errors in a clearer exception message. This makes setup problems easier to understand when the test environment cannot be prepared.The tests are PHP classes that contain the actual test methods that verify your plugin’s behavior. Each test method should focus on one specific functionality or expected outcome. +Below is a simple example of a PHPUnit test for a Shopware command: +Add this test as tests/Command/ExampleCommandTest.php. The *Test.php file name is important because PHPUnit uses this naming pattern to discover test files. +Don’t forget to fix the namespace, based on what plugin name you have chosen. +execute([]); + $commandTester->assertCommandIsSuccessful(); + $this->assertStringContainsString('It works!', $commandTester->getDisplay()); + $this->assertSame('Demonstrates a simple testable command', $command->getDescription()); + } +}Explanation: +TestCase class from the PHPUnit framework, which provides assertion methods (e.g., assertSame) and test lifecycle hooks.CommandTester class simulates the running of the command and allows you to check its output, status, or behavior.assertCommandIsSuccessful() checks that the command finished with a successful exit code.assertStringContainsString() checks that the command printed the expected output.assertSame() ensures that the command’s description matches the expected value.You can write as many test methods as you need to cover different aspects of your command’s behavior. +The code example uses the AcademyPhpUnit namespace because it belongs to the reference plugin. +If you created your own plugin, update the namespace and import: +namespace MyPhpUnitPlugin\Tests\Command; +use MyPhpUnitPlugin\Command\ExampleCommand;Also make sure the file is named ExampleCommandTest.php, not only ExampleCommand.php. Otherwise, PHPUnit may finish with No tests executed! because it does not discover the test class. +As your plugin grows, tests may involve mocking dependencies or stubbing services to isolate functionality. Check out the Shopware testing guide for more examples. +autoload-dev SectionThe autoload-dev section in your composer.json file defines autoloading rules that are only used in development environments, such as during testing. This ensures that PHPUnit can locate and load your test classes automatically without requiring manual includes. +Add the section to your plugin’s composer.json, not to the project root composer.json: +"autoload-dev": { + "psr-4": { + "AcademyPhpUnit\\Tests\\": "tests/" + } +}The psr-4 key defines the namespace mapping for your test classes. The namespace AcademyPhpUnit\\Tests\\ corresponds to the tests directory of your plugin. When running PHPUnit, Composer will automatically use this mapping to autoload your test classes. This setup keeps your production autoloading (autoload) separate from test autoloading (autoload-dev). This ensures that test classes are not included in production builds. +After modifying the autoload-dev section, you need to run composer dump-autoload from your shop’s root directory to update the autoloader and make sure that your test classes are recognized. +If you created your own plugin, use your own test namespace: +"autoload-dev": { + "psr-4": { + "MyPhpUnitPlugin\\Tests\\": "tests/" + } +}phpunit.xml FileThe phpunit.xml file tells PHPUnit how to run the plugin tests. Add it to the root directory of your plugin, for example [shop_root]/custom/plugins/[Your_Plugin]/phpunit.xml. +For the example plugin, the file looks like this: + + + + + ./src/ + + + + + + + + + + + + tests + + +If you created your own plugin, you can keep the same structure and only adjust the testsuite name, for example MyPhpUnitPlugin Testsuite. The important part is bootstrap="tests/TestBootstrap.php" because this is what prepares the Shopware test environment before PHPUnit discovers and runs your test classes. +Before running your tests, make sure you are using the correct project template and that PHPUnit is available in your environment. +The production template does not include the PHPUnit binary. If you are using it, or if vendor/bin/phpunit does not exist in your project, install PHPUnit manually: +composer require --dev phpunit/phpunit +composer dump-autoloadRun PHPUnit from your shop’s root directory, not from inside the plugin directory. +If you cloned the example plugin, use: +vendor/bin/phpunit -c custom/plugins/AcademyPhpUnit/phpunit.xmlIf you created your own plugin, replace the plugin folder name: +vendor/bin/phpunit -c custom/plugins/MyPhpUnitPlugin/phpunit.xmlThis command uses the PHPUnit configuration from the plugin and executes all tests defined there. If your plugin has a different folder name, adjust the path to your own phpunit.xml file. +The first test run prepares the Shopware test environment. This can create or reset the test database, run migrations, refresh plugins, install the example plugin, and clear caches. +This output can be long, but it is expected. The important part is the final PHPUnit result. +If PHPUnit reports deprecations but the test status is OK, the test itself still passed. Deprecations should still be reviewed because they may require future code or configuration updates. +If you encounter an error like: Base table or view not found: 1146 Table 'platform_test.app' doesn't exist, you need to set up the database for the tests. This can be done by executing the following command in your shop’s root directory: composer init:testdb. +If you run the tests in a Docker-based Shopware setup and see an error such as SQLSTATE[HY000] [2002] No such file or directory, check the database host in your test environment configuration. +If you use a Docker setup, check the database host from the environment where PHPUnit runs. For example, if PHPUnit runs inside the Shopware/PHP container, the database host must be reachable from that container. +This error usually does not mean that PHPUnit or the TestBootstrapper is broken. It often means that the PHP process running PHPUnit cannot reach the database. +In Docker, localhost means “inside this container”. If PHPUnit runs in the Shopware/PHP container and the database runs in another container, localhost is the wrong host. Use the database service name from your Docker Compose setup instead, for example database or mysql. +If the database really runs on your host machine, use the host address provided by your Docker setup, for example host.docker.internal where available. Using 127.0.0.1 can force a TCP connection instead of a MySQL Unix socket, but it still only works if the database is reachable from the environment where PHPUnit runs. +So first ask: “Where does PHPUnit run?” Then set the database host from that point of view. +To ensure that your plugin remains stable across all environments, it is best to run your PHPUnit tests in a Continuous Integration (CI) pipeline. This way, your tests run automatically on every pull request. +The example plugin uses a reusable GitHub Actions workflow from shopware/github-actions. This workflow provides predefined steps for setting up Shopware, preparing the database, installing the plugin, and running PHPUnit. +You will find the necessary configuration in the .github/workflows directory. In our example plugin repository, you can find the workflow file. +The workflow file contains the following important part: +jobs: + phpunit: + uses: shopware/github-actions/.github/workflows/phpunit.yml@main + with: + extensionName: ${{ github.event.repository.name }} + shopwareVersion: trunkThe uses line tells GitHub Actions to reuse Shopware’s PHPUnit workflow. The extensionName value tells the workflow which plugin repository should be tested. The shopwareVersion value defines which Shopware version the test environment should use. +Make sure your CI environment includes all required dependencies (e.g., database, PHP extensions) and uses the same Shopware version as your local setup for consistent test results. +In this learning unit, you have learned: +phpunit.xml connects PHPUnit to the plugin bootstrap and test directory.CommandTester.phpunit.xml configuration.With this knowledge, you can confidently integrate PHPUnit tests into your Shopware development workflow and ensure your plugins remain stable and reliable. +If you want to go deeper into PHPUnit testing, continue with the Backend Development Intermediate learning path. +Well done! You have completed this course and with it the entire Backend Development Essentials learning path. You now have a solid foundation to start developing your own Shopware plugins. +From here, you can continue seamlessly with the Shopware Backend Development Intermediate learning path to deepen your knowledge. +Inspect discussions, ask questions and give feedback. +Sign up now + +QUELLE: https://hub.shopware.com/learn/unit/maintaining-code-quality + +Power-up your learning experience now +Benefit from progress tracking, gamification, individualized suggestions and community discussions. +Maintaining code quality is essential for the long-term success and stability of your project. It ensures that your code is readable, maintainable, and scalable; not only for you, but also for your team and future developers. +Code quality begins with a solid foundation. It may sound obvious, but using version control is still one of the most important practices in modern development. Always use Git to manage your source code, whether you are building an extension, Shopware App, or a Shopware Bundle. +Version control allows you to: +Always commit frequently and write clear, descriptive commit messages. This helps you and your team to understand the purpose behind each change. +Automated testing is a crucial part of maintaining code quality. It helps you catch bugs early in the development process, ensures your code works as expected, even after future changes. +There are different types of automated tests, each with its own purpose: +Automated tests serve as a safety net. They help you refactor with confidence, prevent bugs from reappearing, and ensure that your code is always working as expected. +Start small, begin with a few unit tests for critical logic. Over time, extend your coverage with integration tests and end-to-end tests to ensure stability across your entire application. +Code reviews are another essential part of maintaining code quality. They help you identify bugs early, improve code quality, and ensure that your code follows best practices. Code reviews should be done by your peers or a senior developer who can provide valuable feedback and suggest improvements. +A code review should always be seen as a learning opportunity, not as criticism. It is a great way to share knowledge, learn from others, and grow as a developer. +Encourage an open and respectful feedback culture. Constructive reviews lead to better code and stronger teamwork. +There are many tools available that help you analyze, enforce, and maintain code quality. They automatically check your code for common issues, enforce coding standards, and highlight potential bugs before they reach production. Some popular tools include: +Integrate these tools into your CI/CD pipeline or pre-commit hooks. This ensures consistent code quality across your team and prevents low-quality code from being merged. +Good documentation is a key part of maintaining high code quality. It ensures that you and other developers can easily understand your code, its purpose, and how to use or extend it. Well-written documentation not only saves time but also prevents misunderstandings and duplicate work. +It also helps new team members or external contributors to get started quickly, understand your project architecture, and follow established conventions and design decisions. +Keep your documentation close to the code, for example, in a README.md file, PHPDoc blocks, or inline comments. This ensures it stays up to date and relevant. +Continuous integration (CI) is a development practice that helps you catch bugs early in the development process and ensures that your project remains stable, and deployable at all times. +It involves automatically building, testing, and validating your code whenever changes are pushed to the repository, for example, when opening a pull request (merge request) or merging new features. +By integrating CI, you ensure that your codebase is always in a working state, reducing the risk of introducing bugs and improving team collaboration. +Set up CI pipelines (e.g., with GitHub Actions or GitLab CI, or Jenkins) to run tests and code quality checks automatically. This prevents broken code from being merged into the main branch. +Code quality guidelines are a set of rules and best practices that help you maintain a consistent, readable, and scalable codebase. They typically cover areas such as coding standards, naming conventions, file structure, and documentation style. +By following clear guidelines, you ensure that your team writes uniform code, making it easier to review, debug, and extend your project in the future. +Establish your code quality guideline at the very beginning of your project. Introducing them later in a large codebase can be very time-consuming and may require a lot of work. +Refactoring is the process of improving your code without changing its behavior. It helps you keep your code clean, maintainable, and scalable. Regularly refactoring your code can help you catch bugs, improve code quality, and ensure that your code is easy to maintain. +Refactoring should not be a one-time activity; schedule it regularly, for example, once per quarter. This way you can ensure that your codebase is always up to date and follows the latest best practices. +If you are publishing your extension or App in the Shopware Store, this process often comes naturally, as you need to update your code to the latest Shopware version. +For extensions published in the Shopware Store, refer to the extended Quality Guidelines in the official Shopware documentation. +In this learning unit, you have learned: +With these best practices, you can ensure that your Shopware projects remain stable, maintainable, and ready for future growth. +Inspect discussions, ask questions and give feedback. +Sign up now \ No newline at end of file diff --git a/backend/block_calls.py b/backend/block_calls.py index cc39d2e..2deb6d3 100644 --- a/backend/block_calls.py +++ b/backend/block_calls.py @@ -220,104 +220,60 @@ async def _generate_block(ctx: GenContext, files: dict, title: str, description: instructions: str = "", ns: str = "", lbl: str = "", sources: list[str] | None = None, seeds: list[str] | None = None, melde=None) -> dict | None: - """GEN_PANEL unabhängige Generatoren liefern je Subs+Facts+Level/Relevanz in EINEM Call; - Konsens im Code (Variant-Cluster über beide Ausgaben, ≥2 unabhängige Generatoren = - consensus). Einzelnennungen und ungedeckte Seeds werden „unsicher" — der Prüfer - entscheidet mit Material (ersetzt Sättigungsrunden + Clarify-Panel). Degraded: liefert - nur EIN Generator, wird alles unsicher. → {raw, facts, unsicher, votes} | None.""" - topic, provider = ctx.topic, ctx.provider + """Bottom-up-Anreicherung: die Subs stehen FEST — Board 1 hat die Atome geclustert und + als Subs persistiert. EIN Call liefert pro vorgegebenem Sub die Lern-Facts + Level + + Relevanz aus dem Material. Er entdeckt und entfernt KEINE Subs (Discovery/Konsens/Seeds + entfallen; die Vollständigkeit kommt aus Board 1). → {raw, facts, unsicher, votes} | None.""" + topic = ctx.topic work_dir = files["arbeit"] bnorm = _norm_title(title) - source, caps = await asyncio.to_thread( - _inline_source, topic, sources, [f"{title} {description}"]) - seeds_txt = "" - if seeds: - seeds_txt = ("\nAlready identified sub-point CANDIDATES of this block (verify against " - "the material; if backed AND not already covered by another entry, include " - "them — rephrased as a standalone statement):\n" - + "\n".join(f"- {s}" for s in dict.fromkeys(seeds) if s) + "\n") + # feste Subs aus Board 1 (Reihenfolge erhalten, dublettenfrei) + subs: list[str] = [] + seen: set[str] = set() + for r in await db.list_subblocks(topic, bnorm): + st = str(r.get("sub_title") or "").strip() + sn = _norm_title(st) + if st and sn and sn not in seen and r.get("status") in ("consensus", "candidate"): + seen.add(sn) + subs.append(st) + if not subs: + return {"raw": {title: []}, "facts": {title: {}}, "unsicher": [], "votes": {}} + source, caps = await asyncio.to_thread(_inline_source, topic, sources, [title] + subs) if melde: - melde("Generate") - h = _h8(title, description, "gen") - paths = [work_dir / f"gen-{h}-g{g}.json" for g in range(1, GEN_PANEL + 1)] - prompt = _prompt("Subblock-Generate", topic=topic, + melde("Anreichern") + h = _h8(title, description, "enrich") + pfad = work_dir / f"enrich-{h}.json" + sub_liste = "\n".join(f"{i}. {t}" for i, t in enumerate(subs, 1)) + prompt = _prompt("Subblock-Anreichern", topic=topic, block=f"{title} — {description}" if description else title, - source=source, seeds=seeds_txt, extra=_extra(instructions)) - pending = [(g, p) for g, p in enumerate(paths, 1) if _gen_schema(_json_file(p)) is None] - if pending: - slots = [{ - "key": f"blocks-{topic}-{ns}sb-gen-{h}-g{g}", - "prompt": prompt, "role": "quick", "capabilities": caps, - "payload": (lambda result, p=p: _sink_json(result, p, _gen_schema)), - } for g, p in pending] - await _race(topic, f"{lbl}Generate", slots, len(slots), - _timeout("generate", 10), provider, cancelled=ctx.is_cancelled) + subs=sub_liste, source=source, extra=_extra(instructions)) + if _gen_schema(_json_file(pfad)) is None: + status, _v = await run_single_slot( + ctx, f"{lbl}Anreichern", key=f"blocks-{topic}-{ns}sb-enrich-{h}", + prompt=prompt, role="quick", capabilities=caps, + payload=lambda result, p=pfad: _sink_json(result, p, _gen_schema), + timeout=_timeout("generate", len(subs))) + if status == FAILED: + _log(topic, f"Anreichern {title} ohne Ergebnis — Facts bleiben leer, Prüfer misst") if ctx.is_cancelled(): return None - outs = [o for p in paths if (o := _gen_schema(_json_file(p))) is not None] - if not outs: - return None - if len(outs) < len(paths): - _log(topic, f"Generate {title}: nur {len(outs)}/{len(paths)} Generatoren — alles unsicher, Prüfer entscheidet") - - # Mentions in die DB (QA-Beleg-Signal), Karten-Re-Spawn darf nicht kumulieren - await db.delete_subblocks(topic, bnorm) - alle: list[tuple[dict, int]] = [] # (sub-Eintrag, Generator-Index) - for gi, subs in enumerate(outs): - seen: set[str] = set() - for e in subs: - sn = _norm_title(e["title"]) - if not sn or sn in seen: - continue - seen.add(sn) - alle.append((e, gi)) - await db.upsert_subblock(topic, bnorm, sn, title, e["title"]) - if not alle: - return {"raw": {title: []}, "facts": {title: {}}, "unsicher": [], "votes": {}} - - sims = await _sims_of([e["title"] for e, _ in alle]) - raw: list[str] = [] + out = _gen_schema(_json_file(pfad)) or [] + by_norm = {_norm_title(e["title"]): e for e in out} facts: dict[str, dict] = {} votes: dict[str, dict] = {} - unsicher: list[dict] = [] - for c in _variant_clusters([e["title"] for e, _ in alle], [1] * len(alle), sims): - rep = alle[c["rep"]][0] - sn = _norm_title(rep["title"]) - fk = _fk_of(rep) - for m in c["members"]: - if m != c["rep"]: - _facts_union(fk, _fk_of(alle[m][0])) - await db.set_subblock_fields(topic, bnorm, _norm_title(alle[m][0]["title"]), - status="variant") - votes[sn] = {"level": [v for m in c["members"] if (v := alle[m][0]["level"])], - "relevance": [v for m in c["members"] if (v := alle[m][0]["relevance"])]} - gens = {alle[m][1] for m in c["members"]} - # degraded (1 Generator): kein Konsens möglich — alles unsicher, Prüfer entscheidet - if len(gens) >= 2 and len(outs) >= 2: - raw.append(rep["title"]) - facts[sn] = fk - await db.set_subblock_fields(topic, bnorm, sn, status="consensus") - else: - unsicher.append({**rep, **fk}) - - # Seed-Garantie: ungedeckte Seeds gehen als unsicher zum Prüfer (der ist das Beleg-Gate) - for seed in dict.fromkeys(s for s in (seeds or []) if s): - st = _sub_tokens(seed) - gedeckt = [t for t in raw + [u["title"] for u in unsicher]] - if not st or any(st <= _sub_tokens(t) for t in gedeckt): - continue - if gedeckt and EMBEDDING_AKTIV and await asyncio.to_thread(embedding.available): - sims = await asyncio.to_thread(embedding.embed_sims, [seed] + gedeckt) - if sims is not None and max(float(sims[0][j]) for j in range(1, len(gedeckt) + 1)) >= SEED_COVER_COS: - continue - unsicher.append({"title": seed, "level": "", "relevance": "", "key_points": [], - "prerequisites": "", "hurdles": "", "cited_facts": [], "example_idea": ""}) - - raw_map = {title: raw} - await _dedup_subblocks(topic, raw_map) # deterministischer Near-Dup-Filter - facts = {sn: fk for sn, fk in facts.items() - if sn in {_norm_title(s) for s in raw_map[title]}} - return {"raw": raw_map, "facts": {title: facts}, "unsicher": unsicher, "votes": votes} + for st in subs: + sn = _norm_title(st) + e = by_norm.get(sn) + if e: + fk = _fk_of(e) + votes[sn] = {"level": [e["level"]] if e.get("level") else [], + "relevance": [e["relevance"]] if e.get("relevance") else []} + else: # der Call ließ diesen Sub aus — leere Facts, der Prüfer/QA sieht die Lücke + fk = {k: ([] if k in ("key_points", "cited_facts") else "") for k in _FACTS_FIELDS} + votes[sn] = {"level": [], "relevance": []} + facts[sn] = fk + await db.put_subblock(topic, bnorm, sn, title, st, status="consensus") + return {"raw": {title: subs}, "facts": {title: facts}, "unsicher": [], "votes": votes} # ── Verify (+ Fix-Tail) ───────────────────────────────────────────────────────────── @@ -336,11 +292,14 @@ def _default_vote(stimmen: list[str], default: str) -> str: async def _verify_block(ctx: GenContext, files: dict, title: str, gen: dict, q: dict, instructions: str = "", ns: str = "", lbl: str = "", - sources: list[str] | None = None, melde=None) -> dict | None: + sources: list[str] | None = None, melde=None, + keep_all: bool = False) -> dict | None: """VERIFY_PANEL unabhängige Prüfer auditieren den Block in EINEM Call (MECE-Faltung, Fremd, Lücken, Unsicher-Übernahme, Facts-Korrektheit, Level/Relevanz). Auswertung mit Schnittmengen-Semantik pro Befundklasse (Faltung/Fremd/Übernahme einstimmig, Discard 2/2, Korrektur ≥1 Stimme); Fix-Tail ist EIN Call für Korrekturen + belegte Lücken. + keep_all=True (Bottom-up): kein Atom wird entfernt/gefaltet/als neu erfunden — nur + Facts-Korrektur + Level/Relevanz bleiben (Board 1 hat schon MECE geclustert). → {raw, facts, sidecar} | None (nur bei Cancel).""" topic = ctx.topic work_dir = files["arbeit"] @@ -427,6 +386,18 @@ async def _verify_block(ctx: GenContext, files: dict, title: str, gen: dict, q: if einstimmig: v1, v2 = verdicts[0], verdicts[1] + if keep_all: + # Bottom-up: jedes Board-1-Atom bleibt. Destruktive/erzeugende Befunde + # neutralisieren — die Loops unten laufen dann leer. Nur Facts-Korrektur + # (ohne discard) + Level/Relevanz überleben. + for v in (v1, v2): + v["fremd"] = set() + v["gruppen"] = [] + v["kataloge"] = [] + v["uebernehmen"] = {} + v["luecken"] = [] + # discard abschalten (kein Atom entfernen), aber den Korrektur-Hinweis behalten + v["facts_probleme"] = [{**p, "discard": False} for p in v["facts_probleme"]] # 1. Fremd (einstimmig): fürs THEMA fremde Aussagen → discarded for k in sorted(v1["fremd"] & v2["fremd"]): t = _titel(k) diff --git a/backend/board_artefacts.py b/backend/board_artefacts.py index a9ad0fe..ab50d86 100644 --- a/backend/board_artefacts.py +++ b/backend/board_artefacts.py @@ -223,7 +223,8 @@ async def _proc_verify(ctx: GenContext, flow: Flow, files: dict, q: dict, instru "unsicher": p.get("unsicher") or [], "votes": p.get("votes") or {}} res = await _verify_block(ctx, _pfiles(files, norm), title, gen, q, instructions, ns=f"{_safe(norm)}-", lbl=f"{title or norm} · ", - sources=p.get("sources"), melde=_melder(flow, norm)) + sources=p.get("sources"), melde=_melder(flow, norm), + keep_all=True) if res is None: return None # nur Cancel — Karte bleibt liegen p["raw"], p["facts"], p["sidecar"] = res["raw"], res["facts"], res["sidecar"] @@ -484,7 +485,7 @@ async def _proc_finalize(ctx: GenContext, flow: Flow, files: dict, cards): data = json.dumps({k: v for k, v in e.items() if k not in ("block", "subblock")}, ensure_ascii=False) await db.put_sub_artifact(topic, bnorm, sn, typ, data, bt, str(e.get("subblock", ""))) - await db.kanban_advance(topic, BOARD, c["card_id"], "konsolidierung") + await db.kanban_advance(topic, BOARD, c["card_id"], DONE) _log(topic, f"Artefakte fertig: {title}") flow.wake.set() @@ -529,7 +530,8 @@ async def _proc_outline(ctx: GenContext, flow: Flow, files: dict, instructions: # ── Stage list (appended after board 1 in chain order) ───────────────────────────── -_ALT_STAGES = ("subblocks", "facts", "levels", "relevance", "question_pattern", "artefacts") +_ALT_STAGES = ("subblocks", "facts", "levels", "relevance", "question_pattern", "artefacts", + "konsolidierung") # abgeschaltete Stage: verwaiste Karten auf generate zurück async def migriere_alt_karten(topic: str) -> int: @@ -557,12 +559,8 @@ def artefact_stages(ctx: GenContext, flow: Flow, files: dict, q: dict, folder, Stage(BOARD, "verify", lambda cs: _proc_verify(ctx, flow, files, q, instructions, cs)), Stage(BOARD, "artefakte", lambda cs: _proc_artefakte(ctx, flow, files, instructions, cs)), Stage(BOARD, "finalize", lambda cs: _proc_finalize(ctx, flow, files, cs), serial=True), - # Cross-Block-Dedup als END-Barriere: als Mittel-Barriere idelte jede fertige Karte - # auf die langsamste (8:46 min/Block gemessen); jetzt faltet sie nach finalize - # per repair.falte_sub — spät gefundene Dubletten kosten Artefakt-Tokens, keine Wandzeit - Stage(BOARD, "konsolidierung", - lambda cs: _proc_konsolidierung(ctx, flow, files, instructions, cs), - barrier=True, drain=True), + # Cross-Block-Dedup (konsolidierung) entfällt im Bottom-up: Board 1 hat die Atome + # global geclustert/dedupliziert, es gibt keine block-übergreifenden Sub-Dubletten mehr. Stage(BOARD, "outline", lambda cs: _proc_outline(ctx, flow, files, instructions, cs), barrier=True, drain=True, gate=research_done), ] diff --git a/backend/board_inventory.py b/backend/board_inventory.py index d69f5f0..64f3b5b 100644 --- a/backend/board_inventory.py +++ b/backend/board_inventory.py @@ -1323,8 +1323,8 @@ async def _proc_grouping(ctx: GenContext, flow: Flow, cards): # ONE wave: TOP + all cluster judges in parallel. The used-ties precedence lives in the # ORDER of `sources` (TOP first), not in execution order — a failed judge simply leaves # no valid file and is skipped in the collection below (legacy semantics). - jobs = [("TOP", "Scan the ENTIRE block list below and propose EVERY genuine umbrella " - "you find — do not restrict yourself to any subset.", n)] + jobs = [("TOP", "Card-sort the ENTIRE list below into themes: assign as MANY items as " + "possible to a coherent theme, each item into exactly one.", n)] jobs += [(str(ci), "\n".join(f"{g + 1}. {texts[g]}" for g in cluster), len(cluster)) for ci, cluster in enumerate(multi)] await asyncio.gather(*[_assess(tag, cand, count) for tag, cand, count in jobs], @@ -1346,16 +1346,10 @@ async def _proc_grouping(ctx: GenContext, flow: Flow, cards): members = [m for m in members if m not in used] if len(members) < 2: continue + # Bottom-up card-sorting: keine type-gate/min-cos-Vetos mehr — jedes Item soll in + # ein Thema; ein „Fehl-Merge" ist billig (Item bleibt als Sub erhalten, keine Lücke). mrows = [rows[m - 1] for m in members] - if any(_GROUP_STANDALONE.search(r["title"]) for r in mrows): - skipped.append({"umbrella": title, "grund": "type-gate", - "mitglieder": [r["title"] for r in mrows]}) - continue mc = _min_cos([m - 1 for m in members]) - if mc < GROUP_MIN_COS_FLOOR: - skipped.append({"umbrella": title, "grund": "min-cos", "min_cos": mc, - "mitglieder": [r["title"] for r in mrows]}) - continue unorm = _norm_title(title) member_norms = {r["title_norm"] for r in mrows} if unorm not in member_norms and unorm in seen_norm: @@ -1412,7 +1406,7 @@ async def _proc_grouping(ctx: GenContext, flow: Flow, cards): add = [] for k, new_members in (add or []): for m in new_members: - if m in used or not (1 <= m <= n) or _GROUP_STANDALONE.search(rows[m - 1]["title"]): + if m in used or not (1 <= m <= n): continue used.add(m) chosen[k]["members"].append(m) @@ -1622,6 +1616,19 @@ async def _proc_done(ctx: GenContext, flow: Flow, cards): await db.upsert_block(topic, norm, title, p.get("description", ""), p.get("sources") or []) await db.set_block_status(topic, norm, "consensus", title=title, description=p.get("description", "")) + # Bottom-up-Persistenz: die geclusterten Atome bleiben als Subs (jedes Atom genau + # ein Sub). Ein childless Standalone-Block ist sein eigener einziger Sub. Board 2 + # reichert diese an, entdeckt sie nie neu — so überlebt jedes Board-1-Atom. + sub_titles = [(ch.get("title") if isinstance(ch, dict) else ch) + for ch in (p.get("children") or [])] + sub_titles = [s for s in sub_titles if s] or [title] + seen_sub: set[str] = set() + for st in sub_titles: + sn = _norm_title(st) + if not sn or sn in seen_sub: + continue + seen_sub.add(sn) + await db.put_subblock(topic, norm, sn, title, st, status="consensus") mirrored[norm] = c["card_id"] p.update(mirrored_norm=norm, title=title) await db.kanban_set_payload(topic, BOARD, c["card_id"], p) diff --git a/backend/fake_agents.py b/backend/fake_agents.py index 1c6a47e..adf083a 100644 --- a/backend/fake_agents.py +++ b/backend/fake_agents.py @@ -101,11 +101,14 @@ class Welt: j = json.dumps # Board 1 / Inventar if "-research-" in key: + # Bottom-up: Research liefert die ATOME flach; Board 1 gruppiert sie zu den + # Cluster-Bausteinen. Jedes Atom gehört zu genau einem Cluster. zeilen = [] n = 1 - for t, b in self.bloecke.items(): - zeilen.append(f"{n}. {t} — {b['beschreibung']}") - n += 1 + for cl, b in self.bloecke.items(): + for atom in b["subs"]: + zeilen.append(f"{n}. {atom} — Atom aus {cl}") + n += 1 return "\n".join(zeilen) if "-pair-" in key: n = prompt.count("\nA: ") or 1 @@ -130,7 +133,19 @@ class Welt: if "-gruppierung-completion-" in key: return j({"additions": []}) if "-gruppierung-" in key: - return j({"umbrellas": []}) + # jedes Atom seinem Cluster zuordnen (nur die FULL ITEM LIST, 1..n eindeutig) + seg = prompt.split("FULL ITEM LIST", 1)[-1] + atom_cluster = {_norm(a): cl for cl, b in self.bloecke.items() for a in b["subs"]} + umb: dict[str, list[int]] = {} + for m in _NUM_RE.finditer(seg): + nr, zeile = int(m.group(1)), _norm(m.group(2)) + for an, cl in atom_cluster.items(): + if zeile.startswith(an): + umb.setdefault(cl, []).append(nr) + break + umbrellas = [{"title": cl, "description": self.bloecke[cl]["beschreibung"], + "members": ms} for cl, ms in umb.items() if ms] + return j({"umbrellas": umbrellas}) if "-supplement-beleg" in key or "-anker-beleg-" in key: nums = {m.group(1) for m in _NUM_RE.finditer(prompt)} return j({"relevant": {k: "ja" for k in sorted(nums, key=int)} or {"1": "ja"}}) @@ -141,6 +156,16 @@ class Welt: return j({"relevant": {k: "ja" for k in sorted(nums, key=int)} or {"1": "ja"}}) # Board 2 / Artefakte (verschmolzene Calls, block_calls.py) + if "-sb-enrich-" in key: # Bottom-up: feste Subs (aus dem Prompt) mit Facts anreichern + seg = prompt.split("FIXED SUBBLOCKS", 1)[-1].split("\n\n", 1)[0] + subs = [] + for m in re.finditer(r"^\s*\d+\.\s+(.+)$", seg, re.M): + s = m.group(1).strip() + f = self._fakt("", s) + subs.append({"title": s, "level": "beginner", "relevance": "relevant", + **{k: f[k] for k in ("key_points", "prerequisites", "hurdles", + "cited_facts", "example_idea")}}) + return j({"subs": subs}) if "-sb-gen-" in key: subs = [] for t in self._bloecke_im_prompt(prompt): @@ -251,13 +276,15 @@ class Welt: def standard_bloecke() -> dict: - """3 Blöcke; „Gemeinsamer Grundbegriff" liegt in Alpha UND Beta (Cross-Block-Fall).""" + """3 Themen-Cluster; die subs sind die ATOME, die Board 1 bottom-up zu genau diesem + Cluster gruppiert. Jedes Atom liegt in genau einem Cluster (kein geteiltes Atom mehr — + das globale Clustern in Board 1 macht Cross-Block-Dubletten unmöglich).""" return { "Alpha-Konzept": {"beschreibung": "Das erste Grundkonzept", "subs": ["Definition Alpha", "Alpha Eigenschaften", "Gemeinsamer Grundbegriff"]}, "Beta-Verfahren": {"beschreibung": "Das zentrale Verfahren", - "subs": ["Beta Ablauf", "Beta Grenzen", "Gemeinsamer Grundbegriff"]}, + "subs": ["Beta Ablauf", "Beta Grenzen"]}, "Gamma-Anwendung": {"beschreibung": "Praktische Anwendung", "subs": ["Gamma Praxisfall", "Gamma Werkzeuge"]}, } @@ -308,11 +335,18 @@ def aktivieren(welt: Welt, setattr_fn=setattr) -> None: @staticmethod def embed(texts): + import hashlib + import numpy as np - uniq = {t: k for k, t in enumerate(dict.fromkeys(texts))} - arr = np.zeros((len(texts), max(len(uniq), 1))) + # FESTE Dimension (hash → One-hot): identischer Text = gleiche Spalte = cos 1.0, + # verschiedener Text = andere Spalte = cos 0.0. Anders als eine pro-Aufruf + # variable Breite lassen sich so Vektoren aus verschiedenen embed()-Aufrufen + # concatenieren (Board-1-Cluster-Cache) ohne Dimensions-Mismatch. + D = 4096 + arr = np.zeros((len(texts), D)) for r, t in enumerate(texts): - arr[r, uniq[t]] = 1.0 + h = int(hashlib.blake2b(t.encode("utf-8"), digest_size=8).hexdigest(), 16) % D + arr[r, h] = 1.0 return arr @staticmethod @@ -320,9 +354,14 @@ def aktivieren(welt: Welt, setattr_fn=setattr) -> None: arr = _FakeEmb.embed(texts) return arr @ arr.T - for mod in (blocks, ba, qa): - setattr_fn(mod, "embedding", _FakeEmb) + # reine Matrix-/Union-Find-Helfer (modell-unabhängig) ans echte Modul delegieren + import embedding as _real_emb + capped_blocks = staticmethod(_real_emb.capped_blocks) + _find = staticmethod(_real_emb._find) + _union = staticmethod(_real_emb._union) - async def emb_ok(flow): # Board-1-Vektorpfade aus — Judge-Wellen reichen - return False - setattr_fn(bi, "_emb_ok", emb_ok) + for mod in (blocks, ba, qa, bi): + setattr_fn(mod, "embedding", _FakeEmb) + # _emb_ok bleibt echt (True über _FakeEmb): Bottom-up braucht den Grouping-Stage. + # _FakeEmb bildet nur bei identischem Text Nachbarn — verschiedene Atome erzeugen keine + # falschen Vorab-Cluster; das eigentliche Clustern macht der TOP-Judge (Card-sort). diff --git a/backend/tests/test_block_calls.py b/backend/tests/test_block_calls.py index 75f82df..8137dfc 100644 --- a/backend/tests/test_block_calls.py +++ b/backend/tests/test_block_calls.py @@ -123,86 +123,74 @@ def env(testdb, tmp_path, monkeypatch): return testdb, _ctx(), {"arbeit": tmp_path} -def _mk_gen_race(outputs): - """_race-Fake: pro Generator-Slot (…-gN) die gescriptete Antwort als Text; - fehlender Eintrag = Ausfall.""" +def _mk_enrich_slot(output): + """run_single_slot-Fake für die Anreicherung: schreibt die gescriptete Antwort via + payload (wie der Engine-Sink); output=None → Ausfall.""" calls = [] - async def fake_race(topic, label, slots, quorum, timeout, provider, on_update=None, - cancelled=None, **kw): - outs = [] - for slot in slots: - calls.append(slot["key"]) - g = int(slot["key"].rsplit("-g", 1)[1]) - out = outputs.get(g) - if out is not None: - outs.append(slot["payload"]((0, json.dumps(out), ""))) - return [o for o in outs if o] or None + async def fake(ctx, label, *, key, prompt, role, capabilities, payload, timeout, on_line=None): + calls.append({"key": key, "prompt": prompt}) + if output is None: + return FAILED, None + return OK, payload((0, json.dumps(output), "")) - fake_race.calls = calls - return fake_race + fake.calls = calls + return fake -async def test_generate_schnittmenge_wird_consensus(env, monkeypatch): - """Von beiden Generatoren genannt → consensus (Facts-Union); Einzelnennungen - werden unsicher und gehen zum Prüfer.""" +async def test_enrich_reichert_feste_subs_an(env, monkeypatch): + """Die Subs stehen fest (Board 1). EIN Call füllt Facts + Level + Relevanz — er + erfindet und entfernt nichts; die Board-1-Subs bleiben in Reihenfolge.""" db, ctx, files = env - monkeypatch.setattr(bc, "_race", _mk_gen_race({ - 1: {"subs": [_sub("Sub A", kp=["k1"]), _sub("Sub B")]}, - 2: {"subs": [_sub("Sub A", kp=["k2"]), _sub("Sub C")]}, - })) + await _seed_rows(db, "alpha", ["Sub A", "Sub B"]) + monkeypatch.setattr(bc, "run_single_slot", _mk_enrich_slot({"subs": [ + _sub("Sub A", kp=["k1"]), + _sub("Sub B", level="expert", relevance="peripheral", kp=["k2"])]})) gen = await bc._generate_block(ctx, files, "Alpha", "Grundkonzept") - assert gen["raw"] == {"Alpha": ["Sub A"]} - assert gen["facts"]["Alpha"]["sub a"]["key_points"] == ["k1", "k2"] # Union beider Nennungen - assert {u["title"] for u in gen["unsicher"]} == {"Sub B", "Sub C"} - assert gen["votes"]["sub a"]["level"] == ["beginner", "beginner"] + assert gen["raw"] == {"Alpha": ["Sub A", "Sub B"]} + assert gen["facts"]["Alpha"]["sub a"]["key_points"] == ["k1"] + assert gen["unsicher"] == [] + assert gen["votes"]["sub b"] == {"level": ["expert"], "relevance": ["peripheral"]} rows = {r["sub_norm"]: r["status"] for r in await db.list_subblocks(TOPIC, "alpha")} - assert rows["sub a"] == "consensus" - assert rows["sub b"] == rows["sub c"] == "candidate" + assert rows["sub a"] == rows["sub b"] == "consensus" -async def test_generate_degraded_alles_unsicher(env, monkeypatch): - """Liefert nur EIN Generator, ist kein Konsens möglich — alles wird unsicher, - der Prüfer entscheidet mit Material.""" +async def test_enrich_ohne_subs_kein_call(env, monkeypatch): + """Kein Board-1-Sub → nichts anzureichern, kein Modell-Call.""" db, ctx, files = env - monkeypatch.setattr(bc, "_race", _mk_gen_race({ - 1: {"subs": [_sub("Sub A"), _sub("Sub B")]}, # g2 fällt aus - })) - gen = await bc._generate_block(ctx, files, "Alpha", "Grundkonzept") - assert gen["raw"] == {"Alpha": []} - assert {u["title"] for u in gen["unsicher"]} == {"Sub A", "Sub B"} - assert not any(r["status"] == "consensus" for r in await db.list_subblocks(TOPIC, "alpha")) + called = [] + + async def spy(*a, **k): + called.append(1) + return OK, None + monkeypatch.setattr(bc, "run_single_slot", spy) + gen = await bc._generate_block(ctx, files, "Alpha", "d") + assert gen == {"raw": {"Alpha": []}, "facts": {"Alpha": {}}, "unsicher": [], "votes": {}} + assert called == [] -async def test_generate_beide_ausgefallen_ist_none(env, monkeypatch): +async def test_enrich_ausgelassener_sub_bleibt_leer(env, monkeypatch): + """Lässt der Call einen Sub aus, bleibt er erhalten (Vollständigkeit) — mit leeren + Facts, die der Prüfer/QA als Lücke sieht.""" db, ctx, files = env - monkeypatch.setattr(bc, "_race", _mk_gen_race({})) - assert await bc._generate_block(ctx, files, "Alpha", "d") is None + await _seed_rows(db, "alpha", ["Sub A", "Sub B"]) + monkeypatch.setattr(bc, "run_single_slot", _mk_enrich_slot({"subs": [_sub("Sub A", kp=["k1"])]})) + gen = await bc._generate_block(ctx, files, "Alpha", "d") + assert gen["raw"] == {"Alpha": ["Sub A", "Sub B"]} + assert gen["facts"]["Alpha"]["sub b"]["key_points"] == [] + assert gen["facts"]["Alpha"]["sub b"]["cited_facts"] == [] -async def test_generate_seed_garantie(env, monkeypatch): - """Ungedeckte Seeds gehen als unsicher zum Prüfer (Beleg-Gate liegt dort); - lexikalisch gedeckte Seeds erzeugen keine Dublette.""" +async def test_enrich_resume_ohne_neue_calls(env, monkeypatch): + """Vorhandene enrich-Datei → kein neuer Call, Ergebnis wird übernommen.""" db, ctx, files = env - monkeypatch.setattr(bc, "_race", _mk_gen_race({ - 1: {"subs": [_sub("Sub A")]}, 2: {"subs": [_sub("Sub A")]}, - })) - gen = await bc._generate_block(ctx, files, "Alpha", "d", - seeds=["Escaping Regeln", "Sub"]) - assert gen["raw"] == {"Alpha": ["Sub A"]} - assert [u["title"] for u in gen["unsicher"]] == ["Escaping Regeln"] # „Sub" ist gedeckt - assert gen["unsicher"][0]["key_points"] == [] # Seeds kommen ohne Beleg - - -async def test_generate_resume_ohne_neue_calls(env, monkeypatch): - """Vorhandene gen-Dateien → kein neuer _race-Call, Ergebnis wird übernommen.""" - db, ctx, files = env - fake = _mk_gen_race({1: {"subs": [_sub("Sub A")]}, 2: {"subs": [_sub("Sub A")]}}) - monkeypatch.setattr(bc, "_race", fake) + await _seed_rows(db, "alpha", ["Sub A"]) + fake = _mk_enrich_slot({"subs": [_sub("Sub A", kp=["k1"])]}) + monkeypatch.setattr(bc, "run_single_slot", fake) gen1 = await bc._generate_block(ctx, files, "Alpha", "d") n = len(fake.calls) gen2 = await bc._generate_block(ctx, files, "Alpha", "d") - assert len(fake.calls) == n # alles resumed + assert len(fake.calls) == n # aus Datei resumed assert gen2["raw"] == gen1["raw"] @@ -275,24 +263,27 @@ async def test_verify_fremd_nur_einstimmig(env, monkeypatch): assert rows["css regel"] == "discarded" and rows["echte regel"] == "consensus" -async def test_verify_uebernahme_braucht_beide(env, monkeypatch): - """Unsicher-Eintrag wird nur bei 2/2 „ja" consensus (samt Generator-Facts); - sonst discarded.""" +async def test_verify_keep_all_behaelt_alle_atome(env, monkeypatch): + """Bottom-up (keep_all=True): kein Board-1-Atom wird entfernt oder gefaltet, selbst + wenn beide Prüfer fremd/gruppen/discard melden. Nur Facts-Korrektur (Hinweis) und + Level/Relevanz greifen weiter.""" db, ctx, files = env - subs = ["Sub A"] - unsicher = [_sub("Unsicher B", kp=["kp b"]), _sub("Unsicher C")] + subs = ["Sub A", "Sub B", "Sub C"] await _seed_rows(db, "alpha", subs) - await _seed_rows(db, "alpha", ["Unsicher B", "Unsicher C"], status="candidate") - fake = _judge_slot({"1": {"uebernehmen": {"2": "ja", "3": "ja"}}, - "2": {"uebernehmen": {"2": "ja", "3": "nein"}}}) + fix = {"subs": [_sub("Sub A", kp=["korrigiert"])]} + verdikt = {"fremd": [3], "gruppen": [{"haupt": 1, "weitere": [2]}], + "facts_probleme": [{"nr": 1, "discard": True, "hinweis": "Zahl falsch"}, + {"nr": 2, "discard": True}], + "levels": {"2": "expert"}} + fake = _judge_slot({"1": verdikt, "2": verdikt}, fix=fix) monkeypatch.setattr(bc, "run_single_slot", fake) - res = await bc._verify_block(ctx, files, "Alpha", _gen_von("Alpha", subs, unsicher=unsicher), {}) - assert res["raw"] == {"Alpha": ["Sub A", "Unsicher B"]} - assert res["facts"]["Alpha"]["unsicher b"]["key_points"] == ["kp b"] + res = await bc._verify_block(ctx, files, "Alpha", _gen_von("Alpha", subs), {}, keep_all=True) + assert res["raw"] == {"Alpha": subs} # alle drei bleiben — nichts entfernt/gefaltet rows = {r["sub_norm"]: r["status"] for r in await db.list_subblocks(TOPIC, "alpha")} - assert rows["unsicher b"] == "consensus" and rows["unsicher c"] == "discarded" - # der Prüfer-Prompt weist die Unsicher-Nummern aus - assert "UNSICHER" in fake.calls[0]["prompt"] and "entries 2–3" in fake.calls[0]["prompt"] + assert all(rows[_norm_title(s)] == "consensus" for s in subs) + side = {s["title"]: s for s in res["sidecar"]["Alpha"]} + assert side["Sub A"]["facts"]["key_points"] == ["korrigiert"] # Hinweis trotz discard=True + assert side["Sub B"]["level"] == "expert" # Level-Korrektur greift async def test_verify_facts_discard_nur_2von2(env, monkeypatch): diff --git a/backend/tests/test_board_inventory.py b/backend/tests/test_board_inventory.py index 17e7c3e..5e7732e 100644 --- a/backend/tests/test_board_inventory.py +++ b/backend/tests/test_board_inventory.py @@ -78,7 +78,8 @@ async def board_env(testdb, tmp_path, monkeypatch): for s in subs}}, "unsicher": [], "votes": {}} - async def fake_verify(ctx, files, title, gen, q, instructions="", ns="", lbl="", sources=None, melde=None): + async def fake_verify(ctx, files, title, gen, q, instructions="", ns="", lbl="", sources=None, + melde=None, keep_all=False): subs = gen["raw"].get(title) or [] bfacts = gen["facts"].get(title) or {} sidecar = [{"title": s, "level": "beginner", @@ -776,9 +777,11 @@ async def test_outline_runs_before_artefacts_finish(board_env, monkeypatch): base_verify = ba._verify_block snapshot = {} - async def slow_verify(ctx, files, title, gen, q, instructions="", ns="", lbl="", sources=None, melde=None): + async def slow_verify(ctx, files, title, gen, q, instructions="", ns="", lbl="", sources=None, + melde=None, keep_all=False): await asyncio.sleep(0.8) # keeps one card in `verify` while the outline fires - return await base_verify(ctx, files, title, gen, q, instructions, ns=ns, lbl=lbl, sources=sources) + return await base_verify(ctx, files, title, gen, q, instructions, ns=ns, lbl=lbl, + sources=sources, keep_all=keep_all) base_outline = ba._outline_block diff --git a/backend/tests/test_e2e_fake.py b/backend/tests/test_e2e_fake.py index 6cbbc3d..4548bc6 100644 --- a/backend/tests/test_e2e_fake.py +++ b/backend/tests/test_e2e_fake.py @@ -42,11 +42,16 @@ async def test_e2e_thema_vollpfad(fake_welt, testdb, tmp_path): db = testdb done = [c for c in await db.kanban_cards(TOPIC, board="inventory", stage="done_block")] titel = {c["payload"]["title"] for c in done} + # Bottom-up: 7 Atome zu 3 Themen-Clustern gruppiert assert titel == {"Alpha-Konzept", "Beta-Verfahren", "Gamma-Anwendung"} - # Cross-Block-Dublette: „Gemeinsamer Grundbegriff" überlebt in genau EINEM Block subs = [dict(r) for r in await db.list_subblocks(TOPIC)] + # jedes Atom liegt in genau EINEM Cluster — keine Cross-Block-Dublette mehr gemeinsam = [r for r in subs if r["sub_norm"] == "gemeinsamer grundbegriff"] - assert sorted(r["status"] for r in gemeinsam) == ["consensus", "variant"] + assert [r["status"] for r in gemeinsam] == ["consensus"] + # Vollständigkeit: alle 7 Atome sind als consensus-Sub erhalten + consensus = {r["sub_norm"] for r in subs if r["status"] == "consensus"} + assert consensus == {"definition alpha", "alpha eigenschaften", "gemeinsamer grundbegriff", + "beta ablauf", "beta grenzen", "gamma praxisfall", "gamma werkzeuge"} fehler = await pruefe_invarianten(TOPIC, files) assert fehler == [] @@ -83,9 +88,7 @@ async def test_e2e_rerun_idempotent(fake_welt, testdb, tmp_path): @pytest.mark.parametrize("stoerung", [ - {"muster": r"-sub-crossblock-.*-j1$", "modus": "fehler", "mal": 3}, # Ersatzrichter jE {"muster": r"-sb-verify-.*-j1$", "modus": "garbage", "mal": 1}, # Ersatz-Richter jE - {"muster": r"-sb-gen-.*-g1$", "modus": "fehler", "mal": 1}, # degraded: 1 Generator {"muster": r"-art-gen-.*-t1$", "modus": "fehler", "mal": 1}, # Slot-Restart {"muster": r"-research-2$", "modus": "fehler", "mal": 3}, # 1 Producer tot ]) @@ -97,35 +100,6 @@ async def test_e2e_stoerungen_flow_endet(fake_welt, testdb, tmp_path, stoerung): assert await pruefe_invarianten(TOPIC, files) == [] -async def test_e2e_crossblock_dissent_failopen(fake_welt, testdb, tmp_path): - """j1 sagt a, j2 sagt b, j3 fällt aus → Paar bleibt (fail-open), Rest konsistent.""" - fake_welt.stoerungen += [ - {"muster": r"-sub-crossblock-.*-j2$", "modus": "antwort", - "antwort": '{"pairs": {"1": "b"}}', "mal": 1, "rest": 1}, - {"muster": r"-sub-crossblock-.*-j3$", "modus": "fehler", "mal": 3, "rest": 3}, - ] - ok, files = await _lauf(tmp_path) - assert ok - db = testdb - subs = [dict(r) for r in await db.list_subblocks(TOPIC)] - gemeinsam = [r for r in subs if r["sub_norm"] == "gemeinsamer grundbegriff"] - assert sorted(r["status"] for r in gemeinsam) == ["consensus", "consensus"] # kein Fold - assert await pruefe_invarianten(TOPIC, files) == [] - - -async def test_e2e_inblock_gruppe_faltet(fake_welt, testdb, tmp_path): - """Welt-Regel: „Alpha Eigenschaften" faltet unter „Definition Alpha" — beide Judges - liefern die Gruppe, der Verlierer wird variant, seine facts wandern zum Gewinner.""" - fake_welt.gruppen.append(("Definition Alpha", ["Alpha Eigenschaften"])) - ok, files = await _lauf(tmp_path) - assert ok - db = testdb - rows = {r["sub_norm"]: r["status"] for r in await db.list_subblocks(TOPIC, "alpha-konzept")} - assert rows.get("alpha eigenschaften") == "variant" - assert rows.get("definition alpha") == "consensus" - assert await pruefe_invarianten(TOPIC, files) == [] - - async def test_e2e_gate_vollinventur_ohne_fix(fake_welt, testdb, tmp_path): """Gate-Judge liefert eine Voll-Inventur (belegte Claims mit „Belegt…"-Grund) — der Schema-Filter wirft sie raus, es läuft KEIN Fakten-Fix.""" @@ -189,10 +163,11 @@ async def test_e2e_uni_anker_gate(fake_welt, testdb, tmp_path, monkeypatch): assert ok db = testdb alle = [dict(c) for c in await db.kanban_cards(TOPIC, board="inventory")] - assert any(c["stage"] == "rejected" and c["payload"].get("title") == "Kanon-Klassiker" + # das unbelegte Atom „Klassiker Detail" (einziges Atom von Kanon-Klassiker) wird rejected + assert any(c["stage"] == "rejected" and c["payload"].get("title") == "Klassiker Detail" for c in alle) - assert not any(c["kind"] == "block" and c["payload"].get("title") == "Kanon-Klassiker" - for c in alle) # nie zum Block geworden + assert not any(c["payload"].get("title") in ("Klassiker Detail", "Kanon-Klassiker") + and c["stage"] == "done_block" for c in alle) # nie zum Block geworden done = {c["payload"].get("title") for c in alle if c["kind"] == "block" and c["stage"] == "done_block"} assert {"Alpha-Konzept", "Beta-Verfahren", "Gamma-Anwendung"} <= done diff --git a/frontend/src/components/GenerationView.vue b/frontend/src/components/GenerationView.vue index c084668..6568dbb 100644 --- a/frontend/src/components/GenerationView.vue +++ b/frontend/src/components/GenerationView.vue @@ -214,7 +214,7 @@ function removeArtefactsClick() { {{ repairInfo.artefacts }} - + diff --git a/frontend/src/components/TopicDetail.vue b/frontend/src/components/TopicDetail.vue index 12efc3f..81ed874 100644 --- a/frontend/src/components/TopicDetail.vue +++ b/frontend/src/components/TopicDetail.vue @@ -2,7 +2,7 @@ import { computed, reactive, ref, watch, nextTick, onMounted, onUnmounted } from 'vue' import { fetchGuideContent, chatGuide, fetchBlockLearnState } from '../api.js' import { renderMarkdown } from '../markdown.js' -import { stufeFuer, schwelle, SUB_RANK, VIEW_KURZ, VIEW_FARBE } from '../levels.js' +import { stufeFuer, schwelle, SUB_RANK, VIEW_KURZ, VIEW_FARBE, BADGE_VIEW_RANK } from '../levels.js' import { useChat } from '../composables/useChat.js' import BlockPanel from './BlockPanel.vue' import BlockFocus from './BlockFocus.vue' @@ -36,9 +36,12 @@ let mdObserver = null // Auto: Ansichtsstufe des Blocks folgt dem Prüfungs-Score (Erreicht + 1); Override = global fest. function viewLevelFor(title) { if (props.stufeAnsicht !== 'auto') return Number(props.stufeAnsicht) - // Auto = real freigeschaltete Stufe je Baustein (Backend `freie_level` aus dem Prüf-Score): - // Stufe A initial, die nächste Stufe erst, wenn die Prüf-Grenze erreicht ist. - return learnstate.value[title]?.freie_level || 1 + // Auto = an die erreichte Badge-Stufe gekoppelt (gleiche Schwelle wie das Level-Abzeichen): + // Advanced-Badge → advanced-Subs sichtbar. Vorher hing es an `freie_level` (Sub-Zahl × 25), + // das bei vielen beginner-Subs SPÄTER umschaltet als der Badge (0.4 × cap) — dann zeigte der + // Badge „Advanced", die advanced-Inhalte blieben aber verborgen. + const st = levelOf(title) + return st ? (BADGE_VIEW_RANK[st.key] || 1) : 1 } function htmlFor(s) { diff --git a/frontend/src/levels.js b/frontend/src/levels.js index 92a59a5..b51f19f 100644 --- a/frontend/src/levels.js +++ b/frontend/src/levels.js @@ -28,6 +28,10 @@ export function malusRegel(score, cap) { return '−20' } +// Reached badge level (LEVELS key) → view level 1..4, so unlocked SUBS match the badge: +// Advanced-Badge → advanced subs visible. Same threshold as stufeFuer, no drift. +export const BADGE_VIEW_RANK = { beginner: 1, advanced: 2, expert: 3, master: 4 } + // Sub-level tag (from the guide markers) → view level 1..4 (A/F/E/V). export const SUB_RANK = { beginner: 1, advanced: 2, expert: 3, peripheral: 4, einfach: 1, mittel: 2, schwer: 3 } export const VIEW_KURZ = { 1: 'A', 2: 'F', 3: 'E', 4: 'V' } diff --git a/templates/Prompt/Blocks-Gruppierung-Completion.md b/templates/Prompt/Blocks-Gruppierung-Completion.md index 3c50356..ef904db 100644 --- a/templates/Prompt/Blocks-Gruppierung-Completion.md +++ b/templates/Prompt/Blocks-Gruppierung-Completion.md @@ -1,18 +1,17 @@ -Topic "{topic}". Umbrella blocks were formed, each bundling the constituent parts of ONE model/definition. Some parts were missed and are still listed as standalone blocks. Your job: for each umbrella, find which of the remaining standalone blocks are ALSO constituent parts of that same parent, so the model is complete. +Topic "{topic}". Theme blocks were formed by card-sorting a fine-grained item list. Some items were left unassigned. Your job: attach EACH remaining item to the theme it belongs to, so every item lands in a theme. This is organisation, not judgement — items are never discarded. -UMBRELLAS (parent — already-collected parts): +THEMES (heading — already-assigned members): {umbrellas} -REMAINING STANDALONE BLOCKS (numbered): +REMAINING ITEMS (numbered): {rest} -## Rule — attach a block to an umbrella only if BOTH hold -1. **Presupposition:** the block's definition **requires the umbrella's parent to already exist** — it makes no sense as a topic on its own without that model (e.g. „Alphabet Σ", „Übergangsfunktion δ", „Konfiguration", „Akzeptierende Berechnung" all presuppose the Turing-machine; „Literale", „Klausel", „Belegung" presuppose the KNF/logic definition). The parent must NOT presuppose the block (directional). -2. **Not standalone:** the block is a *definitional component / notation*, NOT itself a named **algorithm, problem, theorem, reduction, or complexity class** (those stay their own block — a downstream guard will reject them anyway). - -Do NOT attach a block merely because it shares a topic. When unsure → leave it standalone. Most remaining blocks will NOT be attached; a few genuine missed parts will. +## Rule +- Attach each remaining item to the ONE theme whose subject it shares — the theme a learner would meet it under (a variable/flag/field/instance/facet of that theme's model, system, setup, tool family, or category). +- Assign as many as you reasonably can. Only leave an item unassigned if it fits NO theme at all — it then becomes its own single-item block (a genuinely standalone concept). +- Each item goes into at most ONE theme. Write ONLY the JSON file to: {out_path} -Format (`additions` may be empty; `umbrella` = the UMBRELLA index, `members` = standalone block numbers to attach): +Format (`additions` may be empty; `umbrella` = the THEME index, `members` = item numbers to attach): {{"additions": [{{"umbrella": 0, "members": [8, 12, 34]}}]}} diff --git a/templates/Prompt/Blocks-Gruppierung.md b/templates/Prompt/Blocks-Gruppierung.md index 6060cc5..15a4565 100644 --- a/templates/Prompt/Blocks-Gruppierung.md +++ b/templates/Prompt/Blocks-Gruppierung.md @@ -1,35 +1,23 @@ -Topic "{topic}". A previous step produced a flat list of learning blocks that is TOO FINE-GRAINED — several blocks are **constituent sub-definitions / notation of ONE larger definition or model** and should become a single umbrella block. Find these groups. A good run finds several genuine umbrellas AND leaves most blocks standalone; judge each candidate on its merits. +Topic "{topic}". A previous step produced a flat list of learning items that is TOO FINE-GRAINED for a table of contents — single environment variables, single CLI flags, single config fields, near-synonyms. Your job is **card sorting**: group the items into a small set of **theme blocks**, so that EVERY item lands in exactly one theme. Nothing is discarded — grouping never loses an item; it only organises them. -**Propose real umbrellas, judge each on its merits.** Merge when the members are constituent parts of one whole (test 3); keep a member separate when it stands as a full, usable unit on its own. Do NOT withhold a genuine merge merely because the members are lexically dissimilar (facets of one whole routinely are) or because you are unsure of the parent's exact name. +Think of the result as chapters: ~15–25 themes for a whole topic, each a coherent learning block that a learner meets as one unit. A theme with only one fitting item is fine (a genuinely standalone concept), but prefer pulling related items together over leaving many singletons. -CANDIDATES (your starting point): +CANDIDATES you may group (your starting point — a pre-clustered neighbourhood): {candidates} -FULL BLOCK LIST (you may pull in ANY numbers below that are constituents of the same definition): +FULL ITEM LIST (you may pull in ANY numbers below that share the theme): {list} -## Merge test — propose an umbrella when ALL THREE hold -1. **One parent.** The members are constituent parts/facets of ONE named parent (a model, system, setup, or definition) — each member PRESUPPOSES that parent: you cannot introduce it without first invoking the parent, and it has no purpose outside it. -2. **Studied together.** A learner meets them together as one unit. -3. **No standalone unit among them — autonomy test (decisive).** Remove the OTHER members, then ask of each: does it still stand as a COMPLETE, usable unit with its own goal? YES → autonomous → a SIBLING, keep separate (dropping the others took nothing from it). NO → alone it serves no goal and exists only to build the shared parent → a constituent PART, merge it. Structural containment („X is a file/step/field of Y") is NOT the test — autonomy is: own goal, complete without the siblings. This holds in every domain. +## How to form a theme +- **One coherent subject.** The members belong together because a learner would study them as one block — the parts/facets/instances of one model, system, setup, tool family, or category. Examples of the *kind* of grouping (not domain rules): many individual settings/variables/flags of one configuration → one „…-Konfiguration" theme; the parts of one setup → one „…-Setup" theme; a base concept plus its variants/instances → one theme. +- **Lexical dissimilarity is NORMAL.** Facets of one theme often read very differently — that is not a reason to split them. +- **Title:** name the theme after the shared subject. A real self-contained heading; must NOT contain „ — " (reserved separator) — use „(…)" or „:". +- **Description:** name EVERY member explicitly (the next step recovers them as sub-points). E.g. „Environment-Konfiguration: APP_ENV, APP_SECRET, DATABASE_URL, MAILER_DSN, …". +- **Members:** the item NUMBERS from the full list. Each number goes into exactly ONE theme; do not repeat a number across themes. -*Note on test 1: „can this be defined at all?" is the WRONG question — Alphabet Σ and DTM CAN be stated in isolation, yet in THIS topic they are parts of the Turing-machine model and belong together. The question is whether the member PRESUPPOSES the shared parent, not whether a standalone sentence exists.* - -## Examples -DO NOT MERGE — distinct named units that merely share a topic: -- „Greedy-Algorithmus GA" + „ModifiedGreedy" + „Multiple-Choice-Knapsack" → two algorithms + a problem, each standalone (test 3 fails). Keep separate. -- „Plugin" + „Plugin-Konfiguration (config.xml)" + „Plugin lifecycle" → „Plugin-Konfiguration" is its OWN unit (authoring config fields, snippets, its own tasks), a SIBLING of „Plugin", not a facet (test 3 fails). Structural containment — the file belongs to the plugin — is NOT the merge test; being studied as one definition is. Keep „Plugin-Konfiguration" standalone. - -MERGE — one definition decomposed (the canonical cases — end here so this is your default lens): -- „Alphabet Σ" + „NDTM" + „DTM" + „Akzeptierende Berechnung" + „Folgekonfiguration" → ONE umbrella **„Turingmaschine (Modell)"**. (The members are lexically very different from each other — that is EXPECTED for facets of one model and is NOT a reason to keep them apart.) -- „Klausel" + „Boolesche Variable" + „Erfüllende Belegung" + „KNF" → ONE umbrella **„Aussagenlogik & KNF"**. - -## Synthesize each umbrella -- `title`: the parent concept's name (e.g. „Turingmaschine (Modell)"). A real self-contained definition; must NOT contain „ — " (a reserved separator) — use „(…)" or „:". -- `description`: **name EVERY merged child explicitly** — the next step recovers the children as sub-points from the source. E.g. „Formales TM-Modell: Konfiguration, Übergangsfunktion δ, Alphabet Σ, Akzeptierende Berechnung, Folgekonfiguration." -- `members`: the block NUMBERS (from the full list) folded in. At least 2 per umbrella; each number appears in at most one umbrella. +Assign as many items as you confidently can. Items you are unsure about you may leave out — a later completion step and a singleton fallback catch them; never force a bad fit. Write ONLY the JSON file to: {out_path} -Format (`umbrellas` may be empty): -{{"umbrellas": [{{"title": "Turingmaschine (Modell)", "description": "Formales TM-Modell: Konfiguration, Übergangsfunktion δ, Alphabet Σ, Akzeptierende Berechnung, Folgekonfiguration.", "members": [1, 2, 5, 12, 34]}}]}} +Format (`umbrellas` = the themes; may be empty if nothing fits together): +{{"umbrellas": [{{"title": "Environment-Konfiguration", "description": "Konfiguration über Umgebungsvariablen: APP_ENV, APP_SECRET, DATABASE_URL, MAILER_DSN, .env-Dateien.", "members": [1, 2, 5, 12, 34]}}]}} diff --git a/templates/Prompt/Subblock-Anreichern.md b/templates/Prompt/Subblock-Anreichern.md new file mode 100644 index 0000000..9590b28 --- /dev/null +++ b/templates/Prompt/Subblock-Anreichern.md @@ -0,0 +1,32 @@ +The block below (topic "{topic}") is ALREADY decomposed into its subblocks — the list is FIXED. Your job is NOT to find subblocks, but to extract for EACH given subblock its learning facts from the material. A later guide writes a short text PER subblock; the facts are the binding basis for guide text, levels, and exam questions — they must be **correct**. + +BLOCK: +{block} + +FIXED SUBBLOCKS (numbered — return one entry per subblock, in this order): +{subs} + +{source} + +HARD RULES: +- Return EXACTLY these subblocks — do NOT add, remove, split, merge or rename any. Copy each `title` VERBATIM from the list above (code identifiers stay original). +- One entry per given subblock, same order. If the material barely covers a subblock, still return it with whatever is backable (empty fields are fine) — never drop it. + +Per subblock, collect ONLY the essentials (all field content in GERMAN; technical terms/code identifiers stay original): +- **level**: beginner | advanced | expert — difficulty within this block. +- **relevance**: relevant (core of the topic) | peripheral (edge knowledge). +- **key_points**: 1–3 concise statements — what must one understand? +- **prerequisites**: what must one know beforehand (a half-sentence)? Empty if nothing. +- **hurdles**: typical beginner misconception (a half-sentence). Empty if none. +- **cited_facts**: hard facts (definitions, formulas, values, names) — **only what the excerpts back**, each with the location (e.g. „Skript Def. 6.3, Z.66"). Invent no values, compute nothing yourself. +- **example_idea**: ONE example that carries understanding — freely phrased. Empty if an example adds nothing. + +HARD SEPARATION: `cited_facts` = only backable material. A worked example, an invented sentence, a constructed case → `example_idea`, NEVER `cited_facts`. When in doubt: better to leave a field empty than to claim falsely. + +Reply with ONLY the JSON as your final message — no code fences, no other text. EXACTLY this format: +{{"subs": [ + {{"title": "…", "level": "beginner|advanced|expert", "relevance": "relevant|peripheral", + "key_points": ["…"], "prerequisites": "…", "hurdles": "…", + "cited_facts": [{{"text": "…", "source": "…"}}], "example_idea": "…"}} +]}} +{extra} diff --git a/templates/Prompt/Subblock-Verify.md b/templates/Prompt/Subblock-Verify.md index 0d6e1a3..a7bc181 100644 --- a/templates/Prompt/Subblock-Verify.md +++ b/templates/Prompt/Subblock-Verify.md @@ -1,26 +1,18 @@ -You are auditing the decomposition of ONE block of the topic "{topic}" into subblocks. Below are the numbered subblocks with their captured facts, then source excerpts. Apply the 100%-decomposition test: no entry removable without a gap, none addable without duplication — and verify the facts. +You are auditing ONE block of the topic "{topic}". Its subblocks are FIXED (already decomposed and deduplicated upstream) — you do NOT add, remove, merge or split them. You only verify the captured FACTS against the source and correct the level/relevance classification. BLOCK: {block} SUBBLOCKS (numbered; key points indented): {subs} -{unsicher} {source} -Judge ALL of the following (use the numbers): -1. **gruppen** — entries that state the SAME thing or where one is a subset of the other: group them and name the number that should remain (`haupt` = the base statement, not the detail). -2. **kataloge** — pure enumeration entries of ONE kind (e.g. five option rows): bundle them under ONE short GERMAN collective title. -3. **fremd** — entries off-topic for the topic "{topic}" (not this block — the TOPIC). -4. **luecken** — essential aspects of THIS block that the excerpts cover but no entry captures (short German phrases). Only real gaps, no nice-to-haves. -5. **uebernehmen** — for each UNSICHER-numbered entry: "ja" if the excerpts back it and it fills a real spot, else "nein". -6. **facts_probleme** — entries whose facts contain something wrong or unbackable: `discard: true` ONLY if the entry as a whole is unsupportable in substance; otherwise `discard: false` with a short `hinweis` what to correct. -7. **levels** / **relevanz** — ONLY entries whose level (beginner/advanced/expert) or relevance (relevant/peripheral) is clearly wrong: number → correct value. +Judge from the excerpts only, using the numbers: +1. **facts_probleme** — entries whose captured facts contain something wrong or unbackable: give a short `hinweis` what to correct (keep `discard: false` — the subblock itself always stays; only its facts get fixed). +2. **levels** / **relevanz** — ONLY entries whose level (beginner/advanced/expert) or relevance (relevant/peripheral) is clearly wrong: number → correct value. -Judge ONLY from the excerpts. If everything is fine, return empty lists/objects. +If everything is fine, return empty lists/objects. Reply with ONLY the JSON — no code fences, no other text. Format: -{{"gruppen": [{{"haupt": 1, "weitere": [4]}}], "kataloge": [{{"titel": "…", "mitglieder": [2, 5]}}], - "fremd": [7], "luecken": ["…"], "uebernehmen": {{"9": "ja"}}, - "facts_probleme": [{{"nr": 3, "discard": false, "hinweis": "…"}}], +{{"facts_probleme": [{{"nr": 3, "discard": false, "hinweis": "…"}}], "levels": {{"2": "expert"}}, "relevanz": {{"5": "peripheral"}}}} {extra}