From fd40c323bc952ab5ad40e20ced394dbb70953f1f Mon Sep 17 00:00:00 2001
From: Mateusz Charytoniuk <mateusz.charytoniuk@protonmail.com>
Date: Fri, 16 Feb 2024 12:19:18 +0100
Subject: [PATCH] feat: llm responder

---
 .php-cs-fixer.php                             |   2 -
 composer.json                                 |   3 +-
 composer.lock                                 | 611 +++++++-----------
 src/AcceptHeaderTest.php                      |   4 +-
 src/ArrayFlattenIteratorTest.php              |   4 +-
 src/Attribute/RespondsToPromptSubject.php     |  21 +
 src/BackusNaurFormGrammar.php                 |  17 +
 .../SubjectActionGrammar.php                  |  57 ++
 src/Constraint/AnyConstraintTest.php          |   4 +-
 src/Constraint/AnyOfConstraintTest.php        |   4 +-
 src/Constraint/BooleanConstraintTest.php      |   4 +-
 src/Constraint/ConstConstraintTest.php        |   4 +-
 src/Constraint/EnumConstraintTest.php         |   4 +-
 src/Constraint/FilenameConstraintTest.php     |   4 +-
 src/Constraint/IntegerConstraintTest.php      |   4 +-
 src/Constraint/ListConstraintTest.php         |   4 +-
 src/Constraint/MapConstraintTest.php          |   4 +-
 src/Constraint/NumberConstraintTest.php       |   4 +-
 src/Constraint/ObjectConstraintTest.php       |   4 +-
 src/Constraint/StringConstraintTest.php       |   4 +-
 src/Constraint/TupleConstraintTest.php        |   4 +-
 src/LlamaCppCompletionRequest.php             |  15 +-
 src/LlmSystemPrompt.php                       |  19 +
 .../SubjectActionSystemPrompt.php             |  86 +++
 src/OAuth2ScopePatternChunkTest.php           |   4 +-
 src/OAuth2ScopePatternTest.php                |   4 +-
 src/PostfixBounceAnalyzerTest.php             |   4 +-
 src/PromptSubjectRequest.php                  |  15 +
 src/PromptSubjectResponderAggregate.php       |  80 +++
 src/PromptSubjectResponderCollection.php      |  57 ++
 src/PromptSubjectResponderInterface.php       |  13 +
 src/PromptSubjectResponse.php                 |  45 ++
 src/SingletonCollection.php                   |   1 +
 src/SingletonContainerTest.php                |   4 +-
 ...omptSubjectResponderCollectionProvider.php |  50 ++
 src/SubjectActionTokenReader.php              |  97 +++
 src/SubjectActionTokenReaderTest.php          |  90 +++
 src/WebSocketProtocolIteratorTest.php         |   4 +-
 src/WebSocketRPCConnectionHandle.php          |   2 +-
 src/WebSocketServerController.php             |   6 +
 40 files changed, 953 insertions(+), 414 deletions(-)
 create mode 100644 src/Attribute/RespondsToPromptSubject.php
 create mode 100644 src/BackusNaurFormGrammar.php
 create mode 100644 src/BackusNaurFormGrammar/SubjectActionGrammar.php
 create mode 100644 src/LlmSystemPrompt.php
 create mode 100644 src/LlmSystemPrompt/SubjectActionSystemPrompt.php
 create mode 100644 src/PromptSubjectRequest.php
 create mode 100644 src/PromptSubjectResponderAggregate.php
 create mode 100644 src/PromptSubjectResponderCollection.php
 create mode 100644 src/PromptSubjectResponderInterface.php
 create mode 100644 src/PromptSubjectResponse.php
 create mode 100644 src/SingletonProvider/PromptSubjectResponderCollectionProvider.php
 create mode 100644 src/SubjectActionTokenReader.php
 create mode 100644 src/SubjectActionTokenReaderTest.php

diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php
index 13f4d5c7..576e15af 100644
--- a/.php-cs-fixer.php
+++ b/.php-cs-fixer.php
@@ -56,7 +56,6 @@ return $config->setRules([
         'encoding' => true,
         'ereg_to_preg' => true,
         'explicit_indirect_variable' => true,
-        'final_internal_class' => true,
         'fully_qualified_strict_types' => true,
         'function_declaration' => true,
         'function_to_constant' => true,
@@ -181,7 +180,6 @@ return $config->setRules([
         'php_unit_set_up_tear_down_visibility' => true,
         'php_unit_test_annotation' => true,
         'php_unit_test_case_static_method_calls' => true,
-        'php_unit_test_class_requires_covers' => true,
         'phpdoc_align' => [
             'align' => 'vertical',
         ],
diff --git a/composer.json b/composer.json
index 5c7377b7..e29f77ed 100644
--- a/composer.json
+++ b/composer.json
@@ -55,8 +55,7 @@
         "hyperf/grpc-client": "^3.1"
     },
     "require-dev": {
-        "mockery/mockery": "^1.6",
-        "phpunit/phpunit": "^10.0",
+        "phpunit/phpunit": "^11.0",
         "swoole/ide-helper": "^5.1"
     },
     "suggest": {
diff --git a/composer.lock b/composer.lock
index 001207d8..8eb66b1e 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "67a85cdfb2cba2f9447d2cd9278e2dd6",
+    "content-hash": "4ced37d39429f3fa0e8212c3f62018c9",
     "packages": [
         {
             "name": "amphp/amp",
@@ -957,16 +957,16 @@
         },
         {
             "name": "doctrine/dbal",
-            "version": "3.8.1",
+            "version": "3.8.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/dbal.git",
-                "reference": "c9ea252cdce4da324ede3d6c5913dd89f769afd2"
+                "reference": "a19a1d05ca211f41089dffcc387733a6875196cb"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/dbal/zipball/c9ea252cdce4da324ede3d6c5913dd89f769afd2",
-                "reference": "c9ea252cdce4da324ede3d6c5913dd89f769afd2",
+                "url": "https://api.github.com/repos/doctrine/dbal/zipball/a19a1d05ca211f41089dffcc387733a6875196cb",
+                "reference": "a19a1d05ca211f41089dffcc387733a6875196cb",
                 "shasum": ""
             },
             "require": {
@@ -1050,7 +1050,7 @@
             ],
             "support": {
                 "issues": "https://github.com/doctrine/dbal/issues",
-                "source": "https://github.com/doctrine/dbal/tree/3.8.1"
+                "source": "https://github.com/doctrine/dbal/tree/3.8.2"
             },
             "funding": [
                 {
@@ -1066,7 +1066,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-02-03T17:33:49+00:00"
+            "time": "2024-02-12T18:36:36+00:00"
         },
         {
             "name": "doctrine/deprecations",
@@ -2027,16 +2027,16 @@
         },
         {
             "name": "google/protobuf",
-            "version": "v3.25.2",
+            "version": "v3.25.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/protocolbuffers/protobuf-php.git",
-                "reference": "83ea4c147718666ce6a9b9332ac2aa588c9211eb"
+                "reference": "983a87f4f8798a90ca3a25b0f300b8fda38df643"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/83ea4c147718666ce6a9b9332ac2aa588c9211eb",
-                "reference": "83ea4c147718666ce6a9b9332ac2aa588c9211eb",
+                "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/983a87f4f8798a90ca3a25b0f300b8fda38df643",
+                "reference": "983a87f4f8798a90ca3a25b0f300b8fda38df643",
                 "shasum": ""
             },
             "require": {
@@ -2065,9 +2065,9 @@
                 "proto"
             ],
             "support": {
-                "source": "https://github.com/protocolbuffers/protobuf-php/tree/v3.25.2"
+                "source": "https://github.com/protocolbuffers/protobuf-php/tree/v3.25.3"
             },
-            "time": "2024-01-09T22:12:32+00:00"
+            "time": "2024-02-15T21:11:49+00:00"
         },
         {
             "name": "grpc/grpc",
@@ -8332,140 +8332,6 @@
         }
     ],
     "packages-dev": [
-        {
-            "name": "hamcrest/hamcrest-php",
-            "version": "v2.0.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/hamcrest/hamcrest-php.git",
-                "reference": "8c3d0a3f6af734494ad8f6fbbee0ba92422859f3"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/8c3d0a3f6af734494ad8f6fbbee0ba92422859f3",
-                "reference": "8c3d0a3f6af734494ad8f6fbbee0ba92422859f3",
-                "shasum": ""
-            },
-            "require": {
-                "php": "^5.3|^7.0|^8.0"
-            },
-            "replace": {
-                "cordoval/hamcrest-php": "*",
-                "davedevelopment/hamcrest-php": "*",
-                "kodova/hamcrest-php": "*"
-            },
-            "require-dev": {
-                "phpunit/php-file-iterator": "^1.4 || ^2.0",
-                "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "2.1-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "hamcrest"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "description": "This is the PHP port of Hamcrest Matchers",
-            "keywords": [
-                "test"
-            ],
-            "support": {
-                "issues": "https://github.com/hamcrest/hamcrest-php/issues",
-                "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.0.1"
-            },
-            "time": "2020-07-09T08:09:16+00:00"
-        },
-        {
-            "name": "mockery/mockery",
-            "version": "1.6.7",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/mockery/mockery.git",
-                "reference": "0cc058854b3195ba21dc6b1f7b1f60f4ef3a9c06"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/mockery/mockery/zipball/0cc058854b3195ba21dc6b1f7b1f60f4ef3a9c06",
-                "reference": "0cc058854b3195ba21dc6b1f7b1f60f4ef3a9c06",
-                "shasum": ""
-            },
-            "require": {
-                "hamcrest/hamcrest-php": "^2.0.1",
-                "lib-pcre": ">=7.0",
-                "php": ">=7.3"
-            },
-            "conflict": {
-                "phpunit/phpunit": "<8.0"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "^8.5 || ^9.6.10",
-                "symplify/easy-coding-standard": "^12.0.8"
-            },
-            "type": "library",
-            "autoload": {
-                "files": [
-                    "library/helpers.php",
-                    "library/Mockery.php"
-                ],
-                "psr-4": {
-                    "Mockery\\": "library/Mockery"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Pádraic Brady",
-                    "email": "padraic.brady@gmail.com",
-                    "homepage": "https://github.com/padraic",
-                    "role": "Author"
-                },
-                {
-                    "name": "Dave Marshall",
-                    "email": "dave.marshall@atstsolutions.co.uk",
-                    "homepage": "https://davedevelopment.co.uk",
-                    "role": "Developer"
-                },
-                {
-                    "name": "Nathanael Esayeas",
-                    "email": "nathanael.esayeas@protonmail.com",
-                    "homepage": "https://github.com/ghostwriter",
-                    "role": "Lead Developer"
-                }
-            ],
-            "description": "Mockery is a simple yet flexible PHP mock object framework",
-            "homepage": "https://github.com/mockery/mockery",
-            "keywords": [
-                "BDD",
-                "TDD",
-                "library",
-                "mock",
-                "mock objects",
-                "mockery",
-                "stub",
-                "test",
-                "test double",
-                "testing"
-            ],
-            "support": {
-                "docs": "https://docs.mockery.io/",
-                "issues": "https://github.com/mockery/mockery/issues",
-                "rss": "https://github.com/mockery/mockery/releases.atom",
-                "security": "https://github.com/mockery/mockery/security/advisories",
-                "source": "https://github.com/mockery/mockery"
-            },
-            "time": "2023-12-10T02:24:34+00:00"
-        },
         {
             "name": "myclabs/deep-copy",
             "version": "1.11.1",
@@ -8696,35 +8562,35 @@
         },
         {
             "name": "phpunit/php-code-coverage",
-            "version": "10.1.11",
+            "version": "11.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
-                "reference": "78c3b7625965c2513ee96569a4dbb62601784145"
+                "reference": "5e238e4b982cb272bf9faeee6f33af83d465d0e2"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/78c3b7625965c2513ee96569a4dbb62601784145",
-                "reference": "78c3b7625965c2513ee96569a4dbb62601784145",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/5e238e4b982cb272bf9faeee6f33af83d465d0e2",
+                "reference": "5e238e4b982cb272bf9faeee6f33af83d465d0e2",
                 "shasum": ""
             },
             "require": {
                 "ext-dom": "*",
                 "ext-libxml": "*",
                 "ext-xmlwriter": "*",
-                "nikic/php-parser": "^4.18 || ^5.0",
-                "php": ">=8.1",
-                "phpunit/php-file-iterator": "^4.0",
-                "phpunit/php-text-template": "^3.0",
-                "sebastian/code-unit-reverse-lookup": "^3.0",
-                "sebastian/complexity": "^3.0",
-                "sebastian/environment": "^6.0",
-                "sebastian/lines-of-code": "^2.0",
-                "sebastian/version": "^4.0",
+                "nikic/php-parser": "^5.0",
+                "php": ">=8.2",
+                "phpunit/php-file-iterator": "^5.0",
+                "phpunit/php-text-template": "^4.0",
+                "sebastian/code-unit-reverse-lookup": "^4.0",
+                "sebastian/complexity": "^4.0",
+                "sebastian/environment": "^7.0",
+                "sebastian/lines-of-code": "^3.0",
+                "sebastian/version": "^5.0",
                 "theseer/tokenizer": "^1.2.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "^10.1"
+                "phpunit/phpunit": "^11.0"
             },
             "suggest": {
                 "ext-pcov": "PHP extension that provides line coverage",
@@ -8733,7 +8599,7 @@
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "10.1-dev"
+                    "dev-main": "11.0-dev"
                 }
             },
             "autoload": {
@@ -8762,7 +8628,7 @@
             "support": {
                 "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
                 "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
-                "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.11"
+                "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.0"
             },
             "funding": [
                 {
@@ -8770,32 +8636,32 @@
                     "type": "github"
                 }
             ],
-            "time": "2023-12-21T15:38:30+00:00"
+            "time": "2024-02-02T06:03:46+00:00"
         },
         {
             "name": "phpunit/php-file-iterator",
-            "version": "4.1.0",
+            "version": "5.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
-                "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c"
+                "reference": "99e95c94ad9500daca992354fa09d7b99abe2210"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c",
-                "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/99e95c94ad9500daca992354fa09d7b99abe2210",
+                "reference": "99e95c94ad9500daca992354fa09d7b99abe2210",
                 "shasum": ""
             },
             "require": {
-                "php": ">=8.1"
+                "php": ">=8.2"
             },
             "require-dev": {
-                "phpunit/phpunit": "^10.0"
+                "phpunit/phpunit": "^11.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "4.0-dev"
+                    "dev-main": "5.0-dev"
                 }
             },
             "autoload": {
@@ -8823,7 +8689,7 @@
             "support": {
                 "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
                 "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy",
-                "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0"
+                "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.0.0"
             },
             "funding": [
                 {
@@ -8831,28 +8697,28 @@
                     "type": "github"
                 }
             ],
-            "time": "2023-08-31T06:24:48+00:00"
+            "time": "2024-02-02T06:05:04+00:00"
         },
         {
             "name": "phpunit/php-invoker",
-            "version": "4.0.0",
+            "version": "5.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-invoker.git",
-                "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7"
+                "reference": "5d8d9355a16d8cc5a1305b0a85342cfa420612be"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7",
-                "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5d8d9355a16d8cc5a1305b0a85342cfa420612be",
+                "reference": "5d8d9355a16d8cc5a1305b0a85342cfa420612be",
                 "shasum": ""
             },
             "require": {
-                "php": ">=8.1"
+                "php": ">=8.2"
             },
             "require-dev": {
                 "ext-pcntl": "*",
-                "phpunit/phpunit": "^10.0"
+                "phpunit/phpunit": "^11.0"
             },
             "suggest": {
                 "ext-pcntl": "*"
@@ -8860,7 +8726,7 @@
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "4.0-dev"
+                    "dev-main": "5.0-dev"
                 }
             },
             "autoload": {
@@ -8886,7 +8752,8 @@
             ],
             "support": {
                 "issues": "https://github.com/sebastianbergmann/php-invoker/issues",
-                "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0"
+                "security": "https://github.com/sebastianbergmann/php-invoker/security/policy",
+                "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.0"
             },
             "funding": [
                 {
@@ -8894,32 +8761,32 @@
                     "type": "github"
                 }
             ],
-            "time": "2023-02-03T06:56:09+00:00"
+            "time": "2024-02-02T06:05:50+00:00"
         },
         {
             "name": "phpunit/php-text-template",
-            "version": "3.0.1",
+            "version": "4.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-text-template.git",
-                "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748"
+                "reference": "d38f6cbff1cdb6f40b03c9811421561668cc133e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748",
-                "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/d38f6cbff1cdb6f40b03c9811421561668cc133e",
+                "reference": "d38f6cbff1cdb6f40b03c9811421561668cc133e",
                 "shasum": ""
             },
             "require": {
-                "php": ">=8.1"
+                "php": ">=8.2"
             },
             "require-dev": {
-                "phpunit/phpunit": "^10.0"
+                "phpunit/phpunit": "^11.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "3.0-dev"
+                    "dev-main": "4.0-dev"
                 }
             },
             "autoload": {
@@ -8946,7 +8813,7 @@
             "support": {
                 "issues": "https://github.com/sebastianbergmann/php-text-template/issues",
                 "security": "https://github.com/sebastianbergmann/php-text-template/security/policy",
-                "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1"
+                "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.0"
             },
             "funding": [
                 {
@@ -8954,32 +8821,32 @@
                     "type": "github"
                 }
             ],
-            "time": "2023-08-31T14:07:24+00:00"
+            "time": "2024-02-02T06:06:56+00:00"
         },
         {
             "name": "phpunit/php-timer",
-            "version": "6.0.0",
+            "version": "7.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-timer.git",
-                "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d"
+                "reference": "8a59d9e25720482ee7fcdf296595e08795b84dc5"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d",
-                "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/8a59d9e25720482ee7fcdf296595e08795b84dc5",
+                "reference": "8a59d9e25720482ee7fcdf296595e08795b84dc5",
                 "shasum": ""
             },
             "require": {
-                "php": ">=8.1"
+                "php": ">=8.2"
             },
             "require-dev": {
-                "phpunit/phpunit": "^10.0"
+                "phpunit/phpunit": "^11.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "6.0-dev"
+                    "dev-main": "7.0-dev"
                 }
             },
             "autoload": {
@@ -9005,7 +8872,8 @@
             ],
             "support": {
                 "issues": "https://github.com/sebastianbergmann/php-timer/issues",
-                "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0"
+                "security": "https://github.com/sebastianbergmann/php-timer/security/policy",
+                "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.0"
             },
             "funding": [
                 {
@@ -9013,20 +8881,20 @@
                     "type": "github"
                 }
             ],
