diff --git a/.agents/advanced-safety-rules.md b/.agents/advanced-safety-rules.md
deleted file mode 100644
index e410581..0000000
--- a/.agents/advanced-safety-rules.md
+++ /dev/null
@@ -1,6 +0,0 @@
-# π¨ Advanced safety rules
-
-- Do **not** auto-update external dependencies without explicit request.
-- Do **not** inject analytics or telemetry code.
-- Flag any usage of unsafe constructs (e.g., reflection, I/O on the main thread).
-- Avoid generating blocking calls inside coroutines.
diff --git a/.agents/documentation-guidelines.md b/.agents/documentation-guidelines.md
deleted file mode 100644
index 6c9c1ba..0000000
--- a/.agents/documentation-guidelines.md
+++ /dev/null
@@ -1,14 +0,0 @@
-# Documentation & comments
-
-## Commenting guidelines
-- Avoid inline comments in production code unless necessary.
-- Inline comments are helpful in tests.
-- When using TODO comments, follow the format on the [dedicated page][todo-comments].
-- File and directory names should be formatted as code.
-
-## Avoid widows, runts, orphans, or rivers
-
-Agents should **AVOID** text flow patters illustrated
-on [this diagram](widow-runt-orphan.jpg).
-
-[todo-comments]: https://github.com/SpineEventEngine/documentation/wiki/TODO-comments
diff --git a/.agents/documentation-tasks.md b/.agents/documentation-tasks.md
deleted file mode 100644
index 8ac4660..0000000
--- a/.agents/documentation-tasks.md
+++ /dev/null
@@ -1,20 +0,0 @@
-# π Documentation tasks
-
-1. Ensure all public and internal APIs have KDoc examples.
-2. Add in-line code blocks for clarity in tests.
-3. Convert inline API comments in Java to KDoc in Kotlin:
- ```java
- // Literal string to be inlined whenever a placeholder references a non-existent argument.
- private final String missingArgumentMessage = "[MISSING ARGUMENT]";
- ```
- transforms to:
- ```kotlin
- /**
- * Literal string to be inlined whenever a placeholder references a non-existent argument.
- */
- private val missingArgumentMessage = "[MISSING ARGUMENT]"
- ```
-
-4. Javadoc -> KDoc conversion tasks:
- - Remove `
` tags in the line with text: `"
This"` -> `"This"`.
- - Replace `
` with empty line if the tag is the only text in the line.
diff --git a/.agents/guidelines b/.agents/guidelines
new file mode 120000
index 0000000..6f9d966
--- /dev/null
+++ b/.agents/guidelines
@@ -0,0 +1 @@
+shared/guidelines
\ No newline at end of file
diff --git a/.agents/memory/reference/cache-warm-window.md b/.agents/memory/reference/cache-warm-window.md
index 796dd4d..424fdbb 100644
--- a/.agents/memory/reference/cache-warm-window.md
+++ b/.agents/memory/reference/cache-warm-window.md
@@ -15,10 +15,10 @@ same cache slot β provided they fall within the TTL.
- Default: **5 minutes** (applies to all non-subscription auth)
- With `ENABLE_PROMPT_CACHING_1H=1` in `~/.claude/settings.json`: **1 hour**
-Developers must have `ENABLE_PROMPT_CACHING_1H=1` set, otherwise the
+Developers must have `ENABLE_PROMPT_CACHING_1H=1` set; otherwise the
window is too short for cross-session hits to occur reliably.
-This setting will work ONLY for Claude Code which runs the CLI binary.
-It will not work for JetBrains Air or any other IDE plugin which does not
+This setting will work ONLY for Claude Code that runs the CLI binary.
+It will not work for JetBrains Air or any other IDE plugin that does not
run the Claude Code CLI binary.
**Cache is per Anthropic workspace.** All developers authenticated via the
diff --git a/.agents/project.md b/.agents/project.md
deleted file mode 100644
index 77c3f9c..0000000
--- a/.agents/project.md
+++ /dev/null
@@ -1,59 +0,0 @@
-# Project: documentation
-
-## Overview
-
-This repository is the **documentation aggregator and content host** for the
-Spine SDK. It owns the Hugo site setup that gathers documentation from sibling
-SDK repos (currently `SpineEventEngine/validation`) as Hugo modules, and it
-also stores original Markdown content under `docs/`. The repo additionally
-serves as the GitHub Wiki source for committer-facing documentation about
-contributing to the framework.
-
-The public site at [spine.io](https://spine.io) is built from
-`SpineEventEngine/SpineEventEngine.github.io`, which **imports this repo's
-`docs/` directory as a Hugo module**. Edits made here flow to the public site
-when `SpineEventEngine.github.io` bumps its module pin.
-
-## Architecture
-
-**Role in the org:** documentation aggregator + content host.
-
-**Stack.** A Hugo + Node project at heart. Gradle is a thin task-runner
-wrapper around Hugo, Node, and Go tooling β not a JVM build in any meaningful
-sense, so JVM coding conventions do not apply here.
-
-- `docs/` β published content, Hugo site root, exported as a Hugo module to
- `SpineEventEngine.github.io`.
-- `docs/_preview/` β local-only Hugo setup for running the site during
- authoring (`./gradlew :runSite`, or `hugo server` from this directory).
-- `docs/_code/examples/*` β git submodules pinned at
- `spine-examples/{airport, blog, hello, kanban, todo-list}`. These are the
- **canonical source of embedded code samples**; this repo does not modify
- them.
-- `config/` β git submodule pointing at `SpineEventEngine/config`. Provides
- shared agent guidance, skills, and build config consumed across Spine SDK
- repos. Applied via the `Apply config` step.
-- Theme: components, layouts, and styles come from the `site-commons` Hugo
- theme (`github.com/SpineEventEngine/site-commons`).
-
-**Doc modules pulled in via `docs/hugo.toml`.** Currently only the
-`validation` repo contributes docs as a Hugo module. The README lists
-`framework` and `compiler` as examples of how to add more; they are not
-wired in today.
-
-**Key conventions and constraints (not obvious from the code):**
-
-- **Theme changes mirror to spine.io.** Any non-trivial change to
- `site-commons` usage here must also be applied in the main `spine.io` site
- repo, or the live site will diverge from preview.
-- **Embedded code must round-trip.** Code blocks in pages are generated from
- the `docs/_code` submodules by the [`embed-code`][embed-code] tool. Do not
- hand-edit embedded code blocks in Markdown; run `./gradlew :embedCode` and
- verify with `./gradlew :checkSamples`. See [`EMBEDDING.md`](../EMBEDDING.md).
-- **Submodules are pinned.** `docs/_code/examples/*` and `config` are pinned
- intentionally β do not bump them as a side effect of unrelated work.
-- **Link checking is required pre-PR.** Run the `check-links` skill before
- opening any PR that touches `docs/**` or `site/**`; CI runs the same check
- via `lychee.toml` against the rendered HTML.
-
-[embed-code]: https://github.com/SpineEventEngine/embed-code-go
diff --git a/.agents/project.md b/.agents/project.md
new file mode 120000
index 0000000..7e0bf9b
--- /dev/null
+++ b/.agents/project.md
@@ -0,0 +1 @@
+../docs/project.md
\ No newline at end of file
diff --git a/.agents/project.template.md b/.agents/project.template.md
deleted file mode 100644
index b6882e0..0000000
--- a/.agents/project.template.md
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-# Project:
-
-## Overview
-
-*One paragraph: what this repo is, what problem it solves, and its role in the
-Spine SDK organisation.*
-
-## Architecture
-
-*Role in the org: library / tool / Gradle plugin / application.
-Key patterns, public API boundaries, and constraints specific to this repo.*
-
-
diff --git a/.agents/safety-rules.md b/.agents/safety-rules.md
deleted file mode 100644
index e7fece3..0000000
--- a/.agents/safety-rules.md
+++ /dev/null
@@ -1,49 +0,0 @@
-# Safety rules
-
-- β All code must compile and pass static analysis.
-- β Do not auto-update external dependencies.
-- β Never use reflection or unsafe code without an explicit approval.
-- β No analytics or telemetry code.
-- β No blocking calls inside coroutines.
-
-## Commits and history-writing
-
-**Default: do not write to git history.** This is a hard rule for every
-agent β the main thread, every subagent, every skill. It overrides any
-local convenience or "the change looks done" instinct.
-
-The rule covers all of these operations:
-
-- `git commit`, `git commit-tree`
-- `git push`, `git push --force`
-- `git tag`
-- `git rebase`, `git merge`, `git cherry-pick` against shared history
-- `git reset` that discards committed work
-- `gh release create`, `gh pr merge`
-
-Authorization to perform one of these operations exists only when **one**
-of the following is true *right now*:
-
-1. **Skill-declared.** The currently active skill's `SKILL.md` contains
- a `## Commit authorization` section that explicitly authorizes the
- operation and constrains it (which files may be staged, the exact
- commit subject, the maximum number of commits). The mere mention of
- a commit message inside skill prose is **not** authorization β the
- section heading must be present.
-2. **User-instructed.** The user's *current* prompt explicitly tells
- the agent to perform the operation. Examples that qualify:
- "commit this", "make a commit with subject X", "push the branch",
- "tag this release". Authorization from previous turns, from
- `CLAUDE.md`, or from any memory file does **not** carry over.
-
-If neither holds, the agent:
-
-1. Stages relevant changes with `git add` (only if helpful for review).
-2. Prints the proposed commit subject (if any) and `git diff --staged`.
-3. **Stops.** The user runs the commit themselves, or replies with
- explicit authorization in the next prompt.
-
-The project's `.claude/settings.json` keeps `Bash(git commit:*)` in
-`permissions.ask` as defense-in-depth, but the primary enforcement is
-this rule β agents must not propose commit attempts that rely on the
-user clicking the prompt.
diff --git a/.agents/scripts b/.agents/scripts
new file mode 120000
index 0000000..96bf06e
--- /dev/null
+++ b/.agents/scripts
@@ -0,0 +1 @@
+shared/scripts
\ No newline at end of file
diff --git a/.agents/scripts/pre-pr-gate.sh b/.agents/scripts/pre-pr-gate.sh
deleted file mode 100755
index cb80b31..0000000
--- a/.agents/scripts/pre-pr-gate.sh
+++ /dev/null
@@ -1,73 +0,0 @@
-#!/usr/bin/env bash
-#
-# PreToolUse hook: block `gh pr create` unless /pre-pr has successfully run
-# for the current HEAD. The hook is intentionally unaware of the repository's
-# versioning or build system; the /pre-pr skill decides which checks apply.
-#
-# Input: hook JSON on stdin (tool_name, tool_input.command).
-# Exit: 0 to allow, 2 to block (stderr is surfaced to Claude).
-#
-set -eu
-
-input=$(cat)
-tool=$(printf '%s' "$input" | jq -r '.tool_name // empty')
-[ "$tool" != "Bash" ] && exit 0
-
-cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // empty')
-
-# Split the command on shell separators (`;`, `&`, `|` β `&&`/`||` collapse
-# to repeated newlines, which is fine) and check each segment. Only block
-# when a segment STARTS (after optional whitespace) with `gh pr create`.
-# This avoids false positives like `echo "gh pr create"` or test fixtures
-# that mention the string, while still catching `cd dir && gh pr create`
-# and `cat body | gh pr create`. `tr` is used (not `sed s///`) because
-# BSD `sed` on macOS does not interpret `\n` in the replacement string.
-if ! printf '%s' "$cmd" \
- | tr ';&|' '\n\n\n' \
- | grep -qE '^[[:space:]]*gh[[:space:]]+pr[[:space:]]+create([[:space:]]|$)'; then
- exit 0
-fi
-
-repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
-sentinel="$repo_root/.git/pre-pr.ok"
-
-block() {
- cat >&2
- exit 2
-}
-
-if [ ! -f "$sentinel" ]; then
- block < 1) next; print; next }
- { blank = 0; print }
- ' "$path" > "$tmp" && mv "$tmp" "$path"
-}
-
-if [ -n "$file" ]; then
- sanitize_file "$file"
- exit 0
-fi
-
-printf '%s\n' "$command" \
- | sed -nE 's/^\*\*\* (Add|Update) File: (.*)$/\2/p' \
- | sort -u \
- | while IFS= read -r path; do
- sanitize_file "$path"
- done
diff --git a/.agents/scripts/update-copyright.sh b/.agents/scripts/update-copyright.sh
deleted file mode 100755
index b25282f..0000000
--- a/.agents/scripts/update-copyright.sh
+++ /dev/null
@@ -1,48 +0,0 @@
-#!/usr/bin/env bash
-#
-# PostToolUse hook: refresh the copyright header of source files touched by
-# Edit/Write/MultiEdit. Delegates to
-# .agents/skills/update-copyright/scripts/update_copyright.py, which:
-# - operates only on recognized source extensions,
-# - never adds a header to a file that does not already have one,
-# - rewrites `today.year` to the current year per the IntelliJ profile.
-#
-# Input: hook JSON on stdin. Claude Code passes `tool_input.file_path`;
-# Codex `apply_patch` passes the patch text in `tool_input.command`.
-# Exit: 0 always (post-tool-use; never block).
-#
-set -u
-
-# Required tools β silently no-op if either is missing so the hook never blocks.
-command -v jq >/dev/null 2>&1 || exit 0
-command -v python3 >/dev/null 2>&1 || exit 0
-
-input=$(cat)
-file=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty' 2>/dev/null || true)
-command=$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null || true)
-
-root="${CLAUDE_PROJECT_DIR:-$(pwd)}"
-script="$root/.agents/skills/update-copyright/scripts/update_copyright.py"
-
-[ -f "$script" ] || exit 0
-
-update_path() {
- local path="$1"
- [ -z "$path" ] && return 0
- [ ! -f "$path" ] && return 0
- python3 "$script" --root "$root" "$path" >/dev/null 2>&1 || true
-}
-
-if [ -n "$file" ]; then
- update_path "$file"
- exit 0
-fi
-
-printf '%s\n' "$command" \
- | sed -nE 's/^\*\*\* (Add|Update) File: (.*)$/\2/p' \
- | sort -u \
- | while IFS= read -r path; do
- update_path "$path"
- done
-
-exit 0
diff --git a/.agents/shared b/.agents/shared
new file mode 160000
index 0000000..fefe0ed
--- /dev/null
+++ b/.agents/shared
@@ -0,0 +1 @@
+Subproject commit fefe0ed0bf47066b4b50eb01c2823a18dd2e9702
diff --git a/.agents/skills b/.agents/skills
new file mode 120000
index 0000000..f14734d
--- /dev/null
+++ b/.agents/skills
@@ -0,0 +1 @@
+shared/skills
\ No newline at end of file
diff --git a/.agents/skills/check-links/SKILL.md b/.agents/skills/check-links/SKILL.md
deleted file mode 100644
index 5571e13..0000000
--- a/.agents/skills/check-links/SKILL.md
+++ /dev/null
@@ -1,320 +0,0 @@
----
-name: check-links
-description: >
- Validate the Hugo documentation site under `docs/` or `site/` for broken
- links. Builds the site, starts the Hugo server locally, runs Lychee against
- the rendered HTML using the repo's `lychee.toml`, and reports any broken URLs
- grouped by source Markdown page. Use locally before pushing changes that
- touch `docs/**` or `site/**`, when CI's `Check Links` job fails, or whenever
- the user asks to "check doc links". Read-only with respect to the project
- sources. Does **not** cover Javadoc/KDoc (out of scope for this skill).
----
-
-# Check links in the Hugo docs (repo-specific)
-
-You are the documentation link checker for this Spine Event Engine project.
-You build the site under `docs/` or `site/` (auto-detected; see step 0), serve
-it locally on port `1414`, run Lychee against the rendered HTML, and report
-broken URLs. You mirror what the `.github/workflows/check-links.yml` workflow
-does in CI: same Hugo version, same Lychee version, same Hugo environment
-(`development`), and the same `lychee.toml`. Two deliberate differences remain:
-the skill serves on port `1414` (CI uses `1313`) to avoid clashing with a
-developer's local `hugo server`, and the skill writes a local sentinel that CI
-does not. Both differences are harmless because `--base-url` is rewritten to
-match the local port and the sentinel is consumed only by the local `pre-pr`
-skill.
-
-### Pinned versions
-
-`.github/workflows/check-links.yml` is the **single source of truth** for the
-Hugo and Lychee pins. This file does not duplicate the current values
-because duplicates inevitably drift; see the workflow's `env:` block for
-the canonical `HUGO_VERSION` and `LYCHEE_VERSION_TAG`. The auto-download
-step (Β§2) reads `LYCHEE_VERSION_TAG` out of the workflow at runtime, so a
-workflow bump propagates automatically. Hugo is not auto-installed; the
-skill uses whichever `hugo` is on `$PATH` and only warns (does not block)
-if the installed version is older than the workflow's `HUGO_VERSION` β
-Hugo's HTML output is stable enough across minor versions that a small
-skew does not invalidate link-check results.
-
-The authoritative shared config is `lychee.toml` at the repo root. Do not
-fork its exclude list β fix the source link or, if the failing URL is a known
-flaky external endpoint, add it to `lychee.toml` once (the change applies to
-both the skill and CI).
-
-## When to run
-
-- Any change touches `docs/**` or `site/**` (including reference links,
- `embed-code` blocks, sidenav YAML files, content under `/content/`).
-- A change touches `lychee.toml` itself.
-- CI reported broken links and you want a fast local repro.
-- The user asks to "check the doc links" or invokes `/check-links`.
-
-If none of the above is true, decline with a one-line note rather than
-running the (~30 s) build+check.
-
-## Tooling
-
-The skill needs four binaries:
-
-| Tool | Purpose | Install hint |
-|--------|------------------------------------------|-------------------------------|
-| Hugo | Build and serve the site | `brew install hugo` (extended)|
-| Node | Hugo theme dependencies (`npm ci`) | `brew install node` |
-| npm | Same | bundled with Node |
-| Lychee | Link checker | `brew install lychee` |
-
-For **Lychee**, prefer a pre-installed binary on `$PATH`. If none is found,
-download the pinned release (see `LYCHEE_VERSION_TAG` in
-`.github/workflows/check-links.yml` β the dynamic-read pattern in step 2 below
-keeps this version in lock-step with CI) into
-`.agents/skills/check-links/.cache/lychee/` and use that path. The pinned
-version matches what the CI workflow uses, so behavior is identical.
-
-`.agents/skills/check-links/.cache/` is git-ignored (see `.gitignore`).
-
-## Procedure
-
-Execute the steps in order. On the first failure, stop, write a `FAIL`
-sentinel (step 8), and report the failure with the next action.
-
-### 0. Detect site root
-
-Before any other step, determine `SITE_DIR` β the directory that contains the
-Hugo config file:
-
-```bash
-SITE_DIR=""
-for dir in docs site; do
- for cfg in hugo.toml hugo.yaml; do
- if [ -f "$dir/$cfg" ]; then
- SITE_DIR="$dir"
- break 2
- fi
- done
-done
-if [ -z "$SITE_DIR" ]; then
- echo "ERROR: No Hugo config found under docs/ or site/." >&2
- exit 1
-fi
-```
-
-Use `$SITE_DIR` everywhere a directory path is needed in the steps below.
-
-### 1. Scope check
-
-Run `git diff ...HEAD --name-only` (default `` = `master` unless
-the user provides another). If the change set has **no** files under
-`$SITE_DIR/**` and no changes to `lychee.toml`, and the user did not
-explicitly ask, decline and exit cleanly.
-
-### 2. Preflight binaries
-
-- `hugo version` β must succeed; capture the version. If missing, stop with
- Must-fix: "Install Hugo extended (`brew install hugo`)." If installed but
- older than the workflow's `HUGO_VERSION` (parse with
- `grep -E '^[[:space:]]+HUGO_VERSION:' .github/workflows/check-links.yml | sed -E 's/.*: *"?([^"]+)"?$/\1/'`), warn but
- continue.
-- `node -v` and `npm -v` β must succeed. If missing, stop with Must-fix:
- "Install Node (`brew install node`) at the major version pinned by
- `node-version:` in `.github/workflows/check-links.yml`."
-- `lychee --version` β if it succeeds, record the path and version.
-- If `lychee` is missing:
- 1. Read the canonical pin from the workflow file so the skill cannot drift
- from CI:
- ```bash
- LYCHEE_VERSION_TAG=$(
- grep -E '^[[:space:]]+LYCHEE_VERSION_TAG:' .github/workflows/check-links.yml \
- | sed -E 's/.*: *"?([^"]+)"?$/\1/'
- )
- ```
- Expected shape: `lychee-vX.Y.Z` (the leading `lychee-` is part of the
- upstream release tag, not a typo).
- 2. Determine platform via `uname -s` / `uname -m`. Map to the matching
- Lychee asset (recent releases β `v0.24.2` and later β drop the
- version from the asset filename):
- - `Darwin` + `arm64` β `lychee-aarch64-apple-darwin.tar.gz`
- - `Darwin` + `x86_64` β `lychee-x86_64-apple-darwin.tar.gz`
- - `Linux` + `x86_64` β `lychee-x86_64-unknown-linux-gnu.tar.gz`
- - `Linux` + `aarch64` β `lychee-aarch64-unknown-linux-gnu.tar.gz`
- - any other combination (e.g. Windows, FreeBSD, 32-bit) β stop with
- Must-fix: "Unsupported platform for Lychee auto-download β install
- Lychee manually (`brew install lychee` / `cargo install lychee`)
- and rerun."
- 3. Ensure the cache directory exists *before* the download β
- `mkdir -p .agents/skills/check-links/.cache/lychee/` β
- because the path is git-ignored and absent on a fresh clone,
- and `tar -xzf β¦ -C ` will fail with "no such file or
- directory" if the target does not exist yet. This mirrors the
- `mkdir -p lychee` that `check-links.yml` does before its own
- extract step.
- 4. Download from
- `https://github.com/lycheeverse/lychee/releases/download/${LYCHEE_VERSION_TAG}/`
- into `.agents/skills/check-links/.cache/lychee/` and extract
- with `tar -xzf --strip-components=1 -C .agents/skills/check-links/.cache/lychee/`
- so the binary lands at
- `.agents/skills/check-links/.cache/lychee/lychee`.
- 5. Use `.agents/skills/check-links/.cache/lychee/lychee` for the rest of this run.
- 6. Print a one-line note: "Using auto-downloaded Lychee. For faster runs,
- install with `brew install lychee`."
-
-### 3. Install Hugo deps
-
-Run `( cd ${SITE_DIR}/_preview && npm ci )`. We deliberately use `npm ci`
-(matching the CI workflow's `Install Dependencies` step in `check-links.yml`)
-rather than `npm install`:
-
-- `npm ci` installs exactly the versions pinned by `package-lock.json`;
- `npm install` is allowed to update the lockfile and may resolve to
- different transitive versions than CI, which defeats the "render
- identical HTML to CI" goal.
-- If `package.json` and `package-lock.json` drift out of sync, `npm ci`
- fails fast with a clear error rather than silently healing the
- lockfile β a divergence we want to surface, not paper over.
-
-The helper script `${SITE_DIR}/_script/install-dependencies` exists for
-interactive use but does a relative `cd _preview` and therefore only works
-when invoked from `${SITE_DIR}/` β calling it from the repo root (the skill's
-default CWD) would fail with "No such file or directory: _preview".
-
-### 4. Build the site
-
-Run `( cd ${SITE_DIR}/_preview && hugo -e development )`.
-This emits `${SITE_DIR}/_preview/public/**/*.html`. The `-e development` flag
-matches what CI uses in `check-links.yml` so the two builds render identical
-HTML. (The helper `${SITE_DIR}/_script/hugo-build` exists for interactive use
-but defaults to `production`; we invoke `hugo` directly to keep the env in
-lock-step with CI.)
-
-### 5. Start the Hugo server in the background
-
-The server must survive across multiple `Bash` tool calls (steps 5 β 6 β 8
-typically run in separate shells), so we rely on `nohup` alone β a `trap β¦
-EXIT` would fire when *this* shell exits and kill the server before Lychee
-can query it. Teardown happens explicitly in step 8.
-
-Before launching, kill any leftover server from a previous crashed run so a
-stale process does not hold port `1414`:
-
-```bash
-pkill -F /tmp/check-links.hugo.pid 2>/dev/null || true
-rm -f /tmp/check-links.hugo.pid
-
-( cd ${SITE_DIR}/_preview && nohup hugo server --environment development --port 1414 \
- > /tmp/check-links.hugo.out 2>&1 & echo $! > /tmp/check-links.hugo.pid )
-sleep 5
-
-# Verify the captured PID is alive before relying on it. `$!` for
-# `nohup foo &` is reliable on bash but not portable across shells; the
-# pgrep check turns a silent "Lychee fetches an empty port" failure into
-# a clear error.
-if ! pgrep -F /tmp/check-links.hugo.pid > /dev/null 2>&1; then
- echo "ERROR: Hugo server failed to start. Tail of log:" >&2
- tail -20 /tmp/check-links.hugo.out >&2 || true
- exit 1
-fi
-```
-
-Port `1414` is chosen to avoid clashing with a developer's local `hugo server`
-(default `1313`). The `--environment development` flag matches CI's build env.
-
-### 6. Run Lychee
-
-```bash
- --config lychee.toml --timeout 60 \
- --base-url http://localhost:1414/ \
- "${SITE_DIR}/_preview/public/**/*.html"
-```
-
-Capture exit code. Any non-zero exit means at least one broken link.
-
-### 7. Report
-
-Group the broken URLs from Lychee's output by source page. To reverse-map
-an HTML path to its Markdown source:
-
-`${SITE_DIR}/_preview/public/docs///index.html`
-β `${SITE_DIR}/content/docs//.md` (or `/_index.md`).
-
-Report in this shape:
-
-```
-## Doc link check ( vs )
-
-Hugo:
-Lychee: ()
-Pages scanned:
-Broken URLs:
-
-### /content/docs/<...>/.md
-- β
-- β ...
-
-### /content/docs/<...>/.md
-- ...
-```
-
-If `K == 0`, report a single line: "All links OK."
-
-### 8. Tear down and sentinel
-
-- Kill the Hugo server (and clean up its pid file):
-
- ```bash
- pkill -F /tmp/check-links.hugo.pid 2>/dev/null || true
- rm -f /tmp/check-links.hugo.pid /tmp/check-links.hugo.out
- ```
-
- Run this even if Lychee failed β leaving a server on port `1414` would
- poison the next invocation.
-- Write `.git/check-links.ok` at the repo root:
-
- ```
- head=
- branch=
- status=PASS|FAIL
- timestamp=
- hugo=
- lychee=
- pages=
- broken=
- ```
-
-The sentinel is consumed by the `pre-pr` skill's reviewer step: when it
-sees a sentinel whose `head=` matches the current HEAD SHA and
-`status=PASS`, it skips re-dispatching `check-links` and records it
-as APPROVE with the note "cached from `.git/check-links.ok`". Any
-HEAD advance (commit, amend, rebase) invalidates the cache automatically.
-
-## Notes
-
-- This skill does **not** modify tracked sources. It does, however, write
- several git-ignored build artifacts during a run β listed here so a future
- reader does not mistake them for unrelated side-effects:
- - `.agents/skills/check-links/.cache/lychee/` β auto-downloaded
- Lychee binary, when the system Lychee was unavailable.
- - `${SITE_DIR}/_preview/node_modules/` β installed by `npm ci` in step 3.
- - `${SITE_DIR}/_preview/public/` β Hugo's rendered HTML (the corpus Lychee
- scans).
- - `${SITE_DIR}/_preview/resources/` β Hugo's asset-pipeline cache.
- - `.lycheecache` at the repo root β Lychee's per-URL result cache
- (honoured for `max_cache_age = "3d"` per `lychee.toml`).
- - `/tmp/check-links.hugo.{pid,out}` β server PID file and log, both
- removed in step 8's teardown.
-
- Every path above is matched by an existing `.gitignore` entry; none is
- committed.
-- The `lychee.toml` exclude list is the single source of truth for flaky
- external endpoints. If a real link must be excluded, add it there and
- explain why in a comment so CI and local runs stay in sync.
-- The skill assumes the docs build succeeds. A Hugo build error is treated
- the same as a link failure β surface it and stop.
-- The `include_verbatim = false` setting in `lychee.toml` skips links inside
- code blocks. That is intentional today; flip it on if you specifically need
- to validate examples.
-
-## Related skills
-
-- `review-docs` β prose, KDoc/Javadoc, and Markdown style review. Runs in
- parallel with `check-links` when invoked by `pre-pr`.
-- `pre-pr` β composes the above and gates `gh pr create`.
diff --git a/.agents/skills/move-files/SKILL.md b/.agents/skills/move-files/SKILL.md
deleted file mode 100644
index b92b05d..0000000
--- a/.agents/skills/move-files/SKILL.md
+++ /dev/null
@@ -1,57 +0,0 @@
----
-name: move-files
-description: >
- Move or rename any files/directories in a repo: preserve history, update all
- references and build metadata, verify no stale paths remain.
----
-
-# Move Files
-
-## Workflow
-
-1. Preflight.
- - Run `git status --short`.
- - Map each `source -> destination`.
- - Classify scope: simple same-module moves stay targeted; package, module, or
- cross-module moves need broader inspection.
- - Ask before ambiguous mappings, destination conflicts, or unclear semantic
- package/module changes.
-
-2. Search before moving.
- - Search all old identifiers: paths, names, resource refs, doc links.
- - For Gradle/module/source-set moves, check `settings.gradle.kts`,
- `build.gradle.kts`, and `buildSrc`.
- - For Kotlin/Java, update package declarations only when package intent
- changes.
-
-3. Move safely.
- - Always use `git mv` for tracked files in the repo. If sandboxing blocks
- it, request approval; do not use delete/create as a fallback.
- - Use filesystem moves only for untracked/generated/out-of-git files.
- - Create parent directories first.
- - For case-only renames, move through a temporary name.
-
-4. Repair references.
- - Update all references: imports, build metadata, docs, resources, and scripts.
- - Start search scope narrow: affected directory, then module, then repo-wide.
- - Prefer precise edits; avoid broad replacements on generic names.
-
-5. Verify.
- - Re-run targeted searches for old tokens.
- - Run `git status --short` and confirm the delta matches the move.
- - Run focused validation for moved files, or state what could not run.
-
-6. Ensure the version is bumped.
- Invoke `/version-bumped` so the branch carries a strictly greater
- `version.gradle.kts` than the base ref before any `./gradlew build`
- (which can transitively `publishToMavenLocal` and overwrite
- consumer-facing snapshots). The skill is a no-op if a bump already
- happened earlier on the branch.
-
-## Repo Notes
-
-Follow `.agents/project-structure-expectations.md` for module/source-set/test moves.
-
-## Report
-
-Return: `Moved[]`, `UpdatedRefs[]`, `Verification[]`, `Risks[]`.
diff --git a/.agents/skills/move-files/agents/openai.yaml b/.agents/skills/move-files/agents/openai.yaml
deleted file mode 100644
index ba90a9f..0000000
--- a/.agents/skills/move-files/agents/openai.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-interface:
- display_name: "Move Files"
- short_description: "Move files safely across a repo"
- default_prompt: "Use $move-files to relocate files or directories in this repository while preserving history, updating references, and verifying the result."
diff --git a/.agents/skills/pre-pr/SKILL.md b/.agents/skills/pre-pr/SKILL.md
deleted file mode 100644
index 2c81dfd..0000000
--- a/.agents/skills/pre-pr/SKILL.md
+++ /dev/null
@@ -1,181 +0,0 @@
----
-name: pre-pr
-description: >
- Run the pre-PR checklist for this repo: apply the version gate only when
- the repository has a root `version.gradle.kts`, run the configured
- build/check command per `.agents/running-builds.md`, and invoke the
- configured reviewers (`kotlin-review`, `review-docs`, `dependency-audit`,
- `check-links`) against the branch diff. On success, write a sentinel file at
- `.git/pre-pr.ok` so the `gh pr create` hook can verify the checklist ran
- for the current HEAD. Use before opening a PR, or when CI rejected a
- branch and you want a fast local repro.
----
-
-# Pre-PR checklist (repo-specific)
-
-You are the pre-PR gate for this repository. You compose the existing
-reviewers and the documented repository rules into a single pass that must
-succeed before a pull request is opened.
-
-This skill supports both versioned Gradle Build Tools projects and repositories
-that intentionally do not have `version.gradle.kts`. Do not create
-`version.gradle.kts` just to satisfy this checklist. When the file is absent
-from the project root, the version-bump check is **not applicable**.
-
-The authoritative standards live in `.agents/`:
-
-- `.agents/version-policy.md` β applies only when the repository has a root
- `version.gradle.kts`.
-- `.agents/running-builds.md` β which build/check command to run.
-- `.agents/safety-rules.md` and `.agents/advanced-safety-rules.md` β hard
- constraints checked by the reviewers.
-
-## Procedure
-
-Run steps 1β4 fully before aggregating. Collect all findings; do not stop at
-the first failure.
-
-### 1. Determine scope and repository capabilities
-
-- Base ref: `master` unless the user provides a different one.
-- Changed files: `git diff ...HEAD --name-only`
-- Repository root: `git rev-parse --show-toplevel`
-- Version gate: check only the repository-root `version.gradle.kts`.
- - Absent at both sides β `not-applicable`, continue.
- - Present at `HEAD` β enforce in step 2.
- - Present at `` but missing at `HEAD` β fail unless the user
- explicitly asked to migrate away from Gradle Build Tools versioning.
-- Classify changes:
- - **proto** β any `*.proto` changed
- - **code** β any `*.kt`, `*.kts`, or `*.java` changed
- - **docs** β any `*.md` or doc-only source edits changed
- - **deps** β any file under `buildSrc/src/main/kotlin/io/spine/dependency/` changed
- - **site** β any file under `docs/**` or `lychee.toml` (triggers Hugo link
- check; pure `README.md` or KDoc-only changes do *not* count)
-
-### 2. Version-bump check
-
-- Skip when version gate is `not-applicable`.
-- Read `version.gradle.kts` at `HEAD`. Read `` only if the file exists
- there; if it does not, the file is newly introduced β record the introduced
- version and continue.
-- When both sides have the file: if the version is not strictly greater (semver
- + Spine snapshot rules in `.agents/version-policy.md`): if
- `.agents/skills/bump-version/` exists, **auto-fix immediately** by invoking
- `/bump-version` without asking; otherwise record a Must-fix and continue.
- Re-read the file after the fix. If the version is still not strictly greater,
- record a Must-fix and continue. If the auto-fix succeeded, recompute the
- changed-file list (`git diff ...HEAD --name-only`) before proceeding to
- Step 3 β the bump commit adds `version.gradle.kts` to the diff.
-
-### 3. Build or check
-
-Pick the target per `.agents/running-builds.md`:
-
-- **proto** changed β `./gradlew clean build`
-- Else **code** changed β `./gradlew build`
-- Else **docs**-only β `./gradlew dokka`
-
-If `./gradlew` is absent, read `.agents/running-builds.md` for the
-repository-specific command. If that file is also absent, or if none is
-documented for the change type, record `build_status=skipped` with the
-reason and continue.
-
-Run the chosen command. On failure, record the first failing task and
-continue to step 4 β do not abort. Pass `build_status=FAIL` in the context
-given to reviewers so they can discount false positives from non-compiling
-code.
-
-### 4. Reviewers (run in parallel)
-
-Dispatch relevant reviewers concurrently; collect all verdicts before
-aggregating. Before dispatching, check that the skill directory exists under
-`.agents/skills/`; if a skill is absent, skip it with a note "not applicable
-for this repo" rather than failing.
-
-- **code** changed β `kotlin-review`
-- **docs** or KDoc changed β `review-docs`
-- **deps** changed β `dependency-audit`
-- **site** changed β `check-links` (unless the sentinel short-circuit below
- applies)
-
-**`check-links` sentinel short-circuit.** Read `.git/check-links.ok` (if
-present). If `head=` equals the current **full** HEAD SHA and `status=PASS`, skip
-dispatch and record `APPROVE` with note "cached from `.git/check-links.ok`"
-(caching its ~30 s rebuild+serve cycle; the result is deterministic for a given
-HEAD). Otherwise dispatch normally.
-
-Pass each reviewer: base ref, changed-file list, build result, version result.
-When the version check is `not-applicable`, say so explicitly so reviewers don't flag a
-missing version bump.
-
-**Auto-fix policy for reviewer findings:**
-
-- Findings from `kotlin-review`, `review-docs`, or `dependency-audit` β record
- as Must-fix or Should-fix; do **not** auto-apply. Surface them and wait for
- user action.
-- If a reviewer reports a missing version bump after Step 2 already ran, the
- auto-fix did not take β record a Must-fix and do not silently re-apply.
-- `dependency-audit` reports a **version rollback** β do **not** auto-fix.
- Surface it as a Must-fix and wait for user confirmation, because a rollback
- can be intentional.
-
-### 5. Aggregate
-
-- **PASS**: version check passed or `not-applicable`, build succeeded or
- `build_status=skipped` (no documented command for the change type), every
- reviewer returned `APPROVE` or `APPROVE WITH CHANGES`, and no unaddressed
- Must-fix items remain.
-- **FAIL**: anything else.
-
-### 6. Sentinel
-
-Write `.git/pre-pr.ok` at the repo root (never under `.claude/`). The `gh pr
-create` hook (`.agents/scripts/pre-pr-gate.sh`) checks `head=` and `status=`;
-field names in this block are part of that contract.
-
-```
-head=
-branch=
-status=PASS|FAIL
-timestamp=
-build=
-build_status=PASS|FAIL|skipped
-reviewers=
-version=new, introduced:, or "not-applicable">
-```
-
-## Output format
-
-**On PASS** β single line:
-
-```
-Pre-PR: PASS ( vs ) β ready to `gh pr create`.
-```
-
-**On FAIL** β header line, then only the items that need attention, each
-prefixed with the source reviewer or check:
-
-```
-Pre-PR: FAIL ( vs )
-
-Must fix:
-- [kotlin-review]
-- [review-docs]
-
-Should fix:
-- [dependency-audit]
-```
-
-Report nothing about checks that passed. If auto-fixes were applied, list
-them in one line before the verdict: `Auto-fixed: .`
-
-## Notes
-
-- This skill must NOT create the PR itself.
-- This skill must NOT create `version.gradle.kts`.
-- The sentinel lives under `.git/` β per-clone, never committed.
-- Each reviewer is the source of truth for its own checks; this skill only
- orchestrates and aggregates.
-- This skill may auto-fix a missing version bump by invoking `/bump-version`;
- all other fixes require explicit user confirmation.
diff --git a/.agents/skills/review-docs/SKILL.md b/.agents/skills/review-docs/SKILL.md
deleted file mode 100644
index d936fa2..0000000
--- a/.agents/skills/review-docs/SKILL.md
+++ /dev/null
@@ -1,129 +0,0 @@
----
-name: review-docs
-description: >
- Review documentation changes β KDoc/Javadoc inside Kotlin/Java sources and
- Markdown docs (`README.md`, `docs/**`) β against Spine documentation
- conventions. Use when a diff touches doc comments or Markdown, before
- opening a doc-affecting PR, or when asked for a documentation review.
- Read-only; does not run builds.
----
-
-# Review documentation (repo-specific)
-
-You are the documentation reviewer for a Spine Event Engine project. You
-focus strictly on documentation quality β prose, KDoc/Javadoc, and Markdown β
-and deliberately do **not** duplicate the code-review skill (which owns
-Kotlin idioms, safety rules, tests, and version-gate checks).
-
-The authoritative standards live in `.agents/`:
-
-- `.agents/documentation-guidelines.md` β commenting rules, TODO-comment
- format, "file/dir names as code", widow/runt/orphan/river rule (with the
- diagram at `.agents/widow-runt-orphan.jpg`).
-- `.agents/documentation-tasks.md` β KDoc-example requirement on APIs;
- Javadoc β KDoc conversion rules (`
` removal, etc.).
-- `.agents/skills/writer/SKILL.md` β Markdown conventions (footnote-style
- reference links for external URLs, typographic quotes only on actual
- page/section titles, sidenav-sync rules under `docs/`).
-- `.agents/running-builds.md` β for doc-only Kotlin/Java changes the right
- build is `./gradlew dokka` (no tests required).
-
-## Review procedure
-
-1. **Scope the diff.** Obtain the change set via `git diff --staged` or
- `git diff ...HEAD` depending on what the user describes. Restrict
- to files matching:
- - `**/*.kt`, `**/*.kts`, `**/*.java` (for KDoc/Javadoc inside sources)
- - `**/*.md` (Markdown docs)
- Do **not** review the full repo β only what changed.
-
-2. **Read each affected file fully, not just the hunks.** Prose review
- requires surrounding context β judging widows/runts/orphans, link
- placement, and KDoc completeness needs the whole paragraph and the
- surrounding declarations.
-
-3. **Stay in scope.** If you spot a code-quality issue (idiom, naming,
- tests, version-gate applicability), note it briefly as a "for the code
- reviewer" item under Nits β do not expand the review.
-
-## Checks
-
-### A. KDoc / Javadoc inside sources
-
-- **Public and internal APIs carry KDoc.** Per `documentation-tasks.md`,
- KDoc should include at least one usage example for non-trivial APIs.
- Missing KDoc on a new or modified public/internal symbol is a Should-fix.
-- **No Javadoc residue in Kotlin.** When converting from Java:
- - `
` tags on a text line removed (`"
This"` β `"This"`).
- - `
` on its own line replaced with a blank line.
- - HTML entities (`&`, `<`, β¦) converted to literals where appropriate.
-- **Inline comments in production code are minimized.** Inline comments are
- fine in tests; in production source they should explain *why* (a
- constraint, invariant, surprise) and never restate *what* the code does.
-- **TODO comments follow the Spine format.** Linked from
- `documentation-guidelines.md` to the wiki "TODO-comments" page. A bare
- `// TODO: β¦` without owner/issue reference is a Should-fix.
-- **File and directory names rendered as code.** Within KDoc/Javadoc prose,
- `path/to/file.kt` and `module-name` must use backticks.
-
-### B. Markdown docs
-
-- **Footnote-style reference links** for external `https://` URLs (per the
- `writer` skill). Inline `[label](https://β¦)` in body prose is a
- Should-fix; inline links to local relative paths are fine.
-- **Typographic quotes** (`" "` / `' '`) only when the visible link text is
- an actual page or section title (e.g., the "Getting started" page).
- Do **not** quote generic phrases like "this page", "the next section",
- "What's next", or section numbers (`4.3`).
-- **Sidenav sync.** If the diff adds/removes/renames/moves a page under
- `docs/content/docs//`, the matching current-version
- `sidenav.yml` must be updated (see the `writer` skill for how to
- identify the current version via `docs/data/versions.yml`). A missing
- sidenav update is a Must-fix.
-- **Fenced code blocks** for commands and examples β no indented code
- blocks for shell snippets (they swallow `$` prompts and hurt copy/paste).
-- **Heading hierarchy.** No skipped levels (`#` β `###`); exactly one `#`
- per file.
-
-### C. Prose flow (Spine-specific)
-
-- **Avoid widows, runts, orphans, and rivers** β the rule from
- `documentation-guidelines.md` with the diagram at
- `.agents/widow-runt-orphan.jpg`. Operationally:
- - **Widow / runt**: a paragraph's last line containing only one short
- word (or a hyphenated fragment). Reflow the prior line.
- - **Orphan**: a single trailing line of a paragraph stranded at the top
- of a new block (often appears after a heading or list). Reflow.
- - **River**: a vertical "gap" of aligned spaces running down justified
- text. Rare in Markdown but possible in tables β reflow the table or
- rewrite to break the alignment.
- Quote the offending paragraph and propose a rewording that fixes it.
-
-### D. Terminology and tone
-
-- **Match code identifiers verbatim.** When prose references a class,
- function, or property, the name in backticks must match the source
- exactly (case, plurality).
-- **Consistent terminology across the diff.** If the same concept is
- named two different ways in the same change set, pick one.
-
-## Output format
-
-Three sections, in this order:
-
-- **Must fix** β broken/missing KDoc on a newly-introduced public API,
- missing sidenav sync, broken cross-references, Javadoc residue
- (`
` tags) left in Kotlin KDoc, broken Markdown links.
-- **Should fix** β TODO format, inline-comment overuse in production,
- inline external links that should be footnote-style, missing typographic
- quotes (or unwanted ones), widow/runt/orphan/river paragraphs,
- fenced-vs-indented code blocks.
-- **Nits** β wording, terminology drift, code-identifier capitalization
- in prose, "for the code reviewer" pointers if any code issues surfaced
- incidentally.
-
-For each finding, cite the file and line, quote the offending text, and
-show the recommended rewrite. If a section is empty, write "None."
-
-End with a one-line verdict: `APPROVE`, `APPROVE WITH CHANGES`, or
-`REQUEST CHANGES`.
diff --git a/.agents/skills/update-copyright/SKILL.md b/.agents/skills/update-copyright/SKILL.md
deleted file mode 100644
index 6afc4c7..0000000
--- a/.agents/skills/update-copyright/SKILL.md
+++ /dev/null
@@ -1,16 +0,0 @@
----
-name: update-copyright
-description: >
- Update source file copyright headers from the IntelliJ IDEA copyright profile,
- replacing `today.year` with the current year.
- Automatically apply when source files are modified in a change set.
----
-
-# Copyright Update
-
-**Command:** `python3 .agents/skills/update-copyright/scripts/update_copyright.py`
-
-1. Scope: explicit files/dirs from the user, or all tracked source files if none given.
-2. No explicit paths β run with `--dry-run` first, then without.
-3. Relay stdout (notice source, file count, changed paths) to the user.
-4. Never add a copyright header to a file that does not already have one.
diff --git a/.agents/skills/update-copyright/agents/openai.yaml b/.agents/skills/update-copyright/agents/openai.yaml
deleted file mode 100644
index 246dd64..0000000
--- a/.agents/skills/update-copyright/agents/openai.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-interface:
- display_name: "Copyright Update"
- short_description: "Refresh source copyright headers"
- default_prompt: "Use $update-copyright to refresh source file copyright headers from the IntelliJ IDEA copyright profile in this repository."
diff --git a/.agents/skills/update-copyright/scripts/update_copyright.py b/.agents/skills/update-copyright/scripts/update_copyright.py
deleted file mode 100755
index 2dbf8bb..0000000
--- a/.agents/skills/update-copyright/scripts/update_copyright.py
+++ /dev/null
@@ -1,389 +0,0 @@
-#!/usr/bin/env python3
-"""Update source copyright headers from IntelliJ IDEA copyright profiles."""
-
-from __future__ import annotations
-
-import argparse
-import datetime as dt
-import html
-import re
-import subprocess
-import sys
-from pathlib import Path
-from xml.etree import ElementTree as ET
-
-
-BLOCK_EXTENSIONS = {
- ".c",
- ".cc",
- ".cpp",
- ".cs",
- ".css",
- ".cxx",
- ".dart",
- ".go",
- ".gradle",
- ".groovy",
- ".h",
- ".hh",
- ".hpp",
- ".java",
- ".js",
- ".jsx",
- ".kt",
- ".kts",
- ".less",
- ".m",
- ".mm",
- ".proto",
- ".rs",
- ".scala",
- ".scss",
- ".swift",
- ".ts",
- ".tsx",
-}
-HASH_EXTENSIONS = {
- ".bash",
- ".bzl",
- ".properties",
- ".pl",
- ".py",
- ".rb",
- ".sh",
- ".toml",
- ".yaml",
- ".yml",
- ".zsh",
-}
-XML_EXTENSIONS = {
- ".fxml",
- ".pom",
- ".wsdl",
- ".xml",
- ".xsd",
- ".xsl",
- ".xslt",
-}
-EXCLUDED_DIRS = {
- ".agents",
- ".git",
- ".gradle",
- ".idea",
- ".kotlin",
- "build",
- "generated",
- "out",
- "tmp",
-}
-EXCLUDED_FILES = {
- "gradlew",
- "gradlew.bat",
-}
-
-
-def parse_args() -> argparse.Namespace:
- parser = argparse.ArgumentParser(
- description=(
- "Update source copyright headers from "
- ".idea/copyright/profiles_settings.xml."
- )
- )
- parser.add_argument(
- "paths",
- nargs="*",
- help="Files or directories to update. Defaults to tracked source files.",
- )
- parser.add_argument(
- "--root",
- type=Path,
- default=Path.cwd(),
- help="Repository root. Defaults to the current working directory.",
- )
- parser.add_argument(
- "--year",
- default=str(dt.date.today().year),
- help="Year to substitute for today.year. Defaults to the current year.",
- )
- parser.add_argument(
- "--dry-run",
- action="store_true",
- help="Report files that would change without writing them.",
- )
- parser.add_argument(
- "--check",
- action="store_true",
- help="Exit with status 1 if any file would change; do not write files.",
- )
- return parser.parse_args()
-
-
-def profile_filename(profile_name: str) -> str:
- stem = re.sub(r"[^A-Za-z0-9]+", "_", profile_name).strip("_")
- if not stem:
- raise ValueError("The default copyright profile name is empty.")
- return f"{stem}.xml"
-
-
-def load_notice(root: Path, year: str) -> tuple[str, Path]:
- settings_path = root / ".idea" / "copyright" / "profiles_settings.xml"
- if not settings_path.is_file():
- raise FileNotFoundError(f"Missing {settings_path}")
-
- settings_root = ET.parse(settings_path).getroot()
- settings = settings_root.find(".//settings")
- if settings is None:
- raise ValueError(f"{settings_path} does not contain a settings tag.")
-
- default_profile = settings.get("default")
- if not default_profile:
- raise ValueError(f"{settings_path} settings tag has no default attribute.")
-
- profile_path = settings_path.parent / profile_filename(default_profile)
- if not profile_path.is_file():
- raise FileNotFoundError(
- f"Default profile {default_profile!r} resolves to missing {profile_path}"
- )
-
- profile_root = ET.parse(profile_path).getroot()
- notice = None
- for option in profile_root.findall(".//option"):
- if option.get("name") == "notice":
- notice = option.get("value")
- break
- if notice is None:
- raise ValueError(f"{profile_path} has no option named 'notice'.")
-
- decoded = html.unescape(notice)
- decoded = decoded.replace("${today.year}", year)
- decoded = decoded.replace("$today.year", year)
- decoded = decoded.replace("today.year", year)
- return decoded.rstrip(), profile_path
-
-
-def style_for(path: Path) -> str | None:
- name = path.name
- suffix = path.suffix.lower()
- if name.endswith((".sh.template", ".bash.template", ".zsh.template")):
- return "hash"
- if suffix in BLOCK_EXTENSIONS:
- return "block"
- if suffix in HASH_EXTENSIONS:
- return "hash"
- if suffix in XML_EXTENSIONS:
- return "xml"
- return None
-
-
-def is_excluded(path: Path) -> bool:
- if path.name in EXCLUDED_FILES:
- return True
- parts = path.parts
- if len(parts) >= 2 and parts[0] == "gradle" and parts[1] == "wrapper":
- return True
- return any(part in EXCLUDED_DIRS for part in parts)
-
-
-def tracked_files(root: Path) -> list[Path]:
- try:
- result = subprocess.run(
- ["git", "-C", str(root), "ls-files", "-z"],
- check=True,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- )
- except (FileNotFoundError, subprocess.CalledProcessError):
- return [
- path.relative_to(root)
- for path in root.rglob("*")
- if path.is_file() and not is_excluded(path.relative_to(root))
- ]
-
- paths = []
- for item in result.stdout.decode("utf-8").split("\0"):
- if not item:
- continue
- path = Path(item)
- if (root / path).is_file():
- paths.append(path)
- return paths
-
-
-def expand_requested_paths(root: Path, requested: list[str]) -> list[Path]:
- if not requested:
- paths = tracked_files(root)
- else:
- paths = []
- for item in requested:
- path = (root / item).resolve()
- if not path.exists():
- raise FileNotFoundError(f"Path does not exist: {item}")
- if not path.is_relative_to(root):
- raise ValueError(
- f"Path is outside the repository root: {item!r} "
- f"(resolved to {path}, root is {root})"
- )
- if path.is_dir():
- for child in path.rglob("*"):
- if child.is_file():
- paths.append(child.relative_to(root))
- else:
- paths.append(path.relative_to(root))
-
- unique = sorted(set(paths), key=lambda p: p.as_posix())
- return [
- path
- for path in unique
- if style_for(path) is not None and not is_excluded(path)
- ]
-
-
-def newline_for(text: str) -> str:
- return "\r\n" if "\r\n" in text else "\n"
-
-
-def build_header(notice: str, style: str, newline: str) -> str:
- lines = notice.splitlines()
- if style == "block":
- body = newline.join(f" * {line}" if line else " *" for line in lines)
- return f"/*{newline}{body}{newline} */{newline}{newline}"
- if style == "hash":
- body = newline.join(f"# {line}" if line else "#" for line in lines)
- return f"{body}{newline}{newline}"
- if style == "xml":
- body = newline.join(f" ~ {line}" if line else " ~" for line in lines)
- return f"{newline}{newline}"
- raise ValueError(f"Unsupported comment style: {style}")
-
-
-def split_leading_directive(text: str, style: str, newline: str) -> tuple[str, str]:
- if style == "hash" and text.startswith("#!"):
- line_end = text.find("\n")
- if line_end == -1:
- return text + newline + newline, ""
- prefix = text[: line_end + 1] + newline
- return prefix, strip_leading_blank_lines(text[line_end + 1 :])
-
- if style == "xml" and text.startswith("")
- if close != -1:
- line_end = text.find("\n", close)
- if line_end == -1:
- return text + newline + newline, ""
- prefix = text[: line_end + 1] + newline
- return prefix, strip_leading_blank_lines(text[line_end + 1 :])
-
- return "", strip_leading_blank_lines(text)
-
-
-def strip_leading_blank_lines(text: str) -> str:
- return re.sub(r"^(?:[ \t]*\r?\n)+", "", text)
-
-
-def strip_existing_header(text: str, style: str) -> tuple[str, bool]:
- if style == "block" and text.startswith("/*"):
- close = text.find("*/")
- if close != -1:
- candidate = text[: close + 2]
- if is_copyright_header(candidate):
- return strip_leading_blank_lines(text[close + 2 :]), True
-
- if style == "xml" and text.startswith("")
- if close != -1:
- candidate = text[: close + 3]
- if is_copyright_header(candidate):
- return strip_leading_blank_lines(text[close + 3 :]), True
-
- if style == "hash":
- lines = text.splitlines(keepends=True)
- end = 0
- for line in lines:
- stripped = line.strip()
- if stripped == "" or stripped.startswith("#"):
- end += len(line)
- continue
- break
- candidate = text[:end]
- if candidate and is_copyright_header(candidate):
- return strip_leading_blank_lines(text[end:]), True
-
- return text, False
-
-
-def is_copyright_header(text: str) -> bool:
- limited = text[:5000]
- return "Copyright" in limited and (
- "Licensed under" in limited or "All rights reserved" in limited
- )
-
-
-def updated_text(text: str, notice: str, style: str) -> str:
- original = text
- bom = "\ufeff" if text.startswith("\ufeff") else ""
- if bom:
- text = text[1:]
- newline = newline_for(text)
- prefix, body = split_leading_directive(text, style, newline)
- body, had_header = strip_existing_header(body, style)
- if not had_header:
- return original
- return bom + prefix + build_header(notice, style, newline) + body
-
-
-def update_file(root: Path, path: Path, notice: str, dry_run: bool) -> bool:
- absolute = root / path
- style = style_for(path)
- if style is None:
- return False
-
- try:
- text = absolute.read_text(encoding="utf-8")
- except FileNotFoundError:
- print(f"Skipping missing file: {path}", file=sys.stderr)
- return False
- except UnicodeDecodeError:
- print(f"Skipping non-UTF-8 file: {path}", file=sys.stderr)
- return False
-
- next_text = updated_text(text, notice, style)
- if next_text == text:
- return False
-
- if not dry_run:
- with absolute.open("w", encoding="utf-8", newline="") as file:
- file.write(next_text)
- return True
-
-
-def main() -> int:
- args = parse_args()
- root = args.root.resolve()
- notice, profile_path = load_notice(root, args.year)
- try:
- paths = expand_requested_paths(root, args.paths)
- except (FileNotFoundError, ValueError) as exc:
- print(f"error: {exc}", file=sys.stderr)
- return 2
- dry_run = args.dry_run or args.check
-
- changed = [
- path
- for path in paths
- if update_file(root, path, notice, dry_run=dry_run)
- ]
-
- rel_profile = profile_path.relative_to(root)
- action = "Would update" if dry_run else "Updated"
- print(f"Notice source: {rel_profile}")
- print(f"{action} {len(changed)} file(s).")
- for path in changed:
- print(path.as_posix())
-
- if args.check and changed:
- return 1
- return 0
-
-
-if __name__ == "__main__":
- raise SystemExit(main())
diff --git a/.agents/skills/update-copyright/tests/test_update_copyright.py b/.agents/skills/update-copyright/tests/test_update_copyright.py
deleted file mode 100644
index 8770b32..0000000
--- a/.agents/skills/update-copyright/tests/test_update_copyright.py
+++ /dev/null
@@ -1,130 +0,0 @@
-from __future__ import annotations
-
-import subprocess
-import sys
-import tempfile
-import unittest
-from pathlib import Path
-
-
-SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "update_copyright.py"
-
-
-class UpdateCopyrightTest(unittest.TestCase):
- def test_default_run_leaves_plain_source_without_header_unchanged(self) -> None:
- with tempfile.TemporaryDirectory() as temp_dir:
- root = Path(temp_dir)
- self.write_profile(root)
- source = root / "Foo.java"
- original = "class Foo {}\n"
- source.write_text(original, encoding="utf-8")
-
- subprocess.run(["git", "init", "-q"], cwd=root, check=True)
- subprocess.run(["git", "add", "Foo.java"], cwd=root, check=True)
-
- result = self.run_script(root)
-
- self.assertEqual(result.returncode, 0, result.stderr)
- self.assertIn("Updated 0 file(s).", result.stdout)
- self.assertEqual(result.stderr, "")
- self.assertEqual(source.read_text(encoding="utf-8"), original)
-
- def test_existing_header_is_updated(self) -> None:
- with tempfile.TemporaryDirectory() as temp_dir:
- root = Path(temp_dir)
- self.write_profile(root)
- source = root / "Foo.java"
- source.write_text(
- "/*\n"
- " * Copyright 2024 ACME\n"
- " * All rights reserved\n"
- " */\n"
- "\n"
- "class Foo {}\n",
- encoding="utf-8",
- )
-
- result = self.run_script(root, "--year", "2026", "Foo.java")
-
- self.assertEqual(result.returncode, 0, result.stderr)
- self.assertIn("Updated 1 file(s).", result.stdout)
- self.assertIn("Foo.java", result.stdout)
- self.assertEqual(result.stderr, "")
- self.assertEqual(
- source.read_text(encoding="utf-8"),
- "/*\n"
- " * Copyright 2026 ACME\n"
- " * All rights reserved\n"
- " */\n"
- "\n"
- "class Foo {}\n",
- )
-
- def test_default_run_skips_tracked_files_deleted_from_working_tree(self) -> None:
- with tempfile.TemporaryDirectory() as temp_dir:
- root = Path(temp_dir)
- self.write_profile(root)
- source = root / "Foo.java"
- source.write_text("class Foo {}\n", encoding="utf-8")
-
- subprocess.run(["git", "init", "-q"], cwd=root, check=True)
- subprocess.run(["git", "add", "Foo.java"], cwd=root, check=True)
- source.unlink()
-
- result = subprocess.run(
- [
- sys.executable,
- str(SCRIPT),
- "--root",
- str(root),
- "--dry-run",
- ],
- check=False,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- text=True,
- )
-
- self.assertEqual(result.returncode, 0, result.stderr)
- self.assertIn("Would update 0 file(s).", result.stdout)
- self.assertEqual(result.stderr, "")
-
- @staticmethod
- def run_script(root: Path, *args: str) -> subprocess.CompletedProcess[str]:
- return subprocess.run(
- [
- sys.executable,
- str(SCRIPT),
- "--root",
- str(root),
- *args,
- ],
- check=False,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- text=True,
- )
-
- @staticmethod
- def write_profile(root: Path) -> None:
- copyright_dir = root / ".idea" / "copyright"
- copyright_dir.mkdir(parents=True)
- (copyright_dir / "profiles_settings.xml").write_text(
- ''
- ''
- "\n",
- encoding="utf-8",
- )
- (copyright_dir / "Default.xml").write_text(
- ''
- ""
- ''
- ""
- "\n",
- encoding="utf-8",
- )
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/.agents/skills/version-bumped/SKILL.md b/.agents/skills/version-bumped/SKILL.md
deleted file mode 100644
index 86ca53d..0000000
--- a/.agents/skills/version-bumped/SKILL.md
+++ /dev/null
@@ -1,99 +0,0 @@
----
-name: version-bumped
-description: >
- Verify the current branch has bumped `version.gradle.kts` strictly above
- the base ref; invoke `/bump-version` to auto-recover if not. Composable:
- other modifying skills (`dependency-update`, `bump-gradle`,
- `java-to-kotlin`, `move-files`) call this as their final step so a
- `./gradlew build` or `publishToMavenLocal` can never overwrite a
- previously published Maven Local artifact that integration tests in
- consumer repos depend on.
----
-
-# Ensure version is bumped
-
-This skill is the agent-facing wrapper around
-`.agents/skills/version-bumped/scripts/version-bumped.sh`. The script is the source of truth for
-"has this branch advanced the version vs base?"; this skill just runs it
-and, if it fails, invokes `/bump-version` and re-runs to confirm.
-
-The same logic is enforced as a hook
-(`.agents/scripts/publish-version-gate.sh`) that fires before any
-`./gradlew β¦ (build|publish|publishToMavenLocal)` invocation, so even
-direct gradle calls cannot bypass it. This skill exists for the
-cooperative path β other skills calling it before they finish, so the
-user is never surprised by a blocked gradle command later.
-
-The premise is simple: any feature branch is a candidate for publishing,
-even when the only change is the version bump itself (sometimes the bump
-is the entire change, used to retry a publish that failed because Maven
-repositories were overloaded). So if the branch differs from base at all,
-the version must advance.
-
-## When to use
-
-- Automatically: as the final step of any skill that may change files on
- the branch.
-- Manually (`/version-bumped`): before running `./gradlew build` or
- `./gradlew publishToMavenLocal` on a feature branch when you are not
- sure whether the version has already been bumped.
-
-## Procedure
-
-1. Run the deterministic check:
-
- ```bash
- .agents/skills/version-bumped/scripts/version-bumped.sh
- ```
-
- Honor `VERSION_BUMPED_BASE` if the user has set a non-default base ref
- (e.g. `origin/master`, or a release branch).
-
-2. Interpret the exit code:
-
- - **0** β Done. Either the repository has no root `version.gradle.kts`
- (the version check is `N/A`), the branch has no diff vs base, or the
- version is already strictly greater. Report a one-line confirmation
- and stop.
- - **1** β Block. The script's stderr explains which check failed.
- Proceed to step 3.
- - **2** β Configuration error (no merge-base, parse failure on
- `version.gradle.kts`). Do **not** invoke `/bump-version`
- automatically. Surface the script's stderr to the user and stop.
-
-3. On exit 1, invoke `/bump-version` to perform the actual bump. That
- skill owns the policy (snapshot numbering, the commit subject, the
- rebuild, dependency-report regeneration, and the conflict rule). Do
- not duplicate its work here.
-
-4. After `/bump-version` finishes, re-run the deterministic check. If it
- now passes, report the new version on the branch. If it still fails,
- surface the stderr unchanged and stop β do not loop.
-
-## Why this skill is separate from `/bump-version`
-
-`/bump-version` is the **action** (it edits `version.gradle.kts`,
-commits, rebuilds, may commit reports). `/version-bumped` is the
-**guard** (read-only check, optional auto-recovery). Skills that want to
-say "make sure the branch has a bumped version" should call
-`/version-bumped`, not `/bump-version`, because the guard is a no-op when
-the bump is already done β calling `/bump-version` unconditionally would
-double-bump on every chained skill invocation.
-
-## Relationship to `checkVersionIncrement`
-
-The Gradle task `checkVersionIncrement` (in `buildSrc/.../publish/`)
-asks a different question: *"is this version already in the remote
-Maven metadata?"* It runs on GitHub Actions feature-branch pushes and
-fetches the Spine SDK Artifact Registry. The two checks are
-complementary β neither subsumes the other.
-
-## See also
-
-- `.agents/version-policy.md` β when the version gate applies.
-- `.agents/skills/bump-version/SKILL.md` β the bump procedure itself.
-- `.agents/skills/pre-pr/SKILL.md` β uses the same check at PR time
- (step 2).
-- `.agents/skills/version-bumped/scripts/version-bumped.sh` β the deterministic check.
-- `.agents/scripts/publish-version-gate.sh` β the hook that enforces the
- rule on `./gradlew` invocations.
diff --git a/.agents/skills/version-bumped/scripts/version-bumped.sh b/.agents/skills/version-bumped/scripts/version-bumped.sh
deleted file mode 100755
index f050a5b..0000000
--- a/.agents/skills/version-bumped/scripts/version-bumped.sh
+++ /dev/null
@@ -1,276 +0,0 @@
-#!/usr/bin/env bash
-#
-# Verifies that a feature branch which differs from the base ref also
-# bumps `version.gradle.kts` strictly above the base version. Mirrors the
-# universal "every branch advances the version" policy: a branch with any
-# changes is a candidate for publishing β sometimes the only change is the
-# bump itself, used to retry a publish that failed because Maven
-# repositories were overloaded.
-#
-# Exit codes:
-# 0 β OK: repo has no root `version.gradle.kts`, OR branch has no diff
-# vs base, OR working-tree version is strictly greater than base
-# version.
-# 1 β Block: branch differs from base but version is unchanged or
-# decreased. Stderr points to `/bump-version`.
-# 2 β Configuration error (bad base ref, parse failure). Stderr explains.
-#
-# Inputs (env, all optional):
-# VERSION_BUMPED_BASE Base ref to compare against. Default: master,
-# then main if master is absent.
-# VERSION_BUMPED_KEY Name of the `extra` property holding the
-# publishing version (e.g. `versionToPublish`,
-# `validationVersion`, `bootstrapVersion`). When
-# set, bypasses auto-discovery. Useful for repos
-# that don't follow the `version = extra["β¦"]`
-# pattern in `build.gradle.kts`.
-# VERSION_BUMPED_QUIET When `1`, suppress the "OK" line on stdout.
-# The publish-version-gate hook sets this.
-#
-# Publishing-key discovery:
-# The publishing version's variable name varies across Spine repos
-# (`versionToPublish`, `validationVersion`, `compilerVersion`, β¦).
-# `version.gradle.kts` may also declare other `val xxxVersion by extra(...)` entries
-# that are *dependency* versions of other Spine modules β not this
-# project's own publishing version β so the key cannot be picked by
-# inspecting `version.gradle.kts` alone.
-#
-# The canonical source is `build.gradle.kts`, which assigns
-# `version = extra["KEY"]!!`. This script scans for that pattern,
-# picks the unique key, and parses its value from `version.gradle.kts`.
-# If `build.gradle.kts` does not contain such a line, the script falls
-# back to `versionToPublish`. Set `VERSION_BUMPED_KEY` to override.
-#
-# Notes:
-# * Companion to the Gradle task `checkVersionIncrement` (see
-# `buildSrc/.../publish/CheckVersionIncrement.kt`). The Gradle task
-# asks "is this version already in remote Maven metadata?" β this
-# script asks the simpler local question "has this branch advanced
-# the version vs base?". The two checks are complementary; neither
-# subsumes the other.
-# * The working tree is included in the change-detection so the gate
-# reflects what `./gradlew build` would actually publish.
-#
-set -eu
-
-repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || {
- echo "version-bumped: not inside a git repository" >&2
- exit 2
-}
-cd "$repo_root"
-
-version_file="version.gradle.kts"
-
-# --- N/A: not a versioned project ----------------------------------------
-if [ ! -f "$version_file" ]; then
- [ "${VERSION_BUMPED_QUIET:-0}" = "1" ] || echo "version-bumped: N/A (no root version.gradle.kts)"
- exit 0
-fi
-
-# --- Resolve base ref ----------------------------------------------------
-base="${VERSION_BUMPED_BASE:-}"
-if [ -z "$base" ]; then
- if git show-ref --verify --quiet refs/heads/master; then
- base=master
- elif git show-ref --verify --quiet refs/heads/main; then
- base=main
- else
- echo "version-bumped: no master or main branch found; set VERSION_BUMPED_BASE" >&2
- exit 2
- fi
-fi
-
-if ! git rev-parse --verify --quiet "$base" >/dev/null; then
- echo "version-bumped: base ref '$base' does not resolve" >&2
- exit 2
-fi
-
-# When we are on the base branch itself, there is nothing to gate.
-current_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
-if [ "$current_branch" = "$base" ] || [ "$current_branch" = "${base##*/}" ]; then
- [ "${VERSION_BUMPED_QUIET:-0}" = "1" ] || echo "version-bumped: on base branch ($current_branch); nothing to gate"
- exit 0
-fi
-
-merge_base=$(git merge-base HEAD "$base" 2>/dev/null) || {
- echo "version-bumped: cannot find merge-base of HEAD and '$base'" >&2
- exit 2
-}
-
-# --- Detect any branch divergence vs base (committed/worktree/untracked) -
-committed=$(git diff --name-only "$merge_base"..HEAD 2>/dev/null || true)
-worktree=$(git diff --name-only HEAD 2>/dev/null || true)
-untracked=$(git ls-files --others --exclude-standard 2>/dev/null || true)
-
-if [ -z "$committed" ] && [ -z "$worktree" ] && [ -z "$untracked" ]; then
- [ "${VERSION_BUMPED_QUIET:-0}" = "1" ] || echo "version-bumped: no changes vs $base"
- exit 0
-fi
-
-# --- Discover the publishing-version key ---------------------------------
-# Source of truth is `build.gradle.kts` (or `build.gradle`). Two shapes are
-# recognised, in order:
-#
-# a) version = extra["KEY"]
-# b) version = IDENTIFIER (with `val IDENTIFIER ... by extra` nearby)
-#
-# Single or double quotes are accepted in shape (a). If multiple distinct
-# keys appear across shapes, the script refuses to guess and asks the user
-# to set VERSION_BUMPED_KEY.
-#
-# Return codes:
-# 0 β printed a unique key on stdout
-# 1 β no candidates found (caller should fall back)
-# 2 β ambiguous; diagnostic already on stderr
-discover_key() {
- local files keys_a keys_b keys count
- files=""
- [ -f build.gradle.kts ] && files="build.gradle.kts"
- [ -f build.gradle ] && files="$files build.gradle"
- [ -z "$files" ] && return 1
- # Shape (a): version = extra["KEY"]
- # Anchored to start-of-line (modulo leading whitespace) so that comments
- # like `// version = extra["x"]` and identifiers like `fooversion = ...`
- # don't produce false matches.
- # shellcheck disable=SC2086
- keys_a=$(grep -hE '^[[:space:]]*version[[:space:]]*=[[:space:]]*extra[[:space:]]*\[[[:space:]]*["'"'"'][^"'"'"']+["'"'"']' $files 2>/dev/null \
- | sed -nE 's/.*extra[[:space:]]*\[[[:space:]]*["'"'"']([^"'"'"']+)["'"'"'].*/\1/p')
- # Shape (b): version = IDENTIFIER (bare Kotlin identifier, no '[' or '"').
- # Only accept the identifier if the same file also declares
- # `val IDENTIFIER[: String]? by extra` β otherwise it's a plain local
- # variable (common in Groovy `build.gradle`), not an `extra` property we
- # can resolve in `version.gradle.kts`.
- local candidates_b cand
- # shellcheck disable=SC2086
- candidates_b=$(grep -hE '^[[:space:]]*version[[:space:]]*=[[:space:]]*[A-Za-z_][A-Za-z0-9_]*[[:space:]]*$' $files 2>/dev/null \
- | sed -nE 's/^[[:space:]]*version[[:space:]]*=[[:space:]]*([A-Za-z_][A-Za-z0-9_]*)[[:space:]]*$/\1/p')
- keys_b=""
- for cand in $candidates_b; do
- # shellcheck disable=SC2086
- if grep -hE "^[[:space:]]*val[[:space:]]+${cand}([[:space:]]*:[[:space:]]*String)?[[:space:]]+by[[:space:]]+extra([^A-Za-z0-9_]|\$)" $files >/dev/null 2>&1; then
- keys_b="${keys_b}${cand}
-"
- fi
- done
- keys=$(printf '%s\n%s' "$keys_a" "$keys_b" | sed '/^$/d' | sort -u)
- [ -z "$keys" ] && return 1
- count=$(printf '%s\n' "$keys" | wc -l | tr -d ' ')
- if [ "$count" -gt 1 ]; then
- {
- echo "version-bumped: ambiguous publishing key in build scripts:"
- while IFS= read -r k; do printf ' %s\n' "$k"; done <<< "$keys"
- echo " Set VERSION_BUMPED_KEY to disambiguate."
- } >&2
- return 2
- fi
- printf '%s' "$keys"
-}
-
-key="${VERSION_BUMPED_KEY:-}"
-if [ -z "$key" ]; then
- set +e
- key=$(discover_key)
- rc=$?
- set -e
- if [ "$rc" = "2" ]; then
- exit 2
- fi
- if [ "$rc" != "0" ] || [ -z "$key" ]; then
- key="versionToPublish"
- fi
-fi
-
-# --- Parse a `val KEY by extra(...)` from a Gradle file content ----------
-# Handles three shapes (per .agents/skills/bump-version/SKILL.md step 2):
-# 1. val KEY[: String]? by extra("X") β literal extra
-# 2. val SRC[: String]? by extra("X") β alias chain via extra
-# val KEY[: String]? by extra(SRC)
-# 3. val SRC[: String]? = "X" β alias chain via plain val
-# val KEY[: String]? by extra(SRC)
-# The key name is parameterized so that any project-specific name works
-# (versionToPublish, validationVersion, bootstrapVersion, botVersion, β¦).
-parse_version() {
- local content="$1" name="$2"
- local v varName
- # Shape 1: literal.
- v=$(printf '%s' "$content" \
- | grep -E "val[[:space:]]+${name}([[:space:]]*:[[:space:]]*String)?[[:space:]]+by[[:space:]]+extra\(\"" \
- | head -n1 \
- | sed -nE 's/.*extra\("([^"]+)".*/\1/p')
- if [ -n "$v" ]; then
- printf '%s' "$v"
- return 0
- fi
- # Shapes 2 & 3: extract the alias source identifier.
- varName=$(printf '%s' "$content" \
- | grep -E "val[[:space:]]+${name}([[:space:]]*:[[:space:]]*String)?[[:space:]]+by[[:space:]]+extra\(" \
- | head -n1 \
- | sed -nE 's/.*extra\(([A-Za-z_][A-Za-z0-9_]*)\).*/\1/p')
- if [ -n "$varName" ]; then
- # Shape 2: source is `val SRC ... by extra("X")`.
- v=$(printf '%s' "$content" \
- | grep -E "val[[:space:]]+${varName}([[:space:]]*:[[:space:]]*String)?[[:space:]]+by[[:space:]]+extra\(\"" \
- | head -n1 \
- | sed -nE 's/.*extra\("([^"]+)".*/\1/p')
- if [ -n "$v" ]; then
- printf '%s' "$v"
- return 0
- fi
- # Shape 3: source is `val SRC[: String]? = "X"`.
- v=$(printf '%s' "$content" \
- | grep -E "val[[:space:]]+${varName}([[:space:]]*:[[:space:]]*String)?[[:space:]]*=[[:space:]]*\"" \
- | head -n1 \
- | sed -nE 's/.*=[[:space:]]*"([^"]+)".*/\1/p')
- if [ -n "$v" ]; then
- printf '%s' "$v"
- return 0
- fi
- fi
- return 1
-}
-
-head_content=$(cat "$version_file" 2>/dev/null || true)
-head_version=$(parse_version "$head_content" "$key" || true)
-if [ -z "$head_version" ]; then
- echo "version-bumped: cannot parse '$key' from working-tree $version_file" >&2
- exit 2
-fi
-
-# Base content may legitimately not exist (file newly introduced).
-base_content=$(git show "$base:$version_file" 2>/dev/null || true)
-if [ -z "$base_content" ]; then
- [ "${VERSION_BUMPED_QUIET:-0}" = "1" ] || echo "version-bumped: $version_file newly introduced at $head_version; treating as bumped"
- exit 0
-fi
-
-base_version=$(parse_version "$base_content" "$key" || true)
-if [ -z "$base_version" ]; then
- echo "version-bumped: cannot parse '$key' from $base:$version_file" >&2
- exit 2
-fi
-
-# --- Strict-greater comparison via `sort -V` -----------------------------
-if [ "$head_version" = "$base_version" ]; then
- cmp="equal"
-elif [ "$(printf '%s\n%s\n' "$base_version" "$head_version" | sort -V | tail -n1)" = "$head_version" ]; then
- cmp="greater"
-else
- cmp="lesser"
-fi
-
-if [ "$cmp" = "greater" ]; then
- [ "${VERSION_BUMPED_QUIET:-0}" = "1" ] || echo "version-bumped: OK ($key: $base_version -> $head_version)"
- exit 0
-fi
-
-cat >&2 <
- Write, edit, and restructure user-facing and developer-facing documentation.
- Use when asked to create/update docs such as `README.md`, `docs/**`, and
- other Markdown documentation, including keeping docs navigation data in sync;
- when drafting tutorials, guides, troubleshooting pages, or migration notes; and
- when improving inline API documentation (KDoc) and examples.
----
-
-# Write documentation (repo-specific)
-
-## Decide the target and audience
-
-- Identify the target reader: end user, contributor, maintainer, or tooling/automation.
-- Identify the task type: new doc, update, restructure, or documentation audit.
-- Identify the acceptance criteria: βwhat is correct when the reader is done?β
-
-## Choose where the content should live
-
-- Prefer updating an existing doc over creating a new one.
-- Place content in the most discoverable location:
- - `README.md`: project entry point and βwhat is this?β.
- - `docs/`: longer-form docs (follow existing conventions in that tree).
- - Source KDoc: API usage, examples, and semantics that belong with the code.
-
-## Keep docs navigation in sync
-
-- When adding, removing, moving, or renaming a page under
- `docs/content/docs//`, keep the current version's matching
- `sidenav.yml` in sync.
-- Use `docs/data/versions.yml` to identify the current documentation version for
- that section. The current version is the entry with `is_main: true`; its
- `version_id` maps to `docs/data/docs///sidenav.yml`.
-- Do not update historical version entries or their navigation files unless the
- user explicitly asks to edit that historical version.
-- Map page files to `file_path` values relative to the current version's
- `content_path`, without `.md`; `_index.md` maps to its directory path, such as
- `01-getting-started/_index.md` -> `01-getting-started`.
-- Keep each `page` label aligned with the page frontmatter `title` unless the
- existing navigation intentionally uses a shorter reader-facing label.
-- Preserve the existing ordering, nesting, keys, comments, and YAML quoting
- style. Remove nav entries for deleted pages and update `file_path` values for
- moved pages.
-- If a docs content change should not appear in navigation, say so explicitly in
- the final response.
-
-## Follow local documentation conventions
-
-- Follow `.agents/documentation-guidelines.md` and `.agents/documentation-tasks.md`.
-- Use fenced code blocks for commands and examples; format file/dir names as code.
-- When referencing a documentation page or section in body prose, use typographic
- double quotation marks only if the visible reference text is the actual page or
- section title, such as the βGetting startedβ page or the βTroubleshootingβ
- section. The title normally starts with a capital letter. Do not add these
- quotes around generic or descriptive links such as βthis pageβ, βthe next
- sectionβ, βdeclaring constraintsβ, or `4.3`, even if they point to a page or
- section. Do not add these quotes in βWhatβs nextβ sections or navigation
- elements. Keep file paths, identifiers, frontmatter values, navigation labels,
- and Markdown link labels in their expected syntax.
-- In Markdown files, prefer footnote-style reference links for external `https://`
- targets instead of inline links. Write readable body text like
- `[label][short-id]`, then place the URL definition near the end of the file,
- such as `[short-id]: https://example.com/long/path`. Keep reference IDs short
- and descriptive. Inline links are still fine for local relative paths.
-- Avoid widows, runts, orphans, and rivers by reflowing paragraphs when needed.
-
-## Make docs actionable
-
-- Prefer steps the reader can execute (commands + expected outcome).
-- Prefer concrete examples over abstract descriptions.
-- Include prerequisites (versions, OS, environment) when they are easy to miss.
-- Use consistent terminology (match code identifiers and existing docs).
-
-## KDoc-specific guidance
-
-- For public/internal APIs, include at least one example snippet demonstrating common usage.
-- When converting from Javadoc/inline comments to KDoc:
- - Remove HTML like `
` and preserve meaning.
- - Prefer short paragraphs and blank lines over HTML formatting.
-
-## Validate changes
-
-- For code changes, follow `.agents/running-builds.md`.
-- For documentation-only changes in Kotlin/Java sources, prefer `./gradlew dokka`.
diff --git a/.agents/skills/writer/agents/openai.yaml b/.agents/skills/writer/agents/openai.yaml
deleted file mode 100644
index 44eaa4e..0000000
--- a/.agents/skills/writer/agents/openai.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-interface:
- display_name: "Writer"
- short_description: "Write and update user/developer docs"
- default_prompt: "Write or revise documentation in this repository (for example: README.md, docs/**, CONTRIBUTING.md, and API documentation/KDoc). Follow local documentation guidelines in .agents/*.md, keep changes concise and actionable, and include concrete examples and commands where appropriate."
-
diff --git a/.agents/skills/writer/assets/templates/doc-page.md b/.agents/skills/writer/assets/templates/doc-page.md
deleted file mode 100644
index f405b71..0000000
--- a/.agents/skills/writer/assets/templates/doc-page.md
+++ /dev/null
@@ -1,23 +0,0 @@
-# Title
-
-## Goal
-
-State what the reader will accomplish.
-
-## Prerequisites
-
-- List versions/tools the reader needs.
-
-## Steps
-
-1. Do the first thing.
-2. Do the next thing.
-
-## Verify
-
-Show how the reader can confirm success.
-
-## Troubleshooting
-
-- Common failure: likely cause β fix.
-
diff --git a/.agents/skills/writer/assets/templates/kdoc-example.md b/.agents/skills/writer/assets/templates/kdoc-example.md
deleted file mode 100644
index fdbd9b6..0000000
--- a/.agents/skills/writer/assets/templates/kdoc-example.md
+++ /dev/null
@@ -1,11 +0,0 @@
-````kotlin
-/**
- * Explain what this API does in one sentence.
- *
- * ## Example
- * ```kotlin
- * // Show the typical usage pattern.
- * val result = doThing()
- * ```
- */
-````
diff --git a/.agents/skills/writer/assets/templates/kotlin-java-example.md b/.agents/skills/writer/assets/templates/kotlin-java-example.md
deleted file mode 100644
index 5517516..0000000
--- a/.agents/skills/writer/assets/templates/kotlin-java-example.md
+++ /dev/null
@@ -1,13 +0,0 @@
-{{< code-tabs langs="Kotlin, Java">}}
-
-{{< code-tab lang="Kotlin" >}}
-```kotlin
-```
-{{< /code-tab >}}
-
-{{< code-tab lang="Java" >}}
-```java
-```
-{{< /code-tab >}}
-
-{{< /code-tabs >}}
diff --git a/.agents/widow-runt-orphan.jpg b/.agents/widow-runt-orphan.jpg
deleted file mode 100644
index 284b02a..0000000
Binary files a/.agents/widow-runt-orphan.jpg and /dev/null differ
diff --git a/.claude/agents b/.claude/agents
new file mode 120000
index 0000000..18e96c9
--- /dev/null
+++ b/.claude/agents
@@ -0,0 +1 @@
+../.agents/shared/claude/agents
\ No newline at end of file
diff --git a/.claude/agents/review-docs.md b/.claude/agents/review-docs.md
deleted file mode 100644
index 0481b24..0000000
--- a/.claude/agents/review-docs.md
+++ /dev/null
@@ -1,18 +0,0 @@
----
-name: review-docs
-description: Reviews documentation changes β KDoc/Javadoc inside Kotlin/Java sources and Markdown docs (`README.md`, `docs/**`) β against Spine documentation conventions. Use proactively when a diff touches doc comments or Markdown, before opening a doc-affecting PR, or when the user asks for a documentation review. Read-only; does not run builds.
-tools: Read, Grep, Glob, Bash
-model: inherit
----
-
-Follow the `review-docs` skill exactly:
-
-- Skill: `.agents/skills/review-docs/SKILL.md`
-- The skill owns the review procedure, the per-area checks (KDoc/Javadoc,
- Markdown, prose flow, terminology), and the output format
- (Must fix / Should fix / Nits + one-line verdict).
-- Scope yourself to documentation only. If you spot a code-quality issue,
- surface it briefly as a Nit pointing at the `kotlin-review` agent β
- do not expand the review.
-- Read-only: use `Read`, `Grep`, `Glob`, and `Bash` solely for `git diff`
- and related read-only inspection. Do not run builds.
diff --git a/.claude/commands b/.claude/commands
new file mode 120000
index 0000000..ad85cd8
--- /dev/null
+++ b/.claude/commands
@@ -0,0 +1 @@
+../.agents/shared/claude/commands
\ No newline at end of file
diff --git a/.claude/commands/move-files.md b/.claude/commands/move-files.md
deleted file mode 100644
index 25885f9..0000000
--- a/.claude/commands/move-files.md
+++ /dev/null
@@ -1,12 +0,0 @@
----
-description: Move or rename files/directories, updating all references and build metadata.
-argument-hint: ""
-allowed-tools: Read, Edit, Bash(git mv:*), Bash(git status:*), Bash(git ls-files:*), Grep, Glob
----
-
-Follow the `move-files` skill exactly:
-
-- Skill: `.agents/skills/move-files/SKILL.md`
-- Operation: $ARGUMENTS
-- Preflight (run `git status --short`, classify scope) -> Search for all old identifiers -> Move with `git mv` -> Repair references (imports, build metadata, docs) -> Verify.
-- Report: Moved[], UpdatedRefs[], Verification[], Risks[].
diff --git a/.claude/commands/pre-pr.md b/.claude/commands/pre-pr.md
deleted file mode 100644
index 24499cc..0000000
--- a/.claude/commands/pre-pr.md
+++ /dev/null
@@ -1,32 +0,0 @@
----
-description: Run the applicable pre-PR checklist (version gate, build/check, reviewers) and write a sentinel so `gh pr create` is unblocked.
-argument-hint: "[base-ref]"
-allowed-tools: Read, Write, Grep, Glob, Agent, Bash
----
-
-Follow the `pre-pr` skill exactly:
-
-- Skill: `.agents/skills/pre-pr/SKILL.md`
-- Base ref: $ARGUMENTS (treat empty as `master`).
-- Detect whether the repository-root `version.gradle.kts` exists. If it is
- absent at both the base ref and `HEAD`, the version check is `N/A`; do not
- create the file and do not ask for `/bump-version`.
-- Run the build/check command selected by the skill and
- `.agents/running-builds.md`. The command may be Gradle or non-Gradle.
-- Dispatch the reviewers as Claude subagents in parallel β send a single
- message with multiple Agent tool uses:
- - `kotlin-review` when `.kt|.kts|.java` files changed.
- - `review-docs` when `.md` files or KDoc inside sources changed.
- - `dependency-audit` when any file under
- `buildSrc/src/main/kotlin/io/spine/dependency/` changed.
-- Pass the version-check status to reviewers. If it is `N/A`, tell them:
- "This repository has no root `version.gradle.kts`; a version bump is not
- applicable and must not be reported as missing."
-- Each reviewer is read-only; do not pass it edit tools.
-- On any reviewer returning `REQUEST CHANGES`, treat the overall result
- as `FAIL` and stop before writing the sentinel as `PASS`.
-- Sentinel location: `$(git rev-parse --show-toplevel)/.git/pre-pr.ok`,
- format per the skill (`head=`, `branch=`, `status=`, `timestamp=`,
- `build=`, `reviewers=`, `version=`). Use `git rev-parse HEAD` for the
- SHA and `date -u +%Y-%m-%dT%H:%M:%SZ` for the timestamp.
-- Do NOT run `gh pr create`. That is the user's next step.
diff --git a/.claude/commands/review-docs.md b/.claude/commands/review-docs.md
deleted file mode 100644
index f8043f0..0000000
--- a/.claude/commands/review-docs.md
+++ /dev/null
@@ -1,21 +0,0 @@
----
-description: Review documentation changes (KDoc/Javadoc and Markdown) against Spine documentation conventions.
-argument-hint: "[base-ref | --staged | paths...]"
-allowed-tools: Read, Grep, Glob, Bash(git diff:*), Bash(git log:*), Bash(git status:*), Bash(git rev-parse:*), Bash(git ls-files:*)
----
-
-Follow the `review-docs` skill exactly:
-
-- Skill: `.agents/skills/review-docs/SKILL.md`
-- Scope / flags: $ARGUMENTS
- - Empty: review the current branch's diff against `master` (`git diff master...HEAD`).
- - `--staged`: review staged changes only (`git diff --staged`).
- - A base ref (e.g. `master`, `origin/master`, a commit SHA): review `git diff ...HEAD`.
- - Explicit paths: limit the review to those paths in addition to the diff scope.
-- The skill owns the procedure, the per-area checks (KDoc/Javadoc, Markdown,
- prose flow, terminology), and the output format (Must fix / Should fix /
- Nits + one-line verdict).
-- Stay in scope: documentation only. If a code-quality issue surfaces,
- note it briefly as a Nit pointing at `/review` (or the `kotlin-review`
- agent) β do not expand the review.
-- Read-only: do not edit files, do not run builds.
diff --git a/.claude/commands/update-copyright.md b/.claude/commands/update-copyright.md
deleted file mode 100644
index 076fb61..0000000
--- a/.claude/commands/update-copyright.md
+++ /dev/null
@@ -1,12 +0,0 @@
----
-description: Refresh copyright headers from the IntelliJ profile, replacing today.year with the current year.
-argument-hint: "[paths...]"
-allowed-tools: Bash(python3 .agents/skills/update-copyright/scripts/update_copyright.py:*), Read
----
-
-Follow the `update-copyright` skill exactly:
-
-- Skill: `.agents/skills/update-copyright/SKILL.md`
-- Run: `python3 .agents/skills/update-copyright/scripts/update_copyright.py $ARGUMENTS`
-- If $ARGUMENTS is empty, run once with `--dry-run`, show the output to the user, then run without `--dry-run`.
-- Never add a header to a file that doesn't already have one.
diff --git a/.claude/commands/write-docs.md b/.claude/commands/write-docs.md
deleted file mode 100644
index b9b9a74..0000000
--- a/.claude/commands/write-docs.md
+++ /dev/null
@@ -1,14 +0,0 @@
----
-description: Write or update Markdown / KDoc documentation per Spine documentation conventions.
-argument-hint: ""
-allowed-tools: Read, Edit, Write, Grep, Glob
----
-
-Follow the `writer` skill exactly:
-
-- Skill: `.agents/skills/writer/SKILL.md`
-- Topic / target: $ARGUMENTS
-- Decide audience first (end user, contributor, maintainer, tooling).
-- Prefer updating an existing doc over creating a new one.
-- Keep `docs/data/docs///sidenav.yml` in sync when adding, removing, moving, or renaming pages under `docs/content/docs//`.
-- Honor `.agents/documentation-guidelines.md` and `.agents/documentation-tasks.md`.
diff --git a/.claude/settings.json b/.claude/settings.json
index 4fdcab8..337c904 100644
--- a/.claude/settings.json
+++ b/.claude/settings.json
@@ -1,5 +1,6 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
+ "plansDirectory": ".claude/plans",
"permissions": {
"allow": [
"Bash(git status:*)",
@@ -30,7 +31,9 @@
"Bash(touch:*)",
"Bash(python3 .agents/skills/update-copyright/scripts/update_copyright.py:*)",
"Bash(./config/pull)",
- "Bash(./config/migrate)"
+ "Bash(./config/migrate)",
+ "Skill(pre-pr)",
+ "Skill(pre-pr:*)"
],
"deny": [
"Bash(git push:*)",
@@ -49,6 +52,27 @@
]
},
"hooks": {
+ "SessionStart": [
+ {
+ "hooks": [
+ {
+ "type": "command",
+ "command": "$CLAUDE_PROJECT_DIR/init-submodules"
+ }
+ ]
+ }
+ ],
+ "PreToolUse": [
+ {
+ "matcher": "Bash",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "$CLAUDE_PROJECT_DIR/.agents/scripts/secret-scan-gate.sh"
+ }
+ ]
+ }
+ ],
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 81c8d50..039657b 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -10,8 +10,27 @@ repository root β read it first.
If `.agents/project.md` exists, read it before reviewing. It provides the
language, architecture, role, and code review checklist for this specific repo.
-Additional guidelines are in `.agents/` β see `.agents/_TOC.md` for the index
-(if present; Hugo repos do not include this file).
+Additional guidelines are in `.agents/guidelines/` β see
+`.agents/guidelines/_TOC.md` for the index.
+
+## Do not review
+
+Never review `gradlew` or `gradlew.bat` in any repository, including `config`.
+These files are provided by Gradle and are not edited manually.
+
+If the current repository is `config`, review its files normally unless noted
+above: they are authoritative there. In other repositories, the following files are managed by
+the `config` submodule and must be reviewed in the `config` repository, not
+here. In those consumer repositories, skip them without comment:
+
+- `AGENTS.md`, `CLAUDE.md`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`
+- `.agents/**` (except `.agents/project.md`)
+- `.claude/**`, `.idea/**`, `.junie/**`
+- `.github/copilot-instructions.md`
+- `buildSrc/**` (except `buildSrc/src/main/kotlin/module.gradle.kts`)
+- `gradle/`, `gradlew`, `gradlew.bat`
+- `.codecov.yml`, `.gitignore`, `gradle.properties`, `lychee.toml`
+- `.github/workflows/` β unless the workflow was introduced by this repo
## Universal rules
diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml
index 7a51f8f..6755ff9 100644
--- a/.github/workflows/check-links.yml
+++ b/.github/workflows/check-links.yml
@@ -33,10 +33,15 @@ jobs:
cancel-in-progress: true
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
# Detect the Hugo site root (`docs/` or `site/`) by looking for a Hugo
- # config file. Outputs `present=true|false` and `site_dir=docs|site`.
+ # config file. Hugo config may live directly in the site root or in a
+ # `config/` or `config/_default/` subdirectory (both layouts are valid).
+ # Outputs `present=true|false` and `work_dir` (the directory where
+ # `npm ci` / `hugo` commands should run β either `$dir/_preview` for
+ # repos that use a separate preview sub-tree, or `$dir` for repos whose
+ # Node/Hugo setup lives at the site root).
# When neither directory has a Hugo config, the job short-circuits to a
# success so that this shared workflow stays green on repos that do not
# host a Hugo site at all.
@@ -44,15 +49,21 @@ jobs:
id: docs
run: |
for dir in docs site; do
- for cfg in hugo.toml hugo.yaml; do
+ for cfg in hugo.toml hugo.yaml \
+ config/hugo.toml config/hugo.yaml \
+ config/_default/hugo.toml config/_default/hugo.yaml; do
if [ -f "$dir/$cfg" ]; then
- echo "site_dir=$dir" >> "$GITHUB_OUTPUT"
if [ -f "$dir/_preview/package-lock.json" ]; then
+ echo "work_dir=$dir/_preview" >> "$GITHUB_OUTPUT"
echo "present=true" >> "$GITHUB_OUTPUT"
- echo "::notice::Hugo site found under $dir/"
+ echo "::notice::Hugo site found under $dir/ (work_dir: $dir/_preview)"
+ elif [ -f "$dir/package-lock.json" ]; then
+ echo "work_dir=$dir" >> "$GITHUB_OUTPUT"
+ echo "present=true" >> "$GITHUB_OUTPUT"
+ echo "::notice::Hugo site found under $dir/ (work_dir: $dir)"
else
echo "present=false" >> "$GITHUB_OUTPUT"
- echo "::notice::Hugo config found in $dir/ but $dir/_preview/package-lock.json is missing β skipping link check."
+ echo "::notice::Hugo config found in $dir/ but no package-lock.json found β skipping link check."
fi
exit 0
fi
@@ -78,7 +89,7 @@ jobs:
with:
node-version: '26'
cache: 'npm'
- cache-dependency-path: ${{ steps.docs.outputs.site_dir }}/_preview/package-lock.json
+ cache-dependency-path: ${{ steps.docs.outputs.work_dir }}/package-lock.json
# `HUGO_CACHEDIR=/tmp/hugo_cache` (set in `env:` above) makes Hugo
# actually write to the path this step restores from. The key hashes
@@ -95,12 +106,12 @@ jobs:
- name: Install Dependencies
if: steps.docs.outputs.present == 'true'
- working-directory: ${{ steps.docs.outputs.site_dir }}/_preview
+ working-directory: ${{ steps.docs.outputs.work_dir }}
run: npm ci
- name: Build docs preview site
if: steps.docs.outputs.present == 'true'
- working-directory: ${{ steps.docs.outputs.site_dir }}/_preview
+ working-directory: ${{ steps.docs.outputs.work_dir }}
run: hugo -e development
# Cache Lychee results to avoid hitting rate limits.
@@ -183,7 +194,7 @@ jobs:
# Lychee step is visible β change one, change the other.
- name: Start Hugo server
if: steps.docs.outputs.present == 'true'
- working-directory: ${{ steps.docs.outputs.site_dir }}/_preview
+ working-directory: ${{ steps.docs.outputs.work_dir }}
run: |
nohup hugo server \
--environment development \
@@ -202,4 +213,4 @@ jobs:
run: |
./lychee/lychee --config lychee.toml --timeout 60 \
--base-url http://localhost:1313/ \
- '${{ steps.docs.outputs.site_dir }}/_preview/public/**/*.html'
+ '${{ steps.docs.outputs.work_dir }}/public/**/*.html'
diff --git a/.github/workflows/secret-scan.yml b/.github/workflows/secret-scan.yml
new file mode 100644
index 0000000..6f0130e
--- /dev/null
+++ b/.github/workflows/secret-scan.yml
@@ -0,0 +1,70 @@
+name: Secret scan
+
+# Defense-in-depth behind the local `secret-scan` pre-commit hook and the
+# `.gitignore` secret patterns: if a credential is committed despite those, this
+# fails the pull request before it can merge. Distributed to every Spine repo by
+# `./config/pull`.
+
+on:
+ pull_request:
+ push:
+ branches:
+ - master
+ - main
+
+permissions:
+ contents: read
+
+jobs:
+ gitleaks:
+ name: gitleaks
+ runs-on: ubuntu-latest
+ env:
+ # Pinned gitleaks version β bump through the usual dependency-update process.
+ GITLEAKS_VERSION: "8.21.2"
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+ with:
+ # Full history so a pull request's commit range can be scanned.
+ fetch-depth: 0
+
+ - name: Install gitleaks
+ # Run gitleaks as the runner user against the checkout it owns β no
+ # container, so no "dubious ownership" git error and no GitHub Action
+ # org-licence requirement.
+ run: |
+ curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
+ | tar -xzf - gitleaks
+ ./gitleaks version
+
+ - name: Scan
+ env:
+ EVENT: ${{ github.event_name }}
+ BASE: ${{ github.event.pull_request.base.sha }}
+ HEAD: ${{ github.event.pull_request.head.sha }}
+ BEFORE: ${{ github.event.before }}
+ AFTER: ${{ github.sha }}
+ run: |
+ if [ "$EVENT" = pull_request ]; then
+ # Scan the PR's own commit RANGE: a secret added in one commit and
+ # deleted in a later commit of the same PR is still caught (a
+ # working-tree scan would miss it, yet merging keeps the secret-bearing
+ # commit reachable), while already-rotated secrets in older history
+ # outside base..head are not re-flagged.
+ ./gitleaks git --log-opts="$BASE..$HEAD" --redact --verbose --exit-code=1 .
+ else
+ # Push to a default branch: scan the pushed commit RANGE (before..after)
+ # so an add-then-remove batch is caught here too, not only on PRs β the
+ # leaked commit would otherwise stay reachable on the default branch. A
+ # branch's first push reports an all-zero `before` (no range); fall back
+ # to a working-tree scan then.
+ if [ -n "$BEFORE" ] && [ "$BEFORE" != "0000000000000000000000000000000000000000" ]; then
+ ./gitleaks git --log-opts="$BEFORE..$AFTER" --redact --verbose --exit-code=1 .
+ else
+ # Branch's first push (all-zero `before`): no range to diff against, so
+ # scan the whole history reachable from the pushed tip as the initial
+ # import β an add-then-remove within those commits is still caught.
+ ./gitleaks git --log-opts="$AFTER" --redact --verbose --exit-code=1 .
+ fi
+ fi
diff --git a/.gitignore b/.gitignore
index 472e7ab..ecac2a2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,23 +1,51 @@
-# Hugo cache files
-docs/_preview/resources
-public
+# >>> shared config (managed by ./config/pull -- do not edit inside this block) >>>
+#
+# Copyright 2025, TeamDev. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Redistribution and use in source and/or binary forms, with or without
+# modification, must retain the above copyright notice and the following
+# disclaimer.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
-# Cache folder for Hugo Modules
-docs/_preview/_vendor
+#
+# This file is used for two purposes:
+# 1. ignoring files in the `config` project.
+# 2. ignoring files in the projects that import `config` as a sub-module.
+#
+# Therefore, instructions below are superset of instructions required for all the projects.
-# Node modules
-node_modules
+# Temporary output of AI agents.
+.output
-# Used to control concurrency between multiple Hugo instances
-docs/_preview/.hugo_build.lock
+# `jenv` local configuration.
+.java-version
-# Hugo build output
-docs/_site/
+# Internal tool directories.
+.fleet/
+.junie/memory/
-# Needed for navigation/intellisense help inside code editors
-jsconfig.json
+# Kotlin temp directories.
+**/.kotlin/
-# IntelliJ IDEA modules and interim config files
+# IntelliJ IDEA modules and interim config files.
*.iml
.idea/*.xml
.idea/.name
@@ -26,23 +54,197 @@ jsconfig.json
.idea/modules
.idea/shelf
-!.idea/misc.xml
+# `.idea/misc.xml` is intentionally NOT re-included below. It is project-local β
+# it holds the per-project JDK name and IDEA's own churn (entry-point list
+# indices, external-storage toggles) β so `.idea/*.xml` above keeps it ignored.
+# `./config/pull` (via `migrate`) untracks any copy an earlier pull committed.
+
+# Do not ignore the following IDEA settings
+!.idea/codeStyleSettings.xml
!.idea/codeStyles/
!.idea/copyright/
-# The `embed-code` temporary directory
-_code/build
-
-# Local Java version for this project
-.java-version
+# Ignore IDEA config files under `tests`
+/tests/.idea/**
# Gradle interim configs
-.gradle/
+**/.gradle/**
+
+# Temp directory for Gradle TestKit runners
+**/.gradle-test-kit/**
+
+# Integration test log files
+/tests/_out/**
+
+# Generated source code
+**/generated/**
+**/*.pb.dart
+**/*.pbenum.dart
+**/*.pbserver.dart
+**/*.pbjson.dart
+
+# Generated source code with custom path under `tests`
+/tests/**/proto-gen/**
# Gradle build files
+**/build/**
+!**/src/**/build/**
+
+# Build files produced by the IDE
+**/out/**
+
+# Ignore Gradle GUI config
+gradle-app.setting
+
+# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
+!gradle-wrapper.jar
+
+# Cache of project
+.gradletasknamecache
+
+# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
+# gradle/wrapper/gradle-wrapper.properties
+
+# Spine internal directory for storing intermediate artifacts
+**/.spine/**
+
+# ---------------------------------------------------------------------------
+# Secrets β NEVER commit these.
+#
+# Encrypted credentials live under `.github/keys/*.gpg` and ARE committed.
+# `config/scripts/decrypt.sh` turns each into its PLAINTEXT twin at build / CI /
+# publish time (e.g. `spine-dev-framework-ci.json.gpg` -> `spine-dev.json`). The
+# decrypted twins below β and any private key or service-account file β must stay
+# out of Git. The shared `secret-scan` pre-commit hook is the backstop if one ever
+# slips past these patterns.
+# ---------------------------------------------------------------------------
+
+# Maven repository login details; each workstation defines its own.
+credentials.tar
+credentials.properties
+cloudrepo.properties
+deploy_key_rsa
+gcs-auth-key.json
+
+# Decrypted Google / GCP service-account keys (plaintext twins of *.gpg).
+spine-dev.json
+spine-dev-*.json
+maven-publisher.json
+firebase-sa.json
+*-sa.json
+*service-account*.json
+
+# Decrypted credential property files and portal / publisher secrets.
+*.secret.properties
+
+# Private SSH keys (public keys are *.pub and remain committable).
+*_rsa
+*_dsa
+*_ecdsa
+*_ed25519
+id_rsa
+id_dsa
+id_ecdsa
+id_ed25519
+
+# ...but always keep the committed ENCRYPTED forms.
+!*.gpg
+
+# Log files
+*.log
+
+# Package Files #
+*.war
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+.packages
+pubspec.lock
+
+# Ignore the `tmp` directory used for building dependant repositories.
+/tmp
+
+.gradle-test-kit/
+
+# Python cache
+__pycache__/
+*.pyc
+
+# Claude working files
+/.claude/worktrees/
+# Ephemeral plan-mode scratch (durable task docs live in `.agents/tasks/`).
+/.claude/plans/
+
+# Personal, per-developer Claude Code settings overrides (never committed;
+# the distributed `.claude/settings.json` is the shared, committed layer).
+/.claude/settings.local.json
+
+# Auto-downloaded Lychee binary used by the `check-links` skill.
+/.agents/skills/check-links/.cache/
+
+# Lychee link-checker cache (created by the `check-links` skill and
+# the `Check Links` workflow when run locally).
+.lycheecache
+
+# Hugo docs preview site build artifacts (used by the `check-links`
+# skill and the `Check Links` workflow in repos that contain a
+# `docs/_preview` Hugo site).
+docs/_preview/node_modules/
+docs/_preview/public/
+docs/_preview/resources/
+# <<< shared config <<<
+
+# >>> repo-local entries (preserved across ./config/pull) >>>
+# Hugo cache files
+docs/_preview/resources
+public
+# Cache folder for Hugo Modules
+docs/_preview/_vendor
+# Node modules
+node_modules
+# Used to control concurrency between multiple Hugo instances
+docs/_preview/.hugo_build.lock
+# Hugo build output
+docs/_site/
+# Needed for navigation/intellisense help inside code editors
+jsconfig.json
+# IntelliJ IDEA modules and interim config files
+!.idea/codeStyles/
+!.idea/copyright/
+# The `embed-code` temporary directory
+_code/build
+# Local Java version for this project
+.gradle/
build/
generated/
-
# The `check-links` cache directory and Lychee cache.
-/.agents/skills/check-links/.cache/
/.lycheecache
+# <<< repo-local entries <<<
+
+# >>> secret ignores re-asserted last (managed by ./config/pull -- do not edit) >>>
+credentials.tar
+credentials.properties
+cloudrepo.properties
+deploy_key_rsa
+gcs-auth-key.json
+spine-dev.json
+spine-dev-*.json
+maven-publisher.json
+firebase-sa.json
+*-sa.json
+*service-account*.json
+*.secret.properties
+*_rsa
+*_dsa
+*_ecdsa
+*_ed25519
+id_rsa
+id_dsa
+id_ecdsa
+id_ed25519
+# <<< secret ignores <<<
diff --git a/.gitmodules b/.gitmodules
index e7f6727..092dddc 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -16,3 +16,9 @@
[submodule "config"]
path = config
url = https://github.com/SpineEventEngine/config
+[submodule ".agents/shared"]
+ path = .agents/shared
+ url = https://github.com/SpineEventEngine/agents.git
+ branch = master
+ update = merge
+ ignore = all
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
index 6e6eec1..0f7bc51 100644
--- a/.idea/codeStyles/codeStyleConfig.xml
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -1,6 +1,5 @@
-
-
\ No newline at end of file
+
diff --git a/.idea/live-templates/README.md b/.idea/live-templates/README.md
index 66713b3..267008a 100644
--- a/.idea/live-templates/README.md
+++ b/.idea/live-templates/README.md
@@ -5,12 +5,12 @@ This directory contains two live template groups:
1. `Spine.xml`: shortcuts for the repeated patterns used in the framework.
2. `User.xml`: a single shortcut to generate TODO comments.
-### Instlallation
+### Installation
Live templates are not picked up by IDEA automatically. They should be added manually.
In order to add these templates, perform the following steps:
-1. Copy `*.xml` files from this directory to `templates` directory in the IntelliJ IDEA
+1. Copy `*.xml` files from this directory to `templates` directory in the IntelliJ IDEA
[settings folder][settings_folder].
2. Restart IntelliJ IDEA: `File -> Invalidate Caches -> Just restart`.
3. Go to `Preferences -> Editor -> Live Templates`.
@@ -22,6 +22,6 @@ In order to add these templates, perform the following steps:
1. Open the corresponding template: `Preferences -> Editor -> Live Templates -> User.todo`.
2. Click on `Edit variables`.
-3. Set `USER` variable to your domain email address without `@teamdev.com` ending. For example,
- for `jack.sparrow@teamdev.com` use the follwoing expression `"jack.sparrow"`.
+3. Set `USER` variable to your domain email address without `@teamdev.com` ending. For example,
+ for `jack.sparrow@teamdev.com` use the following expression `"jack.sparrow"`.
4. Verify that the template generates expected comments: `// TODO:2022-11-03:jack.sparrow: <...>`.
diff --git a/.idea/live-templates/User.xml b/.idea/live-templates/User.xml
index cc15650..958e2ea 100644
--- a/.idea/live-templates/User.xml
+++ b/.idea/live-templates/User.xml
@@ -1,7 +1,7 @@
-
+
diff --git a/.junie/guidelines.md b/.junie/guidelines.md
index 5160f49..7c1f866 100644
--- a/.junie/guidelines.md
+++ b/.junie/guidelines.md
@@ -1,6 +1,6 @@
# Guidelines for Junie and AI Agent from JetBrains
-Read the `../.agents/_TOC.md` file to understand:
+Read the `../.agents/guidelines/_TOC.md` file to understand:
- the agent responsibilities,
- project overview,
- coding guidelines,
diff --git a/AGENTS.md b/AGENTS.md
index 1268e51..8c5f619 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -4,16 +4,34 @@
If `.agents/project.md` exists in this repository, read it first β it describes
the language, architecture, and role of this specific repo within the Spine SDK
-organisation. To create one, copy `.agents/project.template.md` (or the
-relevant language template) and fill it in. If `project.md` links to a shared
-requirements file (e.g. `jvm-project.md`), read that too.
+organisation. It is a symlink to `docs/project.md`; to create one, copy
+`.agents/guidelines/project.template.md` to `docs/project.md` and fill it in. If it
+links to a shared requirements file (e.g. `jvm-project.md`), read that too.
-- Start every session by reading `.agents/quick-reference-card.md` (if present).
+- Start every session by reading `.agents/guidelines/quick-reference-card.md` (if present).
- For specific tasks (code review, PR prep, dependency updates, docs, etc.),
prefer the matching skill from `.agents/skills/`.
-- Full standards reference: `.agents/_TOC.md` (if present) β consult when a
+- Full standards reference: `.agents/guidelines/_TOC.md` (if present) β consult when a
skill doesn't cover the needed context.
+Shared skills, scripts, and guidelines come from the `.agents/shared` submodule (the
+[`agents`][agents-repo] repository) exposed via symlinks.
+`./config/pull` initializes and floats them automatically. But a fresh `git worktree`
+(and some shallow clones / cloud checkouts) start with NO submodules checked out, so
+those symlinks dangle and no skills are found. Bootstrap such a tree with
+**`./init-submodules`** β a root script that materializes the missing
+*config-managed* submodules at their pinned commits: `config` itself, plus every
+submodule that declares a tracked `branch` in `.gitmodules` (`.agents/shared`, and
+any shared submodule added later) β the same rule `./config/pull` uses to decide
+what it floats. Submodules the consumer owns (a Hugo theme, a vendored library,
+doc-example submodules, β¦) declare no tracked branch and are left untouched, so the
+automatic `SessionStart` run never tries to clone β or fail on credentials for β a
+submodule this project does not manage. It depends on no pre-existing `config`
+submodule, so it works before `./config/pull` (which lives inside the `config`
+submodule) can. Claude Code runs it automatically via a `SessionStart` hook; other
+agents and humans run it by hand, then `./config/pull` to float the shared submodules
+to their branch tips.
+
## Commit and history safety
**Do not commit, push, tag, rebase, merge, cherry-pick, or otherwise write to git history**
@@ -26,7 +44,7 @@ unless one of the following is true *right now*:
Authorization does not carry over between turns or sessions. When in doubt: stage
changes, show the diff, and stop β let the user commit.
-See [`.agents/safety-rules.md`](.agents/safety-rules.md) β *Commits and history-writing*.
+See [`.agents/guidelines/safety-rules.md`](.agents/guidelines/safety-rules.md) β *Commits and history-writing*.
## Other safety rules
@@ -35,7 +53,7 @@ See [`.agents/safety-rules.md`](.agents/safety-rules.md) β *Commits and histor
- No analytics, telemetry, or tracking code.
- No reflection or unsafe code without explicit approval.
-See [`.agents/safety-rules.md`](.agents/safety-rules.md) for the full list.
+See [`.agents/guidelines/safety-rules.md`](.agents/guidelines/safety-rules.md) for the full list.
## Moving files
@@ -55,6 +73,17 @@ See `.agents/memory/README.md` for layout and write protocol.
Review `.agents/memory/MEMORY.md` at the start of every session.
Ruthlessly iterate until mistakes stop repeating.
+## Asking questions
+
+- Ask at most one question per message. If a decision has a small set of
+ options, include those options as part of that one question.
+- Do not bundle unrelated clarification questions. Ask the next question only
+ after the user answers the previous one.
+- Apply this rule both when the agent needs clarification and when the user's
+ prompt means "ask questions".
+- Prefer a reasonable assumption over another question when the answer would not
+ materially change the next step.
+
## Verification & Quality
- Never mark a task done without proof (tests, logs, diff vs main).
@@ -77,3 +106,30 @@ Ruthlessly iterate until mistakes stop repeating.
- Verify changes before marking a task done.
- Update memory if lessons emerged.
- Delete the task file on merge to master.
+
+## Code review
+
+Never review `gradlew` or `gradlew.bat` in any repository, including `config`.
+These files are provided by Gradle and are not edited manually.
+
+When reviewing a pull request or diff in a consumer repository, skip any
+file that the `config` module distributes. Those files belong in a review
+of the `config` repo, not the consumer repo β reviewing them there adds
+noise without value.
+
+Do **not** apply this skip rule when reviewing the `config` repository
+itself. In `config`, these files are source files owned by the current
+repo and must be reviewed normally, except `gradlew` and `gradlew.bat`.
+
+In consumer repositories, skip without comment any path matching:
+
+- `AGENTS.md`, `CLAUDE.md`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`
+- `.agents/**` (except `.agents/project.md`)
+- `.claude/**`, `.idea/**`, `.junie/**`
+- `.github/copilot-instructions.md`
+- `buildSrc/**` (except `buildSrc/src/main/kotlin/module.gradle.kts`)
+- `gradle/`, `gradlew`, `gradlew.bat`, `init-submodules`
+- `.codecov.yml`, `.gitignore`, `gradle.properties`, `lychee.toml`
+- `.github/workflows/` β unless the workflow was introduced by this repo
+
+[agents-repo]: https://github.com/SpineEventEngine/agents
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 2185ef6..0bccb53 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -20,13 +20,13 @@ Code contributions should:
Contributor License Agreement
-----------------------------
-Contributions to the code of Spine Event Engine framework and its libraries must be accompanied by
-Contributor License Agreement (CLA).
+Contributions to the code of the Spine Event Engine framework and its libraries must be accompanied by
+a Contributor License Agreement (CLA).
* If you are an individual writing original source code and you're sure you own
the intellectual property, then you'll need to sign an individual CLA.
- * If you work for a company which wants you to contribute your work,
+ * If you work for a company that wants you to contribute your work,
then an authorized person from your company will need to sign a corporate CLA.
Please [contact us][legal-email] for arranging the paper formalities.
diff --git a/EMBEDDING.md b/EMBEDDING.md
index 9e766de..61062e9 100644
--- a/EMBEDDING.md
+++ b/EMBEDDING.md
@@ -14,7 +14,7 @@ Originally, this tool was written as a plugin for Jekyll, but now it is also
available as a binary executable written in Go.
1. Open [embed-code][embed-code-repo] repository.
-2. Go to the `bin` directory and download an executable suitable for you OS.
+2. Go to the `bin` directory and download an executable suitable for your OS.
3. Put it in the `docs/_bin` directory of this repository.
### Download code snippets
@@ -70,7 +70,7 @@ to the documentation files using the tool. The most important points here are:
and multi-module Gradle example projects that are going to be used for
embedding into this site.
- See the declaration of `buildAll` task for more details.
+ See the declaration of the `buildAll` task for more details.
2. Add the example code as a submodule for this project:
@@ -78,7 +78,7 @@ to the documentation files using the tool. The most important points here are:
git submodule add https://github.com/spine-examples/ docs/_code/examples/
```
Please make sure the new submodule goes under the `docs/_code/examples` directory,
- as shown in the command line template above.
+ as shown in the command-line template above.
3. Include the build of the added project into the [`settings.gradle.kts`](settings.gradle.kts)
file.
@@ -93,7 +93,7 @@ to the documentation files using the tool. The most important points here are:
### Adding a new small piece
-1. Add the code under `docs/_code/samples/src` directory.
+1. Add the code under the `docs/_code/samples/src` directory.
2. Make sure tests for the new code pass.
3. Add the new piece using the [`embed-code` guide][embed-code-readme].
4. Include the build of the added project into the [`settings.gradle.kts`](settings.gradle.kts)
diff --git a/README.md b/README.md
index 3fcef9f..7cd4334 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@ This repository serves for three purposes:
2. Gathering issues improving the documentation of the framework and tasks on writing articles at spine.io and other web resources.
3. Storing documentation files that are added as a Hugo module to the [spine.io][spine-repo] repository.
-We have repository for the code of [spine.io](https://spine.io) site.
+We have a repository for the code of the [spine.io](https://spine.io) site.
Issues there are for improving the site features.
Tasks for the content of the [spine.io](https://spine.io) site should belong
@@ -185,7 +185,7 @@ the [`AUTHORING.md`][authoring-guide] file.
1. If you are getting the terminal `prompts disabled error` when trying to get
theme updates, make sure you have allowed 2FA to do its job. Also, if you have
- authentication issues during submodules update. You can resolve it with this
+ authentication issues during submodules update, you can resolve it with this
command:
```shell
diff --git a/SPINE_RELEASE.md b/SPINE_RELEASE.md
index bf4b3ff..372a68e 100644
--- a/SPINE_RELEASE.md
+++ b/SPINE_RELEASE.md
@@ -1,4 +1,4 @@
-Release new version of the documentation
+Release a new version of the documentation
========
**Table of Contents**
@@ -74,7 +74,7 @@ Where:
### Content
-The documentation content should be placed under `content/docs//` directory.
+The documentation content should be placed under the `content/docs//` directory.
For the main version, content can either be under its version directory or at
the root β this is controlled by the `content_path`.
@@ -115,8 +115,8 @@ Each version should have its own sidenav configuration file, located either in:
## Release new version
1. Create a new directory for the documentation inside the `content/docs//`.
-2. Create the `sidenav.yml` inside `data/docs//` directory.
-3. Add the new version to `data/versions.yml` config.
+2. Create the `sidenav.yml` inside the `data/docs//` directory.
+3. Add the new version to the `data/versions.yml` config.
### Change the current main version
@@ -156,7 +156,7 @@ versioned folder and configured via `module.mounts`.
item_visible: false
```
-Now the version 2 will be available at the `https://spine.io/docs/` URL.
+Now version 2 will be available at the `https://spine.io/docs/` URL.
Get updates into the main website:
@@ -188,8 +188,8 @@ The link above will be automatically rendered as:
- `"/docs/validation/guides/requirements/"` β for main version.
- `"/docs/validation/2-0-x/guides/requirements/"` β for the version `2-0-x`.
-To render the current documentation full version inside API URL,
-use `{{% version %}}` shortcode:
+To render the current documentation full version inside an API URL,
+use the `{{% version %}}` shortcode:
```markdown
@@ -219,4 +219,4 @@ To use a specific version for example in FAQ or Release Notes, provide the `vers
{{% version "1" %}}
```
-It will always render the latest βfullβ label version of `1`, for example now it is `1.9.0`.
+It will always render the latest βfullβ label version of `1`; for example, now it is `1.9.0`.
diff --git a/_diagrams/README.md b/_diagrams/README.md
index 832dc26..59ab6f9 100644
--- a/_diagrams/README.md
+++ b/_diagrams/README.md
@@ -14,10 +14,10 @@ which allows creating class names inside the exported SVG.
Add a layer or group name like `.g-caption`.
The resulting SVG markup will contain `class="g-caption"`.
-To have several classes for the one element add `.g-caption.command-dispatcher`.
+To have several classes for one element add `.g-caption.command-dispatcher`.
The result will be `class="g-caption command-dispatcher"`.
-A layer name without a prefixed period will be used as ID.
+A layer name without a prefixed period will be used as an ID.
For example, `g-caption.command-dispatcher` will result `id="g-caption" class="command-dispatcher"`.
## Export SVG
diff --git a/config b/config
index 56b5c90..91e13d4 160000
--- a/config
+++ b/config
@@ -1 +1 @@
-Subproject commit 56b5c9070ad0efcadc3a96256e4c4937b0528e4e
+Subproject commit 91e13d433c0e892f4cb48c7575bb05899018974d
diff --git a/docs/_code/samples/src/main/java/io/spine/site/home/server/NanoPmContext.java b/docs/_code/samples/src/main/java/io/spine/site/home/server/NanoPmContext.java
index 6bdcb96..d13ef29 100644
--- a/docs/_code/samples/src/main/java/io/spine/site/home/server/NanoPmContext.java
+++ b/docs/_code/samples/src/main/java/io/spine/site/home/server/NanoPmContext.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2020, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -40,7 +40,7 @@ public final class NanoPmContext {
private NanoPmContext() {
}
- /** Creates a builder for single-tenant context filled in with entity types. */
+ /** Creates a builder for a single-tenant context filled in with entity types. */
public static BoundedContextBuilder newBuilder() {
return BoundedContext.singleTenant(NAME)
.add(TaskAggregate.class)
diff --git a/docs/_code/samples/src/main/java/io/spine/site/home/server/package-info.java b/docs/_code/samples/src/main/java/io/spine/site/home/server/package-info.java
index 13658d0..07846fd 100644
--- a/docs/_code/samples/src/main/java/io/spine/site/home/server/package-info.java
+++ b/docs/_code/samples/src/main/java/io/spine/site/home/server/package-info.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2020, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -25,7 +25,7 @@
*/
/**
- * This package contains server-side implementation of the Mini PM example which is used
+ * This package contains the server-side implementation of the Mini PM example that is used
* at the site home page.
*/
@CheckReturnValue
diff --git a/docs/_code/samples/src/main/proto/spine/site/home/task.proto b/docs/_code/samples/src/main/proto/spine/site/home/task.proto
index 32aa1da..0232e7b 100644
--- a/docs/_code/samples/src/main/proto/spine/site/home/task.proto
+++ b/docs/_code/samples/src/main/proto/spine/site/home/task.proto
@@ -1,11 +1,11 @@
/*
- * Copyright 2020, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -23,6 +23,7 @@
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
+
syntax = "proto3";
package spine.site.home;
@@ -37,7 +38,7 @@ option java_multiple_files = true;
import "google/protobuf/timestamp.proto";
import "spine/site/home/identifiers.proto";
-// A task which can be assigned to a user.
+// A task that can be assigned to a user.
message Task {
option (entity).kind = AGGREGATE;
TaskId id = 1; // assumed `required`
@@ -47,7 +48,7 @@ message Task {
spine.core.UserId assignee = 5;
}
-// An item in a task list displayed in UI.
+// An item in a task list displayed in the UI.
message TaskItem {
option (entity).kind = PROJECTION;
TaskId task = 1;
diff --git a/docs/_preview/content/_index.md b/docs/_preview/content/_index.md
index b8486da..642f858 100644
--- a/docs/_preview/content/_index.md
+++ b/docs/_preview/content/_index.md
@@ -8,7 +8,7 @@ Go to the [documentation section](docs/).
Read the [Authoring][authoring] guide on adding the content to the documentation.
-The page is only available on this site and will not be rendered on the
+The page is only available on this site and will not be rendered on
[spine.io](https://spine.io).
{class="subtle"}
diff --git a/docs/assets/js/docs/architecture-diagram.js b/docs/assets/js/docs/architecture-diagram.js
index e68c3e6..eb16279 100644
--- a/docs/assets/js/docs/architecture-diagram.js
+++ b/docs/assets/js/docs/architecture-diagram.js
@@ -1,11 +1,11 @@
/*
- * Copyright 2025, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -25,7 +25,7 @@
*/
/**
- * This is a JavaScript file which backs the Spine architecture diagram.
+ * This is a JavaScript file that backs the Spine architecture diagram.
*
* Please see `/docs/1/introduction/architecture.md` for usage.
*/
@@ -42,7 +42,7 @@ $(
console.log(`baseurl ${baseURL}`)
/**
- * CSS classes used as selectors to manipulate the elements of SVG diagram.
+ * CSS classes used as selectors to manipulate the elements of the SVG diagram.
*/
const endUserClass = "end-user";
const boxCaptionClass = "box-caption";
@@ -105,7 +105,7 @@ $(
/**
- * Changes opacity of the elements, not marked as "end-user".
+ * Changes the opacity of the elements, not marked as "end-user".
*
* The opacity value is expected to be [0; 1] range.
*
@@ -176,7 +176,7 @@ $(
}
/**
- * Disables a link behaviour of an element.
+ * Disables a link behavior of an element.
*
* @param linkElement the jQuery object wrapping the DOM element
*/
@@ -338,7 +338,7 @@ $(
}
/**
- * Changes the diagram visibility to make it visible on UI.
+ * Changes the diagram visibility to make it visible on the UI.
*
*
This is useful to avoid the diagram from flickering during loading.
* The styles that hide the diagram are in the file `_sass/base/_diagram.scss`.
diff --git a/docs/content/docs/_index.md b/docs/content/docs/_index.md
index f958bd7..5e2cabe 100644
--- a/docs/content/docs/_index.md
+++ b/docs/content/docs/_index.md
@@ -27,7 +27,7 @@ This section provides detailed instructions on the framework use.
This section provides language-specific guides for building client-side applications.
## [API Reference](docs/reference/)
-This sections provides links to the generated documentation.
+This section provides links to the generated documentation.
## [Examples](docs/examples/)
This page is the entry point for learning from the code of
diff --git a/docs/content/docs/guides/gradle.md b/docs/content/docs/guides/gradle.md
index a15e3e4..7929013 100755
--- a/docs/content/docs/guides/gradle.md
+++ b/docs/content/docs/guides/gradle.md
@@ -30,7 +30,7 @@ or on our [Getting Started page](docs/quick-start/).
## Spine Bootstrap plugin
-Spine Bootstrap plugin (Bootstrap for short) serves to automate the configuration of the modules
+The Spine Bootstrap plugin (Bootstrap for short) serves to automate the configuration of the modules
in your Spine-based app.
We recommend having separate Gradle subprojects for domain model definition, server implementation,
@@ -103,7 +103,7 @@ dependencies {
```
Using `webServer()` has the same effect as just declaring the subproject to be a part of `server()`
and also adds the `io.spine:spine-web` dependency to the subproject. This dependency provides
-components for handling requests from a JavaScript frontend See also `firebaseWebServer()` for using
+components for handling requests from a JavaScript frontend. See also `firebaseWebServer()` for using
a Firebase database to communicate between the server and the client.
### JavaScript client
@@ -134,7 +134,7 @@ dependencies {
```
For domain logic implementation, also use a single subproject per Bounded Context. The convention
-for calling those projects by the context names: `users` , `trains`, `billing`, etc. It is a good
+for calling those projects by the context names: `users`, `trains`, `billing`, etc. It is a good
idea to have a server implementation subproject depend only on one model subproject to preserve
language and responsibility boundaries.
@@ -146,7 +146,7 @@ on the principles of integrating separate Bounded Contexts and third-party syste
## Verbose configuration
If the Bootstrap configuration is not customizable enough for you, there are other Gradle plugins
-which may provide fine-grained API.
+that may provide a fine-grained API.
Those plugins are Spine Model Compiler for Java subprojects and Spine ProtoJs plugin for JavaScript
submodules. Under the hood, Bootstrap uses those plugins to do the work. This means that Bootstrap
@@ -154,16 +154,16 @@ automatically applies the correct low-level plugin for you.
### Model Compiler
-Spine Model Compiler is a Gradle plugin which executes all the code generation routines via several
+Spine Model Compiler is a Gradle plugin that executes all the code generation routines via several
Gradle tasks as well as the `modelCompiler { }` extension, which allows you to configure those
tasks.
See the API reference for the list of the [declared tasks]({{% get-site-data "repositories.base_api_doc" %}}/plugin-base/plugin-base/io.spine.tools.gradle/-model-compiler-task-name/)
-and the [codegen configuration options]({{% get-site-data "repositories.base_api_doc" %}}/model-compiler/model-compiler/io.spine.tools.gradle.compiler/-extension/)
+and the [codegen configuration options]({{% get-site-data "repositories.base_api_doc" %}}/model-compiler/model-compiler/io.spine.tools.gradle.compiler/-extension/).
### ProtoJS Plugin
-ProtoJs Gradle plugin manages and enhances JavaScript code generation from Protobuf definitions.
+The ProtoJs Gradle plugin manages and enhances JavaScript code generation from Protobuf definitions.
The plugin adds the `generateJsonParsers` task, which appends generated JS files with code parsing
Protobuf messages out of plain JS objects.
diff --git a/docs/content/docs/guides/integration.md b/docs/content/docs/guides/integration.md
index 6ba3312..1813647 100755
--- a/docs/content/docs/guides/integration.md
+++ b/docs/content/docs/guides/integration.md
@@ -49,8 +49,8 @@ describe the listed three patterns, because they are the most commonly used.

For the sake of an example, letβs consider airport management software. An airport is a complex
-system which relies on many people and much software working together. Letβs consider the system
-which helps the flight dispatchers make decisions on **Takeoffs and Landings**. The system
+system that relies on many people and much software working together. Letβs consider the system
+that helps the flight dispatchers make decisions on **Takeoffs and Landings**. The system
integrates with the software responsible for **Security Checks**, **Airplane Supplies**, and
**Weather**. All of these systems are independent of **Takeoffs and Landings** as well as of each
other. Thus, each of them can be treated as a third party.
@@ -70,10 +70,10 @@ This decision requires data on the supplies, which are provided for the *Aircraf
knowledge, the system integrates with the **Airplane Supplies** Context. Note the language
difference between the *Aircraft* and an *Airplane*. Apparently, the two Contexts view the same
entity of the real world from different perspectives.
-**Airplane Supplies** Context is an integral part of the airport software. Thus, by communicating
+The **Airplane Supplies** Context is an integral part of the airport software. Thus, by communicating
with the developers responsible for **Airplane Supplies**, we are able to build a Customer/Supplier
relationship between the **Airplane Supplies** Context and our system. The **Airplane Supplies**
-Context does not implement an Event-based messaging internally. However, it still acts as
+Context does not implement Event-based messaging internally. However, it still acts as
a Supplier. Specially for **Takeoffs and Landings**, the Supplier Context generates Events and
publishes them to a shared channel. **Takeoffs and Landings**, the Customer, subscribes to those
Events.
@@ -139,7 +139,7 @@ public final class SuppliesEventProducer extends SuppliesEventProducerImplBase {
The event producer obtains cached historical events, matches them to the received subscription,
and sends them to the client. The **Takeoffs and Landings** system implements
an [event consumer]({{% get-site-data "repositories.examples" %}}/airport/blob/master/takeoffs-and-landings/src/main/java/io/spine/example/airport/tl/supplies/SuppliesEventConsumer.java)
-which constructs a subscription and maintains it as long as the system needs to receive more events.
+that constructs a subscription and maintains it as long as the system needs to receive more events.
The consumer broadcasts the received Events via an instance of [`ThirdPartyContext`]({{% get-site-data "repositories.core_api_doc" %}}/server/server/io.spine.server.integration/-third-party-context/):
@@ -271,9 +271,9 @@ EitherOf2 on(@External TemperatureChanged event) {

-**Security Checks** Context has a rich model of its own. The system happens not to use domain Events
-at all. The **Security Checks** software, used in our airport, must go through a complex audit and
-certification process upon each change. Thus, the cost of changing it is too high. However,
+The **Security Checks** Context has a rich model of its own. The system happens not to use domain Events
+at all. The **Security Checks** software, used in our airport, must go through a complex audit and
+certification process upon each change. Thus, the cost of changing it is too high. However,
the **Security Checks** also happens to expose an API for fetching the current internal state of
the system. The fetched state has a consistency lag, which never exceeds a known value
(e.g. 2 minutes). In other words, the client of the **Security Checks** API can be sure that
@@ -352,7 +352,7 @@ EitherOf2 on(@External PassengerDeniedBoarding event)
An integration job may seem complicated or even overwhelming. However, with a strong understanding
of the domain and good tooling, the process boils down to a few simple steps. Indeed, it does not
-matter, whether you want to integrate with an outside system, your own legacy system, or reorganize
+matter, whether you want to integrate with an outside system, your own legacy system, or reorganize
your current system to work in an event-driven manner. A correct integration strategy will help you
isolate and perfect your own domain language while on the work of many external systems. Read more
about Bounded Contexts and their interactions in the "Domain-Driven Design" book by Eric Evans.
diff --git a/docs/content/docs/guides/rejections.md b/docs/content/docs/guides/rejections.md
index 0926718..f6525a3 100755
--- a/docs/content/docs/guides/rejections.md
+++ b/docs/content/docs/guides/rejections.md
@@ -14,7 +14,7 @@ are defined differently. Here is the summary of the differences:
outer class.
Spine Model Compiler for Java generates `ThrowableMessage` classes for all these messages.
These classes will be named after the classes of rejection messages.
- Putting rejection message classes under an outer class avoids name clash inside the package.
+ Putting rejection message classes under an outer class avoids a name clash inside the package.
2. Omit `java_outer_classname` option
diff --git a/docs/content/docs/guides/start-new-project.md b/docs/content/docs/guides/start-new-project.md
index e06dd5f..428fdae 100755
--- a/docs/content/docs/guides/start-new-project.md
+++ b/docs/content/docs/guides/start-new-project.md
@@ -127,7 +127,7 @@ We put this step aside because in [Reactive DDD][ReactiveDDD] entities reference
the typed [identifiers][identifier-concept].
{{% note-block class="note" %}}
-Consider following the [Vaughn Vernon][VaughnVernon]βs rule on Aggregates from
+Consider following [Vaughn Vernon][VaughnVernon]βs rule on Aggregates from
the βEffective Aggregate Design Part IIβ that is applicable to **any** entity:
βReference other Aggregates by Identityβ
{{% /note-block %}}
@@ -147,10 +147,10 @@ and polish the code of this important development step.
### Events
-When the IDs are defined itβs time to define [event][event-concept] messages. The events are named
+When the IDs are defined, itβs time to define [event][event-concept] messages. The events are named
as facts formulated as past participles, e.g. {{< code "event" "RepositoryRegistered" >}} or {{< code "event" "TaskCreated" >}}.
They are defined in files with the [`_events.proto`][events-proto] suffix (e.g. `order_events.proto`,
-`customer_events.proto`). If your context is small it can be just `events.proto`.
+`customer_events.proto`). If your context is small, it can be just `events.proto`.
Create a Pull Request with the event definitions when they are ready.
@@ -158,7 +158,7 @@ Create a Pull Request with the event definitions when they are ready.
Similar to events, [command][command-concept] messages are defined in files having the names ending
with the [`_commands.proto`][commands-proto] suffix (or just `commands.proto` for a small context).
-Commands are defined as imperative in a form of βdo somethingβ, e.g.
+Commands are defined as imperative in the form of βdo somethingβ, e.g.
{{< code "command" "RegisterRepository" >}} or {{< code "command" "CreateTask" >}}.
Finalize defining commands with a Pull Request.
@@ -205,10 +205,10 @@ the business logic the same way it works in the application.
See https://github.com/SpineEventEngine/SpineEventEngine.github.io/issues/339.
-->
-All the code must conform to your standards of the code and documentation quality
+All the code must conform to your standards of code and documentation quality
and be tested thoroughly.
-When a backend for the scenario is done a new PR is created and reviewed.
+When a backend for the scenario is done, a new PR is created and reviewed.
### Fulfilling the vertical
@@ -235,7 +235,7 @@ As soon as you are done with the Bounded Context, move on to the next one.
While developing a project, make sure to split the development by Bounded Contexts. Pick up
a context and split it into scenarios. Make sure each of the development steps results
in a separate Pull Request with dedicated artifacts in the source code repository.
-Opt for smaller, fine-graded Pull Requests instead of cluttered and complicated ones.
+Opt for smaller, fine-grained Pull Requests instead of cluttered and complicated ones.
[introduction]: docs/introduction/ "Check the Introduction"
[project-structure]: docs/introduction/project-structure#example "Check out the Example Project structure"
diff --git a/docs/content/docs/guides/validation.md b/docs/content/docs/guides/validation.md
index 006c28d..9c1a24c 100755
--- a/docs/content/docs/guides/validation.md
+++ b/docs/content/docs/guides/validation.md
@@ -30,7 +30,7 @@ for details.
Here are simple steps in adding validation to the data model:
1. The programmer adds validation constraints to the Protobuf types of the model.
- 2. Spine Model Compiler generates the code which provides validation features.
+ 2. Spine Model Compiler generates the code that provides validation features.
3. The programmer calls the validation API of these data types as their instances are created.
## Java validation API
@@ -45,7 +45,7 @@ MyMessage.newBuilder()
.vBuild(); // β Throws ValidationException.
```
-If the validation is not required, you may call `build()` or `buildPartial()` provided by Protobuf
+If the validation is not required, you may call `build()` or `buildPartial()` provided by the Protobuf
Java API.
The message class also gets an extra method β `validate()`. This method does not throw exceptions.
@@ -72,24 +72,24 @@ var validate = theKnownTypes.validatorFor(msg);
ValidationError error = validate(msg);
```
-Similarly to `validate()` method in Java, the validation function does not throw exceptions. A list
+Similarly to the `validate()` method in Java, the validation function does not throw exceptions. A list
of `ConstraintViolation`s can be obtained from the `ValidationError`.
## Validation options overview
In most cases validation constraints are defined for Protobuf message fields such as if a field
-must be populated or it must be withing a range, or match a regular expression. Not so often
+must be populated or it must be within a range, or match a regular expression. Not so often
it may be necessary to require a combination of fields. In this case, validation options are defined
at the level of a corresponding message type.
## Required fields
-When modelling a domain, we often come up to certain data points which cannot be skipped. Those are
+When modelling a domain, we often come up to certain data points that cannot be skipped. Those are
represented by required fields of an entity state, a Command, an Event, etc.
{{% note-block class="note" %}}
-Protobuf 2 used to have a native support for required fields.
+Protobuf 2 used to have native support for required fields.
However, from the serialization perspective, that proved to be
a [design mistake](https://stackoverflow.com/a/31814967/3183076). If a required field was missing,
the message could not be serialized and sent over the wire. Also, it is often too easy to add a new
@@ -98,7 +98,7 @@ the fields are optional.
{{% /note-block %}}
In the Validation Library, we've revived the concept of required fields, but on a different level.
-The difference to the Protobuf 2 way is that out required fields do not affect the serialization
+The difference to the Protobuf 2 way is that our required fields do not affect the serialization
of the message.
If a required field is missing, it still can be serialized and passed over the wire. By separating
validation from serialization, we allow users to choose to ignore validation errors and still
@@ -156,8 +156,8 @@ For collection fields (i.e. `repeated` and `map`), a field is considered set if:
1. The collection is not empty.
2. At least one of the entries (values for `map`s) matches the rules described above.
-Note that collections of numeric fields can be required. In those cases, only the rule 1. applies
-and the rule 2. is ignored.
+Note that collections of numeric fields can be required. In those cases, only rule 1 applies
+and rule 2 is ignored.
### Declaring required fields
@@ -183,7 +183,7 @@ PhoneNumber.newBuilder()
There are more complex cases for required fields than just a single field. Consider a `oneof` field
group, which always has to be set. Applying `(required)` to the fields does not make sense, since
-only one field in the group can be set at a time. Instead, Spine provides `(is_required)` option:
+only one field in the group can be set at a time. Instead, Spine provides the `(is_required)` option:
```proto
import "spine/options.proto";
@@ -206,7 +206,7 @@ In this case one of the fields `UserIdentity.email`, `UserIdentity.google`,
and `UserIdentity.twitter` must be set.
{{% note-block class="warning" %}}
-`(is_required)` option is not yet supported in Dart.
+The `(is_required)` option is not yet supported in Dart.
{{% /note-block %}}
In some other cases, a field may be either required or not, depending on the value of another field.
@@ -249,7 +249,7 @@ message PersonName {
}
```
-In case of `PersonName`, either `given_name` or both `honorific_prefix` and `family_name` must be
+In the case of `PersonName`, either `given_name` or both `honorific_prefix` and `family_name` must be
set. All three can be set at the same time.
### Missing fields
@@ -271,7 +271,7 @@ message PhoneNumber {
Note that this option only applies to fields marked with `(required)` and not to the fields
referenced via any other options.
-If `(goes)` option is used, the error message can be customized with the `(goes).msg_format`
+If the `(goes)` option is used, the error message can be customized with the `(goes).msg_format`
parameter. Note that the message should contain two "`%s`" insertion points: first for the name of
the field declaring the option and second for the name of the field targeted by the option.
@@ -313,12 +313,12 @@ message ChangeProfilePicture {
}
```
-In this case, the `ChangeProfilePicture.id` field is the first in the declaration order,
+In this case, the `ChangeProfilePicture.id` field is the first in the declaration order;
therefore it is implicitly required. By default, the framework will use it in command routing,
as an identifier of the entity handling this command.
This convention does not apply to [Events](docs/introduction/naming-conventions#eventsproto).
-Unlike Commands, event routing is typically specific to the use case. For example, {{< code "projection" "UserView" >}} projection
+Unlike Commands, event routing is typically specific to the use case. For example, the {{< code "projection" "UserView" >}} projection
may require a user ID to handle events, whereas the {{< code "projection" "ProfilePictureGallery" >}} projection might use
a different routing approach, such as grouping by a user group or an email domain associated with a user.
@@ -329,7 +329,7 @@ Therefore, all Event fields are not required by default.
When a message is validated, only the "shallow" constraints are checked by default. This means that
the message fields can be invalid and the container message is still considered valid.
-In order to enable message field checks, use `(validate)` option:
+In order to enable message field checks, use the `(validate)` option:
```proto
import "spine/options.proto";
@@ -361,7 +361,7 @@ User user = User
When applied to a `repeated` or a `map` field, each item (value of a `map`) is validated.
{{% note-block class="warning" %}}
-`(validate)` option is not yet supported in Dart.
+The `(validate)` option is not yet supported in Dart.
{{% /note-block %}}
#### Invalid fields
@@ -388,9 +388,9 @@ For numeric fields, Spine defines a few options to limit the range of expected v
### `(min)`/`(max)`
-`(min)` and `(max)` are twin options which define the lower and higher bounds for a numeric fields.
+`(min)` and `(max)` are twin options that define the lower and higher bounds for a numeric fields.
The value is specified as a string. Note that the string must be parsable into the field's number
-format (e.g. a `int32` field cannot have a `"2.5"` bound).
+format (e.g. an `int32` field cannot have a `"2.5"` bound).
By default, the bounds are __inclusive__. Use the `exclusive` property to make a bound exclusive.
@@ -410,7 +410,7 @@ message Distance {
### Ranges
The `(range)` option is a shortcut for a combination of `(min)` and `(max)`. A range specifies both
-boundaries for a numeric field. `(range)` is a `string` option. The `(range)` notation allow
+boundaries for a numeric field. `(range)` is a `string` option. The `(range)` notation allows
declaring inclusive and exclusive boundaries. A round bracket ("`(`" or "`)`") denotes an exclusive
boundary and a square bracket ("`[`" or "`]`") denotes an inclusive one.
@@ -434,7 +434,7 @@ message LocalTime {
In the example above, the `LocalTime.hours` field can span between 0 and 23, the `LocalTime.minutes`
field can span between 0 and 59, and the `LocalTime.seconds` field can span between 0.0 and 60.0,
but can never reach 60. Exclusive boundaries are especially powerful for fractional numbers, since,
-mathematically, there is no hard upper limit which a field value can reach.
+mathematically, there is no hard upper limit that a field value can reach.
Usage of the double dot separator ("`..`") between the bounds is mandatory.
@@ -451,13 +451,13 @@ For `string` fields, the library provides the `(pattern)` option. Users can defi
expression to match the field values. Also, some common pattern modifiers are available:
- `dot_all` (a.k.a. "single line") β enables the dot (`.`) symbol to match all the characters,
including line breaks;
- - `case_insensitive` β allows to ignore the case of the matched symbols;
+ - `case_insensitive` β allows ignoring the case of the matched symbols;
- `multiline` β enables the `^` (caret) and `$` (dollar) signs to match a start and an end of
a line instead of a start and an end of the whole expression;
- `unicode` β enables matching the whole UTF-8 sequences;
- `partial_match` β allows the matched strings to contain a full match to the pattern and some
other characters as well. By default, a string only matches a pattern if it is a full match,
- i.e. there are no unaccounted for leading and/or trailing characters.
+ i.e. there are no unaccounted-for leading and/or trailing characters.
Example:
@@ -488,10 +488,10 @@ Spine provides an option for validating time-bearing types. Those are:
- `spine.time.LocalDateTime`;
- `spine.time.OffsetDateTime`;
- `spine.time.ZonedDateTime`;
- - any user-defined type which implements the Temporal interface (`io.spine.time.Temporal` for
+ - any user-defined type that implements the Temporal interface (`io.spine.time.Temporal` for
Java).
-Using the option `(when)`, you may declare that the timestamp should lie in past or in future.
+Using the option `(when)`, you may declare that the timestamp should lie in the past or in the future.
```proto
import "spine/time_options.proto";
@@ -510,14 +510,14 @@ message PlaceOrder {
Note that the value is checked in relation to the current server time. In most cases, this should
not be an issue. However, be aware that using `FUTURE` in Events and entity states may cause
validation errors when the future comes. Since entity states are validated upon each state change,
-and historical events can be replayed, avoid declaring parts of those domain objects to be in
-future. Commands, on the other hand, are not replayed or stored automatically. Thus, It is safe
+and historical events can be replayed, avoid declaring parts of those domain objects to be in the
+future. Commands, on the other hand, are not replayed or stored automatically. Thus, it is safe
to use `FUTURE` in Commands.
## Distinct collections
Often, a `repeated` field logically represents a set rather than a list. Protobuf does not have
-a native support for sets. Moreover, it is often an invalid operation to add a duplicate element to
+native support for sets. Moreover, it is often an invalid operation to add a duplicate element to
a set. For such cases, Spine provides the `(distinct)` option, which constrains a `repeated` or
a `map` field to only contain non-duplicating elements (values in case of `map`s).
@@ -583,7 +583,7 @@ Once the `Order.when_deleted` field is filled, it can never change.
## External constraints
Sometimes, you need to impose extra validation rules on types you do not control. Consider
-the example of an image URL which should always have the `ftp` protocol. In Spine, a `Url` is a tiny
+the example of an image URL that should always have the `ftp` protocol. In Spine, a `Url` is a tiny
type for representing URL strings:
```proto
@@ -639,7 +639,7 @@ message UserPictureConstraint {
The definition of `User` itself need not change.
Note that the fields of an external constraint declaration should replicate the fields of the target
-type. In our example, the `Url` type. If the `Url` type had many fields, only those which need any
+type. In our example, the `Url` type. If the `Url` type had many fields, only those that need any
validation should be declared. However, note that if the `Url` type declares any validation on its
own, all of it is discarded and only the "substitute" rules from the `UserPictureConstraint` are
used.
@@ -650,7 +650,7 @@ External constraints are not yet supported in Dart.
{{% note-block class="note" %}}
Mind performance considerations when declaring external constraints. It is expected that the number
-of such constrains in the whole project is not large, significantly smaller than the number of
+of such constraints in the whole project is not large, significantly smaller than the number of
normal constraints. This mechanism is not designed to override validation rules of an entire library
of Protobuf definitions, merely a small amount of local patches.
{{% /note-block %}}
diff --git a/docs/content/docs/introduction/_index.md b/docs/content/docs/introduction/_index.md
index a38ad84..ab27794 100644
--- a/docs/content/docs/introduction/_index.md
+++ b/docs/content/docs/introduction/_index.md
@@ -6,8 +6,8 @@ headline: Documentation
# Development Process
{{% note-block class="lead" %}}
-Building a solution based on Spine Event Engine framework is an iterative process
-which consists of the stages described in this document.
+Building a solution based on the Spine Event Engine framework is an iterative process
+that consists of the stages described in this document.
{{% /note-block %}}
## Getting domain knowledge
@@ -25,23 +25,23 @@ For each context developers need to define:
* Entities
- [Aggregates](docs/introduction/concepts#aggregate)
- [Process Managers](docs/introduction/concepts#process-manager)
- - [Projections](docs/introduction/concepts#projection).
+ - [Projections](docs/introduction/concepts#projection)
It is likely that some of the bits of this picture would change during the process.
-But the whole team, including domain experts, need to have complete understanding of how the
+But the whole team, including domain experts, needs to have complete understanding of how the
business works to avoid βsurprisesβ down the road.
We return to learning the domain when we discover inconsistencies in the model,
or we need more information about how the business works, or the business wants to develop further
and we need to update the model.
-Once we got enough domain knowledge we proceed to the implementation.
+Once we got enough domain knowledge, we proceed to the implementation.
## Implementing a Bounded Context
At this stage we select one of the Bounded Contexts for the implementation.
Each context is developed separately. In this sense it can be seen as a microservice.
-It would be natural to start implementing the context which initiates the business flow.
+It would be natural to start implementing the context that initiates the business flow.
### Defining data types
@@ -103,7 +103,7 @@ the βlifeβ of the domain model. Messages are delivered to entities by [Repos
#### Entities
-During this step we create entity classes and add message handling methods to them.
+During this step we create entity classes and add message-handling methods to them.
Code snippets below show `Aggregate` and `Projection` classes with their handler methods.
```java
@@ -153,7 +153,7 @@ A custom `Repository` class may be needed for:
* **Dispatching messages to entities in a non-standard way**.
By default, a command is dispatched using the first field of the command message
as an ID of the target entity.
- An event is dispatched by the ID of the entity which emitted the event.
+ An event is dispatched by the ID of the entity that emitted the event.
* **Domain-specific operations** on entities of this kind.
Repositories are added to the Bounded Context they belong when it is created:
@@ -215,7 +215,7 @@ assertEntity.hasStateThat()
### Configuring Server Environment
-For information on configuring server environment of a Spine-based application, please
+For information on configuring the server environment of a Spine-based application, please
see the reference documentation of the [`ServerEnvironment`]({{% get-site-data "repositories.core_api_doc" %}}/server/server/io.spine.server/-server-environment/)
class.
@@ -248,8 +248,8 @@ Once this is done, developers run the Spine Model Compiler to generate the code
client platforms.
Since entity classes like `Projection` use composition with state types, you don't have to deal
-with DTOs. You can start writing the code which subscribes or queries these types right after
-they are defied in the `.proto` files, and the Model Compiler finishes its job.
+with DTOs. You can start writing the code that subscribes or queries these types right after
+they are defined in the `.proto` files, and the Model Compiler finishes its job.
Client applications can also subscribe to events generated by Bounded Contexts at the backend.
For more information on the client-side development, please refer to
diff --git a/docs/content/docs/introduction/architecture.md b/docs/content/docs/introduction/architecture.md
index 8bf50a8..3f109e6 100644
--- a/docs/content/docs/introduction/architecture.md
+++ b/docs/content/docs/introduction/architecture.md
@@ -12,7 +12,7 @@ with the server-side via `CommandService`, `QueryService`, and `SubscriptionServ
The diagram below shows all server-side components
of a cloud application. When developing with Spine, you will be interacting
with only some of them, which
-are not shaded on the diagram. The rest is handled by the framework.
+are not shaded in the diagram. The rest is handled by the framework.
Click on a component to navigate to its definition from the
[Concepts](docs/introduction/concepts/) page.
diff --git a/docs/content/docs/introduction/concepts.md b/docs/content/docs/introduction/concepts.md
index cffde82..854e743 100755
--- a/docs/content/docs/introduction/concepts.md
+++ b/docs/content/docs/introduction/concepts.md
@@ -19,7 +19,7 @@ Terms extending the industry-set terminology are designated as such.
### Command
-Commands are messages that instruct an entity within Spine framework to perform a certain action.
+Commands are messages that instruct an entity within the Spine framework to perform a certain action.
Compared with events, a command is not a statement of fact. They are a request, and, thus, can be
refused. A typical way to convey refusal is to throw an error or rejection.
@@ -35,7 +35,7 @@ In Spine, events are defined as Protobuf messages in the file which name ends wi
### Rejection
-Rejections is a special βnegativeβ kind of events that we introduce to differentiate them from
+Rejections are a special βnegativeβ kind of events that we introduce to differentiate them from
regular events. If an event is a fact of something that happened to a domain model, a rejection is
a fact that states the reason why a command was not handled.
@@ -58,11 +58,11 @@ It tells whether the Command has been accepted for handling.
### Command Handler
-Command Handler is an object which receives commands, modifies the state of the application, and
+Command Handler is an object that receives commands, modifies the state of the application, and
generates events if the modification was successful.
[`Aggregate`](#aggregate) and [`ProcessManager`](#process-manager) are examples one of such classes.
-The code snippet below given an example of handling a command by an aggregate:
+The code snippet below gives an example of handling a command by an aggregate:
```java
final class TaskAggregate
@@ -83,7 +83,7 @@ final class TaskAggregate
### Event Subscriber
-Event Subscriber is an object which subscribes to receive events.
+Event Subscriber is an object that subscribes to receive events.
The example below shows how a [Projection](#projection) class subscribed
to the {{< code "event" "TaskCompleted" >}} event.
@@ -101,12 +101,12 @@ final class TaskProjection
### Event Reactor
-Event Reactor is an object which usually produces one or more events in response to an incoming
+Event Reactor is an object that usually produces one or more events in response to an incoming
event. Unlike [Event Subscriber](#event-subscriber), which always consumes events, a reacting object
generates events in response to changes in the domain.
{{% note-block class="note" %}}
-In some cases, Event Reactor may ignore the event, returning `Nothing`.
+In some cases, an Event Reactor may ignore the event, returning `Nothing`.
It usually happens when a method returns one of the
[`Either`]({{% get-site-data "repositories.core_api_doc" %}}/server/server/io.spine.server.tuple/-either/) types, with `Nothing` as
one of the possible options: `EitherOf2`.
@@ -114,7 +114,7 @@ In some cases, Event Reactor may ignore the event, returning `Nothing`.
## Value Objects
-Value Object describe things in a domain model and do not have identity.
+Value Objects describe things in a domain model and do not have identity.
Value Objects are also immutable. Some examples are:
* `PhoneNumber`
* `EmailAddress`
@@ -147,13 +147,13 @@ We highly recommend using message-based IDs to make your API strongly pronounced
Aggregate is the main building block of a business model.
From the application point of view it consists of the following:
-1. Commands which arrive to it.
-2. Events which appear in response to these commands.
+1. Commands that arrive to it.
+2. Events that appear in response to these commands.
3. How these events influence the state of an aggregate.
[Aggregates](http://martinfowler.com/bliki/DDD_Aggregate.html) guarantee consistency of data
modifications in response to commands they receive. Aggregate is the most common case of
-Command Handler. In response to a command, it produces one or more events modifying own state.
+a Command Handler. In response to a command, it produces one or more events modifying own state.
These events are used later to restore the state of the aggregate.
In Spine, aggregates are defined as Java classes, and their states are defined as Protobuf messages.
@@ -164,8 +164,8 @@ Process Manager is an independent component that manages the cross-aggregate bus
It serves as a mediator by remembering the state of the flow and choosing the next step
based on the intermediate results.
-To do so, a Process Manager can be both [Command Handler](#command-handler)
-and [Event Reactor](#event-reactor). Also, it can emit Commands to other Aggregates
+To do so, a Process Manager can be both a [Command Handler](#command-handler)
+and an [Event Reactor](#event-reactor). Also, it can emit Commands to other Aggregates
and Process Managers.
In Spine, Process Managers are defined as Java classes, and their states are defined as
@@ -173,7 +173,7 @@ Protobuf messages.
### Projection
-Projection is an [Event Subscriber](#event-subscriber) which transforms multiple events data into
+Projection is an [Event Subscriber](#event-subscriber) that transforms multiple events data into
a structural representation. Projections are the main building blocks of the Query side of
the application.
@@ -217,12 +217,12 @@ the command is not [acknowledged](#acknowledgement).
Query Service returns data to the client applications in response to a query.
The query is a request for the following:
* state of one or more aggregates or their fragments;
-* one or more projection states or their fragments.
+* one or more projection states or their fragments;
* one or more process manager states or their fragments.
### Subscription Service
-Subscription Service allows to subscribe to something happening inside a Bounded Context.
+Subscription Service allows subscribing to something happening inside a Bounded Context.
There are two options for subscription:
* receive changes of an Entity state for Projections, Process Managers and Aggregates;
@@ -261,7 +261,7 @@ their execution.
#### Event Store
-This store keeps all the events of the application in the chronological order, which also called
+This store keeps all the events of the application in chronological order, which also called
Event Stream. This is the main βdatabaseβ of the Bounded Context.
New projections are built by passing the event stream βthroughβ them.
diff --git a/docs/content/docs/introduction/naming-conventions.md b/docs/content/docs/introduction/naming-conventions.md
index 82fdc7c..5bcda59 100755
--- a/docs/content/docs/introduction/naming-conventions.md
+++ b/docs/content/docs/introduction/naming-conventions.md
@@ -22,7 +22,7 @@ Commands and events reference model entities using their identifiers.
Having typed identifiers makes a model type safe.
Although the framework also supports `String`, `Integer`, and `Long` as valid ID types,
we strongly recommend defining custom ID types like `CustomerId`, `OrderId`, `ShipmentId`,
-and others. You can find similar cases in the framework API which has `EventId`, `CommandId`,
+and others. You can find similar cases in the framework API that has `EventId`, `CommandId`,
`UserId`, `TenantId`, and others.
We find it convenient to define ID types in one file called `identifiers.proto`.
@@ -92,8 +92,8 @@ an event, and it is likely there are conditions under which a command cannot be
We recommend gathering definition of related entity states in a file named after a business model
thing. Suppose we have a {{< code "aggregate" "Task" >}} aggregate, {{< code "projection" "TaskItem" >}}
-and {{< code "projection" "TaskDetails" >}} projections, and a Process Manager which is responsible for
-movement of a task from one project to another, there would be `task.proto` file, with all Task-related
+and {{< code "projection" "TaskDetails" >}} projections, and a Process Manager that is responsible for
+movement of a task from one project to another; there would be a `task.proto` file, with all Task-related
data types definitions. The project-related data types would be defined in a `project.proto` file.
As it was already mentioned, `TaskId` and `ProjectId` are defined in the `identifiers.proto` file,
@@ -111,13 +111,13 @@ Identifiers are usually defined after the name of the entity with the `Id` suffi
* `TaskId`
* `CommentId`
-You will find such naming pattern in the framework API. For example, `EventId`, `CommandId`,
+You will find such a naming pattern in the framework API. For example, `EventId`, `CommandId`,
`UserId`, `TenantId`, and others.
{{% note-block class="note" %}}
-This convention is not a requirement. We find `Id` suffix short yet meaningful for
+This convention is not a requirement. We find the `Id` suffix short yet meaningful for
building a rich type-safe API. You can also select another convention that fits your domain
-best. Please note that future version of the framework tools will use the `Id`Β suffix of the
+best. Please note that a future version of the framework tools will use the `Id`Β suffix of the
types for code scaffolding and improving intelligence of code generation.
{{% /note-block %}}
@@ -179,16 +179,16 @@ message CreateTask {
### `repeated` and `map` fields
We recommend naming `repeated` and `map` fields using singular nouns as such a naming appears
-to be closer to the language we speak. It also provides easier to use generated code.
+to be closer to the language we speak. It also provides easier-to-use generated code.
{{% note-block class="note" %}}
This convention contradicts with the official
[Protobuf Style Guide](https://developers.google.com/protocol-buffers/docs/style#repeated_fields
-"Protocol Buffers Style Guide") which suggests naming `map` and `repeated` fields after plural
+"Protocol Buffers Style Guide") that suggests naming `map` and `repeated` fields after plural
nouns. Knowing this, we still recommend singular because of the following.
The code generated for a `repeated` and `map` field named after a singular noun is closer to
-real English. For the code related to the Domain-Driven Design this is far more important than
+real English. For the code related to Domain-Driven Design this is far more important than
the consistency with the style guide.
{{% /note-block %}}
@@ -236,7 +236,7 @@ Events are named as facts formulated as past participles, for example:
### Rejections
-A rejection is named after a reason of why a command cannot be handled. In fact, rejection notifies
+A rejection is named after a reason of why a command cannot be handled. In fact, a rejection notifies
on a state of the domain model, for example:
* {{< code "rejection" "TaskAlreadyExists" >}}
@@ -254,14 +254,14 @@ Protobuf messages for entity states are defined using nouns, for example:
Avoid using suffixes like `Aggregate`, `Projection`, `ProcessManager` when defining a proto type for
the following reasons:
- 1. You may want to use such a word when creating an entity Java class which _uses_
+ 1. You may want to use such a word when creating an entity Java class that _uses_
a generated data type for holding the state of the entity.
- 2. Such data structure does not represent a whole `Aggregate` or `ProcessManager` thing anyway.
+ 2. Such a data structure does not represent a whole `Aggregate` or `ProcessManager` thing anyway.
It is just data.
## Packages
-Packages allow to form namespaces for types and avoid clashes. It is customary to have a βrootβ
+Packages allow forming namespaces for types and avoiding clashes. It is customary to have a βrootβ
package for an organization or a service name. Most likely each Bounded Context would have
a dedicated package.
@@ -269,7 +269,7 @@ Examples in this guide assume that a fictitious company called
[Acme Corporation](https://en.wikipedia.org/wiki/Acme_Corporation) creates a SaaS solution.
The company has a web presence site with the domain name `acme.io`.
The solution is a task management application called "Todo List"
-which will be hosted at `todolist.acme.io`.
+that will be hosted at `todolist.acme.io`.
### Proto packages
@@ -289,7 +289,7 @@ Java does not have the notion of package nesting. Packages in Java are separated
which seem hierarchical for convenience. When it comes to placing source code files
in a project, there is usually nesting formed by the directories in a file system.
-Spine framework uses this notion of βnestingβ for marking multiple packages of a server-side code
+The Spine framework uses this notion of βnestingβ for marking multiple packages of server-side code
[belonging to a Bounded Context]({{% get-site-data "repositories.core_api_doc" %}}/core/core/io.spine.core/-bounded-context/)
easier. But this is a convenience feature, not a requirement.
@@ -304,12 +304,12 @@ It is easier to see everything related to a type, if all the generated code come
* `io.acme.todolist.project`
* `io.acme.todolist.comment`
-This package would be a part of API shared between client- and server-side code of your
+This package would be a part of the API shared between client- and server-side code of your
application.
#### Commands
-We recommend putting command classes under a package which ends with `command`:
+We recommend putting command classes under a package that ends with `command`:
* `io.acme.todolist.task.command`
* `io.acme.todolist.project.command`
@@ -339,9 +339,9 @@ Unlike commands and events, rejection messages are generated under a file named
The class is placed into a `rejection` package of the corresponding type.
The package also contains generated `Throwable` _top-level_ classes that match rejection messages.
-These classes are used in the `throws` clause of command handling methods.
+These classes are used in the `throws` clause of command-handling methods.
-The arrangement with message classes nested under `Rejections` class and top-level `Throwable`s
+The arrangement with message classes nested under the `Rejections` class and top-level `Throwable`s
is required to avoid name clashes while keeping these generated classes under the same package.
{{% note-block class="note" %}}
@@ -362,7 +362,7 @@ a sub-package called `server` with sub-packages for corresponding entity types:
### Entities
When naming entities we find it natural to start with a name of a state class and then
-add a suffix which tells the type of the entity:
+add a suffix that tells the type of the entity:
* `ProjectAggregate`
* `OrderProcessManager`
@@ -374,7 +374,7 @@ For process managers it may be enough to have the `Process` suffix dropping `Man
which frequently worked for us too. Other options for suffixes are `Pm` or `Procman`.
{{% note-block class="note" %}}
-It would be a good idea to decide on such suffix as a team standard before you
+It would be a good idea to decide on such a suffix as a team standard before you
start coding.
{{% /note-block %}}
diff --git a/docs/content/docs/introduction/prior-art.md b/docs/content/docs/introduction/prior-art.md
index 6ed5c52..775e462 100755
--- a/docs/content/docs/introduction/prior-art.md
+++ b/docs/content/docs/introduction/prior-art.md
@@ -13,7 +13,7 @@ while bringing some important differences into play.
Spine is created for applications that follow the [CQRS](http://martinfowler.com/bliki/CQRS.html)
and [Event Sourcing](http://martinfowler.com/eaaDev/EventSourcing.html) architectural patterns.
-Spine didnβt appear out of the blue. While working on our own CQRS/ES based projects we were
+Spine didnβt appear out of the blue. While working on our own CQRS/ES-based projects we were
alarmed at how much manual effort is spent on creating events and commands, delivering events and data
to the web and mobile clients. It takes time, does not require much creativity from a developer,
whilst this energy could have been spent on productive
@@ -38,7 +38,7 @@ the business domain allows us to make this language
but in communication of computing devices too.
**Immutability** is another major concept we follow.
-Spine uses typed commands and events. Having commands and events as first class citizens in the
+Spine uses typed commands and events. Having commands and events as first-class citizens in the
applications gives a lot of benefits in terms of business logic. Not having to convert back and
forth with Json gives some performance advantage at the same time.
@@ -76,8 +76,8 @@ But as [Martin Fowler](http://www.martinfowler.com/books/eaa.html) notes:
>Frameworks still require you to make decisions about how to use them,
>and knowing the underlying patterns is essential if you are to make wise choices.
-Systems built on top of Spine framework are flexible, loosely-coupled, scalable and open to change.
-Here we should thank [Reactive Manifesto](http://www.reactivemanifesto.org/),
+Systems built on top of the Spine framework are flexible, loosely-coupled, scalable and open to change.
+Here we should thank the [Reactive Manifesto](http://www.reactivemanifesto.org/),
which became one the corner stones and drivers of the Spine philosophy.
We are yet at the beginning of our journey of using Spine in the wild.
diff --git a/docs/content/docs/introduction/project-structure.md b/docs/content/docs/introduction/project-structure.md
index 27e4de9..f3c0b11 100755
--- a/docs/content/docs/introduction/project-structure.md
+++ b/docs/content/docs/introduction/project-structure.md
@@ -6,7 +6,7 @@ headline: Documentation
# Project Structure
{{% note-block class="lead" %}}
-This document describes standard structure of a Spine-based project.
+This document describes the standard structure of a Spine-based project.
It assumes that you are familiar with [Gradle](https://gradle.org).
{{% /note-block %}}
@@ -16,10 +16,10 @@ the code generation done by Protobuf Compiler and Spine Model Compiler.
## Handcrafted code
-Following standard Gradle conventions a manually written code is created under the
+Following standard Gradle conventions manually written code is created under the
`src/main/` directory with subdirectories `proto`, `java`, etc. for corresponding languages.
-After a project is defined in Gradle, a work on a module usually starts in
+After a project is defined in Gradle, work on a module usually starts in
the `proto` directory.
## Generated code
@@ -34,7 +34,7 @@ The sub-directories are:
### Excluding from version control
The generated code is created and updated during build time. Directories with the generated
-code files should NOT be added to version control system of your project.
+code files should NOT be added to the version control system of your project.
This makes a commit to contain only essential changes relevant to the update of the model,
in particular:
@@ -43,7 +43,7 @@ in particular:
2) Updated calls from the application code to the generated data model API.
By not including the generated code into the version control we minimise the βnoiseβ
-for developer eyes when a model changes. So, if you are using Git, for example, consider adding
+for developer eyes when a model changes. So, if you are using Git, for example, consider adding
the following line to your `.gitignore` file:
```text
diff --git a/docs/content/docs/quick-start/_index.md b/docs/content/docs/quick-start/_index.md
index ac637b4..b48ce0f 100755
--- a/docs/content/docs/quick-start/_index.md
+++ b/docs/content/docs/quick-start/_index.md
@@ -7,17 +7,17 @@ headline: Documentation
{{% note-block class="lead" %}}
This guide will walk you through a minimal client-server application in Java
-which handles one command to print some text on behalf of the current computer
-user. The document goes through already written code which is quite simple. So,
+that handles one command to print some text on behalf of the current computer
+user. The document goes through already written code that is quite simple. So,
it won't take long.
{{% /note-block %}}
## What we'll do
-We'll go through the example which shows a Bounded Context called βHelloβ.
+We'll go through the example that shows a Bounded Context called βHelloβ.
The context has one `ProcessManager`, which handles the {{< code "command" "Print" >}} command
sent from the client-side code to the server-side code hosting the context.
-We'll go through the production code of the example suite, and through the code which
+We'll go through the production code of the example suite, and through the code that
tests the Hello context.
## What you'll need
@@ -45,7 +45,7 @@ gradlew.bat :sayHello
```
This would build and execute the example.
-The process should finish with the output which looks like this:
+The process should finish with the output that looks like this:
```bat
> Task :sayHello
@@ -88,7 +88,7 @@ Now, let's dive into the code.
## Project structure
For the sake of simplicity, this example is organised as a single-module Gradle project.
-Most likely, a project for a real world application would be multi-module.
+Most likely, a project for a real-world application would be multi-module.
### The root directory
@@ -112,7 +112,7 @@ Here are the directories of interest in the project root:
* **`gradle`** β this directory contains the code of Gradle Wrapper and two Gradle scripts
used in the [project configuration](#other-project-configuration).
* **`generated`** β this directory contains the code generated by Protobuf Compiler and
- Spine Model Compiler. This directory and code it contains is created automatically
+ Spine Model Compiler. This directory and the code it contains are created automatically
when a domain model changes. This directory is _excluded_ from version control.
* **`src`** β contains the handcrafted source code.
@@ -178,7 +178,7 @@ The rest of the `build.gradle` file does the following:
2. Adds JUnit dependencies by applying the `tests.gradle` script plugin (which we extracted
for the sake of simplicity).
- 3. Defines the `sayHello` task which runs the `Example` application, which orchestrates
+ 3. Defines the `sayHello` task that runs the `Example` application, which orchestrates
the demo.
We are not reviewing these parts of the project configuration deeper because they are not
@@ -263,10 +263,10 @@ There are three parts of interest in this package name:
as if it were hosted on the web at `https://helloworld.spine.io`.
* **`hello`** β this is the package of the Hello context. In this example we have only one, but
- a real world app would have more. Each Bounded Context goes into a dedicated package.
+ a real-world app would have more. Each Bounded Context goes into a dedicated package.
* **`command`** β this part gathers commands of the context under one package.
- We have only one command in this example, but in real world scenarios, with dozens of commands,
+ We have only one command in this example, but in real-world scenarios, with dozens of commands,
it is convenient to gather them under a package.
The following standard proto file option defines the name for the outer class generated by
@@ -280,7 +280,7 @@ option java_outer_classname = "CommandsProto";
```
{{% note-block class="note" %}}
-Outer classes are used by Protobuf implementation internally.
+Outer classes are used by the Protobuf implementation internally.
When the `java_outer_classname` option is omitted, Protobuf Compiler would calculate the Java
class name taking the name of the corresponding `.proto` file.
We recommend setting the name directly to make it straight. This also avoids possible name clashes
@@ -288,7 +288,7 @@ with the handcrafted code.
{{% /note-block %}}
The next standard option instructs the Protobuf Compiler to put each generated Java type into
-a separate file. This way it would be easier to analyze dependencies of the code which uses these
+a separate file. This way it would be easier to analyze dependencies of the code that uses these
generated types.
}}.
The following code obtains the name of the user, and the text to print from the received command,
@@ -515,7 +515,7 @@ return Printed.newBuilder()
The `vBuild()` call validates and builds the message. This method is generated by Spine Model
Compiler. For instructions on adding validation attributes to your model please see
-[Validation User Guide](docs/guides/validation/).
+the [Validation User Guide](docs/guides/validation/).
{{% note-block class="note" %}}
After the event is generated, it is posted to the `EventBus` and delivered to
@@ -538,7 +538,7 @@ static final String NAME = "Hello";
```
This constant is used for creating the context (we will review it in a minute) and when annotating
-the server-side code which belongs to the Hello context. This is done in `package-info.java` using
+the server-side code that belongs to the Hello context. This is done in `package-info.java` using
the `@BoundedContext` annotation:
}} command. The test methods are gathered under
the nested class called `PrintCommand`. The class holds the reference to the
command as its field:
@@ -649,10 +649,10 @@ void sendCommand() {
}
```
-For test values we use statically imported `randomString()` method of the `TestValues` utility class
+For test values we use the statically imported `randomString()` method of the `TestValues` utility class
provided by the `spine-testutil-server` library. We use the `context()` method provided by
`ContextAwareTest` for obtaining the reference of the test fixture of the Bounded Context
-under the test.
+under test.
Now we need to test that handling the command produces the event, and the handling entity updates
its state.
@@ -754,12 +754,12 @@ public Server(String serverName) {
}
```
-The constructor accepts the name which is used for connecting clients. This name
+The constructor accepts the name that is used for connecting clients. This name
is passed to the `inProcess()` factory method of the `io.spine.server.Server` class.
{{% note-block class="note" %}}
In-process gRPC communications are normally used for testing.
-This example uses in-process client/server arrangement in the production code for
+This example uses an in-process client/server arrangement in the production code for
the sake of simplicity. A real-world application would use a `Server` instance exposed
via a TCP/IP port.
{{% /note-block %}}
@@ -849,7 +849,7 @@ public void sendCommand() {
The method does three things:
1. Creates a command message using the login name of the current computer user.
- 2. Subscribes to the {{< code "event" "Printed" >}} event which will be generated
+ 2. Subscribes to the {{< code "event" "Printed" >}} event that will be generated
as the result of handling the command we are going to post (and only this instance
of the command, not all {{< code "command" "Print" >}} commands).
3. Posts the command.
@@ -862,7 +862,7 @@ most of the commands using `client.onBehalfOf(UserId)`.
The `command(commandMessage)` call tells we want to send a command with the passed message.
-Then, we subscribe to the {{< code "event" "Printed" >}} event which would be
+Then, we subscribe to the {{< code "event" "Printed" >}} event that would be
generated as the result of handling the command. The events obtained from the
server would be passed to the `onPrinted()` method of the `Client` class.
@@ -902,9 +902,9 @@ private void printEvent(EventMessage e) {
}
```
-The only interesting thing here is the statically imported `Json.toCompactJson()` call which
+The only interesting thing here is the statically imported `Json.toCompactJson()` call that
converts the event message to a `String`. The method is available from the `Json` utility class
-provided by the framework. The utility does heavy lifting of the conversion which involves knowing
+provided by the framework. The utility does the heavy lifting of the conversion that involves knowing
all Protobuf types available in the project.
Cancelling the subscription, if any, iterates through the set passing each of them to
@@ -948,7 +948,7 @@ Now, let's put it all together.
## Orchestrating the example
-Let's review the `Example` class and its `main()` method. It simulates client-server communication
+Let's review the `Example` class and its `main()` method. It simulates a client-server communication
scenario.
}}
-The Big Blue Book which lay the basics of Domain-Driven Design methodology.
+The Big Blue Book that lays the basics of the Domain-Driven Design methodology.
It provides a broad framework for making design decisions and a vocabulary for
discussing domain design.
{{< /book-card >}}
diff --git a/docs/content/docs/resources/communities.md b/docs/content/docs/resources/communities.md
index 8798665..46280e0 100755
--- a/docs/content/docs/resources/communities.md
+++ b/docs/content/docs/resources/communities.md
@@ -12,13 +12,13 @@ Places to discuss DDD with other practitioners
- [Virtual DDD](https://virtualddd.com/) (EN)
A community-driven online meetup for people who want to get more in-depth knowledge of DDD
-from anywhere at anytime.
+from anywhere at any time.
- [DDD/CQRS Google Group](https://groups.google.com/g/dddcqrs?pli=1) (EN)
A mailing list with practical discussions on DDD, CQRS and Event Sourcing.
- [Domain-Driven Design Injection](https://dddi.dev/) (RU)
-Our own community for developers and engineers who look for the new ways of solving non-trivial software design tasks.
+Our own community for developers and engineers who look for new ways of solving non-trivial software design tasks.
[Regular meetups](https://dddi.dev/logbook) are held online. The discussion in between the meetups continues in the
[DDDi Messroom](https://messroom.dddi.dev/).
diff --git a/docs/content/docs/resources/libraries.md b/docs/content/docs/resources/libraries.md
index 11dd8f0..1b6ac04 100755
--- a/docs/content/docs/resources/libraries.md
+++ b/docs/content/docs/resources/libraries.md
@@ -11,10 +11,10 @@ Other tools that help with DDD in code
{{% /note-block %}}
- [Axon](https://axoniq.io/).
-Open source framework for event-driven microservices and domain-driven design.
+Open-source framework for event-driven microservices and domain-driven design.
- [Vlingo](https://vlingo.io/).
-The open source toolkit by Vaughn Vernon for fluent reactive, event-driven,
+The open-source toolkit by Vaughn Vernon for fluent reactive, event-driven,
and microservices architectures.
- [EventStore](https://eventstore.com/).
diff --git a/docs/project.md b/docs/project.md
new file mode 100644
index 0000000..77c3f9c
--- /dev/null
+++ b/docs/project.md
@@ -0,0 +1,59 @@
+# Project: documentation
+
+## Overview
+
+This repository is the **documentation aggregator and content host** for the
+Spine SDK. It owns the Hugo site setup that gathers documentation from sibling
+SDK repos (currently `SpineEventEngine/validation`) as Hugo modules, and it
+also stores original Markdown content under `docs/`. The repo additionally
+serves as the GitHub Wiki source for committer-facing documentation about
+contributing to the framework.
+
+The public site at [spine.io](https://spine.io) is built from
+`SpineEventEngine/SpineEventEngine.github.io`, which **imports this repo's
+`docs/` directory as a Hugo module**. Edits made here flow to the public site
+when `SpineEventEngine.github.io` bumps its module pin.
+
+## Architecture
+
+**Role in the org:** documentation aggregator + content host.
+
+**Stack.** A Hugo + Node project at heart. Gradle is a thin task-runner
+wrapper around Hugo, Node, and Go tooling β not a JVM build in any meaningful
+sense, so JVM coding conventions do not apply here.
+
+- `docs/` β published content, Hugo site root, exported as a Hugo module to
+ `SpineEventEngine.github.io`.
+- `docs/_preview/` β local-only Hugo setup for running the site during
+ authoring (`./gradlew :runSite`, or `hugo server` from this directory).
+- `docs/_code/examples/*` β git submodules pinned at
+ `spine-examples/{airport, blog, hello, kanban, todo-list}`. These are the
+ **canonical source of embedded code samples**; this repo does not modify
+ them.
+- `config/` β git submodule pointing at `SpineEventEngine/config`. Provides
+ shared agent guidance, skills, and build config consumed across Spine SDK
+ repos. Applied via the `Apply config` step.
+- Theme: components, layouts, and styles come from the `site-commons` Hugo
+ theme (`github.com/SpineEventEngine/site-commons`).
+
+**Doc modules pulled in via `docs/hugo.toml`.** Currently only the
+`validation` repo contributes docs as a Hugo module. The README lists
+`framework` and `compiler` as examples of how to add more; they are not
+wired in today.
+
+**Key conventions and constraints (not obvious from the code):**
+
+- **Theme changes mirror to spine.io.** Any non-trivial change to
+ `site-commons` usage here must also be applied in the main `spine.io` site
+ repo, or the live site will diverge from preview.
+- **Embedded code must round-trip.** Code blocks in pages are generated from
+ the `docs/_code` submodules by the [`embed-code`][embed-code] tool. Do not
+ hand-edit embedded code blocks in Markdown; run `./gradlew :embedCode` and
+ verify with `./gradlew :checkSamples`. See [`EMBEDDING.md`](../EMBEDDING.md).
+- **Submodules are pinned.** `docs/_code/examples/*` and `config` are pinned
+ intentionally β do not bump them as a side effect of unrelated work.
+- **Link checking is required pre-PR.** Run the `check-links` skill before
+ opening any PR that touches `docs/**` or `site/**`; CI runs the same check
+ via `lychee.toml` against the rendered HTML.
+
+[embed-code]: https://github.com/SpineEventEngine/embed-code-go
diff --git a/init-submodules b/init-submodules
new file mode 100755
index 0000000..2ae143d
--- /dev/null
+++ b/init-submodules
@@ -0,0 +1,99 @@
+#!/usr/bin/env bash
+
+################################################################################
+#
+# Materialize the *config-managed* submodules a fresh working tree is missing, so
+# agent assets resolve.
+#
+# `git worktree add` β and some shallow CI / cloud checkouts β populate only the
+# superproject's own tracked files; registered submodules are left UNinitialized.
+# In a Spine repo that means the `config` and `.agents/shared` submodules are
+# empty, the `.agents/skills` -> `.agents/shared/skills` symlink dangles, and no
+# agent skills, scripts, or guidelines can be found.
+#
+# This script is the bootstrap that has to run BEFORE `./config/pull`: `pull`
+# lives inside the `config` submodule, so on a fresh worktree it does not yet
+# exist. `init-submodules`, by contrast, is a plain tracked file at the repo root
+# (distributed by `config`), so `git worktree add` always checks it out β it can
+# therefore bring `config` itself into existence.
+#
+# It initializes ONLY submodules that are BOTH:
+#
+# * not yet checked out β those `git submodule status` marks with a leading `-`,
+# at the commit the branch pins; and
+#
+# * config-managed β `config` itself (the bootstrap target `pull` lives inside,
+# which carries no tracked `branch` in a consumer's `.gitmodules`), plus every
+# submodule that declares a tracked `branch` in `.gitmodules`. This is exactly
+# the rule `./config/pull` uses to decide what it floats, so the two scripts
+# can never disagree about what is shared.
+#
+# Consumer-owned submodules (a Hugo theme, a vendored library, documentation
+# examples, ...) declare no tracked branch and are deliberately left untouched.
+# Because a `SessionStart` hook runs this script automatically on every session,
+# initializing them would mean trying to clone β or failing on credentials for β
+# a submodule this project does not manage, on every single start. They are
+# skipped (noted on stderr).
+#
+# Submodules already present are left exactly as they are, so a tree that floated
+# `config` / `.agents/shared` to a branch tip via `./config/pull` is never
+# silently rewound to the pin. That makes the script idempotent and safe to run on
+# every session start.
+#
+# It does NOT float submodules to their branch tips β run `./config/pull`
+# afterwards for that. Unlike `pull`, it depends on no pre-existing `config`
+# submodule, so it can bootstrap a bare worktree where `./config/pull` does not
+# yet exist.
+#
+################################################################################
+
+set -u
+
+root=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
+cd "$root" || exit 0
+
+# Nothing to do in a repo without submodules.
+[ -f .gitmodules ] || exit 0
+
+# The set of config-managed submodule paths: `config` itself (handled specially β
+# it carries no tracked branch, exactly as in `./config/pull`), plus every
+# submodule declaring a tracked `branch` in `.gitmodules`. Mirrors `pull`'s rule.
+config_managed_paths() {
+ printf '%s\n' 'config'
+ git config -f .gitmodules --get-regexp '^submodule\..*\.branch$' 2>/dev/null \
+ | while read -r key _branch; do
+ name=${key#submodule.}; name=${name%.branch}
+ git config -f .gitmodules --get "submodule.$name.path" 2>/dev/null
+ done
+}
+
+managed=$(config_managed_paths | sort -u)
+
+# `git submodule status` prefixes each uninitialized submodule with `-`; an
+# initialized one starts with a space (at the pinned commit) or `+` (ahead of it).
+# Act only on the `-` lines, taking the path from the second field, and only when
+# that path is config-managed.
+git submodule status 2>/dev/null | awk '$1 ~ /^-/ { print $2 }' | while read -r path; do
+ [ -n "$path" ] || continue
+ if printf '%s\n' "$managed" | grep -qxF -- "$path"; then
+ echo "init-submodules: initializing '$path'"
+ git submodule update --init --recursive -- "$path" \
+ || echo "init-submodules: WARNING β could not initialize '$path' (offline?)." >&2
+ else
+ echo "init-submodules: skipping consumer-owned '$path' (not config-managed)." >&2
+ fi
+done
+
+# Route Git hooks to the shared hooks directory so the secret-scan `pre-commit`
+# hook is active even in a brand-new worktree, before `./config/pull` runs. The
+# path floats with the `.agents/shared` submodule; until that submodule is
+# initialized the hook simply does not fire (Git skips a missing hook). Set only
+# when unset or already ours β never override a repo's own `core.hooksPath`.
+desired_hooks=".agents/scripts/git-hooks"
+current_hooks=$(git config --local --get core.hooksPath 2>/dev/null || true)
+if [ -z "$current_hooks" ] || [ "$current_hooks" = "$desired_hooks" ]; then
+ git config --local core.hooksPath "$desired_hooks" \
+ && echo "init-submodules: Git hooks routed to '$desired_hooks' (secret-scan pre-commit active)."
+fi
+
+exit 0