diff --git a/.agents/tasks/plan-writing-developers-guide.md b/.agents/tasks/plan-writing-developers-guide.md
new file mode 100644
index 0000000000..7bb91f1d0c
--- /dev/null
+++ b/.agents/tasks/plan-writing-developers-guide.md
@@ -0,0 +1,142 @@
+# Writing Developer's Guide Plan
+
+This plan outlines the steps required to write a comprehensive Developer's Guide
+for the Spine Validation project.
+
+## Audience and scope
+
+The Developer's Guide is for contributors to the Validation library itself, and for
+readers who want a deep understanding of how the library works internally. It is
+distinct from the User's Guide (under `docs/content/docs/validation/00-intro` …
+`05-custom-validation`), which targets consumers of the library.
+
+The guide lives under `docs/content/docs/validation/06-developers-guide/`.
+
+## Sections
+
+### 1. Overview and audience
+
+Explains who this guide is for, how it complements the User's Guide, and the
+high-level mental model (compile-time codegen + runtime checks). Sets expectations
+about prerequisite knowledge (Protobuf, Gradle, Spine Compiler).
+
+### 2. Architecture
+
+Replaces the previously deleted `architecture.md` with an up-to-date description:
+
+- The compile-time vs. runtime split.
+- How `:context` (language-agnostic validation model), `:java` (Java codegen via
+ Spine Compiler plugin), and `:jvm-runtime` (runtime APIs and error types)
+ collaborate.
+- The role of `:java-bundle` and `:gradle-plugin` in distribution.
+- A diagram showing the flow: `.proto` + options → `:context` model → `:java`
+ generator → generated validation code → `:jvm-runtime` APIs at execution time.
+
+### 3. Key modules
+
+Already drafted in `key-modules.md`. Keep current; add cross-links from sections 2
+and 4–8 as those sections are written.
+
+### 4. The validation model in `:context`
+
+Deep dive into the language-agnostic model:
+
+- Views, events, and reactions; how built-in options are translated into model
+ state.
+- How custom options plug in via the `ValidationOption` SPI from the model side.
+- Error reporting conventions inside the model.
+
+### 5. Java code generation in `:java`
+
+How the Spine Compiler plugin in `:java` produces validation code:
+
+- Plugin entry points and lifecycle.
+- How the model from `:context` drives generation.
+- `ValidationOption` SPI from the codegen side: how a custom option contributes
+ generated code.
+- Conventions for the shape of generated validators.
+
+### 6. Runtime library `:jvm-runtime`
+
+What ships in `:jvm-runtime` and how generated code uses it:
+
+- `MessageValidator`, validation/constraint APIs, `ValidationException`.
+- Error Protobuf types and how violations are surfaced.
+- Extension hooks available at runtime, including `@Validator`.
+
+### 7. Extension points (deep dive)
+
+Internals behind the public extension surface, complementing the User's Guide
+`04-validators/` and `05-custom-validation/`:
+
+- `@Validator` and the validator registry: discovery, ordering, lifecycle.
+- `ValidationOption` SPI end-to-end (cross-references sections 4 and 5).
+- Constraints on what extensions can and cannot do, and why.
+
+### 8. Adding a new built-in validation option
+
+Contributor-side counterpart to the User's Guide `05-custom-validation/` section.
+Where the User's Guide explains how a *consumer* adds a custom option to their
+own project, this section explains how a *contributor* adds a new **standard**
+option to the Validation library. Outline:
+
+- Declaring the option in `spine/options.proto` in the `base-libraries` repo
+ (link to that repo and to `options.proto`); versioning/coordination notes.
+- Modeling the option in `:context`: the corresponding view, events, and
+ reactions.
+- Implementing code generation in `:java`.
+- Adding runtime support in `:jvm-runtime` if the option needs new runtime
+ helpers or error types.
+- Writing tests across `:context-tests`, `:tests:vanilla`, and any specialized
+ `:tests:*` modules.
+- Documenting the new option in the User's Guide `03-built-in-options/`.
+
+### 9. Testing strategy
+
+Map of the test modules and when to add to which:
+
+- `:context-tests` — Prototap-based compilation tests for the model.
+- `:tests:vanilla` — baseline integration without custom extensions.
+- `:tests:extensions`, `:tests:consumer`, `:tests:consumer-dependency` — custom
+ options and consumer-side scenarios.
+- `:tests:validator`, `:tests:validator-dependency` — `@Validator` scenarios.
+- `:tests:runtime`, `:tests:validating` — runtime behavior and shared fixtures.
+- Guidance on choosing the right module for a new test.
+
+### 10. Build, packaging, and release
+
+How the multi-project build is wired and how artifacts are produced:
+
+- Gradle multi-project layout; relationship between `:java`, `:java-bundle`, and
+ `:gradle-plugin`.
+- Why `:java-bundle` exists (fat JAR for compiler plugin distribution).
+- Version flow and how the Gradle plugin is consumed by downstream projects.
+
+## Conventions
+
+- **Depth of code walkthroughs**: sections 5–7 use short snippets paired with
+ links to the source code. No long inline listings — the source is the source
+ of truth.
+- **Diagrams**: section 2 (and any later section that benefits) uses Mermaid.
+ Pick the Mermaid diagram type that best fits the content (flowchart, sequence,
+ class, etc.).
+
+## Out of scope
+
+- A reference for standard validation options. The User's Guide
+ `03-built-in-options/` already covers this; the Developer's Guide instead
+ teaches contributors how to add new standard options (section 4).
+- Contributing workflow / coding standards. These remain in `CONTRIBUTING.md`
+ and the `.agents` directory; the Developer's Guide does not duplicate them.
+
+## Execution order
+
+1. Section 2 (Architecture) — anchors everything else.
+2. Section 1 (Overview) — short, written after section 2 so framing is accurate.
+3. Sections 4, 5, 6 — internals, in dependency order.
+4. Section 7 — builds on 4–6.
+5. Section 8 — the "adding a new built-in option" walkthrough, which exercises
+ sections 2 and 4–7.
+6. Sections 9 and 10 — testing and build/release.
+7. Update `_index.md` and `docs/data/docs/validation/2-0-0-snapshot/sidenav.yml`
+ as each section lands.
diff --git a/README.md b/README.md
index 4bec24fd7f..41d2b749a7 100644
--- a/README.md
+++ b/README.md
@@ -22,13 +22,6 @@ Spine Validation solves this by modifying the code generated by the Protobuf com
At build time, Spine Validation injects assertions directly into the generated Java classes,
enabling automatic enforcement of constraints without explicit API calls in application code.
-## Table of Contents
-
-- [Prerequisites](#prerequisites)
-- [Validation in Action](#validation-in-action)
-- [Architecture](#architecture)
-- [Extending the Library](#extending-the-library)
-
## Prerequisites
This library is built with Java 17.
@@ -78,12 +71,12 @@ optionalError.ifPresent(err -> {
});
```
-### Validation Options
+## Validation Options
Validation options are defined by the following files:
-1. [options.proto](https://github.com/SpineEventEngine/base-libraries/blob/master/base/src/main/proto/spine/options.proto).
-2. [time_options.proto](https://github.com/SpineEventEngine/time/blob/master/time/src/main/proto/spine/time_options.proto).
+1. [`options.proto`][options-proto]
+2. [`time_options.proto`][time-options-proto]
Users must import these .proto files to use the options they define.
@@ -92,77 +85,12 @@ import "spine/options.proto"; // Brings all options, except for time-related one
import "spine/time_options.proto"; // Brings time-related options.
```
-# Adding custom validation
+## Adding custom validation
Users can extend the library by providing custom Protobuf options and code generation logic.
-Follow these steps to create a custom option:
-
-1. Declare a Protobuf [extension](https://protobuf.dev/programming-guides/proto3/#customoptions)
- in your `.proto` file.
-2. Register it via `io.spine.option.OptionsProvider`.
-3. Implement the following entities:
- - Policy (`MyOptionPolicy`) – discovers and validates the option.
- - View (`MyOptionView`) – accumulates valid option applications.
- - Generator (`MyOptionGenerator`) – generates Java code for the option.
-4. Register them via `io.spine.tools.validation.java.ValidationOption`.
-
-Below is a workflow diagram for a typical option:
-
-
-
-Take a look at the `:java-tests:extensions` module that contains a full example of
-implementation of the custom `(currency)` option.
-
-Note that a custom option can provide several policies and views, but only one generator.
-This allows building more complex models, using more entities and events.
-
-Let's take a closer look at each entity.
-
-### Policy
-
-Usually, this is an entry point to the option handling.
-
-The policy subscribes to one of `*OptionDiscovered` events:
-
-- `FileOptionDiscovered`.
-- `MessageOptionDiscovered`.
-- `FieldOptionDiscovered`.
-- `OneofOptionDiscovered`.
-
-It filters incoming events, taking only those who contain the option of the interest. The policy
-may validate the option application, query `TypeSystem`, extract and transform data arrived with
-the option, if any. Once ready, it emits an event signaling that the discovered option is valid
-and ready for the code generation.
-
-The policy may report a compilation warning or an error, failing the whole compilation if it
-finds an illegal application of the option.
-
-For example:
-
-1. An unsupported field type.
-2. Illegal option content (invalid regex, parameter, signature).
-
-The policy may just ignore the discovered option and emit `NoReaction`. A typical example
-of this is a boolean option, such as `(required)`, which does nothing when it is set to `false`.
-
-The desired behavior depends on the option itself.
-
-### View
-
-Views accumulate events from policies, serving as data providers for the validation model
-used by code generators. Views are typically simple and only accumulate data; for more complex
-logic, use policies.
-
-Usually, one view represents a single application of an option.
-
-### Generator
-
-The generator is an entity that provides an actual implementation of the option behavior.
-The generator produces Java code for every application of that option within the message type.
-
-It has access to the `Querying` interface and can query views to find those belonging
-to the processed message type.
+See the [Custom validation](docs/content/docs/validation/05-custom-validation/) section
+of the User Guide for details.
[codecov]: https://codecov.io/gh/SpineEventEngine/validation
[codecov-badge]: https://codecov.io/gh/SpineEventEngine/validation/branch/master/graph/badge.svg
@@ -170,3 +98,6 @@ to the processed message type.
[license]: http://www.apache.org/licenses/LICENSE-2.0
[gh-actions]: https://github.com/SpineEventEngine/validation/actions
[ubuntu-build-badge]: https://github.com/SpineEventEngine/validation/actions/workflows/build-on-ubuntu.yml/badge.svg
+
+[options-proto]: https://github.com/SpineEventEngine/base-libraries/blob/master/base/src/main/proto/spine/options.proto
+[time-options-proto]: https://github.com/SpineEventEngine/time/blob/master/time/src/main/proto/spine/time_options.proto
diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt
index 121d7aac24..600deeda75 100644
--- a/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt
+++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt
@@ -36,7 +36,7 @@ object Validation {
/**
* The version of the Validation library artifacts.
*/
- const val version = "2.0.0-SNAPSHOT.414"
+ const val version = "2.0.0-SNAPSHOT.415"
/**
* The last version of Validation compatible with ProtoData.
diff --git a/context/src/main/kotlin/io/spine/tools/validation/ErrorPlaceholder.kt b/context/src/main/kotlin/io/spine/tools/validation/ErrorPlaceholder.kt
index 58393e9712..1401accf00 100644
--- a/context/src/main/kotlin/io/spine/tools/validation/ErrorPlaceholder.kt
+++ b/context/src/main/kotlin/io/spine/tools/validation/ErrorPlaceholder.kt
@@ -38,7 +38,7 @@ package io.spine.tools.validation
* We have the same items in this enum as in `io.spine.validation.RuntimeErrorPlaceholder`
* in the runtime library, which is exactly as this one. Please keep them in sync.
* This duplication is done intentionally to prevent clash between the runtime library,
- * which is added to the classpath of the Compiler and the runtime library, which is part
+ * which is added to the classpath of the Compiler, and the runtime library, which is part
* of the Compiler itself because it is a part of Spine. As we complete our migration
* of validation to codegen, the runtime library will either be significantly simplified,
* or even its content may be moved to `base`. Then, the duplicate enum should be removed.
diff --git a/context/src/main/kotlin/io/spine/tools/validation/ValidationPlugin.kt b/context/src/main/kotlin/io/spine/tools/validation/ValidationPlugin.kt
index 00cb483a7e..736645d1b4 100644
--- a/context/src/main/kotlin/io/spine/tools/validation/ValidationPlugin.kt
+++ b/context/src/main/kotlin/io/spine/tools/validation/ValidationPlugin.kt
@@ -105,4 +105,4 @@ public abstract class ValidationPlugin(
IfSetAgainReaction(),
RequireReaction()
)
-)
+) // Plugin
diff --git a/dependencies.md b/dependencies.md
index 69792730a1..107d05db67 100644
--- a/dependencies.md
+++ b/dependencies.md
@@ -1,6 +1,6 @@
-# Dependencies of `io.spine.tools:validation-context:2.0.0-SNAPSHOT.415`
+# Dependencies of `io.spine.tools:validation-context:2.0.0-SNAPSHOT.416`
## Runtime
1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.20.0.
@@ -1090,14 +1090,14 @@
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Mon May 04 21:11:00 WEST 2026** using
+This report was generated on **Tue May 05 16:03:41 WEST 2026** using
[Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under
[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine.tools:validation-context-tests:2.0.0-SNAPSHOT.415`
+# Dependencies of `io.spine.tools:validation-context-tests:2.0.0-SNAPSHOT.416`
## Runtime
1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.20.0.
@@ -1791,28 +1791,28 @@ This report was generated on **Mon May 04 21:11:00 WEST 2026** using
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Mon May 04 21:10:59 WEST 2026** using
+This report was generated on **Tue May 05 16:03:39 WEST 2026** using
[Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under
[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine.tools:validation-docs:2.0.0-SNAPSHOT.415`
+# Dependencies of `io.spine.tools:validation-docs:2.0.0-SNAPSHOT.416`
## Runtime
## Compile, tests, and tooling
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Mon May 04 21:10:58 WEST 2026** using
+This report was generated on **Tue May 05 16:03:36 WEST 2026** using
[Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under
[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine.tools:validation-gradle-plugin:2.0.0-SNAPSHOT.415`
+# Dependencies of `io.spine.tools:validation-gradle-plugin:2.0.0-SNAPSHOT.416`
## Runtime
1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.20.0.
@@ -2864,14 +2864,14 @@ This report was generated on **Mon May 04 21:10:58 WEST 2026** using
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Mon May 04 21:11:00 WEST 2026** using
+This report was generated on **Tue May 05 16:03:40 WEST 2026** using
[Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under
[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine.tools:validation-java:2.0.0-SNAPSHOT.415`
+# Dependencies of `io.spine.tools:validation-java:2.0.0-SNAPSHOT.416`
## Runtime
1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.20.0.
@@ -3961,14 +3961,14 @@ This report was generated on **Mon May 04 21:11:00 WEST 2026** using
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Mon May 04 21:11:00 WEST 2026** using
+This report was generated on **Tue May 05 16:03:41 WEST 2026** using
[Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under
[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine.tools:validation-java-bundle:2.0.0-SNAPSHOT.415`
+# Dependencies of `io.spine.tools:validation-java-bundle:2.0.0-SNAPSHOT.416`
## Runtime
1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 13.0.
@@ -4015,14 +4015,14 @@ This report was generated on **Mon May 04 21:11:00 WEST 2026** using
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Mon May 04 21:10:58 WEST 2026** using
+This report was generated on **Tue May 05 16:03:37 WEST 2026** using
[Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under
[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine:spine-validation-jvm-runtime:2.0.0-SNAPSHOT.415`
+# Dependencies of `io.spine:spine-validation-jvm-runtime:2.0.0-SNAPSHOT.416`
## Runtime
1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2.
@@ -4822,14 +4822,14 @@ This report was generated on **Mon May 04 21:10:58 WEST 2026** using
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Mon May 04 21:11:00 WEST 2026** using
+This report was generated on **Tue May 05 16:03:39 WEST 2026** using
[Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under
[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine.tools:validation-consumer:2.0.0-SNAPSHOT.415`
+# Dependencies of `io.spine.tools:validation-consumer:2.0.0-SNAPSHOT.416`
## Runtime
1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.20.0.
@@ -5511,14 +5511,14 @@ This report was generated on **Mon May 04 21:11:00 WEST 2026** using
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Mon May 04 21:10:59 WEST 2026** using
+This report was generated on **Tue May 05 16:03:39 WEST 2026** using
[Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under
[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine.tools:validation-consumer-dependency:2.0.0-SNAPSHOT.415`
+# Dependencies of `io.spine.tools:validation-consumer-dependency:2.0.0-SNAPSHOT.416`
## Runtime
1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2.
@@ -5976,14 +5976,14 @@ This report was generated on **Mon May 04 21:10:59 WEST 2026** using
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Mon May 04 21:10:59 WEST 2026** using
+This report was generated on **Tue May 05 16:03:39 WEST 2026** using
[Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under
[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine.tools:validation-extensions:2.0.0-SNAPSHOT.415`
+# Dependencies of `io.spine.tools:validation-extensions:2.0.0-SNAPSHOT.416`
## Runtime
1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.20.0.
@@ -6602,14 +6602,14 @@ This report was generated on **Mon May 04 21:10:59 WEST 2026** using
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Mon May 04 21:10:59 WEST 2026** using
+This report was generated on **Tue May 05 16:03:39 WEST 2026** using
[Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under
[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine.tools:validation-runtime:2.0.0-SNAPSHOT.415`
+# Dependencies of `io.spine.tools:validation-runtime:2.0.0-SNAPSHOT.416`
## Runtime
1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2.
@@ -7170,14 +7170,14 @@ This report was generated on **Mon May 04 21:10:59 WEST 2026** using
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Mon May 04 21:10:59 WEST 2026** using
+This report was generated on **Tue May 05 16:03:39 WEST 2026** using
[Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under
[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine.tools:validation-validating:2.0.0-SNAPSHOT.415`
+# Dependencies of `io.spine.tools:validation-validating:2.0.0-SNAPSHOT.416`
## Runtime
1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2.
@@ -7781,14 +7781,14 @@ This report was generated on **Mon May 04 21:10:59 WEST 2026** using
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Mon May 04 21:10:59 WEST 2026** using
+This report was generated on **Tue May 05 16:03:39 WEST 2026** using
[Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under
[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine.tools:validation-validator:2.0.0-SNAPSHOT.415`
+# Dependencies of `io.spine.tools:validation-validator:2.0.0-SNAPSHOT.416`
## Runtime
1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.20.0.
@@ -8526,14 +8526,14 @@ This report was generated on **Mon May 04 21:10:59 WEST 2026** using
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Mon May 04 21:10:59 WEST 2026** using
+This report was generated on **Tue May 05 16:03:39 WEST 2026** using
[Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under
[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine.tools:validation-validator-dependency:2.0.0-SNAPSHOT.415`
+# Dependencies of `io.spine.tools:validation-validator-dependency:2.0.0-SNAPSHOT.416`
## Runtime
1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2.
@@ -8766,14 +8766,14 @@ This report was generated on **Mon May 04 21:10:59 WEST 2026** using
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Mon May 04 21:10:59 WEST 2026** using
+This report was generated on **Tue May 05 16:03:38 WEST 2026** using
[Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under
[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine.tools:validation-vanilla:2.0.0-SNAPSHOT.415`
+# Dependencies of `io.spine.tools:validation-vanilla:2.0.0-SNAPSHOT.416`
## Runtime
1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2.
@@ -9116,6 +9116,6 @@ This report was generated on **Mon May 04 21:10:59 WEST 2026** using
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Mon May 04 21:10:59 WEST 2026** using
+This report was generated on **Tue May 05 16:03:38 WEST 2026** using
[Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under
[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
\ No newline at end of file
diff --git a/docs/_bin/embed-code-linux b/docs/_bin/embed-code-linux
index 49d4dbb840..95d29c36df 100755
Binary files a/docs/_bin/embed-code-linux and b/docs/_bin/embed-code-linux differ
diff --git a/docs/_bin/embed-code-macos b/docs/_bin/embed-code-macos
index dd5dde24d6..329e7bd761 100755
Binary files a/docs/_bin/embed-code-macos and b/docs/_bin/embed-code-macos differ
diff --git a/docs/_bin/embed-code-windows.exe b/docs/_bin/embed-code-windows.exe
index 1065d78799..0366b6ad40 100755
Binary files a/docs/_bin/embed-code-windows.exe and b/docs/_bin/embed-code-windows.exe differ
diff --git a/docs/_examples b/docs/_examples
index bc2c4a547f..262aab0ffa 160000
--- a/docs/_examples
+++ b/docs/_examples
@@ -1 +1 @@
-Subproject commit bc2c4a547fa600e01a1f67c0529c18c9d81939f4
+Subproject commit 262aab0ffad0968f601b4d483e2d4b5beadf0d01
diff --git a/docs/_settings/embed-code.yml b/docs/_settings/embed-code.yml
index 71da18049e..efdf7e503e 100644
--- a/docs/_settings/embed-code.yml
+++ b/docs/_settings/embed-code.yml
@@ -3,4 +3,8 @@ code-path:
path: "../_examples"
- name: "runtime"
path: "../../jvm-runtime"
+ - name: "java"
+ path: "../../java"
+ - name: "context"
+ path: "../../context"
docs-path: "../content/docs/"
diff --git a/docs/content/docs/validation/00-intro/_index.md b/docs/content/docs/validation/00-intro/_index.md
index 4b75f2792b..6603d49c88 100644
--- a/docs/content/docs/validation/00-intro/_index.md
+++ b/docs/content/docs/validation/00-intro/_index.md
@@ -6,7 +6,7 @@ headline: Documentation
# Overview
-Spine Validation is a Protobuf-centric validation framework that generates
+Spine Validation is a Protobuf-centric validation library that generates
type-safe validation code directly from your `.proto` definitions.
It allows you to describe constraints on fields, messages, and collections using
declarative options and then automatically enforces these constraints at runtime.
@@ -14,6 +14,22 @@ declarative options and then automatically enforces these constraints at runtime
The library is part of the Spine toolchain but can also be used independently
in any Java/Kotlin backend that models data using Protocol Buffers.
+## Why
+
+While Protobuf provides a structured way to define data schemas, it does not include built-in
+mechanisms for enforcing domain-specific rules at runtime. Without validation, invalid or
+inconsistent data can slip through, leading to subtle bugs and potential system failures.
+
+Typically, developers address this by manually adding validation logic in application code
+or by invoking separate validation APIs. This ad-hoc approach can be error-prone and often
+results in duplicated, hard-to-maintain code scattered across the codebase.
+
+## How it works
+
+Spine Validation solves this by modifying the code generated by the Protobuf compiler (`protoc`).
+At build time, Validation injects assertions directly into the generated Java classes,
+enabling automatic enforcement of constraints without explicit API calls in application code.
+
## Components
Spine Validation consists of three main parts:
diff --git a/docs/content/docs/validation/01-getting-started/adding-to-build.md b/docs/content/docs/validation/01-getting-started/adding-to-build.md
index 1a461a8636..6cdec386ae 100644
--- a/docs/content/docs/validation/01-getting-started/adding-to-build.md
+++ b/docs/content/docs/validation/01-getting-started/adding-to-build.md
@@ -90,7 +90,7 @@ Add the Validation plugin to the build.
```kotlin
plugins {
module
- id("io.spine.validation") version "2.0.0-SNAPSHOT.415"
+ id("io.spine.validation") version "2.0.0-SNAPSHOT.416"
}
```
diff --git a/docs/content/docs/validation/03-built-in-options/_index.md b/docs/content/docs/validation/03-built-in-options/_index.md
index 45d4096550..c202ca4514 100644
--- a/docs/content/docs/validation/03-built-in-options/_index.md
+++ b/docs/content/docs/validation/03-built-in-options/_index.md
@@ -35,11 +35,8 @@ Use this section when you want to:
## Source of truth
-This catalog is based on the non-deprecated, validation-related options defined in
-[spine/options.proto](https://github.com/SpineEventEngine/base-libraries/blob/master/base/src/main/proto/spine/options.proto).
-
-For the canonical option definitions, see `spine/options.proto` in the Spine base libraries
-on GitHub: [spine/options.proto](https://github.com/SpineEventEngine/base-libraries/blob/master/base/src/main/proto/spine/options.proto).
+For the canonical option definitions, see [`spine/options.proto`][options.proto] in
+the Spine base libraries on GitHub.
## What’s next
- [Field-level options](field-level-options.md)
@@ -48,3 +45,5 @@ on GitHub: [spine/options.proto](https://github.com/SpineEventEngine/base-librar
- [Options for `repeated` and `map` fields](repeated-and-map-fields.md)
- [Using validators](../04-validators/)
- [Custom validation](../05-custom-validation/)
+
+[options.proto]: https://github.com/SpineEventEngine/base-libraries/blob/master/base/src/main/proto/spine/options.proto
diff --git a/docs/content/docs/validation/04-validators/_index.md b/docs/content/docs/validation/04-validators/_index.md
index e6969bc49a..611c0c15a2 100644
--- a/docs/content/docs/validation/04-validators/_index.md
+++ b/docs/content/docs/validation/04-validators/_index.md
@@ -108,6 +108,6 @@ their violations together.
- [Implement a validator](implement-a-validator.md)
- [Using `ValidatorRegistry`](validator-registry.md)
- [Custom validation](../05-custom-validation/)
-- [Architecture](../09-developers-guide/architecture.md)
+- [Developer's guide](../06-developers-guide/)
[auto-service]: https://github.com/google/auto/tree/main/service
diff --git a/docs/content/docs/validation/04-validators/implement-a-validator.md b/docs/content/docs/validation/04-validators/implement-a-validator.md
index a2f33f235c..868f347e11 100644
--- a/docs/content/docs/validation/04-validators/implement-a-validator.md
+++ b/docs/content/docs/validation/04-validators/implement-a-validator.md
@@ -137,7 +137,7 @@ to the nested field in error (for example, to `starts_at.seconds`).
- [Using `ValidatorRegistry`](validator-registry.md)
- [Custom validation](../05-custom-validation/)
-- [Architecture](../09-developers-guide/architecture.md)
+- [Developer's guide](../06-developers-guide/)
[TimestampValidator]: https://github.com/SpineEventEngine/validation/blob/master/jvm-runtime/src/main/kotlin/io/spine/validation/TimestampValidator.kt
diff --git a/docs/content/docs/validation/05-custom-validation/pass-to-compiler.md b/docs/content/docs/validation/05-custom-validation/pass-to-compiler.md
index 5fee42c9d7..2d51ab1499 100644
--- a/docs/content/docs/validation/05-custom-validation/pass-to-compiler.md
+++ b/docs/content/docs/validation/05-custom-validation/pass-to-compiler.md
@@ -72,7 +72,7 @@ in the Spine Time repository.
## What's next
- [Summary](summary.md)
-- [Architecture](../09-developers-guide/architecture.md)
+- [Developer's guide](../06-developers-guide/)
[time-gradle-plugin-kt]: https://github.com/SpineEventEngine/time/blob/master/gradle-plugin/src/main/kotlin/io/spine/tools/time/gradle/TimeGradlePlugin.kt
[time-validation-kt]: https://github.com/SpineEventEngine/time/blob/master/gradle-plugin/src/main/kotlin/io/spine/tools/time/gradle/TimeValidation.kt
diff --git a/docs/content/docs/validation/05-custom-validation/summary.md b/docs/content/docs/validation/05-custom-validation/summary.md
index 8394c1d23b..e9de403cc6 100644
--- a/docs/content/docs/validation/05-custom-validation/summary.md
+++ b/docs/content/docs/validation/05-custom-validation/summary.md
@@ -41,9 +41,9 @@ Spine Compiler.
## What's next
-- Explore library internals: [Architecture](../09-developers-guide/architecture.md)
+- Explore library internals: [Developer's guide](../06-developers-guide/)
- See the modules that back this pipeline:
- [Key modules](../09-developers-guide/key-modules.md)
+ [Key modules](../06-developers-guide/key-modules.md)
The complete source for all running examples is in the
[`validation` module][time-validation-src] of the Spine Time repository.
diff --git a/docs/content/docs/validation/06-developers-guide/_index.md b/docs/content/docs/validation/06-developers-guide/_index.md
new file mode 100644
index 0000000000..1767fba68c
--- /dev/null
+++ b/docs/content/docs/validation/06-developers-guide/_index.md
@@ -0,0 +1,90 @@
+---
+title: Overview and audience
+description: Deep dive into Spine Validation architecture and internals.
+headline: Documentation
+---
+
+# Overview and audience
+
+This guide is for contributors to Spine Validation and for readers who want a deep
+understanding of how the library works internally. It complements the User's Guide,
+which targets consumers of the library: where the User's Guide shows *how to use*
+Validation, this guide shows *how Validation is built*.
+
+## Who this guide is for
+
+Read this guide if you are:
+
+- contributing changes to the Validation library itself,
+- adding a new built-in validation option,
+- working on a fork or a downstream tool that integrates with Validation's internals,
+- or simply curious about how the pieces fit together.
+
+If you only want to apply validation rules to your own `.proto` files, the
+[User's Guide](../00-intro/) is the right starting point. The two guides are designed to
+be read independently: this one assumes you have already met Validation as a *user*.
+
+## The mental model
+
+Spine Validation has a single, load-bearing architectural decision: constraints declared
+in `.proto` files are *compiled*, not *interpreted*. There is no rule engine running at
+message construction time. Two stages cooperate:
+
+1. **At build time** — a Spine Compiler plugin reads validation options from your
+ Protobuf model, builds a language-agnostic representation of the constraints, and
+ emits Java code that enforces them. This work happens once, during the consumer
+ project's build.
+
+2. **At runtime** — generated code calls into a small library (`:jvm-runtime`) that
+ provides the validator entry points, the violation Protobuf types, and the exception
+ raised on failure. No reflection, no interpretation.
+
+Almost everything in this guide is, ultimately, an explanation of one half of that split
+or of the seam between them. Holding the picture in mind makes the rest of the guide
+easier to navigate; the [Architecture](architecture.md) page expands it in detail.
+
+## Prerequisite knowledge
+
+This guide assumes working familiarity with:
+
+- **Protocol Buffers** — message definitions, custom options, descriptors.
+- **Gradle** — multi-project builds, plugin application, configuration phases.
+- **Spine Compiler** — its role as a `protoc` post-processor and its plugin model.
+ See the [Spine Compiler documentation][spine-compiler] for an introduction.
+- **Spine Bounded Contexts** — events, views (projections), and reactions. The
+ validation model in `:context` is itself a Bounded Context, and several sections
+ describe it in those terms.
+
+You do not need to be an expert in any of these, but you should not be meeting them for
+the first time here.
+
+## How to read this guide
+
+The pages are arranged so that earlier sections introduce vocabulary used by later ones.
+Read [Architecture](architecture.md) first; the rest can be consulted in order or by
+need.
+
+1. [Architecture](architecture.md) — the compile-time/runtime split and the modules
+ that implement it.
+2. [Key modules](key-modules.md) — one-line descriptions of every module in the
+ repository, including the test modules. Use it as a reference.
+3. [The validation model](validation-model.md) — the language-agnostic model in
+ `:context`: views, events, reactions, and the `ValidationOption` SPI from the model side.
+4. [Java code generation](java-code-generation.md) — how the Spine Compiler plugin in
+ `:java` turns the model into Java validators.
+5. [Runtime library](runtime-library.md) — `MessageValidator`, `ValidationException`,
+ the violation Protobuf types, and runtime hooks in `:jvm-runtime`.
+6. [Extension points](extension-points.md) — `@Validator`, the validator
+ registry, and the `ValidationOption` SPI end-to-end.
+7. [Adding a new built-in validation option](adding-a-built-in-option.md) — the
+ contributor walkthrough that exercises the model, codegen, and runtime sections.
+8. [Testing strategy](testing-strategy.md) — a map of the test modules and how to
+ choose the right one for a new test.
+9. [Build, packaging, and release](build-and-release.md) — the multi-project build,
+ `:java-bundle`, and the Gradle plugin distribution flow.
+
+Each section assumes the architecture page has already been read. Sections 3–6 can be
+read in order for a top-down tour of the internals, or jumped into directly when
+diagnosing a specific area.
+
+[spine-compiler]: https://github.com/SpineEventEngine/compiler/
diff --git a/docs/content/docs/validation/06-developers-guide/adding-a-built-in-option.md b/docs/content/docs/validation/06-developers-guide/adding-a-built-in-option.md
new file mode 100644
index 0000000000..b7ab95a601
--- /dev/null
+++ b/docs/content/docs/validation/06-developers-guide/adding-a-built-in-option.md
@@ -0,0 +1,14 @@
+---
+title: Adding a new built-in validation option
+description: How contributors add a new standard validation option to the Spine Validation library.
+headline: Documentation
+---
+
+# Adding a new built-in validation option
+
+> This page is a placeholder. Content is in progress.
+
+This section will describe how a *contributor* adds a new **standard** option to the
+Validation library — the contributor-side counterpart to the User's Guide
+[Custom validation](../05-custom-validation/) section, which covers adding a custom
+option to a consumer project.
diff --git a/docs/content/docs/validation/06-developers-guide/architecture.md b/docs/content/docs/validation/06-developers-guide/architecture.md
new file mode 100644
index 0000000000..1591b8c963
--- /dev/null
+++ b/docs/content/docs/validation/06-developers-guide/architecture.md
@@ -0,0 +1,209 @@
+---
+title: Architecture
+description: How the Spine Validation modules collaborate at build time and at runtime.
+headline: Documentation
+---
+
+# Architecture
+
+Spine Validation has two main responsibilities, and the codebase is organized around the
+boundary between them:
+
+1. **At build time** — translate validation options declared in `.proto` files into Java
+ code that enforces those constraints inside the generated message and builder classes.
+2. **At runtime** — provide the small library of types that the generated code calls into
+ to evaluate constraints and report violations.
+
+This page describes how the modules in this repository implement that split. For an
+inventory of every module, see “[Key modules](key-modules.md)”.
+
+## The compile-time / runtime split
+
+The build-time work is performed by a Spine Compiler plugin. The Spine Compiler runs
+during the consumer project's build, after `protoc` produces the initial Java sources.
+The plugin inspects the Protobuf model, detects validation options on fields and messages,
+and injects validation logic into the generated classes.
+
+The runtime library is small on purpose. It exposes the SPI that user code or generated
+code calls (`MessageValidator`, `ValidatorRegistry`), the exception type
+(`ValidationException`), and the Protobuf types used to describe violations
+(`ConstraintViolation`, `ValidationError`, `TemplateString`). Everything else — the
+constraint logic itself — lives in the generated code, not in a runtime evaluator.
+
+This split is the main architectural decision in the project. Constraints are *compiled*,
+not *interpreted*. There is no rule engine running at message construction time; there is
+only the inlined Java code that the compiler plugin produced.
+
+## The build-time pipeline
+
+The compiler plugin is structured as two layers:
+
+- **`:context`** — a language-agnostic model of the validation rules discovered in a set of
+ `.proto` files. This module is a Spine Bounded Context: validation options become
+ events, events feed projections (views), and reactions wire them together.
+- **`:java`** — a `ValidationPlugin` subclass that consumes the model from `:context` and
+ emits Java code. Code emission is performed by two renderers:
+ `JavaValidationRenderer` for assertion-style options, and `SetOnceRenderer` for
+ `(set_once)`, whose semantics modify builder behavior rather than add a check.
+
+The base plugin class lives in
+[`ValidationPlugin.kt`](https://github.com/SpineEventEngine/validation/blob/master/context/src/main/kotlin/io/spine/tools/validation/ValidationPlugin.kt)
+and registers the built-in views and reactions:
+
+
+
+```kotlin
+public abstract class ValidationPlugin(
+ renderers: List> = emptyList(),
+ views: Set>> = setOf(),
+ viewRepositories: Set> = setOf(),
+ reactions: Set> = setOf(),
+) : Plugin(
+ renderers = renderers,
+ views = views + setOf(
+ RequiredFieldView::class.java,
+ PatternFieldView::class.java,
+ GoesFieldView::class.java,
+ DistinctFieldView::class.java,
+ ValidatedFieldView::class.java,
+ RangeFieldView::class.java,
+ MaxFieldView::class.java,
+ MinFieldView::class.java,
+ SetOnceFieldView::class.java,
+ ChoiceGroupView::class.java,
+ RequireMessageView::class.java,
+ ),
+ viewRepositories = viewRepositories,
+ reactions = reactions + setOf>(
+ RequiredReaction(),
+ IfMissingReaction(),
+ RangeReaction(),
+ MinReaction(),
+ MaxReaction(),
+ DistinctReaction(),
+ IfHasDuplicatesReaction(),
+ ValidateReaction(),
+ IfInvalidReaction(),
+ PatternReaction(),
+ ChoiceReaction(),
+ IsRequiredReaction(),
+ GoesReaction(),
+ SetOnceReaction(),
+ IfSetAgainReaction(),
+ RequireReaction()
+ )
+) // Plugin
+```
+
+The Java implementation in
+[`JavaValidationPlugin.kt`](https://github.com/SpineEventEngine/validation/blob/master/java/src/main/kotlin/io/spine/tools/validation/java/JavaValidationPlugin.kt)
+adds the Java renderers and folds in any custom options discovered through the SPI:
+
+
+
+```kotlin
+public open class JavaValidationPlugin : ValidationPlugin(
+ renderers = listOf(
+ JavaValidationRenderer(customGenerators = customOptions.map { it.generator }),
+ SetOnceRenderer()
+ ),
+ views = customOptions.flatMap { it.view }.toSet(),
+ reactions = customOptions.flatMap { it.reactions }.toSet(),
+)
+```
+
+`customOptions` is loaded via `ServiceLoader`, which is what makes the
+plugin extensible without recompiling the Validation library. See
+[`ValidationOption.kt`](https://github.com/SpineEventEngine/validation/blob/master/java/src/main/kotlin/io/spine/tools/validation/java/ValidationOption.kt)
+for the SPI itself, and the “[Custom validation](../05-custom-validation/)” section of
+the User's Guide for the consumer-facing walkthrough.
+
+## The runtime library
+
+Generated validation code depends only on `:jvm-runtime`. The most important entry points
+are:
+
+- [`MessageValidator`](https://github.com/SpineEventEngine/validation/blob/master/jvm-runtime/src/main/kotlin/io/spine/validation/MessageValidator.kt)
+ — SPI for attaching custom validators to message types, including types declared in
+ third-party `.proto` files. See the User's Guide “[Using validators](../04-validators/)”
+ section.
+- [`ValidatorRegistry`](https://github.com/SpineEventEngine/validation/blob/master/jvm-runtime/src/main/kotlin/io/spine/validation/ValidatorRegistry.kt)
+ — discovers and applies `MessageValidator` implementations.
+- [`validation_error.proto`](https://github.com/SpineEventEngine/validation/blob/master/jvm-runtime/src/main/proto/spine/validation/validation_error.proto)
+ — defines `ValidationError` and `ConstraintViolation`, the structured shape of violation
+ reports.
+- [`error_message.proto`](https://github.com/SpineEventEngine/validation/blob/master/jvm-runtime/src/main/proto/spine/validation/error_message.proto)
+ — defines `TemplateString`, the placeholder format used by error messages.
+
+The runtime library does not parse `.proto` files, does not maintain a rule registry, and
+does not interpret constraints. It contains only the types that the generated code and
+user code share.
+
+## Distribution and consumer wiring
+
+Two small modules exist purely to make the plugin usable from a consumer project:
+
+- **`:java-bundle`** — repackages `:java` and its non-shared transitive dependencies as a
+ single fat JAR. The Spine Compiler loads validation as a single classpath entry, so
+ bundling avoids dependency resolution surprises in the compiler classloader.
+- **`:gradle-plugin`** — the `io.spine.validation` Gradle plugin. When applied to a
+ consumer project it registers `:java-bundle` on the Spine Compiler's user classpath,
+ inserts `JavaValidationPlugin` into the compiler's plugin list, and adds `:jvm-runtime`
+ to the consumer's `implementation` configuration so generated code compiles and runs.
+ See
+ [`ValidationGradlePlugin.kt`](https://github.com/SpineEventEngine/validation/blob/master/gradle-plugin/src/main/kotlin/io/spine/tools/validation/gradle/ValidationGradlePlugin.kt).
+
+## The end-to-end picture
+
+The diagram below shows what happens from the moment a developer writes a `.proto` file
+with validation options through to the runtime check that fires when a message is built.
+
+```mermaid
+%%{init: {"flowchart": {"subGraphTitleMargin": {"top": 10, "bottom": 15}}}}%%
+flowchart TD
+ proto[".proto with validation options"]
+ subgraph build["Consumer build (Gradle)"]
+ gradle[":gradle-plugin io.spine.validation"]
+ compiler["Spine Compiler"]
+ bundle[":java-bundle (JavaValidationPlugin)"]
+ ctx[":context views + reactions"]
+ renderers["JavaValidationRenderer SetOnceRenderer"]
+ gen["Generated Java message + builder classes"]
+ end
+ subgraph rt["Runtime"]
+ runtime[":jvm-runtime MessageValidator, ValidationException, ConstraintViolation"]
+ app["Application code builder.build()"]
+ end
+
+ proto --> compiler
+ gradle --> compiler
+ gradle -. registers .-> bundle
+ compiler --> bundle
+ bundle --> ctx
+ ctx --> renderers
+ renderers --> gen
+ gen --> app
+ app --> runtime
+ runtime -- on violation --> app
+```
+
+At a glance:
+
+- The Gradle plugin is the only thing the consumer applies. It pulls in the bundle and the
+ runtime library transparently.
+- The Spine Compiler invokes `JavaValidationPlugin`, which uses `:context` to build a
+ language-agnostic model of the constraints, then runs Java renderers to inject code into
+ the classes that `protoc` generated.
+- At runtime, the application calls into generated code, typically through
+ `Builder.build()` or a generated `validate()` method.
+ The generated code uses types from `:jvm-runtime` to report violations.
+
+## What's next
+
+- [Key modules](key-modules.md) — the full module inventory, including the test modules.
diff --git a/docs/content/docs/validation/06-developers-guide/build-and-release.md b/docs/content/docs/validation/06-developers-guide/build-and-release.md
new file mode 100644
index 0000000000..1a20595bc2
--- /dev/null
+++ b/docs/content/docs/validation/06-developers-guide/build-and-release.md
@@ -0,0 +1,15 @@
+---
+title: Build, packaging, and release
+description: How the multi-project build is wired and how Spine Validation artifacts are produced.
+headline: Documentation
+---
+
+# Build, packaging, and release
+
+> This page is a placeholder. Content is in progress.
+
+This section will describe how the multi-project build is wired and how artifacts are
+produced: the Gradle multi-project layout and the relationship between `:java`,
+`:java-bundle`, and `:gradle-plugin`; why `:java-bundle` exists (a fat JAR for compiler
+plugin distribution); and the version flow from this repository to downstream
+consumers of the Gradle plugin.
diff --git a/docs/content/docs/validation/06-developers-guide/extension-points.md b/docs/content/docs/validation/06-developers-guide/extension-points.md
new file mode 100644
index 0000000000..05a10c5778
--- /dev/null
+++ b/docs/content/docs/validation/06-developers-guide/extension-points.md
@@ -0,0 +1,15 @@
+---
+title: Extension points
+description: Internals behind the public extension surface of Spine Validation.
+headline: Documentation
+---
+
+# Extension points
+
+> This page is a placeholder. Content is in progress.
+
+This section will cover the internals behind the public extension surface, complementing
+the User's Guide [Using validators](../04-validators/) and
+[Custom validation](../05-custom-validation/) sections: `@Validator` and the validator
+registry (discovery, ordering, lifecycle); the `ValidationOption` SPI end-to-end; and
+the constraints on what extensions can and cannot do, and why.
diff --git a/docs/content/docs/validation/06-developers-guide/java-code-generation.md b/docs/content/docs/validation/06-developers-guide/java-code-generation.md
new file mode 100644
index 0000000000..c90d85b656
--- /dev/null
+++ b/docs/content/docs/validation/06-developers-guide/java-code-generation.md
@@ -0,0 +1,350 @@
+---
+title: Java code generation
+description: How the Spine Compiler plugin in `:java` produces validation code from the model.
+headline: Documentation
+---
+
+# Java code generation
+
+The `:java` module turns the populated projections produced by `:context` (see
+“[The validation model](validation-model.md)”) into Java code that is injected into
+Protobuf-generated message and builder classes. By the time the Spine Compiler invokes
+this module, every constraint discovered in the consumer's `.proto` files already lives
+in a view, and `:java`'s job is to ask each view what it knows and translate that into
+Java.
+
+The work is split across two renderers and a small set of supporting types:
+
+- [`JavaValidationRenderer`][java-validation-renderer] — the main renderer. It walks
+ every compilation message, asks each registered [`OptionGenerator`][option-generator]
+ for the code that implements its option on that message, and hands the result to
+ [`ValidationCodeInjector`][validation-code-injector] for placement inside the message
+ and its builder.
+- [`SetOnceRenderer`][set-once-renderer] — a separate renderer for `(set_once)`. The
+ option does not contribute a check to the `validate()` method; instead it modifies the
+ builder so that setters refuse to overwrite an already-assigned value. The render
+ pipeline is therefore different enough that it lives apart from
+ `JavaValidationRenderer`.
+
+The two renderers and the SPI that lets custom options plug into them are described on
+this page.
+
+## The plugin entry point
+
+`JavaValidationPlugin` extends the language-agnostic `ValidationPlugin` from `:context`,
+adds the two Java renderers, and folds in any custom options discovered through the
+`ValidationOption` SPI:
+
+
+
+```kotlin
+public open class JavaValidationPlugin : ValidationPlugin(
+ renderers = listOf(
+ JavaValidationRenderer(customGenerators = customOptions.map { it.generator }),
+ SetOnceRenderer()
+ ),
+ views = customOptions.flatMap { it.view }.toSet(),
+ reactions = customOptions.flatMap { it.reactions }.toSet(),
+)
+```
+
+Custom options contribute three kinds of artefacts: views and reactions for `:context`
+(covered in “[The validation model](validation-model.md)”) and a `generator` for `:java`.
+The plugin is the single point where all three are registered.
+
+## The render lifecycle
+
+`JavaValidationRenderer` runs once per `SourceFileSet`. The Spine Compiler invokes it
+with the Java sources that `protoc` has already produced. The renderer iterates over
+every message in the set, asks every generator what it has to contribute for that
+message, and emits a single [`MessageValidationCode`][message-validation-code] bundle
+per message:
+
+```kotlin
+override fun render(sources: SourceFileSet) {
+ // We receive `grpc` and `kotlin` output sources roots here as well.
+ // As for now, we modify only `java` sources.
+ if (!sources.hasJavaRoot) {
+ return
+ }
+
+ findMessageTypes()
+ .forEach { message ->
+ val code = generateCode(message)
+ val file = sources.javaFileOf(message)
+ file.render(code)
+ }
+}
+```
+
+Three properties of this loop are worth highlighting:
+
+- The renderer visits **every** message, not just messages with declared constraints.
+ A message with no options still becomes a `ValidatableMessage` whose generated
+ `validate()` consults `ValidatorRegistry` (see
+ “[Runtime library](runtime-library.md)”).
+- The list of generators is fixed during one renderer run. Built-ins come from
+ `builtInGenerators()`; custom generators arrive through the constructor. They are
+ composed once and `inject(querying, typeSystem)` is called on each before the first
+ `codeFor()` invocation.
+- All collaboration with the model happens through `Querying`. The renderer never
+ reads `.proto` files; it only reads the projections that `:context` populated.
+
+## The `OptionGenerator` SPI
+
+`OptionGenerator` is the abstraction that decouples the renderer from the specifics of
+any single option. Every built-in option handled by `JavaValidationRenderer`, and every
+custom Java validation option, is implemented as a subclass:
+
+
+
+```kotlin
+public abstract class OptionGenerator {
+
+ protected lateinit var querying: Querying
+ protected lateinit var typeSystem: TypeSystem
+
+ public abstract fun codeFor(type: TypeName): List
+
+ public fun inject(querying: Querying, typeSystem: TypeSystem) {
+ check(!::querying.isInitialized) {
+ "`inject()` must be called exactly once on `${this::class.simpleName}`."
+ }
+ this.querying = querying
+ this.typeSystem = typeSystem
+ }
+}
+```
+
+A typical generator queries its own projection, filters by the message currently being
+processed, and emits one `SingleOptionCode` per option application. `RequiredGenerator`
+is representative:
+
+
+
+```kotlin
+internal class RequiredGenerator : OptionGeneratorWithConverter() {
+
+ private val allRequiredFields by lazy {
+ querying.select()
+ .all()
+ }
+
+ override fun codeFor(type: TypeName): List =
+ allRequiredFields
+ .filter { it.id.type == type }
+ .map { GenerateRequired(it, converter).code() }
+}
+```
+
+One convenience subclass of `OptionGenerator` exists today:
+
+- [`OptionGeneratorWithConverter`][option-generator-with-converter] — adds a lazily
+ constructed `JavaValueConverter` for translating Protobuf default values into Java
+ literals. Generators that need to compare a field against its type-specific default
+ (`(required)`, `(distinct)`, the bound options) extend this class.
+
+Generators may also keep their own per-run state, as long as nothing that depends on
+`Querying` or `TypeSystem` is touched before `inject()` returns.
+
+## What the generator produces
+
+A generator returns a list of [`SingleOptionCode`][single-option-code] objects, one per
+option application in the message. The shape is intentionally minimal:
+
+
+
+```kotlin
+public class SingleOptionCode(
+ public val constraint: CodeBlock,
+ public val fields: List> = emptyList(),
+ public val methods: List = emptyList(),
+)
+```
+
+- `constraint` is the body that goes into the generated `validate()` method. It runs in
+ a known scope that exposes a few well-defined variables (see “[The validate scope](#the-validate-scope)”
+ below).
+- `fields` and `methods` are class-level declarations. They are how an option can carry
+ precomputed state — for example, `PatternGenerator` declares one
+ `private static final java.util.regex.Pattern` field per `(pattern)` application so the
+ pattern is compiled once and reused across calls.
+
+The generated `constraint` block is plain Java text built from typed expressions. The
+following snippet from `RequiredGenerator` is typical:
+
+```kotlin
+val constraint = CodeBlock(
+ """
+ if (${field.hasDefaultValue()}) {
+ var fieldPath = ${parentPath.resolve(field.name)};
+ var typeName = ${parentName.orElse(declaringType)};
+ var violation = ${violation(ReadVar("fieldPath"), ReadVar("typeName"))};
+ $violations.add(violation);
+ }
+ """.trimIndent()
+)
+```
+
+There is no template engine. Generators interpolate Kotlin values for class names, field
+references, and helper expressions into a Java code string. The expression types under
+[`expression/`][expression-pkg] (`FieldPaths`, `TemplateStrings`, `ConstraintViolations`,
+`ClassNames`, `Strings`, `UnsetValue`, …) are the building blocks; they expose typed
+methods like `parentPath.resolve(field.name)` so the interpolation reads as code rather
+than string concatenation.
+
+### The validate scope
+
+Every constraint block is injected into the same enclosing method, so the generated
+code can rely on a fixed set of in-scope variables. They are declared in
+[`ValidateScope`][validate-scope]:
+
+| Variable | Java type | Role |
+|---------------|----------------------------------------|---------------------------------------------------------------------------------|
+| `violations` | `ArrayList` | Accumulator. A constraint adds a violation by `violations.add(violation)`. |
+| `parentPath` | `io.spine.base.FieldPath` | Path from the validation root to the current message. Empty for top-level use. |
+| `parentName` | `io.spine.type.TypeName?` (nullable) | Name of the type that triggered validation. Non-null only for nested messages. |
+
+The companion [`MessageScope`][message-scope] exposes an implicit `this` reference for
+generators that need to read the message's own fields. Together, the two scopes are the
+only state a constraint block can assume; everything else must be derived from view
+state at generation time or carried by class-level fields the generator declares.
+
+## Injecting the code into the PSI
+
+After `JavaValidationRenderer` has assembled a `MessageValidationCode` for a message, it
+hands the bundle to [`ValidationCodeInjector`][validation-code-injector]. The injector
+operates on the IntelliJ PSI representation of the already-generated Java file. In the
+main validation renderer, it is the component that mutates the message and builder PSI:
+
+```kotlin
+fun inject(code: MessageValidationCode, messageClass: PsiClass) {
+ val builderClass = messageClass.nested("Builder")
+ execute {
+ messageClass.apply {
+ implementValidatableMessage()
+ declareValidateMethod(code.constraints)
+ declareSupportingFields(code.fields)
+ declareSupportingMethods(code.methods)
+ }
+ builderClass.apply {
+ implementValidatingBuilder(messageClass)
+ injectValidationIntoBuildMethod()
+ annotateBuildReturnType()
+ annotateBuildPartialReturnType()
+ }
+ }
+}
+```
+
+The injector encodes the conventions for the shape of every generated validator:
+
+- The message class is made to implement `ValidatableMessage`, gaining a
+ `validate(parentPath, parentName)` method whose body concatenates every constraint
+ block produced by the generators and finishes with a call into `ValidatorRegistry`
+ (see “[Runtime library](runtime-library.md)”). The method returns
+ `Optional` rather than throwing, so a built message can be
+ re-validated without paying for an exception.
+- The builder is made to implement `ValidatingBuilder`. Its `build()` method is wrapped:
+ the existing return is preceded by a call to `validate()`, and any violation is thrown
+ as `ValidationException`. For constraints produced by `OptionGenerator`s, this is where
+ validation errors become exceptions; `(set_once)` is handled separately and throws from
+ builder mutators.
+- The `build()` return type is annotated `@Validated` and `buildPartial()` is annotated
+ `@NonValidated`. These markers are how downstream code (and IDE tooling) tell the two
+ results apart at a glance.
+
+Because the injector controls placement, generators are not allowed to write methods,
+fields, or interface declarations directly into the file. They contribute snippets and
+declarations through `SingleOptionCode`; the injector decides where each lands.
+
+## The `(set_once)` renderer
+
+`SetOnceRenderer` is a `JavaRenderer` in its own right, registered alongside
+`JavaValidationRenderer` by `JavaValidationPlugin`. It exists because `(set_once)`
+semantics modify builder behavior rather than add a check to `validate()`: the option
+must reject any setter call that would change an already-assigned value, and that
+rejection has to fire from inside the setter itself.
+
+The renderer queries `SetOnceField` projections, dispatches to a field-type-specific
+implementation of [`SetOnceJavaConstraints`][set-once-constraints]
+(`SetOnceMessageField`, `SetOnceEnumField`, `SetOnceStringField`,
+`SetOnceBooleanField`, `SetOnceBytesField`, `SetOnceNumberField`), and lets that
+implementation modify every relevant setter, merge method, and the
+`mergeFrom(CodedInputStream, …)` switch in the builder. The mechanics differ enough
+between primitive, string, bytes, enum, and message fields that the per-type split is
+worthwhile.
+
+From the renderer's point of view, the result is a builder whose mutating entry points
+all call `throwIfNotDefaultAndNotSame` before assigning. The `(set_once)` constraint
+therefore never appears in the generated `validate()` method; it is a property of the
+builder, not of the message. This is also why `(set_once)` does not participate in the
+`OptionGenerator` SPI — a custom option that needed similar semantics would need its
+own renderer and its own per-type logic, not a generator slot.
+
+## Plugging in custom options
+
+Custom options participate in code generation through the third member of the
+`ValidationOption` SPI:
+
+
+
+```kotlin
+public interface ValidationOption {
+
+ public val reactions: Set>
+ public val view: Set>>
+ public val generator: OptionGenerator
+}
+```
+
+`reactions` and `view` contribute model-side artefacts (see
+“[The validation model](validation-model.md)”). `generator` is the Java-side
+contribution. `JavaValidationPlugin` discovers `ValidationOption` implementations
+through `ServiceLoader` and passes each `generator` to `JavaValidationRenderer`, which
+appends them to the built-in list. Custom generators are therefore indistinguishable from
+built-ins at run time: they receive the same `Querying` and `TypeSystem`, query their
+own projections, and return `SingleOptionCode` instances that are merged into the same
+`validate()` method with the rest.
+
+The end-to-end walkthrough — declaring the option, modeling it in `:context`, writing a
+generator, and wiring it through `META-INF/services` — lives in
+“[Adding a new built-in validation option](adding-a-built-in-option.md)”. The
+consumer-facing variant of the same SPI is covered by
+“[Custom validation](../05-custom-validation/)” in the User's Guide.
+
+## What's next
+
+- [Runtime library](runtime-library.md) — the types in `:jvm-runtime` that the
+ generated code calls into at execution time.
+- [Extension points](extension-points.md) — the public extension surface that
+ `OptionGenerator`, `ValidationOption`, and `@Validator` collectively expose.
+- [Adding a new built-in validation option](adding-a-built-in-option.md) — the
+ contributor walkthrough that ties this page to the rest of the guide.
+
+[java-validation-renderer]: https://github.com/SpineEventEngine/validation/blob/master/java/src/main/kotlin/io/spine/tools/validation/java/JavaValidationRenderer.kt
+[set-once-renderer]: https://github.com/SpineEventEngine/validation/blob/master/java/src/main/kotlin/io/spine/tools/validation/java/setonce/SetOnceRenderer.kt
+[option-generator]: https://github.com/SpineEventEngine/validation/blob/master/java/src/main/kotlin/io/spine/tools/validation/java/generate/OptionGenerator.kt
+[option-generator-with-converter]: https://github.com/SpineEventEngine/validation/blob/master/java/src/main/kotlin/io/spine/tools/validation/java/generate/OptionGeneratorWithConverter.kt
+[single-option-code]: https://github.com/SpineEventEngine/validation/blob/master/java/src/main/kotlin/io/spine/tools/validation/java/generate/SingleOptionCode.kt
+[message-validation-code]: https://github.com/SpineEventEngine/validation/blob/master/java/src/main/kotlin/io/spine/tools/validation/java/generate/MessageValidationCode.kt
+[validation-code-injector]: https://github.com/SpineEventEngine/validation/blob/master/java/src/main/kotlin/io/spine/tools/validation/java/generate/ValidationCodeInjector.kt
+[validate-scope]: https://github.com/SpineEventEngine/validation/blob/master/java/src/main/kotlin/io/spine/tools/validation/java/generate/ValidateScope.kt
+[message-scope]: https://github.com/SpineEventEngine/validation/blob/master/java/src/main/kotlin/io/spine/tools/validation/java/generate/MessageScope.kt
+[set-once-constraints]: https://github.com/SpineEventEngine/validation/blob/master/java/src/main/kotlin/io/spine/tools/validation/java/setonce/SetOnceJavaConstraints.kt
+[expression-pkg]: https://github.com/SpineEventEngine/validation/tree/master/java/src/main/kotlin/io/spine/tools/validation/java/expression
diff --git a/docs/content/docs/validation/06-developers-guide/key-modules.md b/docs/content/docs/validation/06-developers-guide/key-modules.md
new file mode 100644
index 0000000000..4ab968dcb7
--- /dev/null
+++ b/docs/content/docs/validation/06-developers-guide/key-modules.md
@@ -0,0 +1,32 @@
+---
+title: Key Modules
+description: Overview of the main modules in the Spine Validation project.
+headline: Documentation
+---
+
+# Key modules
+
+This repository is a Gradle multi-project build. Module names below are shown as Gradle
+project paths like `:java` and `:tests:vanilla`.
+
+## Core modules
+
+- **`:context`**: Language-agnostic validation model and built-in option handling for views and reactions shared by language plugins.
+- **`:java`**: Spine Compiler plugin for Java: generates/injects validation code; loads custom options via `ValidationOption` SPI.
+- **`:jvm-runtime`**: Runtime library used by generated code: `ValidationException`, validation/constraint APIs, `MessageValidator`, and error Protobuf types.
+- **`:java-bundle`**: Fat JAR bundling `:java` for distribution, which is the artifact typically used as the compiler plugin dependency.
+- **`:gradle-plugin`**: The `io.spine.validation` Gradle plugin that configures Spine Compiler to run the Validation compiler for consumer projects.
+- **`:docs`**: Sources for the Hugo documentation site, scripts, and example projects used in docs.
+
+## Test modules
+
+- **`:context-tests`**: Prototap-based compilation tests for `:context`, focusing on invalid option usage and error reporting.
+- **`:tests`**: Parent project for integration tests that run the compiler plugins and exercise generated code.
+- **`:tests:vanilla`**: “Vanilla” integration tests: validation without any custom extensions.
+- **`:tests:extensions`**: Example implementation of the `(currency)` custom option used by test suites to verify custom reactions, views, and generators.
+- **`:tests:consumer`**: Integration tests for a consuming project that uses validation plus custom extensions.
+- **`:tests:consumer-dependency`**: A dependency module with `.proto` sources used by `:tests:consumer` to verify “protos in dependencies” scenarios.
+- **`:tests:validator`**: Integration tests for custom `MessageValidator`s annotated with `@Validator`.
+- **`:tests:validator-dependency`**: A dependency module used by `:tests:validator` for validator-related dependency scenarios.
+- **`:tests:runtime`**: Tests focused on runtime behavior of validation APIs and error messages.
+- **`:tests:validating`**: Shared fixtures and tests for validation behavior across multiple scenarios, including `testFixtures`.
diff --git a/docs/content/docs/validation/06-developers-guide/runtime-library.md b/docs/content/docs/validation/06-developers-guide/runtime-library.md
new file mode 100644
index 0000000000..8e686add49
--- /dev/null
+++ b/docs/content/docs/validation/06-developers-guide/runtime-library.md
@@ -0,0 +1,14 @@
+---
+title: Runtime library
+description: What ships in `:jvm-runtime` and how generated code uses it at execution time.
+headline: Documentation
+---
+
+# Runtime library
+
+> This page is a placeholder. Content is in progress.
+
+This section will describe what ships in `:jvm-runtime` and how generated code uses it:
+`MessageValidator`, the validation/constraint APIs, `ValidationException`, the error
+Protobuf types and how violations are surfaced, and the runtime extension hooks
+including `@Validator`.
diff --git a/docs/content/docs/validation/06-developers-guide/testing-strategy.md b/docs/content/docs/validation/06-developers-guide/testing-strategy.md
new file mode 100644
index 0000000000..d1fc765a36
--- /dev/null
+++ b/docs/content/docs/validation/06-developers-guide/testing-strategy.md
@@ -0,0 +1,17 @@
+---
+title: Testing strategy
+description: Map of the test modules in Spine Validation and guidance on choosing the right one.
+headline: Documentation
+---
+
+# Testing strategy
+
+> This page is a placeholder. Content is in progress.
+
+This section will map the test modules and explain when to add to which:
+`:context-tests` for Prototap-based compilation tests of the model;
+`:tests:vanilla` for baseline integration without custom extensions;
+`:tests:extensions`, `:tests:consumer`, and `:tests:consumer-dependency` for custom
+options and consumer-side scenarios; `:tests:validator` and
+`:tests:validator-dependency` for `@Validator` scenarios; and `:tests:runtime` and
+`:tests:validating` for runtime behavior and shared fixtures.
diff --git a/docs/content/docs/validation/06-developers-guide/validation-model.md b/docs/content/docs/validation/06-developers-guide/validation-model.md
new file mode 100644
index 0000000000..3354860a17
--- /dev/null
+++ b/docs/content/docs/validation/06-developers-guide/validation-model.md
@@ -0,0 +1,385 @@
+---
+title: The validation model
+description: Deep dive into the language-agnostic validation model implemented as a Spine Bounded Context.
+headline: Documentation
+---
+
+# The validation model
+
+The `:context` module is the language-agnostic half of the Compiler plugin. It does not
+emit code. It builds a description of the constraints declared in a set of `.proto` files
+and exposes that description as a Bounded Context: an event-sourced model whose state can
+be queried by language-specific renderers.
+
+A code-generating renderer in `:java` (see “[Java code generation](java-code-generation.md)”)
+consumes that state to produce validation logic. A renderer for another target language
+could consume the same state without changes to `:context`.
+
+## The Bounded Context shape
+
+The model is built from four kinds of artefacts that a Spine Bounded Context combines:
+
+- **Domain events** (in `events.proto`) — a closed set of `*Discovered` events, one per
+ built-in validation option. Each event carries the data that the rest of the model
+ needs to know about a single discovered constraint.
+- **Views (projections)** (in `views.proto`) — one projection per option, keyed by the
+ field, oneof group, or message that owns the constraint. Each view holds the data the
+ renderer will read.
+- **Reactions** (Kotlin classes under `option/` and `bound/`) — handlers that subscribe
+ to the upstream `FieldOptionDiscovered`, `OneofOptionDiscovered`, and
+ `MessageOptionDiscovered` events emitted by the Spine Compiler, validate the option's
+ applicability, and emit the matching `*Discovered` domain event.
+- **Plugin wiring** — `ValidationPlugin` registers all built-in views and reactions with
+ the Compiler, so they are instantiated and connected for every consumer build.
+
+`ValidationPlugin` is the entry point that pulls the four pieces together. It is
+language-agnostic and is extended by `JavaValidationPlugin` in `:java` (which only adds
+renderers and folds in custom-option contributions, see
+“[Architecture](architecture.md)”).
+
+## The lifecycle of an option
+
+The model never reads `.proto` files directly. The Spine Compiler does the parsing and
+publishes generic option events. A reaction in `:context` matches each event by option
+name, decides whether the option applies, and either emits a domain event or stays
+silent. The matching projection then folds the event into its state. By the time the
+build leaves `:context`, the model is just a set of populated views.
+
+The flow for a single field-level option looks like this:
+
+```mermaid
+sequenceDiagram
+ participant Compiler as Spine Compiler
+ participant Bus as EventBus
+ participant Reaction
+ participant View as Projection
+
+ Compiler->>Bus: FieldOptionDiscovered
+ Bus->>Reaction: deliver
+ Note over Reaction: Filter by option.name Check applicability Validate placeholders
+ Reaction->>Bus: SomethingDiscovered
+ Bus->>View: deliver
+ Note over View: alter — state ready for renderer
+```
+
+`OneofOptionDiscovered` and `MessageOptionDiscovered` follow the same shape, identifying
+the projection by the corresponding declaration in the parsed Protobuf file.
+
+### Event filtering by option name
+
+Reactions select their input by matching the upstream event's `option.name` field. The
+constant [`OPTION_NAME`][option-name] is the field path; the option-name constants in
+[`OptionNames.kt`][option-names] are the equality values:
+
+
+
+```kotlin
+internal class RequiredReaction : Reaction() {
+
+ @React
+ override fun whenever(
+ @External @Where(field = OPTION_NAME, equals = REQUIRED)
+ event: FieldOptionDiscovered,
+ ): EitherOf2 {
+ val field = event.subject
+ val file = event.file
+ checkFieldType(field, file)
+
+ if (!event.option.boolValue) {
+ return ignore()
+ }
+
+ val defaultMessage = defaultErrorMessage()
+ return requiredFieldDiscovered {
+ id = field.ref
+ subject = field
+ defaultErrorMessage = defaultMessage
+ }.asA()
+ }
+}
+```
+
+Two details are worth highlighting:
+
+- The reaction's return type uses `EitherOf2<…, NoReaction>`. The `(required) = false`
+ case returns `NoReaction`, which is how the model communicates *“the option is applied
+ correctly but disabled”*. No domain event is emitted, so no projection is created, and
+ the renderer sees the field as if the option were absent.
+- The reaction is the only place where applicability is checked. By the time the
+ matching `*Discovered` event reaches the bus, the option has been confirmed valid for
+ this field. Projections trust this contract and never re-check.
+
+### The discovered event
+
+For every option there is a `