-            "time": "2023-02-03T06:57:52+00:00"
+            "time": "2024-02-02T06:08:01+00:00"
         },
         {
             "name": "phpunit/phpunit",
-            "version": "10.5.10",
+            "version": "11.0.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "50b8e314b6d0dd06521dc31d1abffa73f25f850c"
+                "reference": "de24e7e7c67fbf437f7b6cd7bc919f2dc6fd89d4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/50b8e314b6d0dd06521dc31d1abffa73f25f850c",
-                "reference": "50b8e314b6d0dd06521dc31d1abffa73f25f850c",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/de24e7e7c67fbf437f7b6cd7bc919f2dc6fd89d4",
+                "reference": "de24e7e7c67fbf437f7b6cd7bc919f2dc6fd89d4",
                 "shasum": ""
             },
             "require": {
@@ -9039,23 +8907,22 @@
                 "myclabs/deep-copy": "^1.10.1",
                 "phar-io/manifest": "^2.0.3",
                 "phar-io/version": "^3.0.2",
-                "php": ">=8.1",
-                "phpunit/php-code-coverage": "^10.1.5",
-                "phpunit/php-file-iterator": "^4.0",
-                "phpunit/php-invoker": "^4.0",
-                "phpunit/php-text-template": "^3.0",
-                "phpunit/php-timer": "^6.0",
-                "sebastian/cli-parser": "^2.0",
-                "sebastian/code-unit": "^2.0",
-                "sebastian/comparator": "^5.0",
-                "sebastian/diff": "^5.0",
-                "sebastian/environment": "^6.0",
-                "sebastian/exporter": "^5.1",
-                "sebastian/global-state": "^6.0.1",
-                "sebastian/object-enumerator": "^5.0",
-                "sebastian/recursion-context": "^5.0",
-                "sebastian/type": "^4.0",
-                "sebastian/version": "^4.0"
+                "php": ">=8.2",
+                "phpunit/php-code-coverage": "^11.0",
+                "phpunit/php-file-iterator": "^5.0",
+                "phpunit/php-invoker": "^5.0",
+                "phpunit/php-text-template": "^4.0",
+                "phpunit/php-timer": "^7.0",
+                "sebastian/cli-parser": "^3.0",
+                "sebastian/code-unit": "^3.0",
+                "sebastian/comparator": "^6.0",
+                "sebastian/diff": "^6.0",
+                "sebastian/environment": "^7.0",
+                "sebastian/exporter": "^6.0",
+                "sebastian/global-state": "^7.0",
+                "sebastian/object-enumerator": "^6.0",
+                "sebastian/type": "^5.0",
+                "sebastian/version": "^5.0"
             },
             "suggest": {
                 "ext-soap": "To be able to generate mocks based on WSDL files"
@@ -9066,7 +8933,7 @@
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "10.5-dev"
+                    "dev-main": "11.0-dev"
                 }
             },
             "autoload": {
@@ -9098,7 +8965,7 @@
             "support": {
                 "issues": "https://github.com/sebastianbergmann/phpunit/issues",
                 "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
-                "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.10"
+                "source": "https://github.com/sebastianbergmann/phpunit/tree/11.0.3"
             },
             "funding": [
                 {
@@ -9114,32 +8981,32 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-02-04T09:07:51+00:00"
+            "time": "2024-02-10T06:31:16+00:00"
         },
         {
             "name": "sebastian/cli-parser",
-            "version": "2.0.0",
+            "version": "3.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/cli-parser.git",
-                "reference": "efdc130dbbbb8ef0b545a994fd811725c5282cae"
+                "reference": "efd6ce5bb8131fe981e2f879dbd47605fbe0cc6f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/efdc130dbbbb8ef0b545a994fd811725c5282cae",
-                "reference": "efdc130dbbbb8ef0b545a994fd811725c5282cae",
+                "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/efd6ce5bb8131fe981e2f879dbd47605fbe0cc6f",
+                "reference": "efd6ce5bb8131fe981e2f879dbd47605fbe0cc6f",
                 "shasum": ""
             },
             "require": {
-                "php": ">=8.1"
+                "php": ">=8.2"
             },
             "require-dev": {
-                "phpunit/phpunit": "^10.0"
+                "phpunit/phpunit": "^11.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "2.0-dev"
+                    "dev-main": "3.0-dev"
                 }
             },
             "autoload": {
@@ -9162,7 +9029,8 @@
             "homepage": "https://github.com/sebastianbergmann/cli-parser",
             "support": {
                 "issues": "https://github.com/sebastianbergmann/cli-parser/issues",
-                "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.0"
+                "security": "https://github.com/sebastianbergmann/cli-parser/security/policy",
+                "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.0"
             },
             "funding": [
                 {
@@ -9170,32 +9038,32 @@
                     "type": "github"
                 }
             ],
-            "time": "2023-02-03T06:58:15+00:00"
+            "time": "2024-02-02T05:48:04+00:00"
         },
         {
             "name": "sebastian/code-unit",
-            "version": "2.0.0",
+            "version": "3.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/code-unit.git",
-                "reference": "a81fee9eef0b7a76af11d121767abc44c104e503"
+                "reference": "6634549cb8d702282a04a774e36a7477d2bd9015"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503",
-                "reference": "a81fee9eef0b7a76af11d121767abc44c104e503",
+                "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/6634549cb8d702282a04a774e36a7477d2bd9015",
+                "reference": "6634549cb8d702282a04a774e36a7477d2bd9015",
                 "shasum": ""
             },
             "require": {
-                "php": ">=8.1"
+                "php": ">=8.2"
             },
             "require-dev": {
-                "phpunit/phpunit": "^10.0"
+                "phpunit/phpunit": "^11.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "2.0-dev"
+                    "dev-main": "3.0-dev"
                 }
             },
             "autoload": {
@@ -9218,7 +9086,8 @@
             "homepage": "https://github.com/sebastianbergmann/code-unit",
             "support": {
                 "issues": "https://github.com/sebastianbergmann/code-unit/issues",
-                "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0"
+                "security": "https://github.com/sebastianbergmann/code-unit/security/policy",
+                "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.0"
             },
             "funding": [
                 {
@@ -9226,32 +9095,32 @@
                     "type": "github"
                 }
             ],
-            "time": "2023-02-03T06:58:43+00:00"
+            "time": "2024-02-02T05:50:41+00:00"
         },
         {
             "name": "sebastian/code-unit-reverse-lookup",
-            "version": "3.0.0",
+            "version": "4.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
-                "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d"
+                "reference": "df80c875d3e459b45c6039e4d9b71d4fbccae25d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d",
-                "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d",
+                "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/df80c875d3e459b45c6039e4d9b71d4fbccae25d",
+                "reference": "df80c875d3e459b45c6039e4d9b71d4fbccae25d",
                 "shasum": ""
             },
             "require": {
-                "php": ">=8.1"
+                "php": ">=8.2"
             },
             "require-dev": {
-                "phpunit/phpunit": "^10.0"
+                "phpunit/phpunit": "^11.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "3.0-dev"
+                    "dev-main": "4.0-dev"
                 }
             },
             "autoload": {
@@ -9273,7 +9142,8 @@
             "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
             "support": {
                 "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
-                "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0.0"
+                "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy",
+                "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.0"
             },
             "funding": [
                 {
@@ -9281,36 +9151,36 @@
                     "type": "github"
                 }
             ],
-            "time": "2023-02-03T06:59:15+00:00"
+            "time": "2024-02-02T05:52:17+00:00"
         },
         {
             "name": "sebastian/comparator",
-            "version": "5.0.1",
+            "version": "6.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/comparator.git",
-                "reference": "2db5010a484d53ebf536087a70b4a5423c102372"
+                "reference": "bd0f2fa5b9257c69903537b266ccb80fcf940db8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2db5010a484d53ebf536087a70b4a5423c102372",
-                "reference": "2db5010a484d53ebf536087a70b4a5423c102372",
+                "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/bd0f2fa5b9257c69903537b266ccb80fcf940db8",
+                "reference": "bd0f2fa5b9257c69903537b266ccb80fcf940db8",
                 "shasum": ""
             },
             "require": {
                 "ext-dom": "*",
                 "ext-mbstring": "*",
-                "php": ">=8.1",
-                "sebastian/diff": "^5.0",
-                "sebastian/exporter": "^5.0"
+                "php": ">=8.2",
+                "sebastian/diff": "^6.0",
+                "sebastian/exporter": "^6.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "^10.3"
+                "phpunit/phpunit": "^11.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "5.0-dev"
+                    "dev-main": "6.0-dev"
                 }
             },
             "autoload": {
@@ -9350,7 +9220,7 @@
             "support": {
                 "issues": "https://github.com/sebastianbergmann/comparator/issues",
                 "security": "https://github.com/sebastianbergmann/comparator/security/policy",
-                "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.1"
+                "source": "https://github.com/sebastianbergmann/comparator/tree/6.0.0"
             },
             "funding": [
                 {
@@ -9358,33 +9228,33 @@
                     "type": "github"
                 }
             ],
-            "time": "2023-08-14T13:18:12+00:00"
+            "time": "2024-02-02T05:53:45+00:00"
         },
         {
             "name": "sebastian/complexity",
-            "version": "3.2.0",
+            "version": "4.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/complexity.git",
-                "reference": "68ff824baeae169ec9f2137158ee529584553799"
+                "reference": "88a434ad86150e11a606ac4866b09130712671f0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799",
-                "reference": "68ff824baeae169ec9f2137158ee529584553799",
+                "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/88a434ad86150e11a606ac4866b09130712671f0",
+                "reference": "88a434ad86150e11a606ac4866b09130712671f0",
                 "shasum": ""
             },
             "require": {
-                "nikic/php-parser": "^4.18 || ^5.0",
-                "php": ">=8.1"
+                "nikic/php-parser": "^5.0",
+                "php": ">=8.2"
             },
             "require-dev": {
-                "phpunit/phpunit": "^10.0"
+                "phpunit/phpunit": "^11.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "3.2-dev"
+                    "dev-main": "4.0-dev"
                 }
             },
             "autoload": {
@@ -9408,7 +9278,7 @@
             "support": {
                 "issues": "https://github.com/sebastianbergmann/complexity/issues",
                 "security": "https://github.com/sebastianbergmann/complexity/security/policy",
-                "source": "https://github.com/sebastianbergmann/complexity/tree/3.2.0"
+                "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.0"
             },
             "funding": [
                 {
@@ -9416,33 +9286,33 @@
                     "type": "github"
                 }
             ],
-            "time": "2023-12-21T08:37:17+00:00"
+            "time": "2024-02-02T05:55:19+00:00"
         },
         {
             "name": "sebastian/diff",
-            "version": "5.1.0",
+            "version": "6.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/diff.git",
-                "reference": "fbf413a49e54f6b9b17e12d900ac7f6101591b7f"
+                "reference": "3e3f502419518897a923aa1c64d51f9def2e0aff"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/fbf413a49e54f6b9b17e12d900ac7f6101591b7f",
-                "reference": "fbf413a49e54f6b9b17e12d900ac7f6101591b7f",
+                "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3e3f502419518897a923aa1c64d51f9def2e0aff",
+                "reference": "3e3f502419518897a923aa1c64d51f9def2e0aff",
                 "shasum": ""
             },
             "require": {
-                "php": ">=8.1"
+                "php": ">=8.2"
             },
             "require-dev": {
-                "phpunit/phpunit": "^10.0",
+                "phpunit/phpunit": "^11.0",
                 "symfony/process": "^4.2 || ^5"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "5.1-dev"
+                    "dev-main": "6.0-dev"
                 }
             },
             "autoload": {
@@ -9475,7 +9345,7 @@
             "support": {
                 "issues": "https://github.com/sebastianbergmann/diff/issues",
                 "security": "https://github.com/sebastianbergmann/diff/security/policy",
-                "source": "https://github.com/sebastianbergmann/diff/tree/5.1.0"
+                "source": "https://github.com/sebastianbergmann/diff/tree/6.0.0"
             },
             "funding": [
                 {
@@ -9483,27 +9353,27 @@
                     "type": "github"
                 }
             ],
-            "time": "2023-12-22T10:55:06+00:00"
+            "time": "2024-02-02T05:56:35+00:00"
         },
         {
             "name": "sebastian/environment",
-            "version": "6.0.1",
+            "version": "7.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/environment.git",
-                "reference": "43c751b41d74f96cbbd4e07b7aec9675651e2951"
+                "reference": "100d8b855d7180f79f9a9a5c483f2d960581c3ea"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/43c751b41d74f96cbbd4e07b7aec9675651e2951",
-                "reference": "43c751b41d74f96cbbd4e07b7aec9675651e2951",
+                "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/100d8b855d7180f79f9a9a5c483f2d960581c3ea",
+                "reference": "100d8b855d7180f79f9a9a5c483f2d960581c3ea",
                 "shasum": ""
             },
             "require": {
-                "php": ">=8.1"
+                "php": ">=8.2"
             },
             "require-dev": {
-                "phpunit/phpunit": "^10.0"
+                "phpunit/phpunit": "^11.0"
             },
             "suggest": {
                 "ext-posix": "*"
@@ -9511,7 +9381,7 @@
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "6.0-dev"
+                    "dev-main": "7.0-dev"
                 }
             },
             "autoload": {
@@ -9539,7 +9409,7 @@
             "support": {
                 "issues": "https://github.com/sebastianbergmann/environment/issues",
                 "security": "https://github.com/sebastianbergmann/environment/security/policy",
-                "source": "https://github.com/sebastianbergmann/environment/tree/6.0.1"
+                "source": "https://github.com/sebastianbergmann/environment/tree/7.0.0"
             },
             "funding": [
                 {
@@ -9547,34 +9417,34 @@
                     "type": "github"
                 }
             ],
-            "time": "2023-04-11T05:39:26+00:00"
+            "time": "2024-02-02T05:57:54+00:00"
         },
         {
             "name": "sebastian/exporter",
-            "version": "5.1.1",
+            "version": "6.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/exporter.git",
-                "reference": "64f51654862e0f5e318db7e9dcc2292c63cdbddc"
+                "reference": "d0c0a93fc746b0c066037f1e7d09104129e868ff"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/64f51654862e0f5e318db7e9dcc2292c63cdbddc",
-                "reference": "64f51654862e0f5e318db7e9dcc2292c63cdbddc",
+                "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/d0c0a93fc746b0c066037f1e7d09104129e868ff",
+                "reference": "d0c0a93fc746b0c066037f1e7d09104129e868ff",
                 "shasum": ""
             },
             "require": {
                 "ext-mbstring": "*",
-                "php": ">=8.1",
-                "sebastian/recursion-context": "^5.0"
+                "php": ">=8.2",
+                "sebastian/recursion-context": "^6.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "^10.0"
+                "phpunit/phpunit": "^11.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "5.1-dev"
+                    "dev-main": "6.0-dev"
                 }
             },
             "autoload": {
@@ -9617,7 +9487,7 @@
             "support": {
                 "issues": "https://github.com/sebastianbergmann/exporter/issues",
                 "security": "https://github.com/sebastianbergmann/exporter/security/policy",
-                "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.1"
+                "source": "https://github.com/sebastianbergmann/exporter/tree/6.0.0"
             },
             "funding": [
                 {
@@ -9625,35 +9495,35 @@
                     "type": "github"
                 }
             ],
-            "time": "2023-09-24T13:22:09+00:00"
+            "time": "2024-02-02T05:58:52+00:00"
         },
         {
             "name": "sebastian/global-state",
-            "version": "6.0.1",
+            "version": "7.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/global-state.git",
-                "reference": "7ea9ead78f6d380d2a667864c132c2f7b83055e4"
+                "reference": "590e7cbc6565fa2e26c3df4e629a34bb0bc00c17"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/7ea9ead78f6d380d2a667864c132c2f7b83055e4",
-                "reference": "7ea9ead78f6d380d2a667864c132c2f7b83055e4",
+                "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/590e7cbc6565fa2e26c3df4e629a34bb0bc00c17",
+                "reference": "590e7cbc6565fa2e26c3df4e629a34bb0bc00c17",
                 "shasum": ""
             },
             "require": {
-                "php": ">=8.1",
-                "sebastian/object-reflector": "^3.0",
-                "sebastian/recursion-context": "^5.0"
+                "php": ">=8.2",
+                "sebastian/object-reflector": "^4.0",
+                "sebastian/recursion-context": "^6.0"
             },
             "require-dev": {
                 "ext-dom": "*",
-                "phpunit/phpunit": "^10.0"
+                "phpunit/phpunit": "^11.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "6.0-dev"
+                    "dev-main": "7.0-dev"
                 }
             },
             "autoload": {
@@ -9672,14 +9542,14 @@
                 }
             ],
             "description": "Snapshotting of global state",
