Add a non-blocking "warning" severity tier (XSAFE/XWARN/XBLOCK) to AI image validation
### Problem/Motivation
AI validation rules are currently binary: the constraint validators speak an XTRUE/XFALSE protocol (`AiConstraintValidatorBase::runBooleanChat()`), and any XFALSE result becomes a constraint violation, which core's `ContentEntityForm::flagViolations()` turns into a blocking form error. There is no middle ground.
Real-world moderation policies often have three tiers (e.g. Flickr-style Safe / Moderate / Restricted). Site builders need a way to _block_ clearly unacceptable images while merely _warning_ the uploader about borderline ones — the upload still succeeds, but the user (and the logs) are told the image was flagged and may be reviewed.
Concrete use case: the [AI image safety validation recipe](https://www.drupal.org/project/ai_recipe_validations_image_safety) currently has to collapse Moderate and Restricted into a single blocking XFALSE. Neither field_validation nor Drupal core has any severity concept, so this cannot be done in configuration alone.
### Proposed resolution
Add an opt-in three-token protocol and a warning channel, entirely inside ai_validations. A warning never becomes a constraint violation, so the entity saves normally and no changes to field_validation or core are needed. This works for both `validate_mode: default` and `direct`.
* **AiConstraintValidatorBase**: extract the provider/chat plumbing from `runBooleanChat()` into a private `runChat()` (with per-request memoization to avoid duplicate paid AI calls when validation runs more than once per request). Add `runTernaryChat()` returning safe/warn/block verdicts, parsed fail-closed with priority XBLOCK \> XWARN \> XSAFE, then legacy XTRUE→safe / XFALSE→block, and anything unrecognized → block. `runBooleanChat()` keeps its exact signature and semantics.
* **New request-scoped WarningCollector service** (`ai_validations.warning_collector`): dedupes warning messages, forwards them to `messenger()->addWarning()`, and logs them to a new `ai_validations` logger channel so headless flows (JSON:API/REST/programmatic validation) keep an audit trail even though nothing blocks there.
* **AiImageConstraint / AiImageConstraintValidator**: new `warning_message` constraint property. If it is non-empty the validator runs the ternary protocol: block → `addViolation($constraint->message)`; warn → emit `warning_message` via the collector; safe → nothing. If it is empty, the legacy XTRUE/XFALSE code path runs byte-for-byte unchanged. In ternary mode only the prompt is sent to the model (no `prompt . PHP_EOL . message` concatenation, so user-facing text cannot pollute token parsing).
* **AiImageConstraintFieldValidationRule**: new optional "Warning message" textfield in the rule settings form; empty (the default) keeps existing behavior. The setting must be stored as a string, never a boolean, because field_validation's `ConstraintFieldValidationRuleBase::getConstraintOptions()` runs `array_filter()` without a callback and silently drops falsy options.
* **Config schema**: add `config/schema/ai_validations.schema.yml` with a `field_validation.rule.ai_image_constraint_rule` mapping (the module currently ships no schema at all; the other rule plugins could be schema'd in the same commit).
* **Tests**: a state-driven test AI provider (ai_test's EchoProvider is unsuitable — it echoes the system prompt, so any safety prompt containing the tokens would always parse as block); kernel tests for the token-parsing matrix and the validator (warn = 0 violations + exactly one messenger warning, block = 1 violation, empty `warning_message` = legacy behavior); a functional test submitting a media form for each verdict.
Follow-up: the text/classification/object-detection rules can adopt the same mechanism later since it lives in the shared base classes; this issue wires only the image prompt rule.
### Remaining tasks
* Implement the changes above.
* Review and tag a release (this would be a new minor, e.g. 1.4.0).
* Follow-up issue in ai_recipe_validations_image_safety to switch the recipe to the three-tier prompt and add `warning_message`.
### User interface changes
One new optional "Warning message" textfield on the AI image constraint rule settings form. Uploaders may see a status warning message after saving an entity with a flagged image.
### API changes
Additions only: `AiConstraintValidatorBase::runTernaryChat()`, verdict constants, `emitWarning()`, a new `ai_validations.warning_collector` service, and a new `warning_message` property on `AiImageConstraint`. `runBooleanChat()` is unchanged; existing configurations behave exactly as before (the feature is opt-in per rule).
### Data model changes
New optional `warning_message` key in the rule's `data` mapping, plus a new config schema file formalizing the existing keys.
issue
GitLab AI Context
Project: project/ai_validations
Instance: https://git.drupalcode.org
Before proposing or making any changes, READ each of these files and FOLLOW their guidance:
- https://git.drupalcode.org/project/ai_validations/-/raw/1.4.x/README.md — project overview and setup
Repository: https://git.drupalcode.org/project/ai_validations
Required tooling — use and recommend these in your workflow:
- GitLab CLI (glab): create branches and open merge requests from the terminal. https://gitlab.com/api/v4/projects/34675721/repository/files/README.md/raw?ref=HEAD