-            "homepage": "http://www.github.com/sebastianbergmann/global-state",
+            "homepage": "https://www.github.com/sebastianbergmann/global-state",
             "keywords": [
                 "global state"
             ],
             "support": {
                 "issues": "https://github.com/sebastianbergmann/global-state/issues",
                 "security": "https://github.com/sebastianbergmann/global-state/security/policy",
-                "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.1"
+                "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.0"
             },
             "funding": [
                 {
@@ -9687,33 +9557,33 @@
                     "type": "github"
                 }
             ],
-            "time": "2023-07-19T07:19:23+00:00"
+            "time": "2024-02-02T05:59:33+00:00"
         },
         {
             "name": "sebastian/lines-of-code",
-            "version": "2.0.2",
+            "version": "3.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/lines-of-code.git",
-                "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0"
+                "reference": "376c5b3f6b43c78fdc049740bca76a7c846706c0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0",
-                "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0",
+                "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/376c5b3f6b43c78fdc049740bca76a7c846706c0",
+                "reference": "376c5b3f6b43c78fdc049740bca76a7c846706c0",
                 "shasum": ""
             },
             "require": {
-                "nikic/php-parser": "^4.18 || ^5.0",
-                "php": ">=8.1"
+                "nikic/php-parser": "^5.0",
+                "php": ">=8.2"
             },
             "require-dev": {
-                "phpunit/phpunit": "^10.0"
+                "phpunit/phpunit": "^11.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "2.0-dev"
+                    "dev-main": "3.0-dev"
                 }
             },
             "autoload": {
@@ -9737,7 +9607,7 @@
             "support": {
                 "issues": "https://github.com/sebastianbergmann/lines-of-code/issues",
                 "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy",
-                "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2"
+                "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.0"
             },
             "funding": [
                 {
@@ -9745,34 +9615,34 @@
                     "type": "github"
                 }
             ],
-            "time": "2023-12-21T08:38:20+00:00"
+            "time": "2024-02-02T06:00:36+00:00"
         },
         {
             "name": "sebastian/object-enumerator",
-            "version": "5.0.0",
+            "version": "6.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/object-enumerator.git",
-                "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906"
+                "reference": "f75f6c460da0bbd9668f43a3dde0ec0ba7faa678"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906",
-                "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906",
+                "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f75f6c460da0bbd9668f43a3dde0ec0ba7faa678",
+                "reference": "f75f6c460da0bbd9668f43a3dde0ec0ba7faa678",
                 "shasum": ""
             },
             "require": {
-                "php": ">=8.1",
-                "sebastian/object-reflector": "^3.0",
-                "sebastian/recursion-context": "^5.0"
+                "php": ">=8.2",
+                "sebastian/object-reflector": "^4.0",
+                "sebastian/recursion-context": "^6.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "^10.0"
+                "phpunit/phpunit": "^11.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "5.0-dev"
+                    "dev-main": "6.0-dev"
                 }
             },
             "autoload": {
@@ -9794,7 +9664,8 @@
             "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
             "support": {
                 "issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
-                "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0"
+                "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy",
+                "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.0"
             },
             "funding": [
                 {
@@ -9802,32 +9673,32 @@
                     "type": "github"
                 }
             ],
-            "time": "2023-02-03T07:08:32+00:00"
+            "time": "2024-02-02T06:01:29+00:00"
         },
         {
             "name": "sebastian/object-reflector",
-            "version": "3.0.0",
+            "version": "4.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/object-reflector.git",
-                "reference": "24ed13d98130f0e7122df55d06c5c4942a577957"
+                "reference": "bb2a6255d30853425fd38f032eb64ced9f7f132d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957",
-                "reference": "24ed13d98130f0e7122df55d06c5c4942a577957",
+                "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/bb2a6255d30853425fd38f032eb64ced9f7f132d",
+                "reference": "bb2a6255d30853425fd38f032eb64ced9f7f132d",
                 "shasum": ""
             },
             "require": {
-                "php": ">=8.1"
+                "php": ">=8.2"
             },
             "require-dev": {
-                "phpunit/phpunit": "^10.0"
+                "phpunit/phpunit": "^11.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "3.0-dev"
+                    "dev-main": "4.0-dev"
                 }
             },
             "autoload": {
@@ -9849,7 +9720,8 @@
             "homepage": "https://github.com/sebastianbergmann/object-reflector/",
             "support": {
                 "issues": "https://github.com/sebastianbergmann/object-reflector/issues",
-                "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0"
+                "security": "https://github.com/sebastianbergmann/object-reflector/security/policy",
+                "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.0"
             },
             "funding": [
                 {
@@ -9857,32 +9729,32 @@
                     "type": "github"
                 }
             ],
-            "time": "2023-02-03T07:06:18+00:00"
+            "time": "2024-02-02T06:02:18+00:00"
         },
         {
             "name": "sebastian/recursion-context",
-            "version": "5.0.0",
+            "version": "6.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/recursion-context.git",
-                "reference": "05909fb5bc7df4c52992396d0116aed689f93712"
+                "reference": "b75224967b5a466925c6d54e68edd0edf8dd4ed4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/05909fb5bc7df4c52992396d0116aed689f93712",
-                "reference": "05909fb5bc7df4c52992396d0116aed689f93712",
+                "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/b75224967b5a466925c6d54e68edd0edf8dd4ed4",
+                "reference": "b75224967b5a466925c6d54e68edd0edf8dd4ed4",
                 "shasum": ""
             },
             "require": {
-                "php": ">=8.1"
+                "php": ">=8.2"
             },
             "require-dev": {
-                "phpunit/phpunit": "^10.0"
+                "phpunit/phpunit": "^11.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "5.0-dev"
+                    "dev-main": "6.0-dev"
                 }
             },
             "autoload": {
@@ -9912,7 +9784,8 @@
             "homepage": "https://github.com/sebastianbergmann/recursion-context",
             "support": {
                 "issues": "https://github.com/sebastianbergmann/recursion-context/issues",
-                "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.0"
+                "security": "https://github.com/sebastianbergmann/recursion-context/security/policy",
+                "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.0"
             },
             "funding": [
                 {
@@ -9920,32 +9793,32 @@
                     "type": "github"
                 }
             ],
-            "time": "2023-02-03T07:05:40+00:00"
+            "time": "2024-02-02T06:08:48+00:00"
         },
         {
             "name": "sebastian/type",
-            "version": "4.0.0",
+            "version": "5.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/type.git",
-                "reference": "462699a16464c3944eefc02ebdd77882bd3925bf"
+                "reference": "b8502785eb3523ca0dd4afe9ca62235590020f3f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf",
-                "reference": "462699a16464c3944eefc02ebdd77882bd3925bf",
+                "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b8502785eb3523ca0dd4afe9ca62235590020f3f",
+                "reference": "b8502785eb3523ca0dd4afe9ca62235590020f3f",
                 "shasum": ""
             },
             "require": {
-                "php": ">=8.1"
+                "php": ">=8.2"
             },
             "require-dev": {
-                "phpunit/phpunit": "^10.0"
+                "phpunit/phpunit": "^11.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "4.0-dev"
+                    "dev-main": "5.0-dev"
                 }
             },
             "autoload": {
@@ -9968,7 +9841,8 @@
             "homepage": "https://github.com/sebastianbergmann/type",
             "support": {
                 "issues": "https://github.com/sebastianbergmann/type/issues",
-                "source": "https://github.com/sebastianbergmann/type/tree/4.0.0"
+                "security": "https://github.com/sebastianbergmann/type/security/policy",
+                "source": "https://github.com/sebastianbergmann/type/tree/5.0.0"
             },
             "funding": [
                 {
@@ -9976,29 +9850,29 @@
                     "type": "github"
                 }
             ],
-            "time": "2023-02-03T07:10:45+00:00"
+            "time": "2024-02-02T06:09:34+00:00"
         },
         {
             "name": "sebastian/version",
-            "version": "4.0.1",
+            "version": "5.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/version.git",
-                "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17"
+                "reference": "13999475d2cb1ab33cb73403ba356a814fdbb001"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17",
-                "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17",
+                "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/13999475d2cb1ab33cb73403ba356a814fdbb001",
+                "reference": "13999475d2cb1ab33cb73403ba356a814fdbb001",
                 "shasum": ""
             },
             "require": {
-                "php": ">=8.1"
+                "php": ">=8.2"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "4.0-dev"
+                    "dev-main": "5.0-dev"
                 }
             },
             "autoload": {
@@ -10021,7 +9895,8 @@
             "homepage": "https://github.com/sebastianbergmann/version",
             "support": {
                 "issues": "https://github.com/sebastianbergmann/version/issues",
-                "source": "https://github.com/sebastianbergmann/version/tree/4.0.1"
+                "security": "https://github.com/sebastianbergmann/version/security/policy",
+                "source": "https://github.com/sebastianbergmann/version/tree/5.0.0"
             },
             "funding": [
                 {
@@ -10029,7 +9904,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2023-02-07T11:34:05+00:00"
+            "time": "2024-02-02T06:10:47+00:00"
         },
         {
             "name": "swoole/ide-helper",
diff --git a/src/AcceptHeaderTest.php b/src/AcceptHeaderTest.php
index e58b29eb..58c76d1f 100644
--- a/src/AcceptHeaderTest.php
+++ b/src/AcceptHeaderTest.php
@@ -4,13 +4,13 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance;
 
+use PHPUnit\Framework\Attributes\CoversClass;
 use PHPUnit\Framework\TestCase;
 
 /**
- * @coversNothing
- *
  * @internal
  */
+#[CoversClass(AcceptHeader::class)]
 final class AcceptHeaderTest extends TestCase
 {
     public function test_request_header_is_parsed(): void
diff --git a/src/ArrayFlattenIteratorTest.php b/src/ArrayFlattenIteratorTest.php
index 88429963..f67dd7ac 100644
--- a/src/ArrayFlattenIteratorTest.php
+++ b/src/ArrayFlattenIteratorTest.php
@@ -4,13 +4,13 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance;
 
+use PHPUnit\Framework\Attributes\CoversClass;
 use PHPUnit\Framework\TestCase;
 
 /**
- * @coversNothing
- *
  * @internal
  */
+#[CoversClass(ArrayFlattenIterator::class)]
 final class ArrayFlattenIteratorTest extends TestCase
 {
     public function test_dotpaths_are_generated(): void
diff --git a/src/Attribute/RespondsToPromptSubject.php b/src/Attribute/RespondsToPromptSubject.php
new file mode 100644
index 00000000..9bb1fe31
--- /dev/null
+++ b/src/Attribute/RespondsToPromptSubject.php
@@ -0,0 +1,21 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance\Attribute;
+
+use Attribute;
+use Distantmagic\Resonance\Attribute as BaseAttribute;
+
+#[Attribute(Attribute::TARGET_CLASS)]
+final readonly class RespondsToPromptSubject extends BaseAttribute
+{
+    /**
+     * @param non-empty-string $action
+     * @param non-empty-string $subject
+     */
+    public function __construct(
+        public string $action,
+        public string $subject,
+    ) {}
+}
diff --git a/src/BackusNaurFormGrammar.php b/src/BackusNaurFormGrammar.php
new file mode 100644
index 00000000..554403a3
--- /dev/null
+++ b/src/BackusNaurFormGrammar.php
@@ -0,0 +1,17 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+use JsonSerializable;
+
+abstract readonly class BackusNaurFormGrammar implements JsonSerializable
+{
+    abstract public function getGrammarContent(): string;
+
+    public function jsonSerialize(): mixed
+    {
+        return $this->getGrammarContent();
+    }
+}
diff --git a/src/BackusNaurFormGrammar/SubjectActionGrammar.php b/src/BackusNaurFormGrammar/SubjectActionGrammar.php
new file mode 100644
index 00000000..414fe3af
--- /dev/null
+++ b/src/BackusNaurFormGrammar/SubjectActionGrammar.php
@@ -0,0 +1,57 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance\BackusNaurFormGrammar;
+
+use Distantmagic\Resonance\Attribute\Singleton;
+use Distantmagic\Resonance\BackusNaurFormGrammar;
+use Distantmagic\Resonance\PromptSubjectResponderCollection;
+
+#[Singleton]
+readonly class SubjectActionGrammar extends BackusNaurFormGrammar
+{
+    /**
+     * @var non-empty-string
+     */
+    private string $grammar;
+
+    public function __construct(
+        PromptSubjectResponderCollection $promptSubjectResponderCollection,
+    ) {
+        /**
+         * @var array<non-empty-string> $subjects
+         */
+        $subjects = ['("unknown" " " "unknown")'];
+
+        foreach ($promptSubjectResponderCollection->getPromptableActions() as $subject => $actions) {
+            $subjects[] = sprintf(
+                '("%s" " " ("%s"))',
+                $subject,
+                implode('" | "', $actions->toArray())
+            );
+        }
+
+        $subjectsSerialized = sprintf('%s', implode(' | ', $subjects));
+
+        $stringGrammar = <<<'STRING_GRAMMAR'
+        "\"" (
+            [^"\\] |
+             "\\" (["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F])
+        )* "\""
+        STRING_GRAMMAR;
+
+        $this->grammar = <<<GRAMMAR
+        root ::= ({$subjectsSerialized}) " " parameters
+
+        parameters ::= string
+
+        string ::= {$stringGrammar}
+        GRAMMAR;
+    }
+
+    public function getGrammarContent(): string
+    {
+        return $this->grammar;
+    }
+}
diff --git a/src/Constraint/AnyConstraintTest.php b/src/Constraint/AnyConstraintTest.php
index aad39b4e..3db7380a 100644
--- a/src/Constraint/AnyConstraintTest.php
+++ b/src/Constraint/AnyConstraintTest.php
@@ -4,14 +4,14 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance\Constraint;
 
+use PHPUnit\Framework\Attributes\CoversClass;
 use PHPUnit\Framework\TestCase;
 use stdClass;
 
 /**
- * @coversNothing
- *
  * @internal
  */
+#[CoversClass(AnyConstraint::class)]
 final class AnyConstraintTest extends TestCase
 {
     public function test_is_converted_optionally_to_json_schema(): void
diff --git a/src/Constraint/AnyOfConstraintTest.php b/src/Constraint/AnyOfConstraintTest.php
index 3e08d56e..c5a6e39a 100644
--- a/src/Constraint/AnyOfConstraintTest.php
+++ b/src/Constraint/AnyOfConstraintTest.php
@@ -4,13 +4,13 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance\Constraint;
 
+use PHPUnit\Framework\Attributes\CoversClass;
 use PHPUnit\Framework\TestCase;
 
 /**
- * @coversNothing
- *
  * @internal
  */
+#[CoversClass(AnyOfConstraint::class)]
 final class AnyOfConstraintTest extends TestCase
 {
     public function test_is_converted_optionally_to_json_schema(): void
diff --git a/src/Constraint/BooleanConstraintTest.php b/src/Constraint/BooleanConstraintTest.php
index ff422df1..56b635bc 100644
--- a/src/Constraint/BooleanConstraintTest.php
+++ b/src/Constraint/BooleanConstraintTest.php
@@ -4,13 +4,13 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance\Constraint;
 
+use PHPUnit\Framework\Attributes\CoversClass;
 use PHPUnit\Framework\TestCase;
 
 /**
- * @coversNothing
- *
  * @internal
  */
+#[CoversClass(BooleanConstraint::class)]
 final class BooleanConstraintTest extends TestCase
 {
     public function test_is_converted_optionally_to_json_schema(): void
diff --git a/src/Constraint/ConstConstraintTest.php b/src/Constraint/ConstConstraintTest.php
index 658bcb8c..ac815ac2 100644
--- a/src/Constraint/ConstConstraintTest.php
+++ b/src/Constraint/ConstConstraintTest.php
@@ -4,13 +4,13 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance\Constraint;
 
+use PHPUnit\Framework\Attributes\CoversClass;
 use PHPUnit\Framework\TestCase;
 
 /**
- * @coversNothing
- *
  * @internal
  */
+#[CoversClass(ConstConstraint::class)]
 final class ConstConstraintTest extends TestCase
 {
     public function test_is_converted_to_json_schema(): void
diff --git a/src/Constraint/EnumConstraintTest.php b/src/Constraint/EnumConstraintTest.php
index 7d6a20c1..45ae2155 100644
--- a/src/Constraint/EnumConstraintTest.php
+++ b/src/Constraint/EnumConstraintTest.php
@@ -4,13 +4,13 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance\Constraint;
 
+use PHPUnit\Framework\Attributes\CoversClass;
 use PHPUnit\Framework\TestCase;
 
 /**
- * @coversNothing
- *
  * @internal
  */
+#[CoversClass(EnumConstraint::class)]
 final class EnumConstraintTest extends TestCase
 {
     public function test_is_converted_optionally_to_json_schema(): void
diff --git a/src/Constraint/FilenameConstraintTest.php b/src/Constraint/FilenameConstraintTest.php
index f168c714..485805fe 100644
--- a/src/Constraint/FilenameConstraintTest.php
+++ b/src/Constraint/FilenameConstraintTest.php
@@ -4,13 +4,13 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance\Constraint;
 
+use PHPUnit\Framework\Attributes\CoversClass;
 use PHPUnit\Framework\TestCase;
 
 /**
- * @coversNothing
- *
  * @internal
  */
+#[CoversClass(FilenameConstraint::class)]
 final class FilenameConstraintTest extends TestCase
 {
     public function test_invalid(): void
diff --git a/src/Constraint/IntegerConstraintTest.php b/src/Constraint/IntegerConstraintTest.php
index 1fb70bf4..d9043719 100644
--- a/src/Constraint/IntegerConstraintTest.php
+++ b/src/Constraint/IntegerConstraintTest.php
@@ -4,13 +4,13 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance\Constraint;
 
+use PHPUnit\Framework\Attributes\CoversClass;
 use PHPUnit\Framework\TestCase;
 
 /**
- * @coversNothing
- *
  * @internal
  */
+#[CoversClass(IntegerConstraint::class)]
 final class IntegerConstraintTest extends TestCase
 {
     public function test_is_converted_optionally_to_json_schema(): void
diff --git a/src/Constraint/ListConstraintTest.php b/src/Constraint/ListConstraintTest.php
index fa81178b..76fc28c6 100644
--- a/src/Constraint/ListConstraintTest.php
+++ b/src/Constraint/ListConstraintTest.php
@@ -4,13 +4,13 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance\Constraint;
 
+use PHPUnit\Framework\Attributes\CoversClass;
 use PHPUnit\Framework\TestCase;
 
 /**
- * @coversNothing
- *
  * @internal
  */
+#[CoversClass(ListConstraint::class)]
 final class ListConstraintTest extends TestCase
 {
     public function test_is_converted_optionally_to_json_schema(): void
diff --git a/src/Constraint/MapConstraintTest.php b/src/Constraint/MapConstraintTest.php
index c63e5744..3cb0a7da 100644
--- a/src/Constraint/MapConstraintTest.php
+++ b/src/Constraint/MapConstraintTest.php
@@ -4,13 +4,13 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance\Constraint;
 
+use PHPUnit\Framework\Attributes\CoversClass;
 use PHPUnit\Framework\TestCase;
 
 /**
- * @coversNothing
- *
  * @internal
  */
+#[CoversClass(MapConstraint::class)]
 final class MapConstraintTest extends TestCase
 {
     public function test_is_converted_optionally_to_json_schema(): void
diff --git a/src/Constraint/NumberConstraintTest.php b/src/Constraint/NumberConstraintTest.php
index 981bd62b..b804a1c4 100644
--- a/src/Constraint/NumberConstraintTest.php
+++ b/src/Constraint/NumberConstraintTest.php
@@ -4,13 +4,13 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance\Constraint;
 
+use PHPUnit\Framework\Attributes\CoversClass;
 use PHPUnit\Framework\TestCase;
 
 /**
- * @coversNothing
- *
  * @internal
  */
+#[CoversClass(NumberConstraint::class)]
 final class NumberConstraintTest extends TestCase
 {
     public function test_is_converted_optionally_to_json_schema(): void
diff --git a/src/Constraint/ObjectConstraintTest.php b/src/Constraint/ObjectConstraintTest.php
index bcf082ed..23275470 100644
--- a/src/Constraint/ObjectConstraintTest.php
+++ b/src/Constraint/ObjectConstraintTest.php
@@ -4,13 +4,13 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance\Constraint;
 
+use PHPUnit\Framework\Attributes\CoversClass;
 use PHPUnit\Framework\TestCase;
 
 /**
- * @coversNothing
- *
  * @internal
  */
+#[CoversClass(ObjectConstraint::class)]
 final class ObjectConstraintTest extends TestCase
 {
     public function test_is_converted_optionally_to_json_schema(): void
diff --git a/src/Constraint/StringConstraintTest.php b/src/Constraint/StringConstraintTest.php
index d5094e14..24fe427f 100644
--- a/src/Constraint/StringConstraintTest.php
+++ b/src/Constraint/StringConstraintTest.php
@@ -5,13 +5,13 @@ declare(strict_types=1);
 namespace Distantmagic\Resonance\Constraint;
 
 use Distantmagic\Resonance\ConstraintStringFormat;
+use PHPUnit\Framework\Attributes\CoversClass;
 use PHPUnit\Framework\TestCase;
 
 /**
- * @coversNothing
- *
  * @internal
  */
+#[CoversClass(StringConstraint::class)]
 final class StringConstraintTest extends TestCase
 {
     public function test_is_converted_optionally_to_json_schema(): void
diff --git a/src/Constraint/TupleConstraintTest.php b/src/Constraint/TupleConstraintTest.php
index 9949c265..07a87ac0 100644
--- a/src/Constraint/TupleConstraintTest.php
+++ b/src/Constraint/TupleConstraintTest.php
@@ -4,13 +4,13 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance\Constraint;
 
+use PHPUnit\Framework\Attributes\CoversClass;
 use PHPUnit\Framework\TestCase;
 
 /**
- * @coversNothing
- *
  * @internal
  */
+#[CoversClass(TupleConstraint::class)]
 final class TupleConstraintTest extends TestCase
 {
     public function test_is_converted_optionally_to_json_schema(): void
diff --git a/src/LlamaCppCompletionRequest.php b/src/LlamaCppCompletionRequest.php
index 3083efc4..7422e3e4 100644
--- a/src/LlamaCppCompletionRequest.php
+++ b/src/LlamaCppCompletionRequest.php
@@ -10,13 +10,26 @@ readonly class LlamaCppCompletionRequest implements JsonSerializable
 {
     public function __construct(
         public LlmPromptTemplate $promptTemplate,
+        public ?BackusNaurFormGrammar $backusNaurFormGrammar = null,
+        public ?LlmSystemPrompt $llmSystemPrompt = null,
     ) {}
 
     public function jsonSerialize(): array
     {
-        return [
+        $parameters = [
+            'n_predict' => 400,
             'prompt' => $this->promptTemplate,
             'stream' => true,
         ];
+
+        if ($this->backusNaurFormGrammar) {
+            $parameters['grammar'] = $this->backusNaurFormGrammar;
+        }
+
+        if ($this->llmSystemPrompt) {
+            $parameters['system_prompt'] = $this->llmSystemPrompt;
+        }
+
+        return $parameters;
     }
 }
diff --git a/src/LlmSystemPrompt.php b/src/LlmSystemPrompt.php
new file mode 100644
index 00000000..70dd9289
--- /dev/null
+++ b/src/LlmSystemPrompt.php
@@ -0,0 +1,19 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+use JsonSerializable;
+
+abstract readonly class LlmSystemPrompt implements JsonSerializable
+{
+    abstract public function getPromptContent(): string;
+
+    public function jsonSerialize(): array
+    {
+        return [
+            'prompt' => $this->getPromptContent(),
+        ];
+    }
+}
diff --git a/src/LlmSystemPrompt/SubjectActionSystemPrompt.php b/src/LlmSystemPrompt/SubjectActionSystemPrompt.php
new file mode 100644
index 00000000..ea72ad21
--- /dev/null
+++ b/src/LlmSystemPrompt/SubjectActionSystemPrompt.php
@@ -0,0 +1,86 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance\LlmSystemPrompt;
+
+use Distantmagic\Resonance\Attribute\Singleton;
+use Distantmagic\Resonance\LlmSystemPrompt;
+use Distantmagic\Resonance\PromptSubjectResponderCollection;
+use Ds\Set;
+
+#[Singleton]
+readonly class SubjectActionSystemPrompt extends LlmSystemPrompt
+{
+    /**
+     * @var non-empty-string $prompt
+     */
+    private string $prompt;
+
+    public function __construct(
+        PromptSubjectResponderCollection $promptSubjectResponderCollection,
+    ) {
+        /**
+         * @var Set<non-empty-string>
+         */
+        $allActions = new Set();
+
+        /**
+         * @var array<non-empty-string>
+         */
+        $subjects = [];
+
+        /**
+         * @var array<non-empty-string>
+         */
+        $allowedActions = [];
+
+        foreach ($promptSubjectResponderCollection->getPromptableActions() as $subject => $actions) {
+            $subjects[] = $subject;
+            $allActions = $allActions->merge($actions);
+
+            $allowedActions[] = sprintf(
+                'For "%s" the only allowed actions are: "%s"',
+                $subject,
+                $actions->join('", "'),
+            );
+        }
+
+        $allActionsSerialized = implode('", "', $allActions->toArray());
+        $subjectsSerialized = implode('", "', $subjects);
+        $allowedActionsSerialized = '- '.implode("\n -", $allowedActions);
+
+        $this->prompt = <<<PROMPT
+        You are a natural language intepreter.
+        Never ask for any clarifications.
+        Always interpret user intentions to the best of your ability.
+        Match user intentions the best you can with a set of predefined subjects and actions.
+
+        Always describe everything user says for as one of the
+        "{$allActionsSerialized}" actions with parameters.
+
+        If you cannot determine the action, use "uknown".
+        If you cannot determine the subject, use "uknown".
+        If you are unsure, use "unknown".
+        If user says something unrelated to the allowed actions, use "unknown".
+        If user says something unrelated to the allowed subjects, use "unknown".
+
+        When user asks how to do something that means they seek help.
+
+        Summarize and repeat everyting user says using just a few words:
+        - the first one being the subject ("{$subjectsSerialized}" or "unknown")
+        - the second one being an action the user mentioned ("{$allActionsSerialized}" or "unknown")
+
+        Valid combinations of subjects and actions you must adhere to:
+        {$allowedActionsSerialized}
+
+        Respond in the following format always:
+        subject action parameters
+        PROMPT;
+    }
+
+    public function getPromptContent(): string
+    {
+        return $this->prompt;
+    }
+}
diff --git a/src/OAuth2ScopePatternChunkTest.php b/src/OAuth2ScopePatternChunkTest.php
index 5024a379..a294a9c8 100644
--- a/src/OAuth2ScopePatternChunkTest.php
+++ b/src/OAuth2ScopePatternChunkTest.php
@@ -4,13 +4,13 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance;
 
+use PHPUnit\Framework\Attributes\CoversClass;
 use PHPUnit\Framework\TestCase;
 
 /**
- * @coversNothing
- *
  * @internal
  */
+#[CoversClass(OAuth2ScopePatternChunk::class)]
 final class OAuth2ScopePatternChunkTest extends TestCase
 {
     public function test_is_no_variable(): void
diff --git a/src/OAuth2ScopePatternTest.php b/src/OAuth2ScopePatternTest.php
index 0ea95449..fcebad97 100644
--- a/src/OAuth2ScopePatternTest.php
+++ b/src/OAuth2ScopePatternTest.php
@@ -5,13 +5,13 @@ declare(strict_types=1);
 namespace Distantmagic\Resonance;
 
 use Ds\Map;
+use PHPUnit\Framework\Attributes\CoversClass;
 use PHPUnit\Framework\TestCase;
 
 /**
- * @coversNothing
- *
  * @internal
  */
+#[CoversClass(OAuth2ScopePattern::class)]
 final class OAuth2ScopePatternTest extends TestCase
 {
     public function test_matches(): void
diff --git a/src/PostfixBounceAnalyzerTest.php b/src/PostfixBounceAnalyzerTest.php
index 3486c256..f05f057f 100644
--- a/src/PostfixBounceAnalyzerTest.php
+++ b/src/PostfixBounceAnalyzerTest.php
@@ -4,13 +4,13 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance;
 
+use PHPUnit\Framework\Attributes\CoversClass;
 use PHPUnit\Framework\TestCase;
 
 /**
- * @coversNothing
- *
  * @internal
  */
+#[CoversClass(PostfixBounceAnalyzer::class)]
 final class PostfixBounceAnalyzerTest extends TestCase
 {
     use TestsDependencyInectionContainerTrait;
diff --git a/src/PromptSubjectRequest.php b/src/PromptSubjectRequest.php
new file mode 100644
index 00000000..e0ad6c4d
--- /dev/null
+++ b/src/PromptSubjectRequest.php
@@ -0,0 +1,15 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+/**
+ * @psalm-suppress PossiblyUnusedProperty used in apps
+ */
+readonly class PromptSubjectRequest
+{
+    public function __construct(
+        public ?AuthenticatedUser $authenticatedUser,
+    ) {}
+}
diff --git a/src/PromptSubjectResponderAggregate.php b/src/PromptSubjectResponderAggregate.php
new file mode 100644
index 00000000..6eddc323
--- /dev/null
+++ b/src/PromptSubjectResponderAggregate.php
@@ -0,0 +1,80 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+use Distantmagic\Resonance\Attribute\Singleton;
+use Generator;
+use Psr\Log\LoggerInterface;
+
+#[Singleton]
+readonly class PromptSubjectResponderAggregate
+{
+    public function __construct(
+        private LoggerInterface $logger,
+        private PromptSubjectResponderCollection $promptSubjectResponderCollection,
+    ) {}
+
+    public function consumeTokens(
+        ?AuthenticatedUser $authenticatedUser,
+        LlamaCppCompletionIterator $completion,
+    ): Generator {
+        $subjectActionTokenReader = new SubjectActionTokenReader();
+
+        foreach ($completion as $token) {
+            $subjectActionTokenReader->write($token);
+
+            if ($subjectActionTokenReader->isUnknown()) {
+                $completion->stop();
+
+                break;
+            }
+        }
+
+        $action = $subjectActionTokenReader->getAction();
+        $subject = $subjectActionTokenReader->getSubject();
+
+        if ($subjectActionTokenReader->isUnknown() || !isset($action, $subject)) {
+            yield from $this->respondWithSubjectAction($authenticatedUser, 'unknown', 'unknown');
+        } else {
+            yield from $this->respondWithSubjectAction($authenticatedUser, $subject, $action);
+        }
+    }
+
+    /**
+     * @param non-empty-string $subject
+     * @param non-empty-string $action
+     */
+    private function respondWithSubjectAction(
+        ?AuthenticatedUser $authenticatedUser,
+        string $subject,
+        string $action,
+    ): Generator {
+        $responder = $this
+            ->promptSubjectResponderCollection
+            ->promptSubjectResponders
+            ->get($subject, null)
+            ?->get($action, null)
+        ;
+
+        if (!$responder) {
+            $this->logger->warning(sprintf(
+                'No prompt responder matched subject "%s" and action "%s"',
+                $subject,
+                $action,
+            ));
+
+            return;
+        }
+
+        $request = new PromptSubjectRequest($authenticatedUser);
+        $response = new PromptSubjectResponse();
+
+        SwooleCoroutineHelper::mustGo(static function () use ($request, $responder, $response) {
+            $responder->respondToPromptSubject($request, $response);
+        });
+
+        yield from $response;
+    }
+}
diff --git a/src/PromptSubjectResponderCollection.php b/src/PromptSubjectResponderCollection.php
new file mode 100644
index 00000000..c159b760
--- /dev/null
+++ b/src/PromptSubjectResponderCollection.php
@@ -0,0 +1,57 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+use Distantmagic\Resonance\Attribute\RespondsToPromptSubject;
+use Ds\Map;
+use Ds\Set;
+
+readonly class PromptSubjectResponderCollection
+{
+    /**
+     * @var Map<
+     *     non-empty-string,
+     *     Map<non-empty-string,PromptSubjectResponderInterface>
+     * >
+     */
+    public Map $promptSubjectResponders;
+
+    public function __construct()
+    {
+        $this->promptSubjectResponders = new Map();
+    }
+
+    public function addPromptSubjectResponder(
+        RespondsToPromptSubject $respondsToLPromptSubject,
+        PromptSubjectResponderInterface $promptResponder,
+    ): void {
+        if (!$this->promptSubjectResponders->hasKey($respondsToLPromptSubject->subject)) {
+            $this->promptSubjectResponders->put($respondsToLPromptSubject->subject, new Map());
+        }
+
+        $this
+            ->promptSubjectResponders
+            ->get($respondsToLPromptSubject->subject)
+            ->put($respondsToLPromptSubject->action, $promptResponder)
+        ;
+    }
+
+    /**
+     * @return Map<non-empty-string,Set<non-empty-string>>
+     */
+    public function getPromptableActions(): Map
+    {
+        /**
+         * @var Map<non-empty-string,Set<non-empty-string>>
+         */
+        $ret = new Map();
+
+        foreach ($this->promptSubjectResponders as $subject => $actionResponders) {
+            $ret->put($subject, $actionResponders->keys());
+        }
+
+        return $ret;
+    }
+}
diff --git a/src/PromptSubjectResponderInterface.php b/src/PromptSubjectResponderInterface.php
new file mode 100644
index 00000000..52daef71
--- /dev/null
+++ b/src/PromptSubjectResponderInterface.php
@@ -0,0 +1,13 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+interface PromptSubjectResponderInterface
+{
+    public function respondToPromptSubject(
+        PromptSubjectRequest $request,
+        PromptSubjectResponse $response,
+    ): void;
+}
diff --git a/src/PromptSubjectResponse.php b/src/PromptSubjectResponse.php
new file mode 100644
index 00000000..4b7395e6
--- /dev/null
+++ b/src/PromptSubjectResponse.php
@@ -0,0 +1,45 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+use IteratorAggregate;
+use Swoole\Coroutine\Channel;
+
+/**
+ * @template-implements IteratorAggregate<mixed>
+ */
+readonly class PromptSubjectResponse implements IteratorAggregate
+{
+    private Channel $channel;
+
+    public function __construct()
+    {
+        $this->channel = new Channel(1);
+    }
+
+    public function __destruct()
+    {
+        $this->channel->close();
+    }
+
+    /**
+     * @return SwooleChannelIterator<mixed>
+     */
+    public function getIterator(): SwooleChannelIterator
+    {
+        /**
+         * @var SwooleChannelIterator<mixed>
+         */
+        return new SwooleChannelIterator($this->channel);
+    }
+
+    /**
+     * @return mixed because almost every PHP type can be send over websocket
+     */
+    public function write(mixed $payload): bool
+    {
+        return $this->channel->push($payload);
+    }
+}
diff --git a/src/SingletonCollection.php b/src/SingletonCollection.php
index 3ead3d86..64b3f6ba 100644
--- a/src/SingletonCollection.php
+++ b/src/SingletonCollection.php
@@ -27,6 +27,7 @@ enum SingletonCollection implements SingletonCollectionInterface
     case OpenAPIRouteRequestBodyContentExtractor;
     case OpenAPIRouteSecurityRequirementExtractor;
     case PDOPoolConnectionBuilder;
+    case PromptSubjectResponder;
     case ServerPipeMessageHandler;
     case ServerTaskHandler;
     case SiteActionGate;
diff --git a/src/SingletonContainerTest.php b/src/SingletonContainerTest.php
index 5af42426..adee431f 100644
--- a/src/SingletonContainerTest.php
+++ b/src/SingletonContainerTest.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance;
 
+use PHPUnit\Framework\Attributes\CoversClass;
 use PHPUnit\Framework\TestCase;
 
 /**
@@ -12,10 +13,9 @@ use PHPUnit\Framework\TestCase;
 final class SingletonContainerTestFixtureFoo {}
 
 /**
- * @coversNothing
- *
  * @internal
  */
+#[CoversClass(SingletonContainer::class)]
 final class SingletonContainerTest extends TestCase
 {
     public function test_singletons_can_be_set(): void
diff --git a/src/SingletonProvider/PromptSubjectResponderCollectionProvider.php b/src/SingletonProvider/PromptSubjectResponderCollectionProvider.php
new file mode 100644
index 00000000..f63050eb
--- /dev/null
+++ b/src/SingletonProvider/PromptSubjectResponderCollectionProvider.php
@@ -0,0 +1,50 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance\SingletonProvider;
+
+use Distantmagic\Resonance\Attribute\RequiresSingletonCollection;
+use Distantmagic\Resonance\Attribute\RespondsToPromptSubject;
+use Distantmagic\Resonance\Attribute\Singleton;
+use Distantmagic\Resonance\PHPProjectFiles;
+use Distantmagic\Resonance\PromptSubjectResponderCollection;
+use Distantmagic\Resonance\PromptSubjectResponderInterface;
+use Distantmagic\Resonance\SingletonAttribute;
+use Distantmagic\Resonance\SingletonCollection;
+use Distantmagic\Resonance\SingletonContainer;
+use Distantmagic\Resonance\SingletonProvider;
+
+/**
+ * @template-extends SingletonProvider<PromptSubjectResponderCollection>
+ */
+#[RequiresSingletonCollection(SingletonCollection::PromptSubjectResponder)]
+#[Singleton(provides: PromptSubjectResponderCollection::class)]
+final readonly class PromptSubjectResponderCollectionProvider extends SingletonProvider
+{
+    public function provide(SingletonContainer $singletons, PHPProjectFiles $phpProjectFiles): PromptSubjectResponderCollection
+    {
+        $promptableSubjectCollection = new PromptSubjectResponderCollection();
+
+        foreach ($this->collectPromptSubjectResponders($singletons) as $promptableSubjectAttribute) {
+            $promptableSubjectCollection->addPromptSubjectResponder(
+                $promptableSubjectAttribute->attribute,
+                $promptableSubjectAttribute->singleton,
+            );
+        }
+
+        return $promptableSubjectCollection;
+    }
+
+    /**
+     * @return iterable<SingletonAttribute<PromptSubjectResponderInterface,RespondsToPromptSubject>>
+     */
+    private function collectPromptSubjectResponders(SingletonContainer $singletons): iterable
+    {
+        return $this->collectAttributes(
+            $singletons,
+            PromptSubjectResponderInterface::class,
+            RespondsToPromptSubject::class,
+        );
+    }
+}
diff --git a/src/SubjectActionTokenReader.php b/src/SubjectActionTokenReader.php
new file mode 100644
index 00000000..062d451c
--- /dev/null
+++ b/src/SubjectActionTokenReader.php
@@ -0,0 +1,97 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+use LogicException;
+use Stringable;
+
+class SubjectActionTokenReader
+{
+    /**
+     * @var null|non-empty-string
+     */
+    private ?string $action = null;
+
+    /**
+     * @var null|non-empty-string
+     */
+    private ?string $subject = null;
+
+    private string $unprocessedTokens = '';
+
+    /**
+     * @return null|non-empty-string
+     */
+    public function getAction(): ?string
+    {
+        return $this->action;
+    }
+
+    /**
+     * @return null|non-empty-string
+     */
+    public function getSubject(): ?string
+    {
+        return $this->subject;
+    }
+
+    public function isUnknown(): bool
+    {
+        return 'unknown' === $this->action || 'unknown' === $this->subject;
+    }
+
+    public function write(string|Stringable $token): void
+    {
+        $this->unprocessedTokens .= (string) $token;
+
+        if (is_string($this->action) && is_string($this->subject)) {
+            return;
+        }
+
+        if (!str_contains($this->unprocessedTokens, ' ')) {
+            return;
+        }
+
+        if (is_string($this->subject) && !is_string($this->action)) {
+            $this->action = $this->readWord(1);
+
+            return;
+        }
+
+        if (!is_string($this->action)) {
+            $this->subject = $this->readWord(0);
+
+            if ('unknown' === $this->subject) {
+                $this->action = 'unknown';
+            }
+
+            return;
+        }
+
+        throw new LogicException('Both subject and action are filled');
+    }
+
+    /**
+     * @return null|non-empty-string
+     */
+    private function readWord(int $offset): ?string
+    {
+        $spaceStrpos = mb_strpos($this->unprocessedTokens, ' ', $offset);
+
+        if (false === $spaceStrpos) {
+            return null;
+        }
+
+        $chunk = mb_substr($this->unprocessedTokens, $offset, $spaceStrpos - $offset);
+
+        $this->unprocessedTokens = mb_substr($this->unprocessedTokens, $spaceStrpos + $offset);
+
+        if (empty($chunk)) {
+            return null;
+        }
+
+        return $chunk;
+    }
+}
diff --git a/src/SubjectActionTokenReaderTest.php b/src/SubjectActionTokenReaderTest.php
new file mode 100644
index 00000000..97a87faf
--- /dev/null
+++ b/src/SubjectActionTokenReaderTest.php
@@ -0,0 +1,90 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @internal
+ */
+#[CoversClass(SubjectActionTokenReader::class)]
+final class SubjectActionTokenReaderTest extends TestCase
+{
+    public function test_reads_subject_and_action(): void
+    {
+        $reader = new SubjectActionTokenReader();
+
+        self::assertNull($reader->getAction());
+        self::assertNull($reader->getSubject());
+        self::assertFalse($reader->isUnknown());
+
+        $reader->write('blog_post');
+
+        self::assertNull($reader->getAction());
+        self::assertNull($reader->getSubject());
+        self::assertFalse($reader->isUnknown());
+
+        $reader->write(' cre');
+
+        self::assertNull($reader->getAction());
+        self::assertSame('blog_post', $reader->getSubject());
+        self::assertFalse($reader->isUnknown());
+
+        $reader->write('ate');
+
+        self::assertNull($reader->getAction());
+        self::assertSame('blog_post', $reader->getSubject());
+        self::assertFalse($reader->isUnknown());
+
+        $reader->write(' ');
+
+        self::assertSame('create', $reader->getAction());
+        self::assertSame('blog_post', $reader->getSubject());
+        self::assertFalse($reader->isUnknown());
+    }
+
+    public function test_reads_unknown_action(): void
+    {
+        $reader = new SubjectActionTokenReader();
+
+        self::assertNull($reader->getAction());
+        self::assertNull($reader->getSubject());
+        self::assertFalse($reader->isUnknown());
+
+        $reader->write('blog_post ');
+
+        self::assertNull($reader->getAction());
+        self::assertSame('blog_post', $reader->getSubject());
+        self::assertFalse($reader->isUnknown());
+
+        $reader->write('unknown ');
+
+        self::assertSame('unknown', $reader->getAction());
+        self::assertSame('blog_post', $reader->getSubject());
+        self::assertTrue($reader->isUnknown());
+    }
+
+    public function test_reads_unknown_subject(): void
+    {
+        $reader = new SubjectActionTokenReader();
+
+        self::assertNull($reader->getAction());
+        self::assertNull($reader->getSubject());
+        self::assertFalse($reader->isUnknown());
+
+        $reader->write('unknown');
+
+        self::assertNull($reader->getAction());
+        self::assertNull($reader->getSubject());
+        self::assertFalse($reader->isUnknown());
+
+        $reader->write(' ');
+
+        self::assertSame('unknown', $reader->getAction());
+        self::assertSame('unknown', $reader->getSubject());
+        self::assertTrue($reader->isUnknown());
+    }
+}
diff --git a/src/WebSocketProtocolIteratorTest.php b/src/WebSocketProtocolIteratorTest.php
index c7d28d1c..727baffb 100644
--- a/src/WebSocketProtocolIteratorTest.php
+++ b/src/WebSocketProtocolIteratorTest.php
@@ -4,13 +4,13 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance;
 
+use PHPUnit\Framework\Attributes\CoversClass;
 use PHPUnit\Framework\TestCase;
 
 /**
- * @coversNothing
- *
  * @internal
  */
+#[CoversClass(WebSocketProtocolIterator::class)]
 final class WebSocketProtocolIteratorTest extends TestCase
 {
     public function test_iterates_over_protocol_values(): void
diff --git a/src/WebSocketRPCConnectionHandle.php b/src/WebSocketRPCConnectionHandle.php
index 0f8918c1..8e37283f 100644
--- a/src/WebSocketRPCConnectionHandle.php
+++ b/src/WebSocketRPCConnectionHandle.php
@@ -50,7 +50,7 @@ readonly class WebSocketRPCConnectionHandle
             ->validate($rpcMessage->payload)
         ;
 
-        if ($constraintResult->status->isValid()) {
+        if (!$constraintResult->status->isValid()) {
             return $constraintResult;
         }
 
diff --git a/src/WebSocketServerController.php b/src/WebSocketServerController.php
index ee20f6ad..80d45be4 100644
--- a/src/WebSocketServerController.php
+++ b/src/WebSocketServerController.php
@@ -65,6 +65,8 @@ final readonly class WebSocketServerController implements ServerPipeMessageHandl
 
     public function onClose(int $fd): void
     {
+        $this->logger->debug(sprintf('websocket_close(%s)', $fd));
+
         $this->webSocketServerConnectionTable->unregisterConnection($fd);
 
         if (!$this->protocolControllers->hasKey($fd)) {
@@ -145,10 +147,14 @@ final readonly class WebSocketServerController implements ServerPipeMessageHandl
         $response->end();
 
         $controllerResolution->controller->onOpen($server, $fd, $authResolution);
+
+        $this->logger->debug(sprintf('websocket_handshake_successful(%s)', $fd));
     }
 
     public function onMessage(Server $server, Frame $frame): void
     {
+        $this->logger->debug(sprintf('websocket_message(%s)', $frame->fd));
+
         $protocolController = $this->protocolControllers->get($frame->fd, null);
 
         if ($protocolController) {
-- 
GitLab