From dedb58380b3d51008565ae982d3696e2167ddfbc Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Fri, 28 Aug 2026 23:15:08 +0100 Subject: [PATCH 01/44] feat: add `delivery-server`, `BuildSpeed`, and `documentation` submodules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These three repositories consume modules already assembled here as upstream dependencies, so cross-repository rollouts should reach them: * `delivery-server` tracks nearly the whole SDK — `base-libraries`, `core-jvm`, `compiler`, `validation`, and nine more — at versions close to the tips; * `BuildSpeed` exercises the compiler line (`compiler`, `core-jvm-compiler`, `core-jvm`, `validation`) to measure build-tool performance; * `documentation` builds the spine.io site and its code samples. All three are pinned to fixed commits and declare no tracked `branch`, so `init-submodules` treats them as consumer-owned and leaves them alone, exactly as it does the SDK submodules already listed. Co-Authored-By: Claude Opus 5 --- .gitmodules | 9 +++++++ BuildSpeed | 1 + README.md | 65 +++++++++++++++++++++++++++++-------------------- delivery-server | 1 + documentation | 1 + 5 files changed, 50 insertions(+), 27 deletions(-) create mode 160000 BuildSpeed create mode 160000 delivery-server create mode 160000 documentation diff --git a/.gitmodules b/.gitmodules index 7682bb6..ff4e8e4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -58,3 +58,12 @@ [submodule "agents"] path = agents url = https://github.com/SpineEventEngine/agents.git +[submodule "delivery-server"] + path = delivery-server + url = https://github.com/SpineEventEngine/delivery-server.git +[submodule "BuildSpeed"] + path = BuildSpeed + url = https://github.com/SpineEventEngine/BuildSpeed.git +[submodule "documentation"] + path = documentation + url = https://github.com/SpineEventEngine/documentation.git diff --git a/BuildSpeed b/BuildSpeed new file mode 160000 index 0000000..dfc6c11 --- /dev/null +++ b/BuildSpeed @@ -0,0 +1 @@ +Subproject commit dfc6c114ff6657cf8fa7634f07cf5b8399ef9dc0 diff --git a/README.md b/README.md index 162016e..866f169 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ SDK. They are grouped below by function, not by strict dependency order. ### Shared infrastructure -| Submodule | Role | -|-----------|------| +| Submodule | Role | +|--------------------|----------------------------------------------------------------| | [`config`][config] | Dependencies and build configurations shared among subprojects | The shared agent tooling — skills, scripts, and guidelines — lives in the @@ -27,44 +27,52 @@ does against the SDK repositories above. ### Foundation & utilities -| Submodule | Role | -|-----------|------| -| [`base-libraries`][base-libraries] | The framework's base types and utilities | -| [`reflect`][reflect] | Utilities for working with reflection in Java and Kotlin projects | -| [`logging`][logging] | Fluent logging API for Kotlin projects | -| [`testlib`][testlib] | Testing utilities for Spine SDK development and users | +| Submodule | Role | +|------------------------------------|-------------------------------------------------------------------| +| [`base-libraries`][base-libraries] | The framework's base types and utilities | +| [`reflect`][reflect] | Utilities for working with reflection in Java and Kotlin projects | +| [`logging`][logging] | Fluent logging API for Kotlin projects | +| [`testlib`][testlib] | Testing utilities for Spine SDK development and users | ### Domain & value types -| Submodule | Role | -|-----------|------| -| [`base-types`][base-types] | Popular value object types and associated code | -| [`change`][change] | Data types and utilities for changes and mismatches in data values | -| [`time`][time] | Protobuf-based date/time types and utilities | -| [`money`][money] | Currency and money data types and operations | +| Submodule | Role | +|----------------------------|--------------------------------------------------------------------| +| [`base-types`][base-types] | Popular value object types and associated code | +| [`change`][change] | Data types and utilities for changes and mismatches in data values | +| [`time`][time] | Protobuf-based date/time types and utilities | +| [`money`][money] | Currency and money data types and operations | ### Compiler & code generation -| Submodule | Role | -|-----------|------| -| [`compiler`][compiler] | The Spine Compiler — extendable Protobuf compilation | -| [`tool-base`][tool-base] | Common code for development tools | -| [`ProtoTap`][ProtoTap] | Utilities for tapping `protoc` output | -| [`validation`][validation] | Library and Compiler plugins for generating custom validation code | -| [`core-jvm-compiler`][core-jvm-compiler] | Plugins of the CoreJvm library for the Spine Compiler | +| Submodule | Role | +|------------------------------------------|--------------------------------------------------------------------| +| [`compiler`][compiler] | The Spine Compiler — extendable Protobuf compilation | +| [`tool-base`][tool-base] | Common code for development tools | +| [`ProtoTap`][ProtoTap] | Utilities for tapping `protoc` output | +| [`validation`][validation] | Library and Compiler plugins for generating custom validation code | +| [`core-jvm-compiler`][core-jvm-compiler] | Plugins of the CoreJvm library for the Spine Compiler | ### Framework core -| Submodule | Role | -|-----------|------| +| Submodule | Role | +|------------------------|----------------------------------------------------------| | [`core-jvm`][core-jvm] | The JVM-based implementation of the Spine framework core | ### Storage & runtime -| Submodule | Role | -|-----------|------| -| [`jdbc-storage`][jdbc-storage] | Support for storage in JDBC-compliant databases | -| [`gcloud-jvm`][gcloud-jvm] | Support for Spine-based Kotlin and Java apps on Google Cloud | +| Submodule | Role | +|--------------------------------------|--------------------------------------------------------------| +| [`jdbc-storage`][jdbc-storage] | Support for storage in JDBC-compliant databases | +| [`gcloud-jvm`][gcloud-jvm] | Support for Spine-based Kotlin and Java apps on Google Cloud | +| [`delivery-server`][delivery-server] | gRPC services providing sharded message delivery | + +### Benchmarks & documentation + +| Submodule | Role | +|----------------------------------|----------------------------------------------------------------------| +| [`BuildSpeed`][BuildSpeed] | Model definitions for testing the performance of Spine's build tools | +| [`documentation`][documentation] | Documentation and articles at spine.io and other web resources | ## Getting started @@ -117,3 +125,6 @@ available skills are described in [`AGENTS.md`](AGENTS.md). [core-jvm]: https://github.com/SpineEventEngine/core-jvm [jdbc-storage]: https://github.com/SpineEventEngine/jdbc-storage [gcloud-jvm]: https://github.com/SpineEventEngine/gcloud-jvm +[delivery-server]: https://github.com/SpineEventEngine/delivery-server +[BuildSpeed]: https://github.com/SpineEventEngine/BuildSpeed +[documentation]: https://github.com/SpineEventEngine/documentation diff --git a/delivery-server b/delivery-server new file mode 160000 index 0000000..0b79247 --- /dev/null +++ b/delivery-server @@ -0,0 +1 @@ +Subproject commit 0b79247951497e2764a13cbf326b8616255ccffc diff --git a/documentation b/documentation new file mode 160000 index 0000000..7c08fa1 --- /dev/null +++ b/documentation @@ -0,0 +1 @@ +Subproject commit 7c08fa12c19ebfa00e80def76c2b01bd91931bef From a1b067c759ce5d5a50710de7b1ccaa50bb6025c1 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Fri, 28 Aug 2026 23:15:16 +0100 Subject: [PATCH 02/44] chore: float the pinned submodules to their published tips MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Advances 14 submodules to the tip of their `master` branch, refreshing the snapshot of the SDK this superproject records. The remaining pins were already at their tips. `.agents/shared` is deliberately left unstaged: it declares a tracked `branch` and `ignore = all`, so it floats on its own and never shows up as churn here. Nested mounts are synced to the commits their own superprojects pin, not floated — those pointers belong to the repositories that record them. Co-Authored-By: Claude Opus 5 --- ProtoTap | 2 +- agents | 2 +- base-libraries | 2 +- change | 2 +- compiler | 2 +- config | 2 +- core-jvm | 2 +- core-jvm-compiler | 2 +- gcloud-jvm | 2 +- jdbc-storage | 2 +- logging | 2 +- time | 2 +- tool-base | 2 +- validation | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/ProtoTap b/ProtoTap index 43ac1ac..39d9103 160000 --- a/ProtoTap +++ b/ProtoTap @@ -1 +1 @@ -Subproject commit 43ac1acaa282398920c1706230241660a3817041 +Subproject commit 39d9103c676bc9a8e859c8c4dc9363fa8a4bc40b diff --git a/agents b/agents index 2369ab8..cd7c275 160000 --- a/agents +++ b/agents @@ -1 +1 @@ -Subproject commit 2369ab816bb5d0feee562bfd6395b0b2852a3a9d +Subproject commit cd7c275aee61187d685d18ee61a08c9212f153a6 diff --git a/base-libraries b/base-libraries index 7a05857..fa7d185 160000 --- a/base-libraries +++ b/base-libraries @@ -1 +1 @@ -Subproject commit 7a05857b2adc68dc2f9b28a03ae3073e7c3e9df3 +Subproject commit fa7d1854025d1d4e7b44a08b03f1c4cff4cd7235 diff --git a/change b/change index 53fe702..4853940 160000 --- a/change +++ b/change @@ -1 +1 @@ -Subproject commit 53fe7029a18920b8cd5d79c8636affb79f77ce84 +Subproject commit 4853940add7002d6b1c80915190e8a824f9149e3 diff --git a/compiler b/compiler index 313a647..85302f2 160000 --- a/compiler +++ b/compiler @@ -1 +1 @@ -Subproject commit 313a647055faf29942d94047fcbc256386a8ebdc +Subproject commit 85302f29f473df13ea540aad1981211645d8232b diff --git a/config b/config index 6cee3cf..b728a70 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit 6cee3cf52f3044f08a8433cf838713c804ea1084 +Subproject commit b728a708f7f5f5a8d5a866b7c0a8d3a0f924b9e2 diff --git a/core-jvm b/core-jvm index a408b0d..0ddca2c 160000 --- a/core-jvm +++ b/core-jvm @@ -1 +1 @@ -Subproject commit a408b0d70dafd603efc55b89c8b4b6f3e8c19d3b +Subproject commit 0ddca2c79a4a4d7b893edf8be88158763a119a96 diff --git a/core-jvm-compiler b/core-jvm-compiler index 0eb8ec3..2425d25 160000 --- a/core-jvm-compiler +++ b/core-jvm-compiler @@ -1 +1 @@ -Subproject commit 0eb8ec332cbad5731193bea5b22a21759300e4f9 +Subproject commit 2425d257b3829f7531562ae673b845e972853620 diff --git a/gcloud-jvm b/gcloud-jvm index f4ade19..13f6b45 160000 --- a/gcloud-jvm +++ b/gcloud-jvm @@ -1 +1 @@ -Subproject commit f4ade19d8bf7666447f068607426475cda485afe +Subproject commit 13f6b45d15001cbd562e7ae54cdce42bff6600ba diff --git a/jdbc-storage b/jdbc-storage index 1b77832..0582f22 160000 --- a/jdbc-storage +++ b/jdbc-storage @@ -1 +1 @@ -Subproject commit 1b778327a6d24c9834102af86e18f9c532b8ac98 +Subproject commit 0582f2293f3925a11942daab8c8cf03a90b0585a diff --git a/logging b/logging index b883c8f..1ca1eee 160000 --- a/logging +++ b/logging @@ -1 +1 @@ -Subproject commit b883c8f9707c105f6dafceb94b192dfd8a082662 +Subproject commit 1ca1eee7500ba9d89bdac7c60db12c5ddfe99bfc diff --git a/time b/time index 57d3dd9..4daeedf 160000 --- a/time +++ b/time @@ -1 +1 @@ -Subproject commit 57d3dd98fea8efcdc4a3843f91143acc2dce87dc +Subproject commit 4daeedfb6a1f961c0fa8dc3692b330d370dbfbbc diff --git a/tool-base b/tool-base index cca5d03..7faea57 160000 --- a/tool-base +++ b/tool-base @@ -1 +1 @@ -Subproject commit cca5d03ff5f313b5c03beb37c798c1fc65543919 +Subproject commit 7faea573ce9bc5540f4a493817423c4c1160c27f diff --git a/validation b/validation index c737fc1..15a7be4 160000 --- a/validation +++ b/validation @@ -1 +1 @@ -Subproject commit c737fc15ff3b11e72bf3d2540d763015d8cd5908 +Subproject commit 15a7be44f24d4732588626248f43a6b1f6b922d6 From 3024fec6b0a7670cd7e3887ad8d402620fdcd475 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Sat, 29 Aug 2026 16:04:12 +0100 Subject: [PATCH 03/44] feat: introduce the `cascade` cross-repository wave machinery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the deterministic driver for propagating changes across the SDK repositories in dependency order (`refresh` after a `config` merge, `ripple` from one upstream, `retarget` for coordinated version assignment such as `2.0.0-M1`): * `cascade` — wave subcommands (`graph`/`plan`/`status`/`next`/`await`/`close`) and per-repo steps (`prep`/`build`/`ship`/`refresh`/`park`/`resume`), with typed exit codes: 3 = a named skill must act, 4 = waiting on an external event, 5 = registry-throttling halt. State is derived from git, GitHub, the registry, and `~/.m2` — never remembered. * `docs/dependency-graph.md` — the standalone build-order contract: canonical order (audited: one-way dependencies constrain it absolutely), reduced production diagram, test-only and mutual-pair sections, and the complete machine-readable list of declared dependencies. * `docs/rollout/rebuild.md` + `rebuild-pr-body.md` — the playbook pairing the script with the agent-driven judgement steps, per the proofread precedent. * `README.md`/`docs/project.md` — links to the new contract and rollout. * `.agents/tasks/cascade.md` — the reviewed design record with decision log. Co-Authored-By: Claude Opus 5 --- .agents/tasks/cascade.md | 411 +++++++++ README.md | 5 +- cascade | 1440 +++++++++++++++++++++++++++++++ docs/dependency-graph.md | 291 +++++++ docs/project.md | 4 + docs/rollout/rebuild-pr-body.md | 32 + docs/rollout/rebuild.md | 123 +++ 7 files changed, 2305 insertions(+), 1 deletion(-) create mode 100644 .agents/tasks/cascade.md create mode 100755 cascade create mode 100644 docs/dependency-graph.md create mode 100644 docs/rollout/rebuild-pr-body.md create mode 100644 docs/rollout/rebuild.md diff --git a/.agents/tasks/cascade.md b/.agents/tasks/cascade.md new file mode 100644 index 0000000..64c1a5b --- /dev/null +++ b/.agents/tasks/cascade.md @@ -0,0 +1,411 @@ +--- +slug: cascade +branch: cascade-machinery +owner: alexander.yevsyukov +status: draft +started: 2026-08-29 +--- + +# `cascade` — automated cross-repository waves + +Design under discussion — do **not** start implementation until this task is +`approved`. + +## Context + +Changes must regularly propagate across the SDK repos in dependency order: per repo — +`./config/pull`, refresh local dependency pins, bump/assign the version, build, open a +PR — with downstream PRs opening only after upstream artifacts are actually published. +Today this is manual. Goal: humans only review/approve PRs; everything else is +automated. Phase 1 runs from a Claude session in `summit`; phase 2 moves to cloud +workflows. Three wave kinds cover the known triggers: + +- **`refresh`** — `config` merged (e.g. the pending `refresh-dependencies` branch); + every repo rebuilds against the new baseline. +- **`ripple`** — a top upstream repo (e.g. `base-libraries`) published a new version; + only its downstream closure rebuilds. +- **`retarget`** — a coordinated version assignment across all repos, e.g. dropping + the `SNAPSHOT` infix to release `2.0.0-M1` everywhere. + +A fourth kind (`custom`: mass mechanical edits) is a later extension; the kind +abstraction (scope rule + per-repo edit step + version rule) is its seam. + +Key mechanics (verified this session) the design exploits: + +- `config/pull` resets every consumer's `buildSrc/` (and so all + `io.spine.dependency.local/*.kt` pins) to config@master — config's pins **are** the + published version vector; a wave applies only the *delta* per repo. +- Versions are static literals in `version.gradle.kts`; `publish.yml` (on master push) + publishes exactly that literal → deterministic at PR time. +- `standardToSpineSdk()` puts `mavenLocal()` **last** → a *bumped* (not-yet-published) + version resolves from mavenLocal locally and from GCAR in CI after upstream merges. + Same coordinates, no config switching. +- Publishedness is anonymously probeable: + `https://europe-maven.pkg.dev/spine-event-engine/{snapshots,releases}///maven-metadata.xml`. +- House pattern to mirror: `proofread-repo` script (mechanics, git-state idempotency, + exit-3 escalation to agent) + `docs/rollout/` playbook + shared skill (judgement, + commit authorization). + +Decisions made with the user: mechanical breakage fixed autonomously, semantic +breakage parks the repo (downstream holds, independent DAG branches continue); eager +builds against mavenLocal, PR *creation* sequenced by upstream publish; version vector +propagates per-repo during the wave, one closing `config` PR records the final vector; +humans merge (agent `gh pr merge` is deny-listed). + +## Architecture digest + +**Wave** = one cascade run; consumer branch name = wave slug (per-wave branches keep +two waves independent). Kind determines slug, scope, and version rule: + +| kind | slug | scope | version rule | +|------------|---------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------| +| `refresh` | `cascade-refresh-<7-char config SHA>` | all non-excluded repos | snapshot `+1` | +| `ripple` | `cascade-ripple--` | downstream closure of `--seed [@]` (seed itself included when its PR is part of the wave; recorded pre-`done` when already published by hand) | snapshot `+1` | +| `retarget` | `cascade-retarget-` | all non-excluded repos | every repo's version **set to** `--target` (e.g. `2.0.0-M1`); pins point at upstreams' same target | + +Per-repo procedure is identical across kinds; `./config/pull` always runs (keeps repos +current, floats agent tooling) but its commit is skipped when it stages nothing +(ripple/retarget with unmoved config). For `retarget`, published-floor constants still +hold last *published* versions during the wave and advance only at `close`, by which +point the target is published everywhere. `retarget` has a hard prerequisite: the +straggler repos must be caught up first (every module carries the target, and +`reflect`/`testlib` are pinned across the graph). Roadmap (owner's decision): +**wave 1** `refresh` on the healthy set (machinery shakedown) → **wave 2** attended +straggler catch-up (`refresh --include reflect --include money --include testlib`) → +M1 `retarget` unblocked. A follow-up `retarget` reopens development (back to the +snapshot line) with the same machinery. + +**State** — two files per wave in summit, created by the script: +- `.agents/tasks/.md` — human/agent contract (frontmatter, checklist, log, park reasons). +- `.agents/tasks/.json` — machine manifest, script-written only. Records: wave + `kind` + trigger (config SHA / seed@version / wave target), frozen DAG edges + grep + evidence, per-repo `state`, `target_version`, `pin_vector`, + PR refs, park reasons, repo class (`library` | `verify-only`), eager/deferred flag, + `version_property` name. Committed to the wave's summit branch at checkpoints + (subject `Update cascade state`). +- Honesty rule: `status` **re-derives** every derived fact (branch commits via + log-grep, `pinned` **by content** — every `track` constant equals the manifest + `pin_vector`, so it honestly regresses after drift; sentinel via `pre-pr.ok`; PR via + `gh pr view`; publishedness via metadata probe; mavenLocal via `~/.m2` path). + `built` is the one recorded happy-path fact `{success, head_sha}` and holds only + while `head_sha == HEAD` — any later commit demotes the repo and forces a rebuild. + `state` is a cache, never proof. + +**Per-repo states**: `pending → pulled → pinned → bumped → built → published-local → +pr-ready → pr-open → merged → published-remote → done`, plus `parked` (recorded), +`blocked` (derived from upstream parks), `deferred` (pluginManagement gap — build +only after gap-upstreams publish), `superseded`. Wave-level `halted` on HTTP 429. +`merged ≠ published-remote` — publish can fail; downstream gates on the probe. + +**Dependency graph as documentation** — `summit/docs/dependency-graph.md` (NEW, +committed, linked from `README.md`): a fenced ```mermaid `flowchart TD` whose edge +lines (`base-libraries --> tool-base`) are the canonical, agent-parseable adjacency +(GitHub renders it natively for humans), grouped by the README sections via Mermaid +subgraphs; below it a metadata table per repo: class (`library`/`verify-only`/ +`excluded` + reason), probe artifact (e.g. `spine-base`), `version_property` name. +Maintained by `cascade graph`: derives edges by grep (`import +io.spine.dependency.local.` in consumer-owned build files only — root/module +`build.gradle.kts`, `settings.gradle.kts`, `buildSrc/.../module.gradle.kts`; excludes +`config/**` and the rest of distributed `buildSrc/**`; object→repo table in script), +compares with the stored file, and updates it or reports drift. Topology changes ride +normal summit PRs, so the graph is reviewed documentation, not per-wave ephemera. + +**Script** — `summit/cascade` (sibling of `proofread-repo`, same conventions + +`sed_inplace()` BSD/GNU wrapper). Exit codes: 0 ok/idempotent, 1 hard error, 2 usage, +**3 agent required** (message names skill + resume command), **4 waiting on external +event**, **5 throttle halt**. Subcommands: +- `graph [--check]` — regenerate/verify `docs/dependency-graph.md` (above); `--check` + is the freshness gate other subcommands use. +- `plan --kind refresh|ripple|retarget [--seed [@]] [--target ]` — + freeze the trigger (config SHA / seed version / target); **read the stored graph**, + re-verify it against fresh derivation (drift → exit 3: review + commit the graph + update first), freeze the edges + evidence into the manifest; apply the kind's scope + rule (table above); classify repos (`verify-only`: BuildSpeed — no + `version.gradle.kts`, no publishing, settings *generated* via + `substitute-settings.py`, so sed-based `apply_pins` may not reach its pin source: + expected parked in wave 1, per-class pin hook if it earns one; `deferred`: repo applies a Spine Gradle plugin + advancing this wave AND its `pluginManagement` has neither mavenLocal nor Spine + registry — base-libraries, core-jvm, tool-base pattern); default-exclude + `documentation` (Spine 1.x) and stragglers `reflect`/`money`/`testlib` (config + hundreds of commits behind — their pull is a migration; opt-in via `--include`); + detect carryover `cascade-*` PRs (excluded unless `--supersede`); write manifest + + task file; print reviewable plan. +- `status` / `next --json` — re-derive truth / emit action list + (`BUILD|SHIP|REFRESH|ADAPT|WAIT|HALTED|DONE`). +- `prep ` — branch off `origin/master`; `./config/pull` + commit + `Update shared agent tooling and config (./config/pull)` (floats the new skill in; + commit skipped when pull stages nothing); `apply_pins` (read each in-wave upstream's + **actual** version from its wave branch — or the wave target for `retarget` — sed + the consumer pins, commit `Update local dependency pins`); version step per kind: + `+1` snapshot fast path on the repo's `version_property`, or write the retarget + target verbatim; commit ``Bump version -> `X` `` (retarget subject: + ``Set version -> `X` ``); `by extra(...)`/non-snapshot line on a `+1` wave → + exit 3 → `bump-version` skill. +- `build ` — self-run `version-bumped.sh` gate (script-internal gradle bypasses + the PreToolUse hooks, so re-implement); `./gradlew clean build dokkaGenerate`; + `publishToMavenLocal` where `build` doesn't chain it; **strictly sequential across + repos** (Central throttling); 429 → record halt, exit 5, never retry; other failure + → exit 3 → `cascade-adapt` skill (budget: **three** adapt→rebuild rounds per repo + per wave, tracked in the manifest; the third failed rebuild auto-parks with the + full failure history); success → commit `Update dependency reports` + (empty diff while `docs/dependencies/` exists → exit 3: `ensure-reports-updated.yml` + would fail); verify mavenLocal serves the target. +- `ship ` — sentinel gate (`pre-pr.ok` PASS + head==HEAD, exit 3 → re-run + `pre-pr`); **upstream-published gate** (every pinned version present in registry + metadata, else exit 4); drift gate (upstream re-bumped past pin → exit 3 → + `refresh`); discard report noise; push; `gh pr create --body-file + docs/rollout/rebuild-pr-body.md` (PR exists → push-only). Records PR + head SHA; + checkpoints manifest. +- `refresh ` — drift repair: re-apply pins to current published versions, + commit, exit 3 → `pre-pr` (incremental build) → `ship`. Registry collision → + `bump-version` sanctioned re-bump #1. +- `await [--timeout-min 30]` — background watcher (run via `run_in_background`): + polls `gh pr view` (120 s) + metadata (60–90 s when something is `merged`, 5 min + idle); **exits 0 on any change** to the `next` action set, 4 on timeout, 5 on halt. + `PUBLISH_OVERDUE` after 45 min merged-without-publish. +- `park --reason` / `resume `; `close` — final vector into config's + `local/*.kt` **including published-floor constants**, branch `-vector`, + commit, exit 3 → `pre-pr` in config → `close --ship` opens the config PR. + +**Pin-edit mechanics** — per-file constant-role table in the script: +`track` (advance during wave) vs `published-floor` (feed buildscript/pluginManagement +classpaths, which can't see mavenLocal — advanced only by `close`): +`Base.kt` version/versionForBuildScript; `ToolBase.kt` version/dogfoodingVersion +(+`JavadocFilter.version` ignored — separate line); `CoreJvmCompiler.kt` +version/dogfoodingVersion; `Compiler.kt` fallbackVersion/fallbackDfVersion; all others +`version`=track. Edit = name+old-value anchored `sed_inplace`; post-edit occurrence +verification; mismatch → revert file, exit 3. + +**Skill** — `cascade-adapt` (agents repo, `skills/cascade-adapt/SKILL.md`): diagnose +the escalated build failure; classify mechanical (renames, moved packages, signature +changes, documented deprecation successors, missing explicit dependency) vs semantic +(design judgement); fix mechanical + commit; semantic → write diagnosis to task file, +`./cascade park`. Never edits `buildSrc/**` (config-owned → park) or +`version.gradle.kts` (bump-version's). `## Commit authorization` (house wording): +exactly one commit per invocation, stage only the adapted source/test files, subject +`Adapt to refreshed dependencies`, no push/tag/rebase/amend; fresh invocation on later +breakage may commit again; semantic → report, park, stop, no commit. + +**Orchestrator loop (phase 1)** — `summit/.claude/commands/cascade.md` (`/cascade +`): `status` → `next` → execute actions (builds one at a time; exit 3 → invoke +the named skill and rebuild, within the three-round budget; between `built` and +`ship` run `pre-pr` in the repo) → +if only `WAIT`, launch `await` in background and end turn (its exit re-invokes the +session — no sleep-polling; optional `/loop 45m /cascade ` as crash backstop) → +every iteration ends with the wave status table + "Needed from you" lines. +Human touch: review/merge PRs, park decisions, supersede decisions, throttle-halt ack. + +## Change list + +**PR A — agents repo** (branch `cascade-adapt-skill` in `.agents/shared/`, PR to +SpineEventEngine/agents): +- NEW `skills/cascade-adapt/SKILL.md` (+ `agents/openai.yaml` stub, matching siblings). +- NEW `claude/commands/cascade.md` — the `/cascade` loop command. It lives here, + not in summit: summit's `.claude/commands` is a symlink into this repo, so + slash commands are shared org-wide (the command declares itself summit-only). +- Merge before wave 1 — the wave's own `./config/pull` then floats it everywhere. + +**PR B — config repo** (`/Users/sanders/Projects/Spine/config`): +- `.claude/settings.json`: remove `"Bash(git commit:*)"` from `permissions.ask` + (violates `safety-rules.md` §Commits — an ask outranks every allow and freezes + autonomous sessions; `migrate` distributes this file to every consumer). Add to + `allow`: `"Bash(gh pr view:*)"`, `"Bash(gh pr list:*)"`, `"Bash(gh run list:*)"`. +- Merge before the first *unattended* wave (each mid-wave pull redistributes the file). + +**PR C — summit repo** (`/Users/sanders/Projects/Spine/summit`): +- NEW `cascade` (script above; model on `proofread-repo`). +- NEW `docs/dependency-graph.md` (generated by `cascade graph`, reviewed by hand + against the known order; `README.md` links to it from the Repository layout section). +- NEW `docs/rollout/rebuild.md` (playbook: prerequisites — durable `JAVA_HOME`, + `gh auth status` (no per-developer settings step: the commit allow ships in summit's + project settings); kickoff prompt with the session loop-grant wording; per-repo + procedure; sequential-build rule; cost notes). +- NEW `docs/rollout/rebuild-pr-body.md` (modeled on `proofread-pr-body.md`; lists the + standard commits: pull / pins / bump / reports / optional adapt). +- `.claude/settings.json`: same ask-rule removal; add `allow`: + `"Bash(git commit:*)"` (owner's decision: summit — where cascades execute — grants + protocol-governed autonomous commits; repo-rooted sessions elsewhere keep the org + default prompt), `"Bash(./cascade:*)"`, `"Bash(gh pr view:*)"`, + `"Bash(gh pr list:*)"`, `"Bash(gh run list:*)"`. +- `docs/project.md`: add `rollout/rebuild.md` to the Cross-repository workflows list. +- Per AGENTS.md, open `.agents/tasks/rebuild-cascade.md` task file when implementation + starts; delete on merge. + +Ordering: A and B are independent; C references the skill by name only. Attended +wave 1 can run from the C branch before it merges (script invoked by path). + +**Phase 2 (later, design only — PR D):** `repository_dispatch` step appended to +config's distributed `publish.yml` (needs an org PAT secret on consumers — open +prerequisite) + `summit/.github/workflows/cascade.yml` (dispatch + 30-min schedule +backstop) running `status`/`next` and invoking headless Claude only on exit 3. +Phase-1 choices that keep this cheap: per-wave branches, manifest on a pushed summit +branch, typed exit codes, `sed_inplace`, publishedness always re-proved from metadata. +Recorded phase-2 prerequisites: an org secret for the cross-repo dispatch AND a bot +identity for the workflow's pushes/`gh pr create` (cascade PRs must not impersonate a +person). Inherited risk: shared runner egress worsens Central throttling — revive the +on-hold throttling task's cache/proxy phases before unattended cloud waves. + +## Verification + +1. **Graph**: `cascade graph` output renders on GitHub, matches the known order + (base-libraries → logging/testlib/base-types/change/time → tool-base/ProtoTap/ + compiler → validation → core-jvm-compiler → core-jvm → jdbc-storage/gcloud-jvm/ + delivery-server; BuildSpeed verify-only); `graph --check` flags drift after a + hand-edited edge. +2. **Unit-level**: `plan --kind refresh` reads the graph and freezes edges; `plan + --kind ripple --seed base-libraries@` scopes to the downstream closure only. + `prep` twice on one repo (idempotent, three commits max). Pin-edit verification on + the multi-constant files (`Base.kt`, `ToolBase.kt`, `CoreJvmCompiler.kt`, + `Compiler.kt`). `status` after hand-reverting a commit (state honestly regresses). +3. **Dry wave**: a two-repo `--include base-libraries --include time` attended run: + eager builds land in `~/.m2`; `ship` blocks with exit 4 until base-libraries' + version appears in GCAR metadata; drift path exercised by a manual re-bump. +4. **Wave 1 (real)**: after the `config` `refresh-dependencies` PR merges — attended, + full default set. Every park and exit-3 becomes a hardening note in the playbook. + Then **wave 2**: the dedicated straggler catch-up (`--include reflect/money/ + testlib`, attended) — the M1 retarget's prerequisite. +5. **Before any `retarget` wave**: verify `CheckVersionIncrement` and + `version-bumped.sh` accept the jump (Maven ordering of `2.0.0-M1` vs + `2.0.0-SNAPSHOT.NNN` — a milestone qualifier may compare *lower* than the + snapshot line, tripping the increment guard); if rejected, that wave needs a + guard-bypass decision recorded in config first. Also confirm which registry + (`releases` vs `snapshots`) receives non-snapshot versions — the probe checks both. +6. Throughout: no task reported done without the manifest's derived proof (registry + metadata, PR state, sentinel). + +## Risks (mitigation baked in) + +pluginManagement gap → published-floor pins + `deferred` class · Central 429 → +sequential builds, halt-never-retry · straggler migrations → default-excluded, +adapt-parks when included · sentinel voided by trailing commits → fixed commit order, +`ship` re-verifies with exit-3 remedy · merged-but-unpublished upstream → `ship`'s +metadata gate · `build` chains publishToMavenLocal (compiler/core-jvm-compiler/time) +→ bump precedes any gradle run + self-gate · multi-constant pin files → role table + +anchored sed + verify-or-revert · sibling-PR reddening → one cascade PR per repo per +wave, sanctioned re-bump path · `ask: ./gradlew clean:*` could stall an agent-typed +clean build → cascade never types one (script runs builds; pre-pr uses incremental +scope for deps-only changes) · macOS sed → wrapper from day 1 · retarget version +ordering vs increment guards → pre-wave verification item (Verification §5) · stored +graph rots as repos evolve → `plan` re-verifies against fresh derivation every wave +and refuses to run on drift. + +## Plan + +- [ ] PR A (agents): `skills/cascade-adapt/SKILL.md` + `agents/openai.yaml` +- [ ] PR B (config): drop `ask: Bash(git commit:*)` from distributed + `.claude/settings.json`; add read-only `gh` allows +- [ ] PR C (summit): `cascade` script, `docs/dependency-graph.md`, + `docs/rollout/rebuild.md`, `docs/rollout/rebuild-pr-body.md`, + `.claude/commands/cascade.md`, settings fix, `docs/project.md` link +- [ ] Verification ladder (graph → unit-level → two-repo dry wave → real wave 1) +- [ ] Phase 2 design notes recorded (dispatch + summit workflow) — build later + +## Log + +- 2026-08-29 — Canonical order corrected per owner's catch: ProtoTap depends + on tool-base one-way (gradle-plugin module) yet preceded it. Added an + `audit_order` pass to `cascade graph`: every floor edge must close a genuine + cycle, else the order itself is flagged. The audit immediately found three + more one-way violations (time→ProtoTap, time→tool-base, money→validation); + order now: … change, tool-base, ProtoTap, time, compiler, validation, money, + core-jvm-compiler, … Audit silent; 19 remaining floors all close real + cycles. + +- 2026-08-29 — Derivation review round 3 (owner proved ProtoTap→compiler was a + copy-pasted force list, no `dependencies` declaration): version-management + references (`force`/`constraints`/`exclude`) now create **no edge**; the + classifier is block-aware (context stack — bare lines under + `dependencies { listOf(...) }` are real declarations, per compiler/api; + bare lines under `force(` are not). Convention-borne edges added + (`module-testing` → testlib test-dep; `kmp-module` → testlib main). + Net: 138 → 92 edges (47 force-only edges removed, `base-types → testlib` + added); removals spot-verified (reflect→logging, jdbc-storage→testlib, + time→reflect had zero declaration-context usages). Doc prose updated to + state the declared-only contract. + +- 2026-08-29 — Derivation review round 2 (owner's findings): plugin + applications are edges too — bare convention accessors (`prototap`, + `spineCompiler`, `coreJvmCompiler`) and `io.spine.*` plugin-id strings now + map to their provider repos (ProtoTap, compiler, core-jvm-compiler, + tool-base for artifact-meta/descriptor-set-file/generated-sources). + Owner ruling encoded: **ProtoTap is test-only by design** (scope override in + `apply_scope_rules`). Nested submodules of a scanned repo are excluded + (validation/docs/_time is the vendored time repo). Result: 138 edges; + validation/core-jvm-compiler/compiler --> ProtoTap restored (test section); + new true edges BuildSpeed --> core-jvm-compiler, jdbc-storage --> + core-jvm-compiler, base-libraries --> tool-base (mutual). + +- 2026-08-29 — Graph derivation upgraded per review: an edge now requires an + actual usage (imports alone proved nothing) and carries a scope class — + `main` (declared deps/plugins), `test` (test-only), `other` (force blocks, + excludes). The production diagram renders reduced main-scope edges only and + now matches the real Maven structure; test-only deps and the mutual pairs + sit in their own collapsed sections; mutual-pair arrows reversed per review + (published artifact → consumer). Machine list unchanged in format (132 + edges, all scopes — force pins still order a wave). + +- 2026-08-29 — Graph-doc review feedback applied: `dependency-graph.md` is now + a standalone document (no wave vocabulary; the rollout playbook references + it, and owns the wave semantics of forward vs reverse edges); arrows + reversed to depends-on direction (`compiler --> tool-base`, rendered + `flowchart BT` so foundations stay on top; machine block is + `dependent dependency`, script swaps on read); the cryptic "floor pins" + section replaced by a plain-language account of mutual dependencies + resolved by generation lag. + +- 2026-08-29 — Implementation findings (branch `cascade-machinery`): + * JDK **17**, not 21 (owner's correction; `config/.java-version` = 17.0.14). + Pre-existing `proofread-repo:43` still says 21 — flagged, not touched. + * The raw dependency graph is **cyclic** (self-hosting SDK); introduced the + canonical wave order + forward (wave-ordering) vs floor (dashed, + last-published) edge split in `docs/dependency-graph.md`. 132 edges: 94 + forward, 38 floor. One manual edge: core-jvm-compiler → core-jvm. + Per review feedback the rendered diagram is now the transitive reduction + of the forward edges (19 solid edges — the ordering skeleton); the full + 132-edge set stays canonical in a machine-readable ```edges block (each + edge is a direct pin the wave advances, so none can be dropped from the + data — only from the rendering). + * `delivery-server` publishes **only to GitHub Packages** (proprietary) — + probes gained a per-repo registry field; reading GH Packages needs the + `read:packages` token scope (playbook prerequisite; `plan` warns, ship + hard-requires). + * ProtoTap artifacts carry no `spine-` prefix and live in `releases` + (marker: `prototap-gradle-plugin`). + * Parallel builds: the ProjectBuilder/Central issue is retired by the + stub-repository fixture in core-jvm-compiler (owner's correction — the + Aug-21 "on hold" task note was stale). the initial driver stays sequential for + simplicity only; first refinement = a `make --jobs`-style concurrency + option for the `cascade` script running up to N graph-independent builds + at once (prereqs: manifest write-lock, daemon-RAM budget). 429 halt kept + as insurance. + * bash 3.2 portability landed (no assoc arrays, no `case` inside `$()`). + * `/cascade` command moved to PR A (shared commands symlink discovery). + +- 2026-08-29 — Orchestrator discussion: loop confirmed policy-free (script's `next` + is the single policy source; session and future workflow are thin executors); + phase-2 prerequisites recorded (dispatch secret + bot identity); cloud throttling + risk tied to the on-hold maven-central-throttling task. All four discussion areas + now covered. + +- 2026-08-29 — Wave kinds discussion: `retarget` declared bidirectional (release + + reopen-development); straggler catch-up recognized as a hard `retarget` + prerequisite; roadmap fixed (owner's decision): wave 1 refresh on healthy set → + wave 2 attended straggler catch-up → M1 retarget. Milestone-vs-snapshot guard + ordering stays a pre-retarget verification item. + +- 2026-08-29 — Autonomy discussion: `Bash(git commit:*)` allow lands in **summit's + project settings** (owner's decision) — cascade sessions commit dialog-free under + the skills/scripts protocol; other repos keep the org-default prompt once PR B + removes the distributed `ask` rule. Playbook loses the per-developer settings step. + +- 2026-08-29 — State-machine discussion: `pinned` now derived by pin-file content vs + the manifest vector; `built` bound to its recorded HEAD SHA (any commit demotes); + adapt→rebuild budget set to **three rounds** then auto-park (owner's decision); + BuildSpeed flagged: generated settings may defeat `apply_pins` — expect park in + wave 1. + +- 2026-08-29 — Design drafted from two exploration passes + Plan-agent synthesis. + Wave kinds generalized (`refresh` / `ripple` / `retarget`) per review feedback; + dependency graph promoted to maintained documentation (`docs/dependency-graph.md`). + Script renamed `cascade-repo` → `cascade` (wave-level subcommands dominate). + Status: draft — under discussion, implementation not started. diff --git a/README.md b/README.md index 162016e..15cc421 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,10 @@ pinned commits of the submodules below, plus the shared agent tooling under `.ag ## Repository layout The submodules are pinned to fixed commits, giving a reproducible snapshot of the -SDK. They are grouped below by function, not by strict dependency order. +SDK. They are grouped below by function, not by strict dependency order. The +build-order contract between them — including which dependency edges +sequence a cross-repository wave — lives in +[`docs/dependency-graph.md`](docs/dependency-graph.md). ### Shared infrastructure diff --git a/cascade b/cascade new file mode 100755 index 0000000..42baceb --- /dev/null +++ b/cascade @@ -0,0 +1,1440 @@ +#!/usr/bin/env bash +# +# cascade — deterministic mechanics for cross-repository waves across the +# Spine SDK submodules of this superproject. +# +# A *wave* propagates a change through the repos in dependency order: per repo — +# `./config/pull`, refresh local dependency pins, bump/assign the version, build, +# publish to mavenLocal, and open a PR once every pinned upstream version is +# provably published in the public registry. Three wave kinds: +# +# refresh — `config` master advanced; every repo rebuilds on the new baseline. +# ripple — one upstream repo published a new version; its downstream +# transitive closure rebuilds. +# retarget — a coordinated version assignment (e.g. `2.0.0-M1`) across all repos. +# +# Owns only what must NOT be re-interpreted each run (branching, pin edits, the +# version fast path, builds, gates, PR mechanics, state derivation). Judgement +# steps — fixing build breakage (`cascade-adapt` skill), the pre-PR review +# (`pre-pr` skill), and the bump edge cases (`bump-version` skill) — are +# agent-driven and requested via exit code 3. See docs/rollout/rebuild.md. +# +# Exit codes (uniform): +# 0 success or idempotent no-op +# 1 hard environment/usage-independent error +# 2 usage +# 3 agent required — the message names the skill and the resume command +# 4 waiting on an external event (upstream unpublished, await timeout) +# 5 wave halt — registry throttling detected; do NOT retry builds +# +# Re-runnable: state is derived from git, GitHub, the registry, and ~/.m2 — +# never from memory — so any interrupted step resumes cleanly when re-invoked. +# +set -euo pipefail + +SUMMIT="$(cd "$(dirname "$0")" && pwd)" +BASE="master" +ORG="SpineEventEngine" +TASKS_DIR="$SUMMIT/.agents/tasks" +GRAPH_FILE="$SUMMIT/docs/dependency-graph.md" +REGISTRY="https://europe-maven.pkg.dev/spine-event-engine" +M2="${HOME}/.m2/repository" +ADAPT_BUDGET=3 + +note() { echo "-- $*"; } +warn() { echo "!! $*" >&2; } +die() { warn "$*"; exit 1; } + +command -v jq >/dev/null 2>&1 || die "jq is required" +command -v gh >/dev/null 2>&1 || die "gh is required" + +# BSD (macOS) and GNU sed disagree on -i; wrap once so Linux runners need no fork. +sed_inplace() { + if sed --version >/dev/null 2>&1; then sed -i "$@"; else sed -i '' "$@"; fi +} + +# --------------------------------------------------------------------------- +# Static knowledge. Everything here is verified at `plan` time (probe markers +# against the registry; graph vs fresh derivation), so a wrong entry fails +# loudly on the first run instead of silently mis-ordering a wave. +# --------------------------------------------------------------------------- + +# Candidate repos in CANONICAL WAVE ORDER. The raw dependency graph is cyclic — +# the SDK is self-hosting (base-libraries consumes logging and the compiler, +# which consume base-libraries) — so a plain toposort cannot order a wave. +# This recorded order is the decision that cuts the cycles: within a wave a repo +# re-pins the wave versions of upstreams EARLIER in this order (forward edges); +# anything later stays at its last-published (floor) version and catches up in +# the next wave — exactly the generation-lag pattern the repos already live by. +# Changing this order is a reviewed change to docs/dependency-graph.md. +repos_all() { + echo "base-libraries reflect logging testlib base-types change tool-base \ +ProtoTap time compiler validation money core-jvm-compiler core-jvm \ +jdbc-storage gcloud-jvm delivery-server BuildSpeed" +} + +# Position of a repo in the canonical order (1-based; 0 = unknown). +order_pos() { + local i=1 r + for r in $(repos_all); do + [ "$r" = "$1" ] && { echo "$i"; return; } + i=$((i + 1)) + done + echo 0 +} + +# Manual edges the file-grep cannot see (dependencies wired through +# config-distributed convention scripts rather than repo-owned build files). +extra_edges() { + echo "core-jvm-compiler core-jvm" +} + +# Default exclusions, with reasons surfaced by `plan`. +excluded_reason() { + case "$1" in + documentation) echo "Spine 1.x; not part of the SDK build line" ;; + reflect|money|testlib) + echo "straggler: config submodule generations behind — first pull is a migration (opt in via --include)" ;; + *) echo "" ;; + esac +} + +# io.spine.dependency.local object -> owning repo. `Spine` carries only group +# constants and maps to nothing. +obj_to_repo() { + case "$1" in + Base) echo base-libraries ;; + BaseTypes) echo base-types ;; + Change) echo change ;; + Compiler) echo compiler ;; + CoreJvm) echo core-jvm ;; + CoreJvmCompiler) echo core-jvm-compiler ;; + Logging) echo logging ;; + ProtoTap) echo ProtoTap ;; + Reflect) echo reflect ;; + TestLib) echo testlib ;; + Time) echo time ;; + ToolBase) echo tool-base ;; + Validation) echo validation ;; + *) echo "" ;; + esac +} + +# repo -> pin file (relative to a consumer's buildSrc local/ dir). +repo_pin_file() { + case "$1" in + base-libraries) echo Base.kt ;; + base-types) echo BaseTypes.kt ;; + change) echo Change.kt ;; + compiler) echo Compiler.kt ;; + core-jvm) echo CoreJvm.kt ;; + core-jvm-compiler) echo CoreJvmCompiler.kt ;; + logging) echo Logging.kt ;; + ProtoTap) echo ProtoTap.kt ;; + reflect) echo Reflect.kt ;; + testlib) echo TestLib.kt ;; + time) echo Time.kt ;; + tool-base) echo ToolBase.kt ;; + validation) echo Validation.kt ;; + *) echo "" ;; + esac +} + +# Constants that TRACK the wave (advance to the upstream's wave version). +# Everything not listed here stays put during a wave; the published-floor +# constants (versionForBuildScript, dogfoodingVersion, fallbackDfVersion) feed +# buildscript/pluginManagement classpaths that cannot see mavenLocal and are +# advanced only by `close`, when the wave's versions are provably published. +repo_track_constants() { + case "$1" in + compiler) echo "fallbackVersion" ;; + *) echo "version" ;; + esac +} + +# repo -> "group-path|marker-artifact|version-property|class|registry" +# marker-artifact: one definitely-published artifact used for registry probes. +# class: library (full pipeline) | verify-only (no version/publishing steps). +# registry: gcar (public europe-maven.pkg.dev) | github (GitHub Packages — +# delivery-server is proprietary and publishes only there; probes authenticate +# with `gh auth token`). +repo_meta() { + case "$1" in + base-libraries) echo "io/spine|spine-base|versionToPublish|library|gcar" ;; + base-types) echo "io/spine|spine-base-types|versionToPublish|library|gcar" ;; + change) echo "io/spine|spine-change|versionToPublish|library|gcar" ;; + time) echo "io/spine|spine-time|versionToPublish|library|gcar" ;; + money) echo "io/spine|spine-money|versionToPublish|library|gcar" ;; + reflect) echo "io/spine|spine-reflect|versionToPublish|library|gcar" ;; + logging) echo "io/spine|spine-logging|versionToPublish|library|gcar" ;; + testlib) echo "io/spine/tools|spine-testlib|versionToPublish|library|gcar" ;; + tool-base) echo "io/spine/tools|spine-tool-base|versionToPublish|library|gcar" ;; + ProtoTap) echo "io/spine/tools|prototap-gradle-plugin|versionToPublish|library|gcar" ;; + compiler) echo "io/spine/tools|compiler-jvm|compilerVersion|library|gcar" ;; + validation) echo "io/spine/tools|spine-validation-java|validationVersion|library|gcar" ;; + core-jvm-compiler) echo "io/spine/tools|core-jvm-gradle-plugin|coreJvmCompilerVersion|library|gcar" ;; + core-jvm) echo "io/spine|spine-core|versionToPublish|library|gcar" ;; + jdbc-storage) echo "io/spine|spine-rdbms|versionToPublish|library|gcar" ;; + gcloud-jvm) echo "io/spine/gcloud|spine-datastore|versionToPublish|library|gcar" ;; + delivery-server) echo "io/spine/delivery|spine-delivery-server|versionToPublish|library|github" ;; + BuildSpeed) echo "|||verify-only|" ;; + *) echo "" ;; + esac +} +meta_group() { repo_meta "$1" | cut -d'|' -f1; } +meta_marker() { repo_meta "$1" | cut -d'|' -f2; } +meta_vprop() { repo_meta "$1" | cut -d'|' -f3; } +meta_class() { repo_meta "$1" | cut -d'|' -f4; } +meta_reg() { repo_meta "$1" | cut -d'|' -f5; } + +# --------------------------------------------------------------------------- +# Shared helpers +# --------------------------------------------------------------------------- + +require_submodule() { + git -C "$SUMMIT/$1" rev-parse --is-inside-work-tree >/dev/null 2>&1 \ + || die "no initialised submodule at $1 (try: git submodule update --init $1)" +} + +require_java() { + export JAVA_HOME="${JAVA_HOME:-$(/usr/libexec/java_home -v 17 2>/dev/null || true)}" + [ -n "${JAVA_HOME:-}" ] || die "JAVA_HOME unset and no JDK 17 found" +} + +# Version literal fed to the given extra property, both spellings: +# extra.set("prop", "X") | val prop ... by extra("X") +version_of() { # [] -> current version literal (empty if none) + local dir="$SUMMIT/$1" prop; prop="$(meta_vprop "$1")" + local content + if [ -n "${2:-}" ]; then + content="$(git -C "$dir" show "$2:version.gradle.kts" 2>/dev/null || true)" + else + content="$(cat "$dir/version.gradle.kts" 2>/dev/null || true)" + fi + printf '%s\n' "$content" \ + | sed -nE "s/.*${prop}\"?,?[[:space:]]*\"?([0-9A-Za-z.-]+)\".*/\1/p; s/.*by[[:space:]]+extra\(\"([0-9A-Za-z.-]+)\"\).*/\1/p" \ + | head -1 +} + +# All metadata XML the repo's registry serves for its marker artifact. +marker_metadata() { # + local gp mk r + gp="$(meta_group "$1")"; mk="$(meta_marker "$1")" + [ -n "$gp" ] || return 0 + case "$(meta_reg "$1")" in + github) + # Requires a token with read:packages; a 401 is an auth gap, not a + # missing artifact — surfaced separately by gh_packages_code(). + curl -sf --max-time 20 -u "x:$(gh auth token)" \ + "https://maven.pkg.github.com/$ORG/$1/$gp/$mk/maven-metadata.xml" 2>/dev/null || true ;; + *) + for r in snapshots releases; do + curl -sf --max-time 20 "$REGISTRY/$r/$gp/$mk/maven-metadata.xml" 2>/dev/null || true + done ;; + esac +} + +# Does the repo's registry serve of its marker artifact? +registry_has() { # + marker_metadata "$1" | grep -q "$2" +} + +# HTTP code GitHub Packages returns for the repo's marker (github registry only). +gh_packages_code() { # + curl -s -o /dev/null -w '%{http_code}' --max-time 20 -u "x:$(gh auth token)" \ + "https://maven.pkg.github.com/$ORG/$1/$(meta_group "$1")/$(meta_marker "$1")/maven-metadata.xml" 2>/dev/null +} + +# Marker validation for `plan`: 0 = ok, 1 = marker wrong, 2 = auth gap (github +# registry, token without read:packages — probes will work once the scope is +# added; ship/close hard-require it). +registry_marker_ok() { # + [ -n "$(meta_group "$1")" ] || return 0 # verify-only repos carry no marker + [ -n "$(marker_metadata "$1")" ] && return 0 + if [ "$(meta_reg "$1")" = github ] && [ "$(gh_packages_code "$1")" = 401 ]; then + return 2 + fi + return 1 +} + +# --------------------------------------------------------------------------- +# graph — derive/verify docs/dependency-graph.md +# --------------------------------------------------------------------------- + +# Emit "upstream downstream scope" triples derived from consumer-owned build +# files. An edge requires an actual USAGE of the dependency object (a bare +# import proves nothing and stale imports do occur). Scope classes: +# main — declared dependency or applied plugin (api/implementation/...) +# test — test-only declarations (testImplementation/testFixtures*/...) +# References inside version-management constructs (force/constraints/exclude) +# create no edge at all — see the classifier below. +# Every repo's buildSrc *contains* all local objects post-pull, so only files +# the repo itself owns are scanned. +derive_scoped() { + local repo files f objs o up usages subpaths sp skip + for repo in $(repos_all); do + [ -d "$SUMMIT/$repo" ] || continue + # The repo's own nested submodules are other repositories' content + # (validation/docs/_time is the vendored `time` repo) — never scan them. + subpaths="$(git config -f "$SUMMIT/$repo/.gitmodules" --get-regexp '\.path$' 2>/dev/null | awk '{print $2}')" + files="" + for f in "$SUMMIT/$repo"/build.gradle.kts \ + "$SUMMIT/$repo"/*/build.gradle.kts \ + "$SUMMIT/$repo"/*/*/build.gradle.kts \ + "$SUMMIT/$repo"/settings.gradle.kts \ + "$SUMMIT/$repo"/buildSrc/src/main/kotlin/module.gradle.kts; do + case "$f" in + */config/*|*/buildSrc/*/build.gradle.kts) continue ;; + esac + [ -f "$f" ] || continue + skip=0 + for sp in $subpaths; do + case "$f" in "$SUMMIT/$repo/$sp"/*) skip=1 ;; esac + done + [ "$skip" = 1 ] || files="$files $f" + done + [ -n "$files" ] || continue + # shellcheck disable=SC2086 + objs="$(grep -hoE 'io\.spine\.dependency\.local\.[A-Z][A-Za-z]+' $files 2>/dev/null \ + | sed 's/.*\.//' | sort -u)" + # Block-aware classification: a reference creates an edge only where + # it is a declared dependency. References inside version-management + # constructs — force(...), constraints {...}, exclude(...) — create NO + # edge: those lists are routinely copy-pasted between repositories and + # pin versions of artifacts that may not occur in this project at all + # (proven by ProtoTap, whose force list names the Compiler while its + # dependencies blocks never do). Bare `Obj.member,` lines inside a + # `dependencies { listOf(...) }` are real declarations (see + # compiler/api) and default to main scope. + for o in $objs; do + up="$(obj_to_repo "$o")" + [ -n "$up" ] && [ "$up" != "$repo" ] || continue + # shellcheck disable=SC2086 + scopes="$(awk -v obj="$o" ' + function push(tag) { sp++; tags[sp] = tag; depths[sp] = depth } + { + line = $0 + if (line ~ /^[[:space:]]*(import|\/\/|\*)/) { updepth(); next } + use = match(line, "(^|[^.A-Za-z])" obj "\\.[A-Za-z]") + if (use) { + if (line ~ /(^|[^A-Za-z])test[A-Za-z]*\(/) print "test" + else if (line ~ /(^|[^A-Za-z])(api|implementation|compileOnly|compileOnlyApi|runtimeOnly|annotationProcessor|classpath|kapt|ksp)\(/) print "main" + else if (line ~ /(^|[^A-Za-z])(force|exclude|constraints|forceArtifacts)[[:space:]]*[({]/) { } + else if (sp > 0 && tags[sp] == "vm") { } + else if (sp > 0 && tags[sp] == "deps") print "main" + # any other context (dokka config, interpolation, ...) -> no edge + } + # context openers seen on this line, at current depth + if (line ~ /(^|[^A-Za-z])(force|constraints|forceArtifacts|exclude)[[:space:]]*[({]/) push("vm") + else if (line ~ /(^|[^A-Za-z])dependencies[[:space:]]*\{/) push("deps") + updepth() + } + function updepth( i, c) { + for (i = 1; i <= length($0); i++) { + c = substr($0, i, 1) + if (c == "{" || c == "(") depth++ + if (c == "}" || c == ")") { + depth-- + while (sp > 0 && depth <= depths[sp]) sp-- + } + } + }' $files 2>/dev/null | sort -u)" + case "$scopes" in + *main*) echo "$up $repo main" ;; + *test*) echo "$up $repo test" ;; + esac + done + # Plugin applications: bare convention accessors (`prototap` alone in a + # plugins block) and plugin-id strings — dependencies the object-deref + # grep above cannot see. Provider mapping: + # prototap / io.spine.prototap -> ProtoTap + # spineCompiler / io.spine.compiler -> compiler + # coreJvmCompiler / io.spine.core-jvm -> core-jvm-compiler + # io.spine.{artifact-meta,descriptor-set-file, + # generated-sources} -> tool-base + # shellcheck disable=SC2086 + { + grep -hE '^[[:space:]]*(prototap|spineCompiler|coreJvmCompiler)[[:space:]]*$' $files 2>/dev/null \ + | sed 's/[[:space:]]//g' | sort -u \ + | sed 's/^prototap$/ProtoTap/; s/^spineCompiler$/compiler/; s/^coreJvmCompiler$/core-jvm-compiler/' + grep -hoE '"io\.spine\.(prototap|compiler|core-jvm|artifact-meta|descriptor-set-file|generated-sources)"' $files 2>/dev/null \ + | tr -d '"' | sort -u \ + | sed 's/^io\.spine\.prototap$/ProtoTap/; s/^io\.spine\.compiler$/compiler/; s/^io\.spine\.core-jvm$/core-jvm-compiler/; s/^io\.spine\..*$/tool-base/' + } | sort -u | while read -r up; do + [ -n "$up" ] && [ "$up" != "$repo" ] && echo "$up $repo main" + done + # shellcheck disable=SC2086 + if [ "$repo" != testlib ]; then + { grep -qE '^[[:space:]]*`module-testing`[[:space:]]*$' $files 2>/dev/null \ + && echo "testlib $repo test"; } || true + { grep -qE '^[[:space:]]*`kmp-module`[[:space:]]*$' $files 2>/dev/null \ + && echo "testlib $repo main"; } || true + fi + done | { cat; extra_edges | sed 's/$/ main/'; } | apply_scope_rules +} + +# Owner-decided scope overrides, then collapse duplicate (up, down) pairs to +# their strongest scope (main > test > other) so each edge renders exactly once. +# ProtoTap is test-only BY DECISION: it taps `protoc` output in tests and is +# never a production dependency of any repository. +apply_scope_rules() { + awk ' + $1 == "ProtoTap" { $3 = "test" } + { + rank = ($3 == "main") ? 3 : ($3 == "test") ? 2 : 1 + key = $1 " " $2 + if (rank > best[key]) { best[key] = rank; scope[key] = $3 } + } + END { for (k in best) print k " " scope[k] }' | sort +} + +# The plain relation (all scopes) — what the machine list stores and waves use. +derive_edges() { derive_scoped | awk '{print $1 " " $2}' | sort -u; } + +# Split "up down" pairs on stdin into build-ordering (forward) or +# previously-published (floor) edges against the canonical order. +forward_edges() { while read -r a b; do [ "$(order_pos "$a")" -lt "$(order_pos "$b")" ] && echo "$a $b"; done || true; } +floor_edges() { while read -r a b; do [ "$(order_pos "$a")" -ge "$(order_pos "$b")" ] && echo "$a $b"; done || true; } + +# Transitive reduction of the forward-edge DAG on stdin ("a b" lines): +# drop every edge implied by a longer path. Rendering-only — the FULL edge set +# stays canonical (each edge is a direct pin the wave must advance). +reduce_edges() { + awk ' + { e[$1 SUBSEP $2] = 1; seen[$1] = 1; seen[$2] = 1; lines[NR] = $0 } + END { + for (x in seen) nodes[++N] = x + for (i = 1; i <= N; i++) for (j = 1; j <= N; j++) + r[nodes[i] SUBSEP nodes[j]] = e[nodes[i] SUBSEP nodes[j]] + for (k = 1; k <= N; k++) for (i = 1; i <= N; i++) for (j = 1; j <= N; j++) + if (r[nodes[i] SUBSEP nodes[k]] && r[nodes[k] SUBSEP nodes[j]]) + r[nodes[i] SUBSEP nodes[j]] = 1 + for (l = 1; l <= NR; l++) { + split(lines[l], p, " "); a = p[1]; b = p[2]; red = 0 + for (k = 1; k <= N; k++) { + m = nodes[k] + if (m != a && m != b && e[a SUBSEP m] && r[m SUBSEP b]) { red = 1; break } + } + if (!red) print a " " b + } + }' +} + +# stdin lines filtered to members of the space-separated set in $1. +# (Kept out of command substitutions: bash 3.2 cannot parse `case` inside $().) +filter_in_set() { + local set=" $1 " x + while read -r x; do + [ -n "$x" ] || continue + printf '%s' "$set" | grep -qF " $x " && echo "$x" + done + return 0 +} +floor_edges() { while read -r a b; do [ "$(order_pos "$a")" -ge "$(order_pos "$b")" ] && echo "$a $b"; done || true; } + +# Edge pairs currently stored in the graph doc — the canonical machine list in +# the ```edges fenced block (the mermaid diagram is its transitive reduction, +# rendering only). +stored_edges() { + [ -f "$GRAPH_FILE" ] || return 0 + sed -n '/^```edges$/,/^```$/p' "$GRAPH_FILE" | grep -E '^[A-Za-z0-9-]+ [A-Za-z0-9-]+$' \ + | awk '{print $2 " " $1}' | sort -u || true +} + +write_graph() { + local scoped tmp="$GRAPH_FILE.tmp" repo cls line i + scoped="$(derive_scoped)" + mkdir -p "$(dirname "$GRAPH_FILE")" + { + cat <<'EOF' +# Spine SDK dependency graph + +How the Spine SDK repositories depend on each other, which of them must be +built before which, and the artifact coordinates each one publishes. In the +dependency diagrams arrows point **at the dependency**: `compiler --> tool-base` +reads "`compiler` depends on artifacts published by `tool-base`". The +mutual-pair section reverses the direction — its arrows show already-published +artifacts flowing into their consumers — and says so in place. + +## Mutual dependencies and how the SDK stays buildable + +The raw dependency relation is **cyclic**, because the SDK is self-hosting: +`logging` depends on `base-libraries` (its API uses `spine-base` types), while +`base-libraries` also depends on `logging` (its code writes logs); the same +holds for `ProtoTap` and the `compiler`. Taken literally, neither member of +such a pair could ever be built first. + +The cycles resolve because the two directions of a mutual pair are satisfied +by **different generations of artifacts**. One direction — from the repository +*later* in the canonical order below to the one *earlier* — behaves as a +normal build-order dependency on current artifacts. The reverse direction is +satisfied by a version **already published** to the registry, built against an +older generation, so it never constrains what must be built before what. On +each successive release the two sides catch up alternately, staying about one +generation apart. + +## Canonical build order + +One-way dependencies constrain this order absolutely, and regeneration audits +that (a repository is never placed before one it depends on unless the pair is +genuinely mutual). Which member of a **mutual** pair counts as "earlier" is a +convention, not a derivable fact — so the order is recorded here, as reviewed +documentation: + +EOF + i=1 + for repo in $(repos_all); do echo "$i. \`$repo\`"; i=$((i + 1)); done + cat <<'EOF' + +## Production dependencies (transitive reduction) + +Declared production-scope dependencies between the repositories — the +structure of the published artifacts. For readability the diagram omits edges +implied by longer paths: `c --> b --> a` means `c` also depends on `a` +directly. Arrows point at the dependency. The complete relation (production +and test scope) is the machine-readable list at the bottom of this file. +References that merely manage versions — `force(...)`, `constraints`, +`exclude` — are not dependencies and create no edge. + +```mermaid +flowchart BT +EOF + printf '%s\n' "$scoped" | awk '$3 == "main" {print $1 " " $2}' \ + | forward_edges | reduce_edges | sort | while read -r line; do + [ -n "$line" ] && echo " ${line#* } --> ${line% *}" + done + cat <<'EOF' +``` + +
+Test-only dependencies (used by the repository's tests, not by its +published artifacts). `ProtoTap` is test-only by design — it taps `protoc` +output in tests — so every edge pointing at it lives here or among the mutual +pairs, never in the production diagram. + +```mermaid +flowchart BT +EOF + printf '%s\n' "$scoped" | awk '$3 == "test" {print $1 " " $2}' \ + | forward_edges | sort | while read -r line; do + [ -n "$line" ] && echo " ${line#* } --> ${line% *}" + done + cat <<'EOF' +``` + +
+ +
+Reverse directions of the mutual dependencies — satisfied by +previously published versions, so they impose no build order. The arrow leads +from the already-published artifact to the repository consuming it. + +```mermaid +flowchart BT +EOF + printf '%s\n' "$scoped" | awk '{print $1 " " $2}' | sort -u \ + | floor_edges | sort | while read -r line; do + [ -n "$line" ] && echo " ${line% *} -.-> ${line#* }" + done + cat <<'EOF' +``` + +
+ +## Repository metadata + +Class `library` publishes Maven artifacts; `verify-only` builds but publishes +nothing. The marker is one published artifact whose registry metadata answers +"is version X of this repository published?"; the version property is the name +of the literal in the repository's `version.gradle.kts`. + +| Repository | Class | Registry : marker | Version property | +|---|---|---|---| +EOF + for repo in $(repos_all); do + cls="$(meta_class "$repo")" + if [ "$cls" = "verify-only" ]; then + echo "| \`$repo\` | $cls | — | — |" + else + echo "| \`$repo\` | $cls | $(meta_reg "$repo") : \`$(meta_group "$repo" | tr '/' '.'):$(meta_marker "$repo")\` | \`$(meta_vprop "$repo")\` |" + fi + done + cat <<'EOF' + +`documentation` is not on the graph: it is a Spine 1.x site project, not part +of the SDK build line. + +## Maintenance + +The relation is derived from `import io.spine.dependency.local.*` usages in +each repository's own build files (never the config-distributed `buildSrc/`, +whose files exist in every repository regardless of use), plus the manual +edges kept in `extra_edges()` in [`../cascade`](../cascade) for dependencies +wired through convention scripts. Regenerate after a topology change with: + +```bash +./cascade graph +``` + +Cross-repository automation ([`rollout/rebuild.md`](rollout/rebuild.md)) +consumes this file as its ordering contract and verifies it against a fresh +derivation before acting, so changes — including changes to the canonical +order — must land here, reviewed, first. + +## Complete dependency list (machine-readable) + +Every direct **declared** dependency (production and test scope), one +`dependent dependency` pair per line — the left repository depends on +artifacts published by the right one. Both directions of the mutual pairs are +included; version-management references are excluded; the diagrams above are +renderings of this list. + +```edges +EOF + printf '%s\n' "$scoped" | awk '{print $2 " " $1}' | sort -u + echo '```' + } > "$tmp" + mv "$tmp" "$GRAPH_FILE" +} + +# Warn about floor edges that do NOT close a cycle: when consumer C depends on +# provider P, P is later in the canonical order, and P has no (transitive) +# dependency back on C, the pair is a plain one-way dependency and the order +# itself should change — C belongs after P. (This audit is what catches a +# mis-seeded canonical order; it found ProtoTap being placed before tool-base.) +audit_order() { # $1: full "up down" relation + local rel="$1" + { + printf '%s\n' "$rel" | sed 's/^/R /' + printf '%s\n' "$rel" | floor_edges | sed 's/^/F /' + } | awk ' + $1 == "R" { dep[$3 SUBSEP $2] = 1; seen[$2] = 1; seen[$3] = 1; next } + $1 == "F" { M++; fp[M] = $2; fc[M] = $3 } + END { + for (x in seen) nodes[++N] = x + for (i = 1; i <= N; i++) for (j = 1; j <= N; j++) + r[nodes[i] SUBSEP nodes[j]] = dep[nodes[i] SUBSEP nodes[j]] + for (k = 1; k <= N; k++) for (i = 1; i <= N; i++) for (j = 1; j <= N; j++) + if (r[nodes[i] SUBSEP nodes[k]] && r[nodes[k] SUBSEP nodes[j]]) + r[nodes[i] SUBSEP nodes[j]] = 1 + for (m = 1; m <= M; m++) + if (!r[fp[m] SUBSEP fc[m]]) + printf "!! order: %s depends on %s one-way — consider moving %s after %s\n", fc[m], fp[m], fc[m], fp[m] + }' >&2 +} + +cmd_graph() { + local check=0 + [ "${1:-}" = "--check" ] && check=1 + local fresh stored + fresh="$(derive_edges)" + stored="$(stored_edges)" + if [ "$check" = 1 ]; then + [ -f "$GRAPH_FILE" ] || { warn "no $GRAPH_FILE — run: ./cascade graph"; exit 3; } + if [ "$fresh" != "$stored" ]; then + warn "dependency graph drift — stored file disagrees with fresh derivation:" + diff <(printf '%s\n' "$stored") <(printf '%s\n' "$fresh") | sed 's/^/ /' >&2 || true + warn "review + commit the update: ./cascade graph (then re-run your command)" + exit 3 + fi + note "dependency graph is current ($(printf '%s\n' "$fresh" | grep -c . ) edges)" + audit_order "$fresh" + return 0 + fi + write_graph + note "wrote $GRAPH_FILE ($(printf '%s\n' "$fresh" | grep -c .) edges) — review, then commit via the normal docs process" + audit_order "$fresh" +} + +# --------------------------------------------------------------------------- +# Manifest access +# --------------------------------------------------------------------------- + +manifest_path() { echo "$TASKS_DIR/$1.json"; } + +# Resolve the active wave: explicit $CASCADE_WAVE / --wave beats a lone manifest. +active_wave() { + if [ -n "${CASCADE_WAVE:-}" ]; then echo "$CASCADE_WAVE"; return 0; fi + local m n + m="$(ls "$TASKS_DIR"/cascade-*.json 2>/dev/null || true)" + n="$(printf '%s\n' "$m" | grep -c . || true)" + [ "$n" = 1 ] || die "cannot infer the wave: found $n manifests — pass --wave or set CASCADE_WAVE" + basename "$m" .json +} + +# mget [jq-args...] — the program is always the LAST argument. +mget() { + local wave="$1"; shift + local args=("$@") n prog + n=${#args[@]} + prog="${args[$((n - 1))]}" + unset "args[$((n - 1))]" + jq -r ${args[@]+"${args[@]}"} "$prog" "$(manifest_path "$wave")" +} +mset() { # [--arg k v ...] + local wave="$1" prog="$2"; shift 2 + local p tmp; p="$(manifest_path "$wave")"; tmp="$p.tmp" + jq "$@" "$prog" "$p" > "$tmp" && mv "$tmp" "$p" +} + +wave_repos() { mget "$1" '.repos | keys[]'; } + +upstreams_of() { # -> in-wave upstreams (space list) + mget "$1" --arg r "$2" '.repos[$r].upstreams[]?' 2>/dev/null | tr '\n' ' ' | sed 's/ $//' +} + +# --------------------------------------------------------------------------- +# plan — freeze a wave +# --------------------------------------------------------------------------- + +usage_plan() { + cat <[@]] (ripple) + [--target ] (retarget) + [--include ]... [--exclude ]... +EOF + exit 2 +} + +cmd_plan() { + local kind="" seed="" seed_ver="" target="" includes="" excludes="" + while [ $# -gt 0 ]; do + case "$1" in + --kind) kind="$2"; shift 2 ;; + --seed) seed="${2%%@*}"; case "$2" in *@*) seed_ver="${2#*@}" ;; esac; shift 2 ;; + --target) target="$2"; shift 2 ;; + --include) includes="$includes $2"; shift 2 ;; + --exclude) excludes="$excludes $2"; shift 2 ;; + *) usage_plan ;; + esac + done + case "$kind" in refresh|ripple|retarget) ;; *) usage_plan ;; esac + [ "$kind" = ripple ] && [ -z "$seed" ] && usage_plan + [ "$kind" = retarget ] && [ -z "$target" ] && usage_plan + + "$0" graph --check + + # Trigger + slug. + local cfg_sha slug + git -C "$SUMMIT/config" fetch --quiet origin "$BASE" 2>/dev/null || true + cfg_sha="$(git -C "$SUMMIT/config" rev-parse "origin/$BASE" 2>/dev/null \ + || git -C "$SUMMIT/config" rev-parse HEAD)" + case "$kind" in + refresh) slug="cascade-refresh-$(echo "$cfg_sha" | cut -c1-7)" ;; + ripple) slug="cascade-ripple-$seed-${seed_ver##*.}" ;; + retarget) slug="cascade-retarget-$target" ;; + esac + + # Scope. Only forward (wave-ordering) edges scope and order a wave; floor + # edges are consumed at last-published versions and impose nothing. + local edges fwd scope repo + edges="$(stored_edges)" + fwd="$(printf '%s\n' "$edges" | forward_edges)" + case "$kind" in + ripple) + # Downstream transitive closure of the seed (seed included). + scope="$(printf '%s\n' "$fwd" | awk -v s="$seed" ' + { down[$1] = down[$1] " " $2 } + END { + q = s; seen[s] = 1 + while (q != "") { + n = split(q, a, " "); q = "" + for (i = 1; i <= n; i++) { + m = split(down[a[i]], b, " ") + for (j = 1; j <= m; j++) + if (b[j] != "" && !seen[b[j]]) { seen[b[j]] = 1; q = q " " b[j] } + } + } + for (r in seen) print r + }')" + ;; + *) scope="$(repos_all | tr ' ' '\n')" ;; + esac + + # Exclusions: defaults minus --include, plus --exclude. + local final="" excluded_json="{}" reason + for repo in $scope; do + reason="$(excluded_reason "$repo")" + case " $includes " in *" $repo "*) reason="" ;; esac + case " $excludes " in *" $repo "*) reason="excluded by --exclude" ;; esac + if [ -n "$reason" ]; then + excluded_json="$(printf '%s' "$excluded_json" | jq --arg r "$repo" --arg why "$reason" '. + {($r): $why}')" + else + final="$final $repo" + fi + done + + # Validate probe markers once, loudly. Auth gaps (GitHub Packages without + # read:packages) warn instead of dying — the scope must exist before ship. + local bad="" noauth="" rc + for repo in $final; do + rc=0; registry_marker_ok "$repo" || rc=$? + case "$rc" in + 1) bad="$bad $repo" ;; + 2) noauth="$noauth $repo" ;; + esac + done + [ -z "$bad" ] || die "probe marker not found in the registry for:$bad — fix repo_meta() in this script first" + [ -z "$noauth" ] || warn "GitHub Packages probe unauthorized for:$noauth — before shipping those repos run: gh auth refresh -h github.com -s read:packages" + + # Carryover: an open cascade-* PR from an earlier wave keeps the repo out. + local carry="" + for repo in $final; do + [ "$(meta_class "$repo")" = "verify-only" ] && continue + if gh pr list -R "$ORG/$repo" --state open --json headRefName \ + --jq '.[].headRefName' 2>/dev/null | grep -q '^cascade-'; then + carry="$carry $repo" + fi + done + if [ -n "$carry" ]; then + warn "open cascade PRs already exist in:$carry" + warn "close or merge them first, or exclude those repos (--exclude); superseding a reviewed PR is a human decision." + exit 1 + fi + + # Wave order = the canonical order (which is what makes the forward-edge + # set acyclic by construction). Sanity-check that invariant anyway. + local order + order="$(repos_all | tr ' ' '\n')" + printf '%s\n' "$fwd" | tsort >/dev/null 2>&1 \ + || { warn "forward edges are not acyclic — canonical order and derivation disagree; fix docs/dependency-graph.md first"; exit 3; } + + # Manifest. + mkdir -p "$TASKS_DIR" + local repos_json="{}" ups gp mk vp cls tv + for repo in $final; do + ups="$(printf '%s\n' "$fwd" | awk -v r="$repo" '$2==r{print $1}' \ + | filter_in_set "$final" | jq -R . | jq -s 'map(select(length > 0))')" + gp="$(meta_group "$repo")"; mk="$(meta_marker "$repo")" + vp="$(meta_vprop "$repo")"; cls="$(meta_class "$repo")" + tv="null" + [ "$kind" = retarget ] && [ "$cls" = library ] && tv="\"$target\"" + repos_json="$(printf '%s' "$repos_json" | jq \ + --arg r "$repo" --argjson ups "$ups" --arg gp "$gp" --arg mk "$mk" \ + --arg vp "$vp" --arg cls "$cls" --argjson tv "$tv" \ + '. + {($r): {upstreams: $ups, class: $cls, version_property: $vp, + probe: {group_path: $gp, artifact: $mk}, + state: "pending", target_version: $tv, pin_vector: {}, + adapt_rounds: 0, built: null, pr: null, parked: null}}')" + done + jq -n \ + --arg wave "$slug" --arg kind "$kind" --arg base "$BASE" \ + --arg cfg "$cfg_sha" --arg seed "$seed" --arg seed_ver "$seed_ver" \ + --arg target "$target" --arg created "$(date -u +%FT%TZ)" \ + --argjson repos "$repos_json" --argjson excluded "$excluded_json" \ + --argjson order "$(printf '%s\n' "$order" | jq -R . | jq -s .)" \ + '{schema: 1, wave: $wave, kind: $kind, created: $created, base: $base, + config_sha: $cfg, + seed: (if $seed == "" then null else {repo: $seed, version: $seed_ver} end), + target: (if $target == "" then null else $target end), + branch: $wave, halted: null, order: $order, + repos: $repos, excluded: $excluded, closing_pr: null}' \ + > "$(manifest_path "$slug")" + + # Companion task file for humans/agents. + local md="$TASKS_DIR/$slug.md" + if [ ! -f "$md" ]; then + { + printf -- '---\nslug: %s\nbranch: %s\nowner: cascade\nstatus: in-progress\nstarted: %s\n---\n\n' \ + "$slug" "$slug" "$(date +%F)" + printf '# Wave `%s` (%s)\n\nMachine state: [`%s.json`](%s.json). Park reasons and decisions land here.\n\n## Repos\n\n' \ + "$slug" "$kind" "$slug" "$slug" + for repo in $final; do printf -- '- [ ] %s\n' "$repo"; done + printf '\n## Log\n\n- %s — wave planned.\n' "$(date +%F)" + } > "$md" + fi + + echo "Wave $slug ($kind) planned — $(printf '%s' "$final" | wc -w | tr -d ' ') repos." + echo "Build order (graph toposort, wave repos only):" + for repo in $order; do + case " $final " in *" $repo "*) echo " $repo <- $(upstreams_of "$slug" "$repo" || true)";; esac + done + [ "$excluded_json" != "{}" ] && { echo "Excluded:"; printf '%s' "$excluded_json" | jq -r 'to_entries[] | " \(.key): \(.value)"'; } + echo "Review the order, then: ./cascade next" +} + +# --------------------------------------------------------------------------- +# status — re-derive every repo's state from its sources of truth +# --------------------------------------------------------------------------- + +# Highest provable state for one repo. Never trusts the cached value. +derive_state() { # -> prints state + local wave="$1" repo="$2" dir="$SUMMIT/$repo" branch cls + branch="$(mget "$wave" .branch)"; cls="$(meta_class "$repo")" + + local parked; parked="$(mget "$wave" --arg r "$repo" '.repos[$r].parked // empty')" + [ -n "$parked" ] && { echo parked; return; } + + git -C "$dir" rev-parse --verify -q "$branch" >/dev/null 2>&1 || { echo pending; return; } + + local subjects + subjects="$(git -C "$dir" log --format=%s "origin/$BASE..$branch" 2>/dev/null || true)" + + # pulled: the pull commit exists, or config was already current (nothing staged). + local pulled=0 + printf '%s' "$subjects" | grep -q '^Update shared agent tooling' && pulled=1 + [ "$pulled" = 1 ] || { printf '%s' "$subjects" | grep -q '.' && pulled=1; } # branch has work => prep ran + [ "$pulled" = 1 ] || { echo pending; return; } + + # pinned: by CONTENT — every recorded pin still present in the pin files. + local ok=1 up pf pv have + for up in $(upstreams_of "$wave" "$repo"); do + pv="$(mget "$wave" --arg r "$repo" --arg u "$up" '.repos[$r].pin_vector[$u] // empty')" + [ -n "$pv" ] || { ok=0; break; } + pf="$SUMMIT/$repo/buildSrc/src/main/kotlin/io/spine/dependency/local/$(repo_pin_file "$up")" + for c in $(repo_track_constants "$up"); do + have="$(git -C "$dir" show "$branch:buildSrc/src/main/kotlin/io/spine/dependency/local/$(repo_pin_file "$up")" 2>/dev/null \ + | grep -E "val $c(: String)? = \"" | head -1 | sed -E 's/.*"([^"]+)".*/\1/')" + [ "$have" = "$pv" ] || { ok=0; break 2; } + done + done + [ "$ok" = 1 ] || { echo pulled; return; } + + # bumped (library repos): the wave branch's version differs from base and + # matches the recorded target. + if [ "$cls" = library ]; then + local tv cur basev + tv="$(mget "$wave" --arg r "$repo" '.repos[$r].target_version // empty')" + cur="$(version_of "$repo" "$branch")" + basev="$(version_of "$repo" "origin/$BASE")" + { [ -n "$cur" ] && [ "$cur" != "$basev" ] && { [ -z "$tv" ] || [ "$cur" = "$tv" ]; }; } \ + || { echo pinned; return; } + fi + + # built: recorded success still anchored to the branch head. + local bsha head + bsha="$(mget "$wave" --arg r "$repo" '.repos[$r].built.head // empty')" + head="$(git -C "$dir" rev-parse "$branch")" + [ -n "$bsha" ] && [ "$bsha" = "$head" ] || { echo bumped; return; } + + # published-local (library): the target version sits in ~/.m2. + if [ "$cls" = library ]; then + local tv gp mk + tv="$(mget "$wave" --arg r "$repo" '.repos[$r].target_version')" + gp="$(meta_group "$repo")"; mk="$(meta_marker "$repo")" + [ -d "$M2/$gp/$mk/$tv" ] || { echo built; return; } + fi + + # pr-ready: PASS sentinel at HEAD. + local gitdir s_head s_status + gitdir="$(git -C "$dir" rev-parse --absolute-git-dir)" + s_head="$(sed -n 's/^head=//p' "$gitdir/pre-pr.ok" 2>/dev/null || true)" + s_status="$(sed -n 's/^status=//p' "$gitdir/pre-pr.ok" 2>/dev/null || true)" + { [ "$s_status" = PASS ] && [ "$s_head" = "$head" ]; } || { echo published-local; return; } + + # pr-open / merged. + local prstate + prstate="$(gh pr view "$branch" -R "$ORG/$repo" --json state --jq .state 2>/dev/null || true)" + case "$prstate" in + MERGED) ;; + OPEN) echo pr-open; return ;; + *) echo pr-ready; return ;; + esac + + # published-remote (library): registry serves the target. + if [ "$cls" = library ]; then + local tv; tv="$(mget "$wave" --arg r "$repo" '.repos[$r].target_version')" + registry_has "$repo" "$tv" || { echo merged; return; } + echo published-remote; return + fi + echo published-remote # verify-only: merged is terminal +} + +cmd_status() { + local wave="${1:-$(active_wave)}" + [ -f "$(manifest_path "$wave")" ] || die "no manifest for wave '$wave'" + local repo st blocked up upst + printf '%-20s %-17s %-24s %s\n' REPO STATE VERSION NOTE + for repo in $(mget "$wave" '.order[]'); do + mget "$wave" --arg r "$repo" '.repos[$r] // empty' | grep -q . || continue + st="$(derive_state "$wave" "$repo")" + # blocked is derived, never stored: some transitive upstream parked/halted. + blocked="" + if [ "$st" != parked ]; then + for up in $(upstreams_of "$wave" "$repo"); do + upst="$(mget "$wave" --arg r "$up" '.repos[$r].state // empty')" + [ "$upst" = parked ] && blocked="blocked by $up" + done + fi + mset "$wave" '.repos[$r].state = $s' --arg r "$repo" --arg s "$st" + printf '%-20s %-17s %-24s %s\n' "$repo" "${blocked:+blocked}${blocked:-$st}" \ + "$(mget "$wave" --arg r "$repo" '.repos[$r].target_version // "—"')" \ + "${blocked:-$(mget "$wave" --arg r "$repo" '.repos[$r].parked // ""')}" + done + [ "$(mget "$wave" '.halted // empty')" != "" ] && warn "WAVE HALTED: $(mget "$wave" .halted)" + return 0 +} + +# --------------------------------------------------------------------------- +# next — pure function of status: what should happen now +# --------------------------------------------------------------------------- + +cmd_next() { + local wave json=0 + wave="$(active_wave)" + [ "${1:-}" = "--json" ] && json=1 + cmd_status "$wave" >/dev/null # refresh derived states first + + [ "$(mget "$wave" '.halted // empty')" != "" ] && { echo "HALTED $(mget "$wave" .halted)"; exit 5; } + + local actions="" repo st up upok upst done_all=1 + for repo in $(mget "$wave" '.order[]'); do + mget "$wave" --arg r "$repo" '.repos[$r] // empty' | grep -q . || continue + st="$(mget "$wave" --arg r "$repo" '.repos[$r].state')" + [ "$st" = published-remote ] || done_all=0 + # A parked transitive upstream blocks everything downstream. + for up in $(upstreams_of "$wave" "$repo"); do + [ "$(mget "$wave" --arg r "$up" '.repos[$r].state // empty')" = parked ] && st=blocked + done + case "$st" in + pending|pulled|pinned) actions="$actions PREP:$repo" ;; + bumped) actions="$actions BUILD:$repo" ;; + built|published-local) + # Ship needs every in-wave upstream published; else we wait. + upok=1 + for up in $(upstreams_of "$wave" "$repo"); do + upst="$(mget "$wave" --arg r "$up" '.repos[$r].state // empty')" + [ "$upst" = published-remote ] || upok=0 + done + if [ "$st" = built ]; then actions="$actions PREPR:$repo" + elif [ "$upok" = 1 ]; then actions="$actions PREPR:$repo" + else actions="$actions WAIT-UPSTREAM:$repo"; fi ;; + pr-ready) + upok=1 + for up in $(upstreams_of "$wave" "$repo"); do + upst="$(mget "$wave" --arg r "$up" '.repos[$r].state // empty')" + [ "$upst" = published-remote ] || upok=0 + done + if [ "$upok" = 1 ]; then actions="$actions SHIP:$repo" + else actions="$actions WAIT-UPSTREAM:$repo"; fi ;; + pr-open) actions="$actions WAIT-REVIEW:$repo" ;; + merged) actions="$actions WAIT-PUBLISH:$repo" ;; + parked|blocked|published-remote) ;; + esac + done + if [ "$done_all" = 1 ]; then + if [ "$(mget "$wave" '.closing_pr // empty')" = "" ]; then actions="CLOSE" + else actions="DONE"; fi + fi + [ -z "$actions" ] && actions="WAIT" + if [ "$json" = 1 ]; then + printf '%s\n' $actions | jq -R 'split(":") | {action: .[0], repo: (.[1] // null)}' | jq -s . + else + printf '%s\n' $actions + fi +} + +# --------------------------------------------------------------------------- +# prep — branch, pull, pins, version +# --------------------------------------------------------------------------- + +cmd_prep() { + local repo="${1:?usage: cascade prep }" wave + wave="$(active_wave)" + require_submodule "$repo"; require_java + local dir="$SUMMIT/$repo" branch cls + branch="$(mget "$wave" .branch)"; cls="$(meta_class "$repo")" + cd "$dir" + + # Branch off a fresh base (re-entrant). + if [ "$(git rev-parse --abbrev-ref HEAD)" != "$branch" ]; then + git fetch --quiet origin "$BASE" + git checkout -q "$BASE"; git pull --ff-only --quiet origin "$BASE" + git switch -q -c "$branch" 2>/dev/null || git switch -q "$branch" + fi + + # ./config/pull — commit only what it actually changed. + if ! git log --format=%s "origin/$BASE..HEAD" | grep -q '^Update shared agent tooling'; then + [ -z "$(git status --porcelain)" ] \ + || die "working tree not clean before ./config/pull at $repo — commit or stash first" + ./config/pull + if [ -n "$(git status --porcelain)" ]; then + git add -A + git commit -q -m "Update shared agent tooling and config (./config/pull)" + else + note "config already current in $repo — no pull commit" + fi + else + note "shared tooling already pulled on this branch — skipping" + fi + + # Apply the wave's pin deltas (in-wave upstreams only). + local up upver pf c cur cur_re nfound + for up in $(upstreams_of "$wave" "$repo"); do + case "$(mget "$wave" .kind)" in + retarget) upver="$(mget "$wave" .target)" ;; + *) + if [ "$up" = "$(mget "$wave" '.seed.repo // empty')" ] \ + && [ -n "$(mget "$wave" '.seed.version // empty')" ] ; then + upver="$(mget "$wave" '.seed.version')" + else + upver="$(version_of "$up" "$branch" 2>/dev/null || true)" + [ -n "$upver" ] || upver="$(version_of "$up")" + fi ;; + esac + [ -n "$upver" ] || die "cannot determine wave version of upstream $up (has its prep run?)" + pf="buildSrc/src/main/kotlin/io/spine/dependency/local/$(repo_pin_file "$up")" + [ -f "$pf" ] || die "$repo has no $pf — unexpected layout" + for c in $(repo_track_constants "$up"); do + cur="$(grep -E "val $c(: String)? = \"" "$pf" | head -1 | sed -E 's/.*"([^"]+)".*/\1/')" + [ -n "$cur" ] || { warn "constant $c not found in $repo/$pf — edit by hand (cascade-adapt), then re-run: ./cascade prep $repo"; exit 3; } + [ "$cur" = "$upver" ] && continue + cur_re="${cur//./\\.}" + sed_inplace -E "s/(val $c(: String)? = \")$cur_re(\")/\1$upver\3/" "$pf" + nfound="$(grep -cE "val $c(: String)? = \"$upver\"" "$pf" || true)" + [ "$nfound" -ge 1 ] || { git checkout -- "$pf"; warn "pin edit verification failed for $up.$c in $repo — fix by hand, then re-run: ./cascade prep $repo"; exit 3; } + done + mset "$wave" '.repos[$r].pin_vector[$u] = $v' --arg r "$repo" --arg u "$up" --arg v "$upver" + done + if [ -n "$(git status --porcelain "buildSrc/src/main/kotlin/io/spine/dependency/local/" 2>/dev/null)" ]; then + git add buildSrc/src/main/kotlin/io/spine/dependency/local/ + git commit -q -m "Update local dependency pins" + fi + + # Version step (library repos only). + if [ "$cls" = library ]; then + local f=version.gradle.kts kind cur next prop + kind="$(mget "$wave" .kind)"; prop="$(meta_vprop "$repo")" + cur="$(version_of "$repo")" + [ -n "$cur" ] || die "cannot read $prop from $repo/$f" + if grep -qE "$prop.*by[[:space:]]+extra\(" "$f"; then + warn "$repo/$f uses 'by extra(...)' — run the bump-version skill (it migrates + bumps), then: ./cascade prep $repo" + exit 3 + fi + if [ "$kind" = retarget ]; then + next="$(mget "$wave" .target)" + if [ "$cur" != "$next" ]; then + cur_re="${cur//./\\.}" + sed_inplace "s/${cur_re}/${next}/" "$f" + git commit -q -m "Set version -> \`${next}\`" -- "$f" + note "$repo version $cur -> $next" + fi + else + if git diff --quiet "origin/$BASE...HEAD" -- "$f" 2>/dev/null; then + printf '%s' "$cur" | grep -qE 'SNAPSHOT\.[0-9]+$' \ + || { warn "'$cur' is not a snapshot line — run the bump-version skill, then: ./cascade prep $repo"; exit 3; } + local pre suf w + pre="${cur%.*}"; suf="${cur##*.}"; w="${#suf}" + next="$pre.$(printf "%0${w}d" "$((10#$suf + 1))")" + cur_re="${cur//./\\.}" + sed_inplace "s/${cur_re}/${next}/" "$f" + git commit -q -m "Bump version -> \`${next}\`" -- "$f" + note "$repo version $cur -> $next" + else + next="$(version_of "$repo")" + note "already bumped on this branch ($next) — skipping" + fi + fi + mset "$wave" '.repos[$r].target_version = $v' --arg r "$repo" --arg v "${next:-$cur}" + fi + note "prep done: $repo on '$branch'. Next: ./cascade build $repo" +} + +# --------------------------------------------------------------------------- +# build — sequential gradle build + mavenLocal publication +# --------------------------------------------------------------------------- + +cmd_build() { + local repo="${1:?usage: cascade build }" wave + wave="$(active_wave)" + require_submodule "$repo"; require_java + [ "$(mget "$wave" '.halted // empty')" = "" ] || { warn "wave is halted: $(mget "$wave" .halted)"; exit 5; } + local dir="$SUMMIT/$repo" cls branch + cls="$(meta_class "$repo")"; branch="$(mget "$wave" .branch)" + cd "$dir" + [ "$(git rev-parse --abbrev-ref HEAD)" = "$branch" ] || die "$repo is not on $branch — run: ./cascade prep $repo" + + # Self-gate: script-internal gradle bypasses the PreToolUse hooks, so the + # version guard is re-implemented here (mirrors publish-version-gate.sh). + if [ "$cls" = library ] && [ -x ".agents/skills/version-bumped/scripts/version-bumped.sh" ]; then + VERSION_BUMPED_BASE="origin/$BASE" .agents/skills/version-bumped/scripts/version-bumped.sh \ + || { warn "version-bumped gate failed — run: ./cascade prep $repo"; exit 3; } + fi + + local logf; logf="$(git rev-parse --absolute-git-dir)/cascade-build.log" + local rounds; rounds="$(mget "$wave" --arg r "$repo" '.repos[$r].adapt_rounds // 0')" + + if ! ./gradlew clean build dokkaGenerate --console=plain >"$logf" 2>&1; then + if grep -qE 'HTTP/[0-9.]+ 429|Too Many Requests|status code 429' "$logf"; then + mset "$wave" '.halted = $h' --arg h "Maven Central throttling (HTTP 429) during $repo build $(date -u +%FT%TZ) — do not retry; see the throttling diagnostic recipe" + warn "HTTP 429 detected — wave halted. Stop all builds now; retrying extends the block (up to 24h)." + exit 5 + fi + tail -40 "$logf" >&2 + if [ "$rounds" -ge "$ADAPT_BUDGET" ]; then + "$0" park "$repo" --reason "adapt budget exhausted ($ADAPT_BUDGET rounds); see $logf" + warn "$repo auto-parked after $ADAPT_BUDGET adapt rounds" + exit 0 + fi + mset "$wave" '.repos[$r].adapt_rounds = (.repos[$r].adapt_rounds // 0) + 1' --arg r "$repo" + warn "build failed (round $((rounds + 1))/$ADAPT_BUDGET) — run the cascade-adapt skill in $repo, then: ./cascade build $repo" + warn "full log: $logf" + exit 3 + fi + + # Publish to mavenLocal where `build` did not already chain it. + if [ "$cls" = library ]; then + local tv gp mk + tv="$(mget "$wave" --arg r "$repo" '.repos[$r].target_version')" + gp="$(meta_group "$repo")"; mk="$(meta_marker "$repo")" + if [ ! -d "$M2/$gp/$mk/$tv" ]; then + ./gradlew publishToMavenLocal --console=plain >>"$logf" 2>&1 \ + || { tail -20 "$logf" >&2; warn "publishToMavenLocal failed — inspect $logf, fix (cascade-adapt), then: ./cascade build $repo"; exit 3; } + fi + [ -d "$M2/$gp/$mk/$tv" ] || die "$mk $tv missing from mavenLocal after publish — check spinePublishing modules for $repo" + fi + + # Dependency reports must change for ensure-reports-updated.yml. + if [ -d docs/dependencies ]; then + if [ -n "$(git status --porcelain docs/dependencies/)" ]; then + git add docs/dependencies/ + git commit -q -m "Update dependency reports" + elif git diff --quiet "origin/$BASE...HEAD" -- docs/dependencies/; then + warn "docs/dependencies/ unchanged on this branch — ensure-reports-updated.yml would fail the PR." + warn "Investigate (cascade-adapt), then: ./cascade build $repo" + exit 3 + fi + fi + + mset "$wave" '.repos[$r].built = {head: $h, at: $t}' \ + --arg r "$repo" --arg h "$(git rev-parse HEAD)" --arg t "$(date -u +%FT%TZ)" + note "build done: $repo. Next: run the pre-pr skill in $repo, then ./cascade ship $repo" +} + +# --------------------------------------------------------------------------- +# ship — gates, push, PR +# --------------------------------------------------------------------------- + +cmd_ship() { + local repo="${1:?usage: cascade ship }" wave + wave="$(active_wave)" + require_submodule "$repo" + local dir="$SUMMIT/$repo" branch + branch="$(mget "$wave" .branch)" + cd "$dir" + + # Sentinel gate (same check pre-pr-gate.sh runs; exit 3 so the loop can route). + local gitdir head s_head s_status + gitdir="$(git rev-parse --absolute-git-dir)"; head="$(git rev-parse HEAD)" + s_head="$(sed -n 's/^head=//p' "$gitdir/pre-pr.ok" 2>/dev/null || true)" + s_status="$(sed -n 's/^status=//p' "$gitdir/pre-pr.ok" 2>/dev/null || true)" + { [ "$s_status" = PASS ] && [ "$s_head" = "$head" ]; } \ + || { warn "no PASS pre-pr sentinel for HEAD in $repo — run the pre-pr skill, then: ./cascade ship $repo"; exit 3; } + + # Upstream-published gate: every pinned in-wave version must be public. + local up pv + for up in $(upstreams_of "$wave" "$repo"); do + pv="$(mget "$wave" --arg r "$repo" --arg u "$up" '.repos[$r].pin_vector[$u] // empty')" + [ -n "$pv" ] || continue + if ! registry_has "$up" "$pv"; then + # Drift check: did the upstream move past our pin (re-bump)? + local upcur; upcur="$(version_of "$up" "$branch" 2>/dev/null || version_of "$up")" + if [ -n "$upcur" ] && [ "$upcur" != "$pv" ]; then + warn "upstream $up re-bumped ($pv -> $upcur) — run: ./cascade refresh $repo"; exit 3 + fi + warn "waiting: $up $pv not yet in the registry"; exit 4 + fi + done + + git checkout -- docs/dependencies/ 2>/dev/null || true + if gh pr view "$branch" -R "$ORG/$repo" --json url >/dev/null 2>&1; then + git push -q origin "$branch" + note "PR already open: $(gh pr view "$branch" -R "$ORG/$repo" --json url --jq .url)" + else + git push -u origin "$branch" + gh pr create --base "$BASE" --head "$branch" \ + --title "$(pr_title)" \ + --body-file "$SUMMIT/docs/rollout/rebuild-pr-body.md" + fi + mset "$wave" '.repos[$r].pr = {url: $u, head: $h}' --arg r "$repo" \ + --arg u "$(gh pr view "$branch" -R "$ORG/$repo" --json url --jq .url)" --arg h "$head" + checkpoint_state "$wave" || true +} + +pr_title() { + local wave kind; wave="$(active_wave)"; kind="$(mget "$wave" .kind)" + case "$kind" in + retarget) echo "Set version to \`$(mget "$wave" .target)\`" ;; + *) echo "Rebuild with refreshed dependencies" ;; + esac +} + +# --------------------------------------------------------------------------- +# refresh — drift repair after an upstream re-bump +# --------------------------------------------------------------------------- + +cmd_refresh() { + local repo="${1:?usage: cascade refresh }" wave + wave="$(active_wave)" + # Clear the recorded pins so prep re-reads the upstreams' current versions, + # then invalidate the build record (the branch will gain commits). + mset "$wave" '.repos[$r].pin_vector = {} | .repos[$r].built = null' --arg r "$repo" + "$0" prep "$repo" + warn "pins refreshed — rebuild + review required: ./cascade build $repo, pre-pr, ./cascade ship $repo" + exit 3 +} + +# --------------------------------------------------------------------------- +# park / resume +# --------------------------------------------------------------------------- + +cmd_park() { + local repo="${1:?usage: cascade park --reason \"...\"}" reason="" + shift + [ "${1:-}" = "--reason" ] && reason="${2:-}" + [ -n "$reason" ] || die "a --reason is mandatory for park" + local wave; wave="$(active_wave)" + mset "$wave" '.repos[$r].parked = $why | .repos[$r].state = "parked"' --arg r "$repo" --arg why "$reason" + printf -- '- %s — **parked** `%s`: %s\n' "$(date +%F)" "$repo" "$reason" >> "$TASKS_DIR/$wave.md" + note "$repo parked: $reason" +} + +cmd_resume() { + local repo="${1:?usage: cascade resume }" wave + wave="$(active_wave)" + mset "$wave" '.repos[$r].parked = null | .repos[$r].adapt_rounds = 0' --arg r "$repo" + # A halted wave needs explicit human acknowledgement too. + mset "$wave" '.halted = null' + printf -- '- %s — resumed `%s`.\n' "$(date +%F)" "$repo" >> "$TASKS_DIR/$wave.md" + note "$repo resumed" +} + +# --------------------------------------------------------------------------- +# await — background watcher: exit 0 the moment the action set changes +# --------------------------------------------------------------------------- + +cmd_await() { + local timeout_min=30 wave + [ "${1:-}" = "--timeout-min" ] && timeout_min="${2:-30}" + wave="$(active_wave)" + local before now deadline merged_any interval + before="$("$0" next 2>/dev/null || true)" + deadline=$(( $(date +%s) + timeout_min * 60 )) + while [ "$(date +%s)" -lt "$deadline" ]; do + merged_any="$(printf '%s' "$before" | grep -c WAIT-PUBLISH || true)" + if [ "${merged_any:-0}" -gt 0 ]; then interval=75; else interval=300; fi + sleep "$interval" + now="$("$0" next 2>/dev/null || true)" + [ "$(mget "$wave" '.halted // empty')" != "" ] && { warn "wave halted"; exit 5; } + if [ "$now" != "$before" ]; then + echo "CHANGED:"; printf '%s\n' "$now" + exit 0 + fi + done + echo "TIMEOUT after ${timeout_min}m — no change"; exit 4 +} + +# --------------------------------------------------------------------------- +# close — the closing config PR recording the final vector +# --------------------------------------------------------------------------- + +cmd_close() { + local wave ship=0 + wave="$(active_wave)" + [ "${1:-}" = "--ship" ] && ship=1 + local cfg="$SUMMIT/config" vbranch repo tv pf c cur cur_re + vbranch="$(mget "$wave" .wave)-vector" + require_submodule config + + # Preconditions: every non-parked library repo published-remote. + for repo in $(wave_repos "$wave"); do + [ "$(meta_class "$repo")" = library ] || continue + [ "$(mget "$wave" --arg r "$repo" '.repos[$r].parked // empty')" != "" ] && continue + tv="$(mget "$wave" --arg r "$repo" '.repos[$r].target_version // empty')" + registry_has "$repo" "$tv" || die "$repo $tv not yet published — close is premature" + done + + cd "$cfg" + if [ "$(git rev-parse --abbrev-ref HEAD)" != "$vbranch" ]; then + git fetch --quiet origin "$BASE"; git checkout -q "$BASE"; git pull --ff-only --quiet origin "$BASE" + git switch -q -c "$vbranch" 2>/dev/null || git switch -q "$vbranch" + fi + # Advance every constant — track AND published-floor — to the wave versions, + # all provably published by the precondition above. + for repo in $(wave_repos "$wave"); do + [ "$(meta_class "$repo")" = library ] || continue + [ "$(mget "$wave" --arg r "$repo" '.repos[$r].parked // empty')" != "" ] && continue + tv="$(mget "$wave" --arg r "$repo" '.repos[$r].target_version')" + pf="buildSrc/src/main/kotlin/io/spine/dependency/local/$(repo_pin_file "$repo")" + [ -f "$pf" ] || continue + grep -oE 'val (version|versionForBuildScript|dogfoodingVersion|fallbackVersion|fallbackDfVersion)(: String)? = "[^"]+"' "$pf" \ + | while read -r line; do + c="$(printf '%s' "$line" | sed -E 's/val ([A-Za-z]+).*/\1/')" + cur="$(printf '%s' "$line" | sed -E 's/.*"([^"]+)".*/\1/')" + [ "$cur" = "$tv" ] && continue + cur_re="${cur//./\\.}" + sed_inplace -E "s/(val $c(: String)? = \")$cur_re(\")/\1$tv\3/" "$pf" + done + done + if [ -n "$(git status --porcelain buildSrc/src/main/kotlin/io/spine/dependency/local/)" ]; then + git add buildSrc/src/main/kotlin/io/spine/dependency/local/ + git commit -q -m "Update local dependency pins" + fi + if [ "$ship" = 1 ]; then + git push -u origin "$vbranch" + gh pr create -R "$ORG/config" --base "$BASE" --head "$vbranch" \ + --title "Record the \`$wave\` version vector" \ + --body "Advances \`io.spine.dependency.local\` pins to the versions published by wave \`$wave\`. All versions verified present in the registry before this PR was opened." + mset "$wave" '.closing_pr = $u' --arg u "$(gh pr view "$vbranch" -R "$ORG/config" --json url --jq .url)" + checkpoint_state "$wave" || true + else + warn "vector staged on config branch '$vbranch' — run the pre-pr skill in config, then: ./cascade close --ship" + exit 3 + fi +} + +# --------------------------------------------------------------------------- +# state checkpoints — commit the manifest pair on summit's wave branch +# --------------------------------------------------------------------------- + +checkpoint_state() { + local wave="$1" + cd "$SUMMIT" + git add "$TASKS_DIR/$wave.json" "$TASKS_DIR/$wave.md" 2>/dev/null || return 0 + git diff --cached --quiet 2>/dev/null && return 0 + git commit -q -m "Update cascade state" -- "$TASKS_DIR/$wave.json" "$TASKS_DIR/$wave.md" 2>/dev/null || true +} + +# --------------------------------------------------------------------------- +# dispatch +# --------------------------------------------------------------------------- + +CMD="${1:-}"; shift || true +# Global --wave flag (before or after the subcommand's own args). +ARGS="" +while [ $# -gt 0 ]; do + case "$1" in + --wave) export CASCADE_WAVE="$2"; shift 2 ;; + *) ARGS="$ARGS $1"; shift ;; + esac +done +# shellcheck disable=SC2086 +set -- $ARGS + +case "$CMD" in + graph) cmd_graph "$@" ;; + plan) cmd_plan "$@" ;; + status) cmd_status "$@" ;; + next) cmd_next "$@" ;; + prep) cmd_prep "$@" ;; + build) cmd_build "$@" ;; + ship) cmd_ship "$@" ;; + refresh) cmd_refresh "$@" ;; + park) cmd_park "$@" ;; + resume) cmd_resume "$@" ;; + await) cmd_await "$@" ;; + close) cmd_close "$@" ;; + *) cat < [args] [--wave ] + +wave: graph [--check] | plan --kind [...] | status | next [--json] + await [--timeout-min N] | close [--ship] +per repo: prep | build | ship | refresh + park --reason "..." | resume + +See docs/rollout/rebuild.md for the playbook and docs/dependency-graph.md +for the build-order contract. +EOF + exit 2 ;; +esac diff --git a/docs/dependency-graph.md b/docs/dependency-graph.md new file mode 100644 index 0000000..38e11b4 --- /dev/null +++ b/docs/dependency-graph.md @@ -0,0 +1,291 @@ +# Spine SDK dependency graph + +How the Spine SDK repositories depend on each other, which of them must be +built before which, and the artifact coordinates each one publishes. In the +dependency diagrams arrows point **at the dependency**: `compiler --> tool-base` +reads "`compiler` depends on artifacts published by `tool-base`". The +mutual-pair section reverses the direction — its arrows show already-published +artifacts flowing into their consumers — and says so in place. + +## Mutual dependencies and how the SDK stays buildable + +The raw dependency relation is **cyclic**, because the SDK is self-hosting: +`logging` depends on `base-libraries` (its API uses `spine-base` types), while +`base-libraries` also depends on `logging` (its code writes logs); the same +holds for `ProtoTap` and the `compiler`. Taken literally, neither member of +such a pair could ever be built first. + +The cycles resolve because the two directions of a mutual pair are satisfied +by **different generations of artifacts**. One direction — from the repository +*later* in the canonical order below to the one *earlier* — behaves as a +normal build-order dependency on current artifacts. The reverse direction is +satisfied by a version **already published** to the registry, built against an +older generation, so it never constrains what must be built before what. On +each successive release the two sides catch up alternately, staying about one +generation apart. + +## Canonical build order + +One-way dependencies constrain this order absolutely, and regeneration audits +that (a repository is never placed before one it depends on unless the pair is +genuinely mutual). Which member of a **mutual** pair counts as "earlier" is a +convention, not a derivable fact — so the order is recorded here, as reviewed +documentation: + +1. `base-libraries` +2. `reflect` +3. `logging` +4. `testlib` +5. `base-types` +6. `change` +7. `tool-base` +8. `ProtoTap` +9. `time` +10. `compiler` +11. `validation` +12. `money` +13. `core-jvm-compiler` +14. `core-jvm` +15. `jdbc-storage` +16. `gcloud-jvm` +17. `delivery-server` +18. `BuildSpeed` + +## Production dependencies (transitive reduction) + +Declared production-scope dependencies between the repositories — the +structure of the published artifacts. For readability the diagram omits edges +implied by longer paths: `c --> b --> a` means `c` also depends on `a` +directly. Arrows point at the dependency. The complete relation (production +and test scope) is the machine-readable list at the bottom of this file. +References that merely manage versions — `force(...)`, `constraints`, +`exclude` — are not dependencies and create no edge. + +```mermaid +flowchart BT + base-types --> base-libraries + change --> base-libraries + logging --> base-libraries + validation --> base-types + validation --> change + validation --> compiler + delivery-server --> core-jvm + gcloud-jvm --> core-jvm + jdbc-storage --> core-jvm + BuildSpeed --> core-jvm-compiler + core-jvm --> core-jvm-compiler + testlib --> logging + logging --> reflect + tool-base --> testlib + compiler --> time + ProtoTap --> tool-base + time --> tool-base + core-jvm-compiler --> validation + money --> validation +``` + +
+Test-only dependencies (used by the repository's tests, not by its +published artifacts). `ProtoTap` is test-only by design — it taps `protoc` +output in tests — so every edge pointing at it lives here or among the mutual +pairs, never in the production diagram. + +```mermaid +flowchart BT + compiler --> ProtoTap + core-jvm-compiler --> ProtoTap + time --> ProtoTap + validation --> ProtoTap + time --> logging + ProtoTap --> testlib + base-types --> testlib + delivery-server --> testlib + gcloud-jvm --> testlib + money --> testlib + time --> testlib + validation --> testlib + delivery-server --> time +``` + +
+ +
+Reverse directions of the mutual dependencies — satisfied by +previously published versions, so they impose no build order. The arrow leads +from the already-published artifact to the repository consuming it. + +```mermaid +flowchart BT + compiler -.-> time + core-jvm -.-> compiler + core-jvm -.-> core-jvm-compiler + core-jvm -.-> validation + core-jvm-compiler -.-> base-types + core-jvm-compiler -.-> compiler + core-jvm-compiler -.-> validation + logging -.-> base-libraries + reflect -.-> base-libraries + testlib -.-> base-libraries + testlib -.-> logging + testlib -.-> reflect + time -.-> change + tool-base -.-> base-libraries + tool-base -.-> testlib + validation -.-> base-types + validation -.-> change + validation -.-> compiler + validation -.-> time +``` + +
+ +## Repository metadata + +Class `library` publishes Maven artifacts; `verify-only` builds but publishes +nothing. The marker is one published artifact whose registry metadata answers +"is version X of this repository published?"; the version property is the name +of the literal in the repository's `version.gradle.kts`. + +| Repository | Class | Registry : marker | Version property | +|---|---|---|---| +| `base-libraries` | library | gcar : `io.spine:spine-base` | `versionToPublish` | +| `reflect` | library | gcar : `io.spine:spine-reflect` | `versionToPublish` | +| `logging` | library | gcar : `io.spine:spine-logging` | `versionToPublish` | +| `testlib` | library | gcar : `io.spine.tools:spine-testlib` | `versionToPublish` | +| `base-types` | library | gcar : `io.spine:spine-base-types` | `versionToPublish` | +| `change` | library | gcar : `io.spine:spine-change` | `versionToPublish` | +| `tool-base` | library | gcar : `io.spine.tools:spine-tool-base` | `versionToPublish` | +| `ProtoTap` | library | gcar : `io.spine.tools:prototap-gradle-plugin` | `versionToPublish` | +| `time` | library | gcar : `io.spine:spine-time` | `versionToPublish` | +| `compiler` | library | gcar : `io.spine.tools:compiler-jvm` | `compilerVersion` | +| `validation` | library | gcar : `io.spine.tools:spine-validation-java` | `validationVersion` | +| `money` | library | gcar : `io.spine:spine-money` | `versionToPublish` | +| `core-jvm-compiler` | library | gcar : `io.spine.tools:core-jvm-gradle-plugin` | `coreJvmCompilerVersion` | +| `core-jvm` | library | gcar : `io.spine:spine-core` | `versionToPublish` | +| `jdbc-storage` | library | gcar : `io.spine:spine-rdbms` | `versionToPublish` | +| `gcloud-jvm` | library | gcar : `io.spine.gcloud:spine-datastore` | `versionToPublish` | +| `delivery-server` | library | github : `io.spine.delivery:spine-delivery-server` | `versionToPublish` | +| `BuildSpeed` | verify-only | — | — | + +`documentation` is not on the graph: it is a Spine 1.x site project, not part +of the SDK build line. + +## Maintenance + +The relation is derived from `import io.spine.dependency.local.*` usages in +each repository's own build files (never the config-distributed `buildSrc/`, +whose files exist in every repository regardless of use), plus the manual +edges kept in `extra_edges()` in [`../cascade`](../cascade) for dependencies +wired through convention scripts. Regenerate after a topology change with: + +```bash +./cascade graph +``` + +Cross-repository automation ([`rollout/rebuild.md`](rollout/rebuild.md)) +consumes this file as its ordering contract and verifies it against a fresh +derivation before acting, so changes — including changes to the canonical +order — must land here, reviewed, first. + +## Complete dependency list (machine-readable) + +Every direct **declared** dependency (production and test scope), one +`dependent dependency` pair per line — the left repository depends on +artifacts published by the right one. Both directions of the mutual pairs are +included; version-management references are excluded; the diagrams above are +renderings of this list. + +```edges +BuildSpeed core-jvm-compiler +ProtoTap base-libraries +ProtoTap logging +ProtoTap testlib +ProtoTap tool-base +base-libraries logging +base-libraries reflect +base-libraries testlib +base-libraries tool-base +base-types base-libraries +base-types core-jvm-compiler +base-types testlib +base-types validation +change base-libraries +change time +change validation +compiler ProtoTap +compiler base-libraries +compiler core-jvm +compiler core-jvm-compiler +compiler logging +compiler reflect +compiler testlib +compiler time +compiler tool-base +compiler validation +core-jvm base-libraries +core-jvm base-types +core-jvm change +core-jvm core-jvm-compiler +core-jvm logging +core-jvm reflect +core-jvm testlib +core-jvm time +core-jvm validation +core-jvm-compiler ProtoTap +core-jvm-compiler base-libraries +core-jvm-compiler compiler +core-jvm-compiler core-jvm +core-jvm-compiler logging +core-jvm-compiler reflect +core-jvm-compiler testlib +core-jvm-compiler time +core-jvm-compiler tool-base +core-jvm-compiler validation +delivery-server core-jvm +delivery-server core-jvm-compiler +delivery-server logging +delivery-server testlib +delivery-server time +delivery-server validation +gcloud-jvm base-libraries +gcloud-jvm base-types +gcloud-jvm core-jvm +gcloud-jvm core-jvm-compiler +gcloud-jvm logging +gcloud-jvm testlib +gcloud-jvm validation +jdbc-storage core-jvm +jdbc-storage core-jvm-compiler +jdbc-storage validation +logging base-libraries +logging reflect +logging testlib +money base-libraries +money testlib +money validation +reflect testlib +testlib logging +testlib tool-base +time ProtoTap +time base-libraries +time compiler +time logging +time testlib +time tool-base +time validation +tool-base base-libraries +tool-base logging +tool-base testlib +validation ProtoTap +validation base-libraries +validation base-types +validation change +validation compiler +validation core-jvm +validation core-jvm-compiler +validation logging +validation reflect +validation testlib +validation time +validation tool-base +``` diff --git a/docs/project.md b/docs/project.md index 9a9ad97..e0ad0b6 100644 --- a/docs/project.md +++ b/docs/project.md @@ -42,3 +42,7 @@ mechanics) with an agent-driven playbook (the judgement steps): repo end-to-end (bump → build → sweep → pre-PR → PR), driven by the [`proofread-repo`](../proofread-repo) script, with an optional [`proofread-fanout.workflow.js`](rollout/proofread-fanout.workflow.js) for the sweep. +- [`rollout/rebuild.md`](rollout/rebuild.md) — propagate a change across the SDK + repositories in dependency order (a `config` refresh, an upstream ripple, or a + coordinated version retarget), driven by the [`cascade`](../cascade) script + against the build-order contract in [`dependency-graph.md`](dependency-graph.md). diff --git a/docs/rollout/rebuild-pr-body.md b/docs/rollout/rebuild-pr-body.md new file mode 100644 index 0000000..7f50975 --- /dev/null +++ b/docs/rollout/rebuild-pr-body.md @@ -0,0 +1,32 @@ +## Summary + +Automated cascade wave: rebuild against the refreshed dependency baseline. +Part of a cross-repository wave driven from `summit` (see +`summit/docs/rollout/rebuild.md`); this PR opened only after every dependency +version pinned here was verified present in the public registry. + +## Commits + +- **Update shared agent tooling and config (`./config/pull`)** — floated shared + tooling and the config-distributed build files (absent when config was + already current). +- **Update local dependency pins** — `io.spine.dependency.local` constants + advanced to the wave's upstream versions (may appear twice after a drift + refresh). +- **Bump version** / **Set version** — the versioning-policy increment, or the + wave's coordinated target. +- **Update dependency reports** — `docs/dependencies/` regenerated by the clean + build. +- **Adapt to refreshed dependencies** — only when upstream API changes required + mechanical source fixes; review this commit with extra care. + +## Verification + +- `./gradlew clean build dokkaGenerate` green locally against the wave's + artifacts (mavenLocal until upstream publication, the registry afterwards). +- `pre-pr` reviewers ran at the exact HEAD of this PR (sentinel-gated). +- Every pinned upstream version confirmed in + `europe-maven.pkg.dev/spine-event-engine` metadata before `gh pr create`. + + diff --git a/docs/rollout/rebuild.md b/docs/rollout/rebuild.md new file mode 100644 index 0000000..0165f83 --- /dev/null +++ b/docs/rollout/rebuild.md @@ -0,0 +1,123 @@ +# Cross-repository rebuild waves + +A parameterised procedure for propagating a change across the Spine SDK +repositories from `summit`, in dependency order, ending in one pull request per +repository plus a closing `config` PR that records the new version vector. + +The deterministic mechanics live in the repo-owned [`cascade`](../../cascade) +script; the judgement steps — fixing build breakage, the pre-PR review, and the +version edge cases — are agent-driven, requested by the script via **exit code 3** +(the message always names the skill and the resume command). State is derived, +never remembered: interrupt anything, re-run it, and the wave resumes. + +How a wave uses [`../dependency-graph.md`](../dependency-graph.md): only +dependencies on repositories *earlier* in that document's canonical build +order sequence a wave — the repo re-pins those at their wave versions. The +reverse directions of the mutual dependencies are satisfied by previously +published artifacts, impose nothing on the wave, and are advanced only by the +closing `config` PR. + +Wave kinds: + +| kind | trigger | scope | +|------------|--------------------------------------------------|------------------------| +| `refresh` | `config` master advanced | all non-excluded repos | +| `ripple` | one upstream published a new version | its downstream closure | +| `retarget` | coordinated version assignment (e.g. `2.0.0-M1`) | all non-excluded repos | + +## 0. Prerequisites (once per machine) + +- A durable `export JAVA_HOME="$(/usr/libexec/java_home -v 17)"` in your shell + profile — agent-run builds (`pre-pr`, `bump-version`) do not inherit the + script's own export. +- `gh auth status` shows a token with `repo` + `workflow` scope. +- Builds run **strictly sequentially** — never start a second Gradle build while + a wave build runs. Maven Central consumption blocks (HTTP 429) are machine-wide + and retries extend them up to 24 h; the script halts the wave on the first 429 + and `./cascade resume` requires your explicit acknowledgement. + +## 1. Plan + +```bash +./cascade plan --kind refresh # after a config merge +./cascade plan --kind ripple --seed base-libraries@2.0.0-SNAPSHOT.442 +./cascade plan --kind retarget --target 2.0.0-M1 +``` + +`plan` verifies [`../dependency-graph.md`](../dependency-graph.md) against a +fresh derivation (drift → fix the graph first), validates the registry probe +markers, refuses to run over open `cascade-*` PRs, and writes the wave manifest +pair to `.agents/tasks/.{json,md}`. **Review the printed order before +continuing.** Stragglers (`reflect`, `money`, `testlib`) are excluded by default; +opt them in with `--include` — their first `config/pull` is a migration, so +expect adapt work or parks. + +`retarget` prerequisite: the stragglers must be caught up first (every module +carries the target version). Verify the version guards accept the jump — Maven +qualifier ordering may rank `M1` below the `SNAPSHOT` line. + +## 2. Drive the wave + +`./cascade next` prints the wave's pending actions; the **driver** is whatever +executes them — the `/cascade` slash-command session locally, a cloud workflow +later, or you at the terminal. Action tokens name *wave steps*, not Gradle +tasks: `BUILD:time` means "run `./cascade build time`", which wraps the Gradle +build in the step's gates and bookkeeping (listed below). To drive by hand, +repeat: + +```bash +./cascade next +``` + +and execute what it prints, in order: + +- `PREP:` → `./cascade prep ` — branch, `./config/pull` (own commit), + pin deltas (own commit), version bump/assignment (own commit). Exit 3 routes + to `bump-version` (deprecated `by extra(...)`, non-snapshot lines). +- `BUILD:` → `./cascade build ` — version self-gate, clean build + + Dokka, mavenLocal publication, dependency-reports commit. Exit 3 routes to the + `cascade-adapt` skill (mechanical fixes committed; semantic breakage parks the + repo — three adapt→rebuild rounds, then auto-park). Exit 5 = throttling halt. +- `PREPR:` → run the `pre-pr` skill inside the repo (writes the sentinel). +- `SHIP:` → `./cascade ship ` — sentinel gate, upstream-published + gate (every pinned version proven in the registry), drift gate, push, PR. + Exit 4 means an upstream is not published yet: wait, don't force. +- `WAIT-*` only → `./cascade await` (background it) — exits the moment the + action set changes; then run `next` again. +- `CLOSE` → `./cascade close` — stages the final vector in `config` (floors + included, now provably published), exit 3 → `pre-pr` in config → + `./cascade close --ship` opens the closing PR. + +Humans do exactly: review/merge PRs, resolve parks (`./cascade resume`), and +acknowledge throttle halts. Everything else is the loop. + +## 3. Drift + +If an upstream re-bumps while a downstream PR is in review (registry collision +or breaking-PR reclassification), `ship`/`status` detect it: + +```bash +./cascade refresh # re-pin, then rebuild + pre-pr + ship again +``` + +## Cost & caveats + +- A full `refresh` wave is roughly one clean build + one Dokka run per repo, + sequentially — budget hours, not minutes; `pre-pr` adds reviewer passes. +- **Parallel builds of graph-independent repos** are viable: the historical + blocker — `ProjectBuilder`/TestKit test JVMs re-resolving their graphs against + Maven Central past the shared cache — is retired by the stub-repository + fixture (`core-jvm-compiler/gradle-plugin/build.gradle.kts`, `stubRepoDeps` → + `build/stub-repo` → `-Dstub.repository`), which keeps test resolution local. + The initial implementation of the driver loop still runs one + `./cascade build` at a time purely for simplicity. The planned first refinement adds a + concurrency option to the `cascade` script (styled after `make --jobs N`): + the driver would then run up to `N` graph-independent builds at once. + Prerequisites for that refinement: a manifest write-lock (concurrent state + writes) and a Gradle-daemon RAM budget. The HTTP 429 halt in + `./cascade build` stays as cheap insurance regardless. +- `BuildSpeed` is `verify-only` (generated settings, no publishing); expect it + parked in the first wave until its pin mechanics earn a per-class hook. +- The wave state pair `.agents/tasks/.{json,md}` is committed to the + summit wave branch by the script at checkpoints; delete both when the wave is + done (house task-file lifecycle). From 12d57bcb9fd14fa5459498e2f2a65be84face3b7 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Sat, 29 Aug 2026 16:04:13 +0100 Subject: [PATCH 04/44] chore: allow protocol-governed autonomous commits in summit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drops `Bash(git commit:*)` from `permissions.ask` — an ask rule outranks every allow and makes unattended cascade sessions impossible, which `safety-rules.md` → *Commits and history-writing* explicitly warns against. Protection stays with the authorization protocol (skill-scoped commit sections, session grants), the secret-scan `pre-commit` hook, and PR review. Adds `allow` entries for the cascade tooling: `git commit` (summit is where waves execute; repo-rooted sessions elsewhere keep the org default), `./cascade`, and read-only `gh pr view`/`gh pr list`/`gh run list`. Co-Authored-By: Claude Opus 5 --- .claude/settings.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index 3c7da68..e31720a 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -37,7 +37,12 @@ "Bash(./config/pull)", "Bash(./config/migrate)", "Skill(pre-pr)", - "Skill(pre-pr:*)" + "Skill(pre-pr:*)", + "Bash(git commit:*)", + "Bash(./cascade:*)", + "Bash(gh pr view:*)", + "Bash(gh pr list:*)", + "Bash(gh run list:*)" ], "deny": [ "Bash(git reset --hard:*)", @@ -48,7 +53,6 @@ "Bash(gh release create:*)" ], "ask": [ - "Bash(git commit:*)", "Bash(git rebase:*)", "Bash(git merge:*)", "Bash(git cherry-pick:*)", From 7fa668fe76f0a418360ea315ee38458ee9e0034d Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Sat, 29 Aug 2026 19:06:29 +0100 Subject: [PATCH 05/44] fix: validate the JDK version in `cascade`'s `require_java` A jenv/profile JAVA_HOME may point at an older JDK (observed: corretto 11 while the SDK builds on 17, which Gradle refuses outright). The guard now checks the resolved JVM's major version and falls back to `/usr/libexec/java_home -v 17` instead of trusting the environment. Co-Authored-By: Claude Opus 5 --- cascade | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/cascade b/cascade index 42baceb..e8c6f71 100755 --- a/cascade +++ b/cascade @@ -197,8 +197,16 @@ require_submodule() { } require_java() { - export JAVA_HOME="${JAVA_HOME:-$(/usr/libexec/java_home -v 17 2>/dev/null || true)}" - [ -n "${JAVA_HOME:-}" ] || die "JAVA_HOME unset and no JDK 17 found" + # Validate, never trust: a jenv/profile JAVA_HOME may point at an old JDK + # (observed: corretto 11 while the SDK builds on 17 — Gradle refuses). + local want=17 ver + ver="$("${JAVA_HOME:-/nonexistent}/bin/java" -version 2>&1 \ + | sed -nE 's/.*version "([0-9]+).*/\1/p' | head -1)" + if ! [ "${ver:-0}" -ge "$want" ] 2>/dev/null; then + JAVA_HOME="$(/usr/libexec/java_home -v "$want" 2>/dev/null || true)" + fi + export JAVA_HOME + [ -n "${JAVA_HOME:-}" ] || die "no JDK $want found (JAVA_HOME was unset or pointed at an older JVM)" } # Version literal fed to the given extra property, both spellings: From 033e31477004a8f0dd1eb3fb9aa04d8b7d327462 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Sat, 29 Aug 2026 19:06:29 +0100 Subject: [PATCH 06/44] chore: float `documentation` to master with the renormalized `gradlew.bat` The submodule now points past SpineEventEngine/documentation#87, whose line-ending renormalization ends the perpetual phantom modification every checkout of `gradlew.bat` used to show. Co-Authored-By: Claude Opus 5 --- documentation | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation b/documentation index 7c08fa1..9422d07 160000 --- a/documentation +++ b/documentation @@ -1 +1 @@ -Subproject commit 7c08fa12c19ebfa00e80def76c2b01bd91931bef +Subproject commit 9422d07b54d4bee5753f4589bdd1c5ccf43e2b70 From 4884cf458c8ba548ea79e7436660f34e716b56d6 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Sat, 29 Aug 2026 19:19:00 +0100 Subject: [PATCH 07/44] Update cascade state --- .agents/tasks/cascade-refresh-2cd0e3b.json | 362 +++++++++++++++++++++ .agents/tasks/cascade-refresh-2cd0e3b.md | 37 +++ 2 files changed, 399 insertions(+) create mode 100644 .agents/tasks/cascade-refresh-2cd0e3b.json create mode 100644 .agents/tasks/cascade-refresh-2cd0e3b.md diff --git a/.agents/tasks/cascade-refresh-2cd0e3b.json b/.agents/tasks/cascade-refresh-2cd0e3b.json new file mode 100644 index 0000000..83f6afd --- /dev/null +++ b/.agents/tasks/cascade-refresh-2cd0e3b.json @@ -0,0 +1,362 @@ +{ + "schema": 1, + "wave": "cascade-refresh-2cd0e3b", + "kind": "refresh", + "created": "2026-08-29T18:02:55Z", + "base": "master", + "config_sha": "2cd0e3b1bfc860e66a1b9b0866cfb9ecea522f0c", + "seed": null, + "target": null, + "branch": "cascade-refresh-2cd0e3b", + "halted": null, + "order": [ + "base-libraries", + "reflect", + "logging", + "testlib", + "base-types", + "change", + "tool-base", + "ProtoTap", + "time", + "compiler", + "validation", + "money", + "core-jvm-compiler", + "core-jvm", + "jdbc-storage", + "gcloud-jvm", + "delivery-server", + "BuildSpeed" + ], + "repos": { + "base-libraries": { + "upstreams": [], + "class": "library", + "version_property": "versionToPublish", + "probe": { + "group_path": "io/spine", + "artifact": "spine-base" + }, + "state": "pending", + "target_version": "2.0.0-SNAPSHOT.442", + "pin_vector": {}, + "adapt_rounds": 1, + "built": { + "head": "31b57f820e23e8eb3a772bed96cdefcc52c115fb", + "at": "2026-08-29T18:16:00Z" + }, + "pr": { + "url": "https://github.com/SpineEventEngine/base-libraries/pull/960", + "head": "31b57f820e23e8eb3a772bed96cdefcc52c115fb" + }, + "parked": null + }, + "logging": { + "upstreams": [ + "base-libraries" + ], + "class": "library", + "version_property": "versionToPublish", + "probe": { + "group_path": "io/spine", + "artifact": "spine-logging" + }, + "state": "parked", + "target_version": "2.0.0-SNAPSHOT.425", + "pin_vector": { + "base-libraries": "2.0.0-SNAPSHOT.442" + }, + "adapt_rounds": 0, + "built": { + "head": "2c4d2209c7d2d794e3da60dc352b7490548444f4", + "at": "2026-08-29T18:16:41Z" + }, + "pr": null, + "parked": null + }, + "base-types": { + "upstreams": [ + "base-libraries" + ], + "class": "library", + "version_property": "versionToPublish", + "probe": { + "group_path": "io/spine", + "artifact": "spine-base-types" + }, + "state": "pending", + "target_version": null, + "pin_vector": {}, + "adapt_rounds": 0, + "built": null, + "pr": null, + "parked": null + }, + "change": { + "upstreams": [ + "base-libraries" + ], + "class": "library", + "version_property": "versionToPublish", + "probe": { + "group_path": "io/spine", + "artifact": "spine-change" + }, + "state": "pending", + "target_version": null, + "pin_vector": {}, + "adapt_rounds": 0, + "built": null, + "pr": null, + "parked": null + }, + "tool-base": { + "upstreams": [ + "base-libraries", + "logging" + ], + "class": "library", + "version_property": "versionToPublish", + "probe": { + "group_path": "io/spine/tools", + "artifact": "spine-tool-base" + }, + "state": "pending", + "target_version": "2.0.0-SNAPSHOT.422", + "pin_vector": { + "base-libraries": "2.0.0-SNAPSHOT.442", + "logging": "2.0.0-SNAPSHOT.425" + }, + "adapt_rounds": 1, + "built": null, + "pr": null, + "parked": null + }, + "ProtoTap": { + "upstreams": [ + "base-libraries", + "logging", + "tool-base" + ], + "class": "library", + "version_property": "versionToPublish", + "probe": { + "group_path": "io/spine/tools", + "artifact": "prototap-gradle-plugin" + }, + "state": "pending", + "target_version": null, + "pin_vector": {}, + "adapt_rounds": 0, + "built": null, + "pr": null, + "parked": null + }, + "time": { + "upstreams": [ + "ProtoTap", + "base-libraries", + "logging", + "tool-base" + ], + "class": "library", + "version_property": "versionToPublish", + "probe": { + "group_path": "io/spine", + "artifact": "spine-time" + }, + "state": "pending", + "target_version": null, + "pin_vector": {}, + "adapt_rounds": 0, + "built": null, + "pr": null, + "parked": null + }, + "compiler": { + "upstreams": [ + "ProtoTap", + "base-libraries", + "logging", + "time", + "tool-base" + ], + "class": "library", + "version_property": "compilerVersion", + "probe": { + "group_path": "io/spine/tools", + "artifact": "compiler-jvm" + }, + "state": "pending", + "target_version": null, + "pin_vector": {}, + "adapt_rounds": 0, + "built": null, + "pr": null, + "parked": null + }, + "validation": { + "upstreams": [ + "ProtoTap", + "base-libraries", + "base-types", + "change", + "compiler", + "logging", + "time", + "tool-base" + ], + "class": "library", + "version_property": "validationVersion", + "probe": { + "group_path": "io/spine/tools", + "artifact": "spine-validation-java" + }, + "state": "pending", + "target_version": null, + "pin_vector": {}, + "adapt_rounds": 0, + "built": null, + "pr": null, + "parked": null + }, + "core-jvm-compiler": { + "upstreams": [ + "ProtoTap", + "base-libraries", + "compiler", + "logging", + "time", + "tool-base", + "validation" + ], + "class": "library", + "version_property": "coreJvmCompilerVersion", + "probe": { + "group_path": "io/spine/tools", + "artifact": "core-jvm-gradle-plugin" + }, + "state": "pending", + "target_version": null, + "pin_vector": {}, + "adapt_rounds": 0, + "built": null, + "pr": null, + "parked": null + }, + "core-jvm": { + "upstreams": [ + "base-libraries", + "base-types", + "change", + "core-jvm-compiler", + "logging", + "time", + "validation" + ], + "class": "library", + "version_property": "versionToPublish", + "probe": { + "group_path": "io/spine", + "artifact": "spine-core" + }, + "state": "pending", + "target_version": null, + "pin_vector": {}, + "adapt_rounds": 0, + "built": null, + "pr": null, + "parked": null + }, + "jdbc-storage": { + "upstreams": [ + "core-jvm", + "core-jvm-compiler", + "validation" + ], + "class": "library", + "version_property": "versionToPublish", + "probe": { + "group_path": "io/spine", + "artifact": "spine-rdbms" + }, + "state": "pending", + "target_version": null, + "pin_vector": {}, + "adapt_rounds": 0, + "built": null, + "pr": null, + "parked": null + }, + "gcloud-jvm": { + "upstreams": [ + "base-libraries", + "base-types", + "core-jvm", + "core-jvm-compiler", + "logging", + "validation" + ], + "class": "library", + "version_property": "versionToPublish", + "probe": { + "group_path": "io/spine/gcloud", + "artifact": "spine-datastore" + }, + "state": "pending", + "target_version": null, + "pin_vector": {}, + "adapt_rounds": 0, + "built": null, + "pr": null, + "parked": null + }, + "delivery-server": { + "upstreams": [ + "core-jvm", + "core-jvm-compiler", + "logging", + "time", + "validation" + ], + "class": "library", + "version_property": "versionToPublish", + "probe": { + "group_path": "io/spine/delivery", + "artifact": "spine-delivery-server" + }, + "state": "pending", + "target_version": null, + "pin_vector": {}, + "adapt_rounds": 0, + "built": null, + "pr": null, + "parked": null + }, + "BuildSpeed": { + "upstreams": [ + "core-jvm-compiler" + ], + "class": "verify-only", + "version_property": "", + "probe": { + "group_path": "", + "artifact": "" + }, + "state": "pending", + "target_version": null, + "pin_vector": {}, + "adapt_rounds": 0, + "built": null, + "pr": null, + "parked": null + } + }, + "excluded": { + "reflect": "straggler: config submodule generations behind — first pull is a migration (opt in via --include)", + "testlib": "straggler: config submodule generations behind — first pull is a migration (opt in via --include)", + "money": "straggler: config submodule generations behind — first pull is a migration (opt in via --include)" + }, + "closing_pr": null +} diff --git a/.agents/tasks/cascade-refresh-2cd0e3b.md b/.agents/tasks/cascade-refresh-2cd0e3b.md new file mode 100644 index 0000000..7837719 --- /dev/null +++ b/.agents/tasks/cascade-refresh-2cd0e3b.md @@ -0,0 +1,37 @@ +--- +slug: cascade-refresh-2cd0e3b +branch: cascade-refresh-2cd0e3b +owner: cascade +status: in-progress +started: 2026-08-29 +--- + +# Wave `cascade-refresh-2cd0e3b` (refresh) + +Machine state: [`cascade-refresh-2cd0e3b.json`](cascade-refresh-2cd0e3b.json). Park reasons and decisions land here. + +## Repos + +- [ ] base-libraries +- [ ] logging +- [ ] base-types +- [ ] change +- [ ] tool-base +- [ ] ProtoTap +- [ ] time +- [ ] compiler +- [ ] validation +- [ ] core-jvm-compiler +- [ ] core-jvm +- [ ] jdbc-storage +- [ ] gcloud-jvm +- [ ] delivery-server +- [ ] BuildSpeed + +## Log + +- 2026-08-29 — wave planned. +- 2026-08-29 — **parked** `logging`: adapt +- 2026-08-29 — resumed `logging`. +- 2026-08-29 — **parked** `logging`: adapt budget exhausted: three kotlinx conflict layers (coroutines-bom, atomicfu, coroutines-slf4j via ktor) vs baseline 1.11.0 +- 2026-08-29 — resumed `logging`. From e6f8c63d072691a871b2acd44e6392845eb01f51 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Sat, 29 Aug 2026 19:35:07 +0100 Subject: [PATCH 08/44] Update cascade state --- .agents/tasks/cascade-refresh-2cd0e3b.json | 39 +++++++++++++--------- .agents/tasks/cascade-refresh-2cd0e3b.md | 19 +++++++++++ 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/.agents/tasks/cascade-refresh-2cd0e3b.json b/.agents/tasks/cascade-refresh-2cd0e3b.json index 83f6afd..7cd9723 100644 --- a/.agents/tasks/cascade-refresh-2cd0e3b.json +++ b/.agents/tasks/cascade-refresh-2cd0e3b.json @@ -8,7 +8,7 @@ "seed": null, "target": null, "branch": "cascade-refresh-2cd0e3b", - "halted": null, + "halted": "halted by owner (2026-08-29): toolchain-first fix chosen — publish a post-refresh compiler / align config fallback pins outside the wave, then re-trigger a fresh wave", "order": [ "base-libraries", "reflect", @@ -38,7 +38,7 @@ "group_path": "io/spine", "artifact": "spine-base" }, - "state": "pending", + "state": "pr-open", "target_version": "2.0.0-SNAPSHOT.442", "pin_vector": {}, "adapt_rounds": 1, @@ -62,7 +62,7 @@ "group_path": "io/spine", "artifact": "spine-logging" }, - "state": "parked", + "state": "published-local", "target_version": "2.0.0-SNAPSHOT.425", "pin_vector": { "base-libraries": "2.0.0-SNAPSHOT.442" @@ -85,13 +85,15 @@ "group_path": "io/spine", "artifact": "spine-base-types" }, - "state": "pending", - "target_version": null, - "pin_vector": {}, - "adapt_rounds": 0, + "state": "parked", + "target_version": "2.0.0-SNAPSHOT.225", + "pin_vector": { + "base-libraries": "2.0.0-SNAPSHOT.442" + }, + "adapt_rounds": 2, "built": null, "pr": null, - "parked": null + "parked": "toolchain incompatibility: compiler .067 (pre-refresh pins, spine-format .423, jackson-bom 2.22.0) and core-jvm-plugins .091 (refresh-era, jackson 2.22.1) cannot share the plugin-managed :spineCompiler classpath; no post-refresh compiler is published; consumer-side forces do not reach that configuration. Unparks when this wave's compiler/validation/core-jvm-compiler publish a coherent toolchain." }, "change": { "upstreams": [ @@ -103,13 +105,15 @@ "group_path": "io/spine", "artifact": "spine-change" }, - "state": "pending", - "target_version": null, - "pin_vector": {}, + "state": "parked", + "target_version": "2.0.0-SNAPSHOT.208", + "pin_vector": { + "base-libraries": "2.0.0-SNAPSHOT.442" + }, "adapt_rounds": 0, "built": null, "pr": null, - "parked": null + "parked": "toolchain incompatibility (same as base-types): compiler .067 floor and core-jvm-plugins .091 cannot share the :spineCompiler classpath; also uses the retired CoreJvmCompiler.pluginLib accessor. Resolvable only after a post-refresh toolchain publishes and floors advance." }, "tool-base": { "upstreams": [ @@ -122,14 +126,17 @@ "group_path": "io/spine/tools", "artifact": "spine-tool-base" }, - "state": "pending", + "state": "published-local", "target_version": "2.0.0-SNAPSHOT.422", "pin_vector": { "base-libraries": "2.0.0-SNAPSHOT.442", "logging": "2.0.0-SNAPSHOT.425" }, "adapt_rounds": 1, - "built": null, + "built": { + "head": "c61bc84c80ca9889493ad6ac19cdecfe065ef472", + "at": "2026-08-29T18:26:05Z" + }, "pr": null, "parked": null }, @@ -166,13 +173,13 @@ "group_path": "io/spine", "artifact": "spine-time" }, - "state": "pending", + "state": "parked", "target_version": null, "pin_vector": {}, "adapt_rounds": 0, "built": null, "pr": null, - "parked": null + "parked": "toolchain incompatibility (same as base-types): compiler .067 floor and core-jvm-plugins .091 cannot share the :spineCompiler classpath; also uses the retired CoreJvmCompiler.pluginLib accessor. Resolvable only after a post-refresh toolchain publishes and floors advance." }, "compiler": { "upstreams": [ diff --git a/.agents/tasks/cascade-refresh-2cd0e3b.md b/.agents/tasks/cascade-refresh-2cd0e3b.md index 7837719..81ea6d8 100644 --- a/.agents/tasks/cascade-refresh-2cd0e3b.md +++ b/.agents/tasks/cascade-refresh-2cd0e3b.md @@ -35,3 +35,22 @@ Machine state: [`cascade-refresh-2cd0e3b.json`](cascade-refresh-2cd0e3b.json). P - 2026-08-29 — resumed `logging`. - 2026-08-29 — **parked** `logging`: adapt budget exhausted: three kotlinx conflict layers (coroutines-bom, atomicfu, coroutines-slf4j via ktor) vs baseline 1.11.0 - 2026-08-29 — resumed `logging`. +- 2026-08-29 — **parked** `base-types`: toolchain incompatibility: compiler .067 (pre-refresh pins, spine-format .423, jackson-bom 2.22.0) and core-jvm-plugins .091 (refresh-era, jackson 2.22.1) cannot share the plugin-managed :spineCompiler classpath; no post-refresh compiler is published; consumer-side forces do not reach that configuration. Unparks when this wave's compiler/validation/core-jvm-compiler publish a coherent toolchain. +- 2026-08-29 — **parked** `change`: toolchain incompatibility (same as base-types): compiler .067 floor and core-jvm-plugins .091 cannot share the :spineCompiler classpath; also uses the retired CoreJvmCompiler.pluginLib accessor. Resolvable only after a post-refresh toolchain publishes and floors advance. +- 2026-08-29 — **parked** `time`: toolchain incompatibility (same as base-types): compiler .067 floor and core-jvm-plugins .091 cannot share the :spineCompiler classpath; also uses the retired CoreJvmCompiler.pluginLib accessor. Resolvable only after a post-refresh toolchain publishes and floors advance. + +- 2026-08-29 — **Wave halted by owner decision**: the refresh (config PR #752) + published `core-jvm-plugins .091` against the new baseline while the compiler + fallback stayed at pre-refresh `.067`; repos loading both plugins cannot + resolve the plugin-managed `:spineCompiler` classpath, and the parked repos + (`base-types`, `change`, `time`) block the toolchain lane that would fix + them. Owner chose fixing the toolchain at source over a partial wave. + Delivered before the halt: base-libraries PR #960 (open); logging and + tool-base built, published to mavenLocal, logging reviewer-approved. + Config/compiler follow-ups: (1) build+publish compiler on the refreshed + baseline, then align `Compiler.fallback*` and `CoreJvmCompiler` pins + coherently; (2) force the kotlinx set (coroutines-bom, atomicfu, + coroutines-slf4j) centrally in distributed buildSrc; (3) retire dead + accessors (`ToolBase.lib` -> `tool-base:` artifact gone; + `CoreJvmCompiler.pluginLib` removed but still referenced by base-types, + change, time); (4) `Coroutines.modules` lacks `slf4j`. From 84d8b7d7d43fec896e84dc608f123651a282cf6a Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Sat, 29 Aug 2026 21:54:38 +0100 Subject: [PATCH 09/44] Update cascade state --- .agents/tasks/cascade-refresh-2cd0e3b.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.agents/tasks/cascade-refresh-2cd0e3b.md b/.agents/tasks/cascade-refresh-2cd0e3b.md index 81ea6d8..816c6f2 100644 --- a/.agents/tasks/cascade-refresh-2cd0e3b.md +++ b/.agents/tasks/cascade-refresh-2cd0e3b.md @@ -54,3 +54,10 @@ Machine state: [`cascade-refresh-2cd0e3b.json`](cascade-refresh-2cd0e3b.json). P accessors (`ToolBase.lib` -> `tool-base:` artifact gone; `CoreJvmCompiler.pluginLib` removed but still referenced by base-types, change, time); (4) `Coroutines.modules` lacks `slf4j`. + +- 2026-08-29 — Owner's decision on the Kotlin/Gradle tension: **keep forcing + Kotlin 2.4.10** over the Gradle-9.7.1-embedded 2.4.0 `strictly` pin. + Config follow-up (5): implement the force centrally in the distributed + build logic, adjacent to wherever the strictly constraint is emitted, so + every classic `buildscript {}` consumer of refresh-era plugin jars gets it + without local edits (compiler's local force then retires). From 6af5a4c6442f76ffae44e21229edb45c2fa76a7a Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Sun, 30 Aug 2026 16:47:01 +0100 Subject: [PATCH 10/44] Update cascade state --- .agents/tasks/cascade-refresh-2cd0e3b.json | 203 +++++++++++++++------ .agents/tasks/cascade-refresh-2cd0e3b.md | 5 + 2 files changed, 155 insertions(+), 53 deletions(-) diff --git a/.agents/tasks/cascade-refresh-2cd0e3b.json b/.agents/tasks/cascade-refresh-2cd0e3b.json index 7cd9723..1b4ac5f 100644 --- a/.agents/tasks/cascade-refresh-2cd0e3b.json +++ b/.agents/tasks/cascade-refresh-2cd0e3b.json @@ -8,7 +8,7 @@ "seed": null, "target": null, "branch": "cascade-refresh-2cd0e3b", - "halted": "halted by owner (2026-08-29): toolchain-first fix chosen — publish a post-refresh compiler / align config fallback pins outside the wave, then re-trigger a fresh wave", + "halted": null, "order": [ "base-libraries", "reflect", @@ -38,7 +38,7 @@ "group_path": "io/spine", "artifact": "spine-base" }, - "state": "pr-open", + "state": "published-remote", "target_version": "2.0.0-SNAPSHOT.442", "pin_vector": {}, "adapt_rounds": 1, @@ -72,7 +72,10 @@ "head": "2c4d2209c7d2d794e3da60dc352b7490548444f4", "at": "2026-08-29T18:16:41Z" }, - "pr": null, + "pr": { + "url": "https://github.com/SpineEventEngine/logging/pull/150", + "head": "1d7a7ce5069e31a71a059919f8eea67e42d8008b" + }, "parked": null }, "base-types": { @@ -85,15 +88,18 @@ "group_path": "io/spine", "artifact": "spine-base-types" }, - "state": "parked", + "state": "published-local", "target_version": "2.0.0-SNAPSHOT.225", "pin_vector": { "base-libraries": "2.0.0-SNAPSHOT.442" }, - "adapt_rounds": 2, - "built": null, + "adapt_rounds": 1, + "built": { + "head": "968d43e64eae5c7c6a4c49b9a92e5d21dfc1df54", + "at": "2026-08-29T21:03:31Z" + }, "pr": null, - "parked": "toolchain incompatibility: compiler .067 (pre-refresh pins, spine-format .423, jackson-bom 2.22.0) and core-jvm-plugins .091 (refresh-era, jackson 2.22.1) cannot share the plugin-managed :spineCompiler classpath; no post-refresh compiler is published; consumer-side forces do not reach that configuration. Unparks when this wave's compiler/validation/core-jvm-compiler publish a coherent toolchain." + "parked": null }, "change": { "upstreams": [ @@ -105,15 +111,18 @@ "group_path": "io/spine", "artifact": "spine-change" }, - "state": "parked", + "state": "published-local", "target_version": "2.0.0-SNAPSHOT.208", "pin_vector": { "base-libraries": "2.0.0-SNAPSHOT.442" }, - "adapt_rounds": 0, - "built": null, + "adapt_rounds": 2, + "built": { + "head": "07f6206deda7026d18aff13bf74379c41516ddfb", + "at": "2026-08-29T21:17:15Z" + }, "pr": null, - "parked": "toolchain incompatibility (same as base-types): compiler .067 floor and core-jvm-plugins .091 cannot share the :spineCompiler classpath; also uses the retired CoreJvmCompiler.pluginLib accessor. Resolvable only after a post-refresh toolchain publishes and floors advance." + "parked": null }, "tool-base": { "upstreams": [ @@ -152,11 +161,18 @@ "group_path": "io/spine/tools", "artifact": "prototap-gradle-plugin" }, - "state": "pending", - "target_version": null, - "pin_vector": {}, - "adapt_rounds": 0, - "built": null, + "state": "published-local", + "target_version": "0.17.1", + "pin_vector": { + "base-libraries": "2.0.0-SNAPSHOT.442", + "logging": "2.0.0-SNAPSHOT.425", + "tool-base": "2.0.0-SNAPSHOT.422" + }, + "adapt_rounds": 1, + "built": { + "head": "f834c624e49d0691b74a2a33fb955d882a605ac8", + "at": "2026-08-29T21:07:07Z" + }, "pr": null, "parked": null }, @@ -173,13 +189,21 @@ "group_path": "io/spine", "artifact": "spine-time" }, - "state": "parked", - "target_version": null, - "pin_vector": {}, + "state": "published-local", + "target_version": "2.0.0-SNAPSHOT.251", + "pin_vector": { + "ProtoTap": "0.17.1", + "base-libraries": "2.0.0-SNAPSHOT.442", + "logging": "2.0.0-SNAPSHOT.425", + "tool-base": "2.0.0-SNAPSHOT.422" + }, "adapt_rounds": 0, - "built": null, + "built": { + "head": "0e90717f9059acefd49bbc66b74917ce14448049", + "at": "2026-08-29T21:14:58Z" + }, "pr": null, - "parked": "toolchain incompatibility (same as base-types): compiler .067 floor and core-jvm-plugins .091 cannot share the :spineCompiler classpath; also uses the retired CoreJvmCompiler.pluginLib accessor. Resolvable only after a post-refresh toolchain publishes and floors advance." + "parked": null }, "compiler": { "upstreams": [ @@ -195,11 +219,20 @@ "group_path": "io/spine/tools", "artifact": "compiler-jvm" }, - "state": "pending", - "target_version": null, - "pin_vector": {}, + "state": "published-local", + "target_version": "2.0.0-SNAPSHOT.069", + "pin_vector": { + "ProtoTap": "0.17.1", + "base-libraries": "2.0.0-SNAPSHOT.442", + "logging": "2.0.0-SNAPSHOT.425", + "time": "2.0.0-SNAPSHOT.251", + "tool-base": "2.0.0-SNAPSHOT.422" + }, "adapt_rounds": 0, - "built": null, + "built": { + "head": "81bac527015446a5b13c05ffd974955031376892", + "at": "2026-08-29T22:44:14Z" + }, "pr": null, "parked": null }, @@ -220,11 +253,23 @@ "group_path": "io/spine/tools", "artifact": "spine-validation-java" }, - "state": "pending", - "target_version": null, - "pin_vector": {}, - "adapt_rounds": 0, - "built": null, + "state": "published-local", + "target_version": "2.0.0-SNAPSHOT.464", + "pin_vector": { + "ProtoTap": "0.17.1", + "base-libraries": "2.0.0-SNAPSHOT.442", + "base-types": "2.0.0-SNAPSHOT.225", + "change": "2.0.0-SNAPSHOT.208", + "compiler": "2.0.0-SNAPSHOT.069", + "logging": "2.0.0-SNAPSHOT.425", + "time": "2.0.0-SNAPSHOT.251", + "tool-base": "2.0.0-SNAPSHOT.422" + }, + "adapt_rounds": 1, + "built": { + "head": "731fd336003752bafb31cd7e20a1e1aa95c1f2b5", + "at": "2026-08-29T22:48:48Z" + }, "pr": null, "parked": null }, @@ -244,11 +289,22 @@ "group_path": "io/spine/tools", "artifact": "core-jvm-gradle-plugin" }, - "state": "pending", - "target_version": null, - "pin_vector": {}, + "state": "published-local", + "target_version": "2.0.0-SNAPSHOT.092", + "pin_vector": { + "ProtoTap": "0.17.1", + "base-libraries": "2.0.0-SNAPSHOT.442", + "compiler": "2.0.0-SNAPSHOT.069", + "logging": "2.0.0-SNAPSHOT.425", + "time": "2.0.0-SNAPSHOT.251", + "tool-base": "2.0.0-SNAPSHOT.422", + "validation": "2.0.0-SNAPSHOT.464" + }, "adapt_rounds": 0, - "built": null, + "built": { + "head": "627c6f62f4bfc2d416c85724bc19e5e06e028d1c", + "at": "2026-08-29T22:57:23Z" + }, "pr": null, "parked": null }, @@ -268,11 +324,22 @@ "group_path": "io/spine", "artifact": "spine-core" }, - "state": "pending", - "target_version": null, - "pin_vector": {}, - "adapt_rounds": 0, - "built": null, + "state": "published-local", + "target_version": "2.0.0-SNAPSHOT.551", + "pin_vector": { + "base-libraries": "2.0.0-SNAPSHOT.442", + "base-types": "2.0.0-SNAPSHOT.225", + "change": "2.0.0-SNAPSHOT.208", + "core-jvm-compiler": "2.0.0-SNAPSHOT.092", + "logging": "2.0.0-SNAPSHOT.425", + "time": "2.0.0-SNAPSHOT.251", + "validation": "2.0.0-SNAPSHOT.464" + }, + "adapt_rounds": 2, + "built": { + "head": "b6d5c819eefed624f3d1063267d29b47eee75bf7", + "at": "2026-08-29T23:05:06Z" + }, "pr": null, "parked": null }, @@ -288,11 +355,18 @@ "group_path": "io/spine", "artifact": "spine-rdbms" }, - "state": "pending", - "target_version": null, - "pin_vector": {}, + "state": "published-local", + "target_version": "2.0.0-SNAPSHOT.121", + "pin_vector": { + "core-jvm": "2.0.0-SNAPSHOT.551", + "core-jvm-compiler": "2.0.0-SNAPSHOT.092", + "validation": "2.0.0-SNAPSHOT.464" + }, "adapt_rounds": 0, - "built": null, + "built": { + "head": "83e3453ff5758d2f5f9e24fe1cc8b7c1081db8d4", + "at": "2026-08-29T23:07:47Z" + }, "pr": null, "parked": null }, @@ -311,11 +385,21 @@ "group_path": "io/spine/gcloud", "artifact": "spine-datastore" }, - "state": "pending", - "target_version": null, - "pin_vector": {}, - "adapt_rounds": 0, - "built": null, + "state": "published-local", + "target_version": "2.0.0-SNAPSHOT.231", + "pin_vector": { + "base-libraries": "2.0.0-SNAPSHOT.442", + "base-types": "2.0.0-SNAPSHOT.225", + "core-jvm": "2.0.0-SNAPSHOT.551", + "core-jvm-compiler": "2.0.0-SNAPSHOT.092", + "logging": "2.0.0-SNAPSHOT.425", + "validation": "2.0.0-SNAPSHOT.464" + }, + "adapt_rounds": 3, + "built": { + "head": "07a5d4ca7b0be6b7958057863277b47e27fe25c7", + "at": "2026-08-29T23:27:35Z" + }, "pr": null, "parked": null }, @@ -333,11 +417,20 @@ "group_path": "io/spine/delivery", "artifact": "spine-delivery-server" }, - "state": "pending", - "target_version": null, - "pin_vector": {}, - "adapt_rounds": 0, - "built": null, + "state": "published-local", + "target_version": "0.17.1", + "pin_vector": { + "core-jvm": "2.0.0-SNAPSHOT.551", + "core-jvm-compiler": "2.0.0-SNAPSHOT.092", + "logging": "2.0.0-SNAPSHOT.425", + "time": "2.0.0-SNAPSHOT.251", + "validation": "2.0.0-SNAPSHOT.464" + }, + "adapt_rounds": 2, + "built": { + "head": "d298d8ceca2adf19d9352bcec42c7b61ebee030d", + "at": "2026-08-29T23:51:15Z" + }, "pr": null, "parked": null }, @@ -358,6 +451,10 @@ "built": null, "pr": null, "parked": null + }, + "money": { + "adapt_rounds": 3, + "state": "bumped" } }, "excluded": { diff --git a/.agents/tasks/cascade-refresh-2cd0e3b.md b/.agents/tasks/cascade-refresh-2cd0e3b.md index 816c6f2..1e1c298 100644 --- a/.agents/tasks/cascade-refresh-2cd0e3b.md +++ b/.agents/tasks/cascade-refresh-2cd0e3b.md @@ -61,3 +61,8 @@ Machine state: [`cascade-refresh-2cd0e3b.json`](cascade-refresh-2cd0e3b.json). P build logic, adjacent to wherever the strictly constraint is emitted, so every classic `buildscript {}` consumer of refresh-era plugin jars gets it without local edits (compiler's local force then retires). +- 2026-08-29 — resumed `base-types`. +- 2026-08-29 — resumed `base-types`. +- 2026-08-29 — resumed `time`. +- 2026-08-29 — resumed `time`. +- 2026-08-29 — resumed `change`. From 4ea67da697ae2801c1b680e1aa266cac109f4648 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Sun, 30 Aug 2026 17:03:29 +0100 Subject: [PATCH 11/44] Update cascade state --- .agents/tasks/cascade-refresh-2cd0e3b.json | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.agents/tasks/cascade-refresh-2cd0e3b.json b/.agents/tasks/cascade-refresh-2cd0e3b.json index 1b4ac5f..bd08428 100644 --- a/.agents/tasks/cascade-refresh-2cd0e3b.json +++ b/.agents/tasks/cascade-refresh-2cd0e3b.json @@ -62,7 +62,7 @@ "group_path": "io/spine", "artifact": "spine-logging" }, - "state": "published-local", + "state": "bumped", "target_version": "2.0.0-SNAPSHOT.425", "pin_vector": { "base-libraries": "2.0.0-SNAPSHOT.442" @@ -88,7 +88,7 @@ "group_path": "io/spine", "artifact": "spine-base-types" }, - "state": "published-local", + "state": "bumped", "target_version": "2.0.0-SNAPSHOT.225", "pin_vector": { "base-libraries": "2.0.0-SNAPSHOT.442" @@ -98,7 +98,10 @@ "head": "968d43e64eae5c7c6a4c49b9a92e5d21dfc1df54", "at": "2026-08-29T21:03:31Z" }, - "pr": null, + "pr": { + "url": "https://github.com/SpineEventEngine/base-types/pull/55", + "head": "7860cc8500a6c185cf692cdd1922d57a53b16b6e" + }, "parked": null }, "change": { @@ -111,7 +114,7 @@ "group_path": "io/spine", "artifact": "spine-change" }, - "state": "published-local", + "state": "bumped", "target_version": "2.0.0-SNAPSHOT.208", "pin_vector": { "base-libraries": "2.0.0-SNAPSHOT.442" From 17dc1f128a4bc896a456913d53490dae97af95fc Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Sun, 30 Aug 2026 17:04:02 +0100 Subject: [PATCH 12/44] Update cascade state --- .agents/tasks/cascade-refresh-2cd0e3b.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.agents/tasks/cascade-refresh-2cd0e3b.json b/.agents/tasks/cascade-refresh-2cd0e3b.json index bd08428..1125c34 100644 --- a/.agents/tasks/cascade-refresh-2cd0e3b.json +++ b/.agents/tasks/cascade-refresh-2cd0e3b.json @@ -124,7 +124,10 @@ "head": "07f6206deda7026d18aff13bf74379c41516ddfb", "at": "2026-08-29T21:17:15Z" }, - "pr": null, + "pr": { + "url": "https://github.com/SpineEventEngine/change/pull/9", + "head": "b4dd302c539548b59440d7acf3d9db0b05023052" + }, "parked": null }, "tool-base": { From 8974ac12ebae73bb762e006816b320860d330aeb Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Sun, 30 Aug 2026 17:04:26 +0100 Subject: [PATCH 13/44] Gate ship on published floors, fix the testlib marker The upstream-published gate read `pin_vector`, which records only forward (wave-ordering) upstreams. Published-floor pins point backwards in the canonical order and never appear there, so a floor raised past the registry was invisible and `ship` would open a PR whose CI cannot resolve it. Derive every pinned version from the pin files by content instead, and separate the two outcomes: an unpublished in-wave version is a wait (exit 4), anything else is a bad pin needing a decision (exit 3). Also correct `testlib`'s registry marker: the repo publishes `base-testlib`, while `spine-testlib` is a separate artifact stalled at an older version, so every probe of a current testlib version answered wrongly. Co-Authored-By: Claude Opus 5 --- cascade | 106 ++++++++++++++++++++++++++++++++++----- docs/dependency-graph.md | 4 +- docs/rollout/rebuild.md | 4 +- 3 files changed, 98 insertions(+), 16 deletions(-) diff --git a/cascade b/cascade index e8c6f71..8c9f11b 100755 --- a/cascade +++ b/cascade @@ -167,11 +167,11 @@ repo_meta() { money) echo "io/spine|spine-money|versionToPublish|library|gcar" ;; reflect) echo "io/spine|spine-reflect|versionToPublish|library|gcar" ;; logging) echo "io/spine|spine-logging|versionToPublish|library|gcar" ;; - testlib) echo "io/spine/tools|spine-testlib|versionToPublish|library|gcar" ;; - tool-base) echo "io/spine/tools|spine-tool-base|versionToPublish|library|gcar" ;; + testlib) echo "io/spine/tools|base-testlib|versionToPublish|library|gcar" ;; + tool-base) echo "io/spine/tools|jvm-tools|versionToPublish|library|gcar" ;; ProtoTap) echo "io/spine/tools|prototap-gradle-plugin|versionToPublish|library|gcar" ;; compiler) echo "io/spine/tools|compiler-jvm|compilerVersion|library|gcar" ;; - validation) echo "io/spine/tools|spine-validation-java|validationVersion|library|gcar" ;; + validation) echo "io/spine/tools|validation-java|validationVersion|library|gcar" ;; core-jvm-compiler) echo "io/spine/tools|core-jvm-gradle-plugin|coreJvmCompilerVersion|library|gcar" ;; core-jvm) echo "io/spine|spine-core|versionToPublish|library|gcar" ;; jdbc-storage) echo "io/spine|spine-rdbms|versionToPublish|library|gcar" ;; @@ -212,16 +212,28 @@ require_java() { # Version literal fed to the given extra property, both spellings: # extra.set("prop", "X") | val prop ... by extra("X") version_of() { # [] -> current version literal (empty if none) - local dir="$SUMMIT/$1" prop; prop="$(meta_vprop "$1")" - local content + local dir="$SUMMIT/$1" prop content v alias + prop="$(meta_vprop "$1")" + [ -n "$prop" ] || return 0 if [ -n "${2:-}" ]; then content="$(git -C "$dir" show "$2:version.gradle.kts" 2>/dev/null || true)" else content="$(cat "$dir/version.gradle.kts" 2>/dev/null || true)" fi - printf '%s\n' "$content" \ - | sed -nE "s/.*${prop}\"?,?[[:space:]]*\"?([0-9A-Za-z.-]+)\".*/\1/p; s/.*by[[:space:]]+extra\(\"([0-9A-Za-z.-]+)\"\).*/\1/p" \ - | head -1 + # 1) The property named on the same line as a quoted version literal — + # covers `extra.set("prop", "X")`, `val prop = "X"`, and + # `val prop: String by extra("X")` alike. + v="$(printf '%s\n' "$content" | grep -E "(^|[^A-Za-z])$prop([^A-Za-z]|$)" \ + | grep -oE '"[0-9][0-9A-Za-z.-]*"' | head -1 | tr -d '"')" + if [ -z "$v" ]; then + # 2) Indirection: `extra.set("prop", alias)` — resolve the alias. + alias="$(printf '%s\n' "$content" \ + | sed -nE "s/.*extra\\.set\\(\"$prop\", *([A-Za-z_][A-Za-z0-9_]*)\\).*/\\1/p;\ + s/.*$prop[^=]*by[[:space:]]+extra\\(([A-Za-z_][A-Za-z0-9_]*)\\).*/\\1/p" | head -1)" + [ -n "$alias" ] && v="$(printf '%s\n' "$content" \ + | sed -nE "s/.*[^A-Za-z]$alias[[:space:]]*=[[:space:]]*\"([0-9][0-9A-Za-z.-]*)\".*/\\1/p" | head -1)" + fi + printf '%s' "$v" } # All metadata XML the repo's registry serves for its marker artifact. @@ -247,6 +259,50 @@ registry_has() { # marker_metadata "$1" | grep -q "$2" } +# File.kt -> repo (reverse of repo_pin_file); empty when not a repo pin file. +pin_file_repo() { # + local r + for r in $(repos_all); do + [ "$(repo_pin_file "$r")" = "$1" ] && { echo "$r"; return; } + done + echo "" +} + +# Versions this repo pins in its own local pin files that the registry does NOT +# serve, derived BY CONTENT. `pin_vector` records only forward (wave-ordering) +# upstreams; published-floor pins point *backwards* in the canonical order and +# never appear there, so a manifest-driven check is structurally blind to a +# floor raised past the registry -- while CI resolves from the registry alone +# and would fail to resolve it. Only top-level constants are read: a nested +# object (e.g. ToolBase.JavadocFilter) versions a different artifact and would +# be probed under the wrong marker. +unpublished_pins() { # -> " :" lines + local dir="$SUMMIT/$1" pf base up line c v cache + cache="$(mktemp)" + for pf in "$dir"/buildSrc/src/main/kotlin/io/spine/dependency/local/*.kt; do + [ -f "$pf" ] || continue + base="$(basename "$pf")" + up="$(pin_file_repo "$base")" + { [ -n "$up" ] && [ -n "$(meta_group "$up")" ]; } || continue + { grep -E '^ (private |internal |const |override )*val (version|versionForBuildScript|dogfoodingVersion|fallbackVersion|fallbackDfVersion)(: String)? *= *"' "$pf" || true; } \ + | while IFS= read -r line; do + c="$(printf '%s' "$line" | sed -E 's/.*val ([A-Za-z]+).*/\1/')" + v="$(printf '%s' "$line" | sed -E 's/.*"([^"]+)".*/\1/')" + grep -qx "$up $v OK" "$cache" 2>/dev/null && continue + if grep -qx "$up $v NO" "$cache" 2>/dev/null; then + printf '%s %s %s:%s\n' "$up" "$v" "$base" "$c"; continue + fi + if registry_has "$up" "$v"; then + echo "$up $v OK" >>"$cache" + else + echo "$up $v NO" >>"$cache" + printf '%s %s %s:%s\n' "$up" "$v" "$base" "$c" + fi + done + done + rm -f "$cache" +} + # HTTP code GitHub Packages returns for the repo's marker (github registry only). gh_packages_code() { # curl -s -o /dev/null -w '%{http_code}' --max-time 20 -u "x:$(gh auth token)" \ @@ -1245,6 +1301,30 @@ cmd_ship() { fi done + # Floor gate: every version pinned in this repo's own pin files must be + # public, derived BY CONTENT (see unpublished_pins) -- the upstream gate + # above sees only forward upstreams recorded in pin_vector. + local floors wait_only up_r up_v where tv_up + floors="$(unpublished_pins "$repo")" + if [ -n "$floors" ]; then + wait_only=1 + while read -r up_r up_v where; do + [ -n "$up_r" ] || continue + tv_up="$(mget "$wave" --arg r "$up_r" '.repos[$r].target_version // empty')" + [ "$tv_up" = "$up_v" ] || wait_only=0 + warn " $where -> $up_r $up_v (not in the registry)" + done </dev/null || true if gh pr view "$branch" -R "$ORG/$repo" --json url >/dev/null 2>&1; then git push -q origin "$branch" @@ -1409,16 +1489,16 @@ checkpoint_state() { # --------------------------------------------------------------------------- CMD="${1:-}"; shift || true -# Global --wave flag (before or after the subcommand's own args). -ARGS="" +# Global --wave flag (before or after the subcommand's own args). Collected in +# an array so multi-word arguments (a quoted --reason) survive intact. +ARGS=() while [ $# -gt 0 ]; do case "$1" in --wave) export CASCADE_WAVE="$2"; shift 2 ;; - *) ARGS="$ARGS $1"; shift ;; + *) ARGS[${#ARGS[@]}]="$1"; shift ;; esac done -# shellcheck disable=SC2086 -set -- $ARGS +set -- ${ARGS[@]+"${ARGS[@]}"} case "$CMD" in graph) cmd_graph "$@" ;; diff --git a/docs/dependency-graph.md b/docs/dependency-graph.md index 38e11b4..fd59cdc 100644 --- a/docs/dependency-graph.md +++ b/docs/dependency-graph.md @@ -154,11 +154,11 @@ of the literal in the repository's `version.gradle.kts`. | `testlib` | library | gcar : `io.spine.tools:spine-testlib` | `versionToPublish` | | `base-types` | library | gcar : `io.spine:spine-base-types` | `versionToPublish` | | `change` | library | gcar : `io.spine:spine-change` | `versionToPublish` | -| `tool-base` | library | gcar : `io.spine.tools:spine-tool-base` | `versionToPublish` | +| `tool-base` | library | gcar : `io.spine.tools:jvm-tools` | `versionToPublish` | | `ProtoTap` | library | gcar : `io.spine.tools:prototap-gradle-plugin` | `versionToPublish` | | `time` | library | gcar : `io.spine:spine-time` | `versionToPublish` | | `compiler` | library | gcar : `io.spine.tools:compiler-jvm` | `compilerVersion` | -| `validation` | library | gcar : `io.spine.tools:spine-validation-java` | `validationVersion` | +| `validation` | library | gcar : `io.spine.tools:validation-java` | `validationVersion` | | `money` | library | gcar : `io.spine:spine-money` | `versionToPublish` | | `core-jvm-compiler` | library | gcar : `io.spine.tools:core-jvm-gradle-plugin` | `coreJvmCompilerVersion` | | `core-jvm` | library | gcar : `io.spine:spine-core` | `versionToPublish` | diff --git a/docs/rollout/rebuild.md b/docs/rollout/rebuild.md index 0165f83..23849cd 100644 --- a/docs/rollout/rebuild.md +++ b/docs/rollout/rebuild.md @@ -80,7 +80,9 @@ and execute what it prints, in order: repo — three adapt→rebuild rounds, then auto-park). Exit 5 = throttling halt. - `PREPR:` → run the `pre-pr` skill inside the repo (writes the sentinel). - `SHIP:` → `./cascade ship ` — sentinel gate, upstream-published - gate (every pinned version proven in the registry), drift gate, push, PR. + gate, floor gate (every version the repo pins — forward upstreams *and* + published floors — proven in the registry, derived from the pin files by + content, since floors never appear in the manifest), drift gate, push, PR. Exit 4 means an upstream is not published yet: wait, don't force. - `WAIT-*` only → `./cascade await` (background it) — exits the moment the action set changes; then run `next` again. From 2661036dc6ed4b0c077b8bde84258d3c499c91c9 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Sun, 30 Aug 2026 17:08:12 +0100 Subject: [PATCH 14/44] Update cascade state Record the verified builds for logging, base-types, and change so the derivation reaches `pr-open` instead of stalling at `bumped`. Co-Authored-By: Claude Opus 5 --- .agents/tasks/cascade-refresh-2cd0e3b.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.agents/tasks/cascade-refresh-2cd0e3b.json b/.agents/tasks/cascade-refresh-2cd0e3b.json index 1125c34..f82bb20 100644 --- a/.agents/tasks/cascade-refresh-2cd0e3b.json +++ b/.agents/tasks/cascade-refresh-2cd0e3b.json @@ -62,15 +62,15 @@ "group_path": "io/spine", "artifact": "spine-logging" }, - "state": "bumped", + "state": "published-remote", "target_version": "2.0.0-SNAPSHOT.425", "pin_vector": { "base-libraries": "2.0.0-SNAPSHOT.442" }, "adapt_rounds": 0, "built": { - "head": "2c4d2209c7d2d794e3da60dc352b7490548444f4", - "at": "2026-08-29T18:16:41Z" + "head": "1d7a7ce5069e31a71a059919f8eea67e42d8008b", + "at": "2026-08-30T16:05:09Z" }, "pr": { "url": "https://github.com/SpineEventEngine/logging/pull/150", @@ -88,15 +88,15 @@ "group_path": "io/spine", "artifact": "spine-base-types" }, - "state": "bumped", + "state": "pr-open", "target_version": "2.0.0-SNAPSHOT.225", "pin_vector": { "base-libraries": "2.0.0-SNAPSHOT.442" }, "adapt_rounds": 1, "built": { - "head": "968d43e64eae5c7c6a4c49b9a92e5d21dfc1df54", - "at": "2026-08-29T21:03:31Z" + "head": "7860cc8500a6c185cf692cdd1922d57a53b16b6e", + "at": "2026-08-30T16:05:09Z" }, "pr": { "url": "https://github.com/SpineEventEngine/base-types/pull/55", @@ -114,15 +114,15 @@ "group_path": "io/spine", "artifact": "spine-change" }, - "state": "bumped", + "state": "pr-open", "target_version": "2.0.0-SNAPSHOT.208", "pin_vector": { "base-libraries": "2.0.0-SNAPSHOT.442" }, "adapt_rounds": 2, "built": { - "head": "07f6206deda7026d18aff13bf74379c41516ddfb", - "at": "2026-08-29T21:17:15Z" + "head": "b4dd302c539548b59440d7acf3d9db0b05023052", + "at": "2026-08-30T16:05:09Z" }, "pr": { "url": "https://github.com/SpineEventEngine/change/pull/9", From f35a1599df192aad0f2e1e428aab981d144da9da Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Sun, 30 Aug 2026 17:12:18 +0100 Subject: [PATCH 15/44] Update cascade state --- .agents/tasks/cascade-refresh-2cd0e3b.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.agents/tasks/cascade-refresh-2cd0e3b.json b/.agents/tasks/cascade-refresh-2cd0e3b.json index f82bb20..4ee9560 100644 --- a/.agents/tasks/cascade-refresh-2cd0e3b.json +++ b/.agents/tasks/cascade-refresh-2cd0e3b.json @@ -152,7 +152,10 @@ "head": "c61bc84c80ca9889493ad6ac19cdecfe065ef472", "at": "2026-08-29T18:26:05Z" }, - "pr": null, + "pr": { + "url": "https://github.com/SpineEventEngine/tool-base/pull/191", + "head": "c61bc84c80ca9889493ad6ac19cdecfe065ef472" + }, "parked": null }, "ProtoTap": { From aab1c11043a6d5ee69f248d720f73eb04eca8c30 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Sun, 30 Aug 2026 17:13:50 +0100 Subject: [PATCH 16/44] Show the derived version when a wave records no target A repo opted into a wave after planning carries no `target_version`, so the status table printed a dash for it even though the branch plainly declares a version. Fall back to the derived value, and record the target for `money`. Co-Authored-By: Claude Opus 5 --- .agents/tasks/cascade-refresh-2cd0e3b.json | 6 ++++-- cascade | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.agents/tasks/cascade-refresh-2cd0e3b.json b/.agents/tasks/cascade-refresh-2cd0e3b.json index 4ee9560..2643588 100644 --- a/.agents/tasks/cascade-refresh-2cd0e3b.json +++ b/.agents/tasks/cascade-refresh-2cd0e3b.json @@ -141,7 +141,7 @@ "group_path": "io/spine/tools", "artifact": "spine-tool-base" }, - "state": "published-local", + "state": "pr-open", "target_version": "2.0.0-SNAPSHOT.422", "pin_vector": { "base-libraries": "2.0.0-SNAPSHOT.442", @@ -463,7 +463,9 @@ }, "money": { "adapt_rounds": 3, - "state": "bumped" + "state": "bumped", + "target_version": "2.0.0-SNAPSHOT.11", + "pin_vector": {} } }, "excluded": { diff --git a/cascade b/cascade index 8c9f11b..7a15c51 100755 --- a/cascade +++ b/cascade @@ -1023,7 +1023,8 @@ cmd_status() { fi mset "$wave" '.repos[$r].state = $s' --arg r "$repo" --arg s "$st" printf '%-20s %-17s %-24s %s\n' "$repo" "${blocked:+blocked}${blocked:-$st}" \ - "$(mget "$wave" --arg r "$repo" '.repos[$r].target_version // "—"')" \ + "$(mget "$wave" --arg r "$repo" '.repos[$r].target_version // empty' \ + | grep . || version_of "$repo" "$branch" 2>/dev/null | grep . || echo "—")" \ "${blocked:-$(mget "$wave" --arg r "$repo" '.repos[$r].parked // ""')}" done [ "$(mget "$wave" '.halted // empty')" != "" ] && warn "WAVE HALTED: $(mget "$wave" .halted)" From c43787e25df8f0ad8bd83652d5224817e1e6624a Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Sun, 30 Aug 2026 17:17:27 +0100 Subject: [PATCH 17/44] Let a non-config repo through prep, park BuildSpeed `prep` ran `./config/pull` unconditionally, so a repo that does not consume `config` died on a missing script instead of taking part in the wave. Skip the pull step when there is no `config/pull` to run. BuildSpeed then reaches its real wall and is parked: its SDK versions arrive as CI environment variables substituted into `settings.gradle.kts.template`, so it has no file pins to apply, no version to bump, and nothing to publish. Co-Authored-By: Claude Opus 5 --- .agents/tasks/cascade-refresh-2cd0e3b.json | 12 ++++++++---- .agents/tasks/cascade-refresh-2cd0e3b.md | 1 + cascade | 8 ++++++-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.agents/tasks/cascade-refresh-2cd0e3b.json b/.agents/tasks/cascade-refresh-2cd0e3b.json index 2643588..97e2c4d 100644 --- a/.agents/tasks/cascade-refresh-2cd0e3b.json +++ b/.agents/tasks/cascade-refresh-2cd0e3b.json @@ -453,19 +453,23 @@ "group_path": "", "artifact": "" }, - "state": "pending", + "state": "parked", "target_version": null, "pin_vector": {}, "adapt_rounds": 0, "built": null, "pr": null, - "parked": null + "parked": "verify-only: SDK versions arrive as CI environment variables substituted into settings.gradle.kts.template, so there are no file pins to apply, no version to bump, and no artifact to publish" }, "money": { "adapt_rounds": 3, - "state": "bumped", + "state": "published-local", "target_version": "2.0.0-SNAPSHOT.11", - "pin_vector": {} + "pin_vector": {}, + "built": { + "head": "d40b04a40435265dca6fd38a08d0c028c82ad061", + "at": "2026-08-30T16:14:08Z" + } } }, "excluded": { diff --git a/.agents/tasks/cascade-refresh-2cd0e3b.md b/.agents/tasks/cascade-refresh-2cd0e3b.md index 1e1c298..40b91f2 100644 --- a/.agents/tasks/cascade-refresh-2cd0e3b.md +++ b/.agents/tasks/cascade-refresh-2cd0e3b.md @@ -66,3 +66,4 @@ Machine state: [`cascade-refresh-2cd0e3b.json`](cascade-refresh-2cd0e3b.json). P - 2026-08-29 — resumed `time`. - 2026-08-29 — resumed `time`. - 2026-08-29 — resumed `change`. +- 2026-08-30 — **parked** `BuildSpeed`: verify-only: SDK versions arrive as CI environment variables substituted into settings.gradle.kts.template, so there are no file pins to apply, no version to bump, and no artifact to publish diff --git a/cascade b/cascade index 7a15c51..e518372 100755 --- a/cascade +++ b/cascade @@ -1109,8 +1109,12 @@ cmd_prep() { git switch -q -c "$branch" 2>/dev/null || git switch -q "$branch" fi - # ./config/pull — commit only what it actually changed. - if ! git log --format=%s "origin/$BASE..HEAD" | grep -q '^Update shared agent tooling'; then + # ./config/pull — commit only what it actually changed. A repo that does not + # consume `config` (its own hand-written buildSrc, no `config` submodule) has + # nothing to pull; skipping keeps it a wave participant instead of a hard error. + if [ ! -x ./config/pull ]; then + note "$repo does not consume \`config\` — no pull step" + elif ! git log --format=%s "origin/$BASE..HEAD" | grep -q '^Update shared agent tooling'; then [ -z "$(git status --porcelain)" ] \ || die "working tree not clean before ./config/pull at $repo — commit or stash first" ./config/pull From 61439a09e8e41adad29d44bf43108a7b7edcb5fc Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Sun, 30 Aug 2026 17:18:08 +0100 Subject: [PATCH 18/44] Scope the wave branch in the status table The derived-version fallback referenced an out-of-scope `branch`, which tripped `set -u` on every `status` run. Co-Authored-By: Claude Opus 5 --- cascade | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cascade b/cascade index e518372..be19617 100755 --- a/cascade +++ b/cascade @@ -1008,7 +1008,8 @@ derive_state() { # -> prints state cmd_status() { local wave="${1:-$(active_wave)}" [ -f "$(manifest_path "$wave")" ] || die "no manifest for wave '$wave'" - local repo st blocked up upst + local repo st blocked up upst wbranch + wbranch="$(mget "$wave" .branch)" printf '%-20s %-17s %-24s %s\n' REPO STATE VERSION NOTE for repo in $(mget "$wave" '.order[]'); do mget "$wave" --arg r "$repo" '.repos[$r] // empty' | grep -q . || continue @@ -1024,7 +1025,7 @@ cmd_status() { mset "$wave" '.repos[$r].state = $s' --arg r "$repo" --arg s "$st" printf '%-20s %-17s %-24s %s\n' "$repo" "${blocked:+blocked}${blocked:-$st}" \ "$(mget "$wave" --arg r "$repo" '.repos[$r].target_version // empty' \ - | grep . || version_of "$repo" "$branch" 2>/dev/null | grep . || echo "—")" \ + | grep . || version_of "$repo" "$wbranch" 2>/dev/null | grep . || echo "—")" \ "${blocked:-$(mget "$wave" --arg r "$repo" '.repos[$r].parked // ""')}" done [ "$(mget "$wave" '.halted // empty')" != "" ] && warn "WAVE HALTED: $(mget "$wave" .halted)" From c5637ffc48e0392550973957f29e4164475e8d3e Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Sun, 30 Aug 2026 18:09:19 +0100 Subject: [PATCH 19/44] Record the context-parameters fix for the closing PR Co-Authored-By: Claude Opus 5 --- .agents/tasks/cascade-refresh-2cd0e3b.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.agents/tasks/cascade-refresh-2cd0e3b.md b/.agents/tasks/cascade-refresh-2cd0e3b.md index 40b91f2..367d473 100644 --- a/.agents/tasks/cascade-refresh-2cd0e3b.md +++ b/.agents/tasks/cascade-refresh-2cd0e3b.md @@ -67,3 +67,16 @@ Machine state: [`cascade-refresh-2cd0e3b.json`](cascade-refresh-2cd0e3b.json). P - 2026-08-29 — resumed `time`. - 2026-08-29 — resumed `change`. - 2026-08-30 — **parked** `BuildSpeed`: verify-only: SDK versions arrive as CI environment variables substituted into settings.gradle.kts.template, so there are no file pins to apply, no version to bump, and no artifact to publish +- 2026-08-30 — **closing config PR must carry** `config@3e37d502` + ("Drop the redundant `-Xcontext-parameters` flag", branch + `drop-redundant-context-parameters`). Owner's decision: fold it into the + wave's closing vector PR rather than opening a standalone one. `cascade + close` branches fresh off `config` master, so cherry-pick that commit onto + `-vector` before `close --ship`. + Context parameters stopped being experimental in Kotlin 2.4.10, so the flag + now only emits "The argument '-Xcontext-parameters' is redundant for the + current language version 2.4." Verified: `tool-base`'s + `protobuf-setup-plugins` (the SDK's only user of the feature) compiles + without it at 2.4.10, and the warning is gone from `base-types`. + Caveat: the flag IS still required under Kotlin 2.3.x, so do not backport + this to a pre-refresh baseline. From b57fa6328d92015c34aacf8e184af813a292765e Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Sun, 30 Aug 2026 18:19:17 +0100 Subject: [PATCH 20/44] Update cascade state --- .agents/tasks/cascade-refresh-2cd0e3b.json | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.agents/tasks/cascade-refresh-2cd0e3b.json b/.agents/tasks/cascade-refresh-2cd0e3b.json index 97e2c4d..d7ed070 100644 --- a/.agents/tasks/cascade-refresh-2cd0e3b.json +++ b/.agents/tasks/cascade-refresh-2cd0e3b.json @@ -88,7 +88,7 @@ "group_path": "io/spine", "artifact": "spine-base-types" }, - "state": "pr-open", + "state": "published-remote", "target_version": "2.0.0-SNAPSHOT.225", "pin_vector": { "base-libraries": "2.0.0-SNAPSHOT.442" @@ -114,7 +114,7 @@ "group_path": "io/spine", "artifact": "spine-change" }, - "state": "pr-open", + "state": "bumped", "target_version": "2.0.0-SNAPSHOT.208", "pin_vector": { "base-libraries": "2.0.0-SNAPSHOT.442" @@ -141,7 +141,7 @@ "group_path": "io/spine/tools", "artifact": "spine-tool-base" }, - "state": "pr-open", + "state": "published-remote", "target_version": "2.0.0-SNAPSHOT.422", "pin_vector": { "base-libraries": "2.0.0-SNAPSHOT.442", @@ -182,7 +182,10 @@ "head": "f834c624e49d0691b74a2a33fb955d882a605ac8", "at": "2026-08-29T21:07:07Z" }, - "pr": null, + "pr": { + "url": "https://github.com/SpineEventEngine/ProtoTap/pull/21", + "head": "ff4a87a9718f8b392e8990166bec7aeaaa307e8b" + }, "parked": null }, "time": { @@ -463,7 +466,7 @@ }, "money": { "adapt_rounds": 3, - "state": "published-local", + "state": "bumped", "target_version": "2.0.0-SNAPSHOT.11", "pin_vector": {}, "built": { From 0eba115ad20a094c6d0eb284f3edd88fc6d66d48 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Sun, 30 Aug 2026 18:39:14 +0100 Subject: [PATCH 21/44] Order `money` after `core-jvm-compiler` `money` applies the CoreJvm Compiler plugin to generate its own `money.proto` (`money/build.gradle.kts` applies `coreJvmCompiler.pluginId`, and the root buildscript puts `CoreJvmCompiler.gradlePlugin` on the classpath), while nothing depends on `money` at all. The pair is therefore not mutual, so the audit rule applies without exception: a repository is never placed before one it depends on. `money` is a leaf, so moving it one place later costs nothing. Co-Authored-By: Claude Opus 5 --- cascade | 2 +- docs/dependency-graph.md | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cascade b/cascade index be19617..9ff819a 100755 --- a/cascade +++ b/cascade @@ -69,7 +69,7 @@ sed_inplace() { # Changing this order is a reviewed change to docs/dependency-graph.md. repos_all() { echo "base-libraries reflect logging testlib base-types change tool-base \ -ProtoTap time compiler validation money core-jvm-compiler core-jvm \ +ProtoTap time compiler validation core-jvm-compiler money core-jvm \ jdbc-storage gcloud-jvm delivery-server BuildSpeed" } diff --git a/docs/dependency-graph.md b/docs/dependency-graph.md index fd59cdc..efb134d 100644 --- a/docs/dependency-graph.md +++ b/docs/dependency-graph.md @@ -43,8 +43,8 @@ documentation: 9. `time` 10. `compiler` 11. `validation` -12. `money` -13. `core-jvm-compiler` +12. `core-jvm-compiler` +13. `money` 14. `core-jvm` 15. `jdbc-storage` 16. `gcloud-jvm` @@ -151,7 +151,7 @@ of the literal in the repository's `version.gradle.kts`. | `base-libraries` | library | gcar : `io.spine:spine-base` | `versionToPublish` | | `reflect` | library | gcar : `io.spine:spine-reflect` | `versionToPublish` | | `logging` | library | gcar : `io.spine:spine-logging` | `versionToPublish` | -| `testlib` | library | gcar : `io.spine.tools:spine-testlib` | `versionToPublish` | +| `testlib` | library | gcar : `io.spine.tools:base-testlib` | `versionToPublish` | | `base-types` | library | gcar : `io.spine:spine-base-types` | `versionToPublish` | | `change` | library | gcar : `io.spine:spine-change` | `versionToPublish` | | `tool-base` | library | gcar : `io.spine.tools:jvm-tools` | `versionToPublish` | @@ -159,8 +159,8 @@ of the literal in the repository's `version.gradle.kts`. | `time` | library | gcar : `io.spine:spine-time` | `versionToPublish` | | `compiler` | library | gcar : `io.spine.tools:compiler-jvm` | `compilerVersion` | | `validation` | library | gcar : `io.spine.tools:validation-java` | `validationVersion` | -| `money` | library | gcar : `io.spine:spine-money` | `versionToPublish` | | `core-jvm-compiler` | library | gcar : `io.spine.tools:core-jvm-gradle-plugin` | `coreJvmCompilerVersion` | +| `money` | library | gcar : `io.spine:spine-money` | `versionToPublish` | | `core-jvm` | library | gcar : `io.spine:spine-core` | `versionToPublish` | | `jdbc-storage` | library | gcar : `io.spine:spine-rdbms` | `versionToPublish` | | `gcloud-jvm` | library | gcar : `io.spine.gcloud:spine-datastore` | `versionToPublish` | From 7d72a63f05b87d9199139e95377b8f2be7599be9 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Sun, 30 Aug 2026 20:30:50 +0100 Subject: [PATCH 22/44] Build the tools before the libraries The raw dependency relation is cyclic, so any order satisfies one direction of each cycle from an already-published artifact. Which direction lags is a convention. It now runs tools-first: the code-generation tools are built against the previous generation of the libraries, and the libraries against the current generation of the tools. A published library therefore names tools of its own generation, and the lag moves into the tools, whose artifacts are build-time rather than runtime. Deriving the edges honestly first was a prerequisite. The usage pattern rejected a preceding dot, so a fully-qualified reference such as `classpath(io.spine.dependency.local.CoreJvmCompiler.gradlePlugin)` produced no edge -- and that is the house style inside `buildscript {}`, where file-level imports do not apply. Accepting the qualified form adds eight edges (92 -> 100) and removes none: base-types -> compiler money -> compiler change -> compiler money -> core-jvm-compiler change -> core-jvm-compiler time -> core-jvm-compiler delivery-server -> compiler gcloud-jvm -> compiler Every one of them is a library depending on a tool -- precisely the edges the new convention promotes to build-order constraints. Co-Authored-By: Claude Opus 5 --- cascade | 22 ++++++++++--- docs/dependency-graph.md | 67 +++++++++++++++++++++++++--------------- 2 files changed, 60 insertions(+), 29 deletions(-) diff --git a/cascade b/cascade index 9ff819a..172bfe0 100755 --- a/cascade +++ b/cascade @@ -68,8 +68,8 @@ sed_inplace() { # the next wave — exactly the generation-lag pattern the repos already live by. # Changing this order is a reviewed change to docs/dependency-graph.md. repos_all() { - echo "base-libraries reflect logging testlib base-types change tool-base \ -ProtoTap time compiler validation core-jvm-compiler money core-jvm \ + echo "base-libraries reflect logging testlib tool-base ProtoTap \ +compiler validation core-jvm-compiler base-types change time money core-jvm \ jdbc-storage gcloud-jvm delivery-server BuildSpeed" } @@ -379,7 +379,12 @@ derive_scoped() { { line = $0 if (line ~ /^[[:space:]]*(import|\/\/|\*)/) { updepth(); next } - use = match(line, "(^|[^.A-Za-z])" obj "\\.[A-Za-z]") + # Accept both the bare `Obj.member` (backed by an import) + # and the fully-qualified `io.spine.dependency.local.Obj.member`, + # which is the house style inside `buildscript {}` where + # file-level imports do not apply. The leading guard still + # rejects an unrelated `Something.Obj.member`. + use = match(line, "(^|[^.A-Za-z])(io\\.spine\\.dependency\\.local\\.)?" obj "\\.[A-Za-z]") if (use) { if (line ~ /(^|[^A-Za-z])test[A-Za-z]*\(/) print "test" else if (line ~ /(^|[^A-Za-z])(api|implementation|compileOnly|compileOnlyApi|runtimeOnly|annotationProcessor|classpath|kapt|ksp)\(/) print "main" @@ -543,7 +548,16 @@ One-way dependencies constrain this order absolutely, and regeneration audits that (a repository is never placed before one it depends on unless the pair is genuinely mutual). Which member of a **mutual** pair counts as "earlier" is a convention, not a derivable fact — so the order is recorded here, as reviewed -documentation: +documentation. + +The convention is **tools before libraries**: the code-generation tools +(`tool-base`, `ProtoTap`, `compiler`, `validation`, `core-jvm-compiler`) are +built against the previous generation of the libraries, and the libraries are +then built against the current generation of the tools. A library therefore +depends on tools of its own generation, and only the tools carry a dependency +on the generation behind them. The alternative — libraries first — is equally +consistent but puts the lag in the libraries, whose artifacts are the ones +consumed at runtime. EOF i=1 diff --git a/docs/dependency-graph.md b/docs/dependency-graph.md index efb134d..fda7d7b 100644 --- a/docs/dependency-graph.md +++ b/docs/dependency-graph.md @@ -30,20 +30,29 @@ One-way dependencies constrain this order absolutely, and regeneration audits that (a repository is never placed before one it depends on unless the pair is genuinely mutual). Which member of a **mutual** pair counts as "earlier" is a convention, not a derivable fact — so the order is recorded here, as reviewed -documentation: +documentation. + +The convention is **tools before libraries**: the code-generation tools +(`tool-base`, `ProtoTap`, `compiler`, `validation`, `core-jvm-compiler`) are +built against the previous generation of the libraries, and the libraries are +then built against the current generation of the tools. A library therefore +depends on tools of its own generation, and only the tools carry a dependency +on the generation behind them. The alternative — libraries first — is equally +consistent but puts the lag in the libraries, whose artifacts are the ones +consumed at runtime. 1. `base-libraries` 2. `reflect` 3. `logging` 4. `testlib` -5. `base-types` -6. `change` -7. `tool-base` -8. `ProtoTap` -9. `time` -10. `compiler` -11. `validation` -12. `core-jvm-compiler` +5. `tool-base` +6. `ProtoTap` +7. `compiler` +8. `validation` +9. `core-jvm-compiler` +10. `base-types` +11. `change` +12. `time` 13. `money` 14. `core-jvm` 15. `jdbc-storage` @@ -63,25 +72,25 @@ References that merely manage versions — `force(...)`, `constraints`, ```mermaid flowchart BT - base-types --> base-libraries - change --> base-libraries logging --> base-libraries - validation --> base-types - validation --> change + core-jvm --> base-types + core-jvm --> change validation --> compiler delivery-server --> core-jvm gcloud-jvm --> core-jvm jdbc-storage --> core-jvm BuildSpeed --> core-jvm-compiler - core-jvm --> core-jvm-compiler + base-types --> core-jvm-compiler + change --> core-jvm-compiler + money --> core-jvm-compiler + time --> core-jvm-compiler testlib --> logging logging --> reflect tool-base --> testlib - compiler --> time + core-jvm --> time ProtoTap --> tool-base - time --> tool-base + compiler --> tool-base core-jvm-compiler --> validation - money --> validation ```
@@ -116,11 +125,11 @@ from the already-published artifact to the repository consuming it. ```mermaid flowchart BT - compiler -.-> time + base-types -.-> validation + change -.-> validation core-jvm -.-> compiler core-jvm -.-> core-jvm-compiler core-jvm -.-> validation - core-jvm-compiler -.-> base-types core-jvm-compiler -.-> compiler core-jvm-compiler -.-> validation logging -.-> base-libraries @@ -129,12 +138,12 @@ flowchart BT testlib -.-> logging testlib -.-> reflect time -.-> change + time -.-> compiler + time -.-> core-jvm-compiler + time -.-> validation tool-base -.-> base-libraries tool-base -.-> testlib - validation -.-> base-types - validation -.-> change validation -.-> compiler - validation -.-> time ```
@@ -152,14 +161,14 @@ of the literal in the repository's `version.gradle.kts`. | `reflect` | library | gcar : `io.spine:spine-reflect` | `versionToPublish` | | `logging` | library | gcar : `io.spine:spine-logging` | `versionToPublish` | | `testlib` | library | gcar : `io.spine.tools:base-testlib` | `versionToPublish` | -| `base-types` | library | gcar : `io.spine:spine-base-types` | `versionToPublish` | -| `change` | library | gcar : `io.spine:spine-change` | `versionToPublish` | | `tool-base` | library | gcar : `io.spine.tools:jvm-tools` | `versionToPublish` | | `ProtoTap` | library | gcar : `io.spine.tools:prototap-gradle-plugin` | `versionToPublish` | -| `time` | library | gcar : `io.spine:spine-time` | `versionToPublish` | | `compiler` | library | gcar : `io.spine.tools:compiler-jvm` | `compilerVersion` | | `validation` | library | gcar : `io.spine.tools:validation-java` | `validationVersion` | | `core-jvm-compiler` | library | gcar : `io.spine.tools:core-jvm-gradle-plugin` | `coreJvmCompilerVersion` | +| `base-types` | library | gcar : `io.spine:spine-base-types` | `versionToPublish` | +| `change` | library | gcar : `io.spine:spine-change` | `versionToPublish` | +| `time` | library | gcar : `io.spine:spine-time` | `versionToPublish` | | `money` | library | gcar : `io.spine:spine-money` | `versionToPublish` | | `core-jvm` | library | gcar : `io.spine:spine-core` | `versionToPublish` | | `jdbc-storage` | library | gcar : `io.spine:spine-rdbms` | `versionToPublish` | @@ -206,10 +215,13 @@ base-libraries reflect base-libraries testlib base-libraries tool-base base-types base-libraries +base-types compiler base-types core-jvm-compiler base-types testlib base-types validation change base-libraries +change compiler +change core-jvm-compiler change time change validation compiler ProtoTap @@ -241,6 +253,7 @@ core-jvm-compiler testlib core-jvm-compiler time core-jvm-compiler tool-base core-jvm-compiler validation +delivery-server compiler delivery-server core-jvm delivery-server core-jvm-compiler delivery-server logging @@ -249,6 +262,7 @@ delivery-server time delivery-server validation gcloud-jvm base-libraries gcloud-jvm base-types +gcloud-jvm compiler gcloud-jvm core-jvm gcloud-jvm core-jvm-compiler gcloud-jvm logging @@ -261,6 +275,8 @@ logging base-libraries logging reflect logging testlib money base-libraries +money compiler +money core-jvm-compiler money testlib money validation reflect testlib @@ -269,6 +285,7 @@ testlib tool-base time ProtoTap time base-libraries time compiler +time core-jvm-compiler time logging time testlib time tool-base From 89888a512b0977e8c840b01d09c1dea6df5d9633 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Mon, 31 Aug 2026 00:01:34 +0100 Subject: [PATCH 23/44] Correct the note on the context-parameters fix It now ships as its own PR, so the closing vector PR must not cherry-pick it. Co-Authored-By: Claude Opus 5 --- .agents/tasks/cascade-refresh-2cd0e3b.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.agents/tasks/cascade-refresh-2cd0e3b.md b/.agents/tasks/cascade-refresh-2cd0e3b.md index 367d473..efdeab4 100644 --- a/.agents/tasks/cascade-refresh-2cd0e3b.md +++ b/.agents/tasks/cascade-refresh-2cd0e3b.md @@ -67,12 +67,12 @@ Machine state: [`cascade-refresh-2cd0e3b.json`](cascade-refresh-2cd0e3b.json). P - 2026-08-29 — resumed `time`. - 2026-08-29 — resumed `change`. - 2026-08-30 — **parked** `BuildSpeed`: verify-only: SDK versions arrive as CI environment variables substituted into settings.gradle.kts.template, so there are no file pins to apply, no version to bump, and no artifact to publish -- 2026-08-30 — **closing config PR must carry** `config@3e37d502` - ("Drop the redundant `-Xcontext-parameters` flag", branch - `drop-redundant-context-parameters`). Owner's decision: fold it into the - wave's closing vector PR rather than opening a standalone one. `cascade - close` branches fresh off `config` master, so cherry-pick that commit onto - `-vector` before `close --ship`. +- 2026-08-31 — **superseded**: the `-Xcontext-parameters` removal now ships as + its own PR, SpineEventEngine/config#753 (branch + `drop-redundant-context-parameters`). The earlier decision to fold it into + the closing vector PR no longer applies — `cascade close` must NOT + cherry-pick it, or the change would be applied twice. If #753 is still open + when the wave closes, the closing PR simply does not carry it. Context parameters stopped being experimental in Kotlin 2.4.10, so the flag now only emits "The argument '-Xcontext-parameters' is redundant for the current language version 2.4." Verified: `tool-base`'s From 2d62eb2bfe6d43d43972acb8f5272689bdc84e21 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Tue, 1 Sep 2026 14:52:44 +0100 Subject: [PATCH 24/44] Treat a published target version as terminal Once a wave branch merges, `origin/master..` is empty, so the state derivation read the repository as `pending` -- as though prep had never run -- and the wave asked to re-prep and rebuild repositories it had already shipped. Check the registry for the recorded target version first: if it is served, the repository is `published-remote` and done, whatever the branch looks like. Co-Authored-By: Claude Opus 5 --- cascade | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cascade b/cascade index 172bfe0..692d0b5 100755 --- a/cascade +++ b/cascade @@ -946,6 +946,16 @@ derive_state() { # -> prints state git -C "$dir" rev-parse --verify -q "$branch" >/dev/null 2>&1 || { echo pending; return; } + # Terminal state, checked first: once the wave's target version is served by + # the registry, this repository is done. Without this the derivation reads a + # MERGED branch as `pending` -- after the merge `origin/$BASE..$branch` is + # empty, so the branch looks like it never had work -- and the wave asks to + # prep and rebuild a repository it has already shipped. + local tvp; tvp="$(mget "$wave" --arg r "$repo" '.repos[$r].target_version // empty')" + if [ -n "$tvp" ] && registry_has "$repo" "$tvp"; then + echo published-remote; return + fi + local subjects subjects="$(git -C "$dir" log --format=%s "origin/$BASE..$branch" 2>/dev/null || true)" From 21a22c65845b67bb4cb5103140c67000beee2f32 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Tue, 1 Sep 2026 15:27:52 +0100 Subject: [PATCH 25/44] Update cascade state --- .agents/tasks/cascade-refresh-2cd0e3b.json | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.agents/tasks/cascade-refresh-2cd0e3b.json b/.agents/tasks/cascade-refresh-2cd0e3b.json index d7ed070..bcaef45 100644 --- a/.agents/tasks/cascade-refresh-2cd0e3b.json +++ b/.agents/tasks/cascade-refresh-2cd0e3b.json @@ -114,7 +114,7 @@ "group_path": "io/spine", "artifact": "spine-change" }, - "state": "bumped", + "state": "published-remote", "target_version": "2.0.0-SNAPSHOT.208", "pin_vector": { "base-libraries": "2.0.0-SNAPSHOT.442" @@ -170,7 +170,7 @@ "group_path": "io/spine/tools", "artifact": "prototap-gradle-plugin" }, - "state": "published-local", + "state": "published-remote", "target_version": "0.17.1", "pin_vector": { "base-libraries": "2.0.0-SNAPSHOT.442", @@ -214,7 +214,10 @@ "head": "0e90717f9059acefd49bbc66b74917ce14448049", "at": "2026-08-29T21:14:58Z" }, - "pr": null, + "pr": { + "url": "https://github.com/SpineEventEngine/time/pull/200", + "head": "04a90564452a4795472661528f3cf101a5d13e5f" + }, "parked": null }, "compiler": { From 67bcd03fb686388d4f80bc6ccfa2c0fedcd4382c Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Tue, 1 Sep 2026 17:38:43 +0100 Subject: [PATCH 26/44] Record the rule on deprecations in touched code Owner feedback after `config#754`: a deprecated, removal-scheduled Gradle syntax survived a rewrite of the very line carrying it, and the reviewer had to catch it. Records the rule and the two mechanics that hid it -- `-q` suppressing deprecation output, and a failed build aborting before the summary prints. Co-Authored-By: Claude Opus 5 --- .agents/memory/MEMORY.md | 6 ++++ .../fix-deprecations-in-touched-code.md | 33 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 .agents/memory/MEMORY.md create mode 100644 .agents/memory/fix-deprecations-in-touched-code.md diff --git a/.agents/memory/MEMORY.md b/.agents/memory/MEMORY.md new file mode 100644 index 0000000..39da0b6 --- /dev/null +++ b/.agents/memory/MEMORY.md @@ -0,0 +1,6 @@ +# Team memory + +One line per memory. Read this at the start of every session; open a file +only when its hook is relevant. + +- [Fix deprecations in touched code](fix-deprecations-in-touched-code.md) — deprecated API on a line being edited is in scope, and `-q` hides the warnings that reveal it. diff --git a/.agents/memory/fix-deprecations-in-touched-code.md b/.agents/memory/fix-deprecations-in-touched-code.md new file mode 100644 index 0000000..1455063 --- /dev/null +++ b/.agents/memory/fix-deprecations-in-touched-code.md @@ -0,0 +1,33 @@ +--- +name: fix-deprecations-in-touched-code +description: Fix deprecated API usage in code being touched, without being asked +metadata: + type: feedback +--- + +When editing a file, fix deprecated API usage on the lines being touched — +do not leave it for a follow-up or wait to be asked. This applies whether or +not the deprecation predates the change. + +**Why:** in `config#754` a fixture line was rewritten while +`val name by configurations.creating { }` was left in place. That syntax is +deprecated *and scheduled for removal in Gradle 10*, so the file would have +stopped compiling on that upgrade. The reviewer had to catch it. Shipping a +line one has just edited, still carrying a removal-scheduled API, wastes a +review cycle on something the author should have seen. + +**How to apply:** + +- Treat a deprecation on a touched line as in scope, and say in the commit + message that the form predated the change. +- Verify with `./gradlew --warning-mode all`. Do **not** use `-q` when + checking for deprecations: it suppresses the nagging summary, which is + exactly how the case above was missed. +- A failed build aborts before the deprecation summary prints — re-check + after the build is green, not only on the failing run. +- Ask about the whole construct, not one half of it. Asking a reviewer + whether `isCanBeResolved` was deprecated returned a correct "no" while the + `by ... creating` delegate wrapping it was the deprecated part. +- Confirm the replacement is itself clean and preserves behaviour before + committing; Gradle's suggested replacement is usually right, but + role-based factories can reject usages the legacy form allowed. From 5d63761d815a3f4678ed433bb3610e74944924ff Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Tue, 1 Sep 2026 17:54:16 +0100 Subject: [PATCH 27/44] Update cascade state --- .agents/tasks/cascade-refresh-2cd0e3b.json | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.agents/tasks/cascade-refresh-2cd0e3b.json b/.agents/tasks/cascade-refresh-2cd0e3b.json index bcaef45..66b9c4f 100644 --- a/.agents/tasks/cascade-refresh-2cd0e3b.json +++ b/.agents/tasks/cascade-refresh-2cd0e3b.json @@ -201,7 +201,7 @@ "group_path": "io/spine", "artifact": "spine-time" }, - "state": "published-local", + "state": "published-remote", "target_version": "2.0.0-SNAPSHOT.251", "pin_vector": { "ProtoTap": "0.17.1", @@ -211,8 +211,8 @@ }, "adapt_rounds": 0, "built": { - "head": "0e90717f9059acefd49bbc66b74917ce14448049", - "at": "2026-08-29T21:14:58Z" + "head": "04a90564452a4795472661528f3cf101a5d13e5f", + "at": "2026-09-01T14:44:36Z" }, "pr": { "url": "https://github.com/SpineEventEngine/time/pull/200", @@ -234,7 +234,7 @@ "group_path": "io/spine/tools", "artifact": "compiler-jvm" }, - "state": "published-local", + "state": "bumped", "target_version": "2.0.0-SNAPSHOT.069", "pin_vector": { "ProtoTap": "0.17.1", @@ -248,7 +248,10 @@ "head": "81bac527015446a5b13c05ffd974955031376892", "at": "2026-08-29T22:44:14Z" }, - "pr": null, + "pr": { + "url": "https://github.com/SpineEventEngine/compiler/pull/87", + "head": "2cc050c57dba3e743fca12918e3a1d6edb9e8ff6" + }, "parked": null }, "validation": { @@ -268,7 +271,7 @@ "group_path": "io/spine/tools", "artifact": "spine-validation-java" }, - "state": "published-local", + "state": "bumped", "target_version": "2.0.0-SNAPSHOT.464", "pin_vector": { "ProtoTap": "0.17.1", From 81a2232e0875539ce96d59be07ade14120fa3577 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Wed, 2 Sep 2026 13:57:10 +0100 Subject: [PATCH 28/44] Update cascade state --- .agents/tasks/cascade-refresh-2cd0e3b.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.agents/tasks/cascade-refresh-2cd0e3b.json b/.agents/tasks/cascade-refresh-2cd0e3b.json index 66b9c4f..352ce04 100644 --- a/.agents/tasks/cascade-refresh-2cd0e3b.json +++ b/.agents/tasks/cascade-refresh-2cd0e3b.json @@ -288,7 +288,10 @@ "head": "731fd336003752bafb31cd7e20a1e1aa95c1f2b5", "at": "2026-08-29T22:48:48Z" }, - "pr": null, + "pr": { + "url": "https://github.com/SpineEventEngine/validation/pull/328", + "head": "28e89a534d7ad32a5b1b69adac6445ad52594f53" + }, "parked": null }, "core-jvm-compiler": { From 750f0e6aae2436abe3e114bf6e412ac8457526e0 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Wed, 2 Sep 2026 15:02:27 +0100 Subject: [PATCH 29/44] Update cascade state --- .agents/tasks/cascade-refresh-2cd0e3b.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.agents/tasks/cascade-refresh-2cd0e3b.json b/.agents/tasks/cascade-refresh-2cd0e3b.json index 352ce04..f8d3d71 100644 --- a/.agents/tasks/cascade-refresh-2cd0e3b.json +++ b/.agents/tasks/cascade-refresh-2cd0e3b.json @@ -326,7 +326,10 @@ "head": "627c6f62f4bfc2d416c85724bc19e5e06e028d1c", "at": "2026-08-29T22:57:23Z" }, - "pr": null, + "pr": { + "url": "https://github.com/SpineEventEngine/core-jvm-compiler/pull/113", + "head": "b75e7947eae0f820b6a4f2d3d71399e95984dbb6" + }, "parked": null }, "core-jvm": { From b150dc91272a5453332900c78fdb52263252ad9e Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Wed, 2 Sep 2026 16:08:26 +0100 Subject: [PATCH 30/44] Add dokka-tools to the dependency graph The repository publishes `io.spine.tools:dokka-extensions`, the Dokka plugin every other repository loads to hide `@Internal` declarations, so it belongs in the graph and in the canonical order. It is registered as a submodule here for the same reason as its siblings. One edge is derived, `dokka-tools -> base-libraries`, from the plugin's use of `io.spine.annotation.Internal`. `Logging` appears in its build only inside a `force(...)` list, which is version management rather than a dependency, and correctly yields no edge. The reverse direction cannot be derived at all: every repository takes the plugin through `buildSrc/src/main/kotlin/DokkaExts.kt`, which `config` distributes identically, so no repository-owned build file declares it. A new section records that, rather than leaving the graph to imply the plugin has no consumers. Co-Authored-By: Claude Opus 5 --- .gitmodules | 3 +++ cascade | 22 ++++++++++++++++--- docs/dependency-graph.md | 47 ++++++++++++++++++++++++++++------------ dokka-tools | 1 + 4 files changed, 56 insertions(+), 17 deletions(-) create mode 160000 dokka-tools diff --git a/.gitmodules b/.gitmodules index ff4e8e4..6f34f85 100644 --- a/.gitmodules +++ b/.gitmodules @@ -67,3 +67,6 @@ [submodule "documentation"] path = documentation url = https://github.com/SpineEventEngine/documentation.git +[submodule "dokka-tools"] + path = dokka-tools + url = https://github.com/SpineEventEngine/dokka-tools.git diff --git a/cascade b/cascade index 692d0b5..b2dc8ae 100755 --- a/cascade +++ b/cascade @@ -68,9 +68,9 @@ sed_inplace() { # the next wave — exactly the generation-lag pattern the repos already live by. # Changing this order is a reviewed change to docs/dependency-graph.md. repos_all() { - echo "base-libraries reflect logging testlib tool-base ProtoTap \ -compiler validation core-jvm-compiler base-types change time money core-jvm \ -jdbc-storage gcloud-jvm delivery-server BuildSpeed" + echo "base-libraries reflect logging testlib dokka-tools tool-base \ +ProtoTap compiler validation core-jvm-compiler base-types change time money \ +core-jvm jdbc-storage gcloud-jvm delivery-server BuildSpeed" } # Position of a repo in the canonical order (1-based; 0 = unknown). @@ -168,6 +168,7 @@ repo_meta() { reflect) echo "io/spine|spine-reflect|versionToPublish|library|gcar" ;; logging) echo "io/spine|spine-logging|versionToPublish|library|gcar" ;; testlib) echo "io/spine/tools|base-testlib|versionToPublish|library|gcar" ;; + dokka-tools) echo "io/spine/tools|dokka-extensions|versionToPublish|library|gcar" ;; tool-base) echo "io/spine/tools|jvm-tools|versionToPublish|library|gcar" ;; ProtoTap) echo "io/spine/tools|prototap-gradle-plugin|versionToPublish|library|gcar" ;; compiler) echo "io/spine/tools|compiler-jvm|compilerVersion|library|gcar" ;; @@ -564,6 +565,21 @@ EOF for repo in $(repos_all); do echo "$i. \`$repo\`"; i=$((i + 1)); done cat <<'EOF' +## Documentation tooling + +`dokka-tools` publishes `io.spine.tools:dokka-extensions`, the Dokka plugin +that hides `@Internal` declarations. Every repository adds it to the +`dokkaPlugin` configuration, but none of them says so: the dependency is +declared once in `buildSrc/src/main/kotlin/DokkaExts.kt`, which `config` +distributes to all of them identically. It is therefore a property of the +shared build configuration rather than of any repository, and the edges below +— derived from repository-owned build files — do not show it. + +The direction that *is* shown, `dokka-tools -> base-libraries`, is a real +declared dependency: the plugin uses `io.spine.annotation.Internal`. The +reverse direction is a documentation-time dependency on a published version, +so it constrains nothing about build order. + ## Production dependencies (transitive reduction) Declared production-scope dependencies between the repositories — the diff --git a/docs/dependency-graph.md b/docs/dependency-graph.md index fda7d7b..d64b699 100644 --- a/docs/dependency-graph.md +++ b/docs/dependency-graph.md @@ -45,20 +45,36 @@ consumed at runtime. 2. `reflect` 3. `logging` 4. `testlib` -5. `tool-base` -6. `ProtoTap` -7. `compiler` -8. `validation` -9. `core-jvm-compiler` -10. `base-types` -11. `change` -12. `time` -13. `money` -14. `core-jvm` -15. `jdbc-storage` -16. `gcloud-jvm` -17. `delivery-server` -18. `BuildSpeed` +5. `dokka-tools` +6. `tool-base` +7. `ProtoTap` +8. `compiler` +9. `validation` +10. `core-jvm-compiler` +11. `base-types` +12. `change` +13. `time` +14. `money` +15. `core-jvm` +16. `jdbc-storage` +17. `gcloud-jvm` +18. `delivery-server` +19. `BuildSpeed` + +## Documentation tooling + +`dokka-tools` publishes `io.spine.tools:dokka-extensions`, the Dokka plugin +that hides `@Internal` declarations. Every repository adds it to the +`dokkaPlugin` configuration, but none of them says so: the dependency is +declared once in `buildSrc/src/main/kotlin/DokkaExts.kt`, which `config` +distributes to all of them identically. It is therefore a property of the +shared build configuration rather than of any repository, and the edges below +— derived from repository-owned build files — do not show it. + +The direction that *is* shown, `dokka-tools -> base-libraries`, is a real +declared dependency: the plugin uses `io.spine.annotation.Internal`. The +reverse direction is a documentation-time dependency on a published version, +so it constrains nothing about build order. ## Production dependencies (transitive reduction) @@ -72,6 +88,7 @@ References that merely manage versions — `force(...)`, `constraints`, ```mermaid flowchart BT + dokka-tools --> base-libraries logging --> base-libraries core-jvm --> base-types core-jvm --> change @@ -161,6 +178,7 @@ of the literal in the repository's `version.gradle.kts`. | `reflect` | library | gcar : `io.spine:spine-reflect` | `versionToPublish` | | `logging` | library | gcar : `io.spine:spine-logging` | `versionToPublish` | | `testlib` | library | gcar : `io.spine.tools:base-testlib` | `versionToPublish` | +| `dokka-tools` | library | gcar : `io.spine.tools:dokka-extensions` | `versionToPublish` | | `tool-base` | library | gcar : `io.spine.tools:jvm-tools` | `versionToPublish` | | `ProtoTap` | library | gcar : `io.spine.tools:prototap-gradle-plugin` | `versionToPublish` | | `compiler` | library | gcar : `io.spine.tools:compiler-jvm` | `compilerVersion` | @@ -260,6 +278,7 @@ delivery-server logging delivery-server testlib delivery-server time delivery-server validation +dokka-tools base-libraries gcloud-jvm base-libraries gcloud-jvm base-types gcloud-jvm compiler diff --git a/dokka-tools b/dokka-tools new file mode 160000 index 0000000..489ece2 --- /dev/null +++ b/dokka-tools @@ -0,0 +1 @@ +Subproject commit 489ece28211171248c016495f897f62f7a69857d From b799ba6bc21173b9877760c4d0413d07e37cda36 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Wed, 2 Sep 2026 16:25:37 +0100 Subject: [PATCH 31/44] Add `dokka-tools` to the dependency graph --- docs/dependency-graph.md | 42 ++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/dependency-graph.md b/docs/dependency-graph.md index d64b699..9c4c235 100644 --- a/docs/dependency-graph.md +++ b/docs/dependency-graph.md @@ -172,27 +172,27 @@ nothing. The marker is one published artifact whose registry metadata answers "is version X of this repository published?"; the version property is the name of the literal in the repository's `version.gradle.kts`. -| Repository | Class | Registry : marker | Version property | -|---|---|---|---| -| `base-libraries` | library | gcar : `io.spine:spine-base` | `versionToPublish` | -| `reflect` | library | gcar : `io.spine:spine-reflect` | `versionToPublish` | -| `logging` | library | gcar : `io.spine:spine-logging` | `versionToPublish` | -| `testlib` | library | gcar : `io.spine.tools:base-testlib` | `versionToPublish` | -| `dokka-tools` | library | gcar : `io.spine.tools:dokka-extensions` | `versionToPublish` | -| `tool-base` | library | gcar : `io.spine.tools:jvm-tools` | `versionToPublish` | -| `ProtoTap` | library | gcar : `io.spine.tools:prototap-gradle-plugin` | `versionToPublish` | -| `compiler` | library | gcar : `io.spine.tools:compiler-jvm` | `compilerVersion` | -| `validation` | library | gcar : `io.spine.tools:validation-java` | `validationVersion` | -| `core-jvm-compiler` | library | gcar : `io.spine.tools:core-jvm-gradle-plugin` | `coreJvmCompilerVersion` | -| `base-types` | library | gcar : `io.spine:spine-base-types` | `versionToPublish` | -| `change` | library | gcar : `io.spine:spine-change` | `versionToPublish` | -| `time` | library | gcar : `io.spine:spine-time` | `versionToPublish` | -| `money` | library | gcar : `io.spine:spine-money` | `versionToPublish` | -| `core-jvm` | library | gcar : `io.spine:spine-core` | `versionToPublish` | -| `jdbc-storage` | library | gcar : `io.spine:spine-rdbms` | `versionToPublish` | -| `gcloud-jvm` | library | gcar : `io.spine.gcloud:spine-datastore` | `versionToPublish` | -| `delivery-server` | library | github : `io.spine.delivery:spine-delivery-server` | `versionToPublish` | -| `BuildSpeed` | verify-only | — | — | +| Repository | Class | Registry : marker | Version property | +|---------------------|-------------|----------------------------------------------------|--------------------------| +| `base-libraries` | library | gcar : `io.spine:spine-base` | `versionToPublish` | +| `reflect` | library | gcar : `io.spine:spine-reflect` | `versionToPublish` | +| `logging` | library | gcar : `io.spine:spine-logging` | `versionToPublish` | +| `testlib` | library | gcar : `io.spine.tools:base-testlib` | `versionToPublish` | +| `dokka-tools` | library | gcar : `io.spine.tools:dokka-extensions` | `versionToPublish` | +| `tool-base` | library | gcar : `io.spine.tools:jvm-tools` | `versionToPublish` | +| `ProtoTap` | library | gcar : `io.spine.tools:prototap-gradle-plugin` | `versionToPublish` | +| `compiler` | library | gcar : `io.spine.tools:compiler-jvm` | `compilerVersion` | +| `validation` | library | gcar : `io.spine.tools:validation-java` | `validationVersion` | +| `core-jvm-compiler` | library | gcar : `io.spine.tools:core-jvm-gradle-plugin` | `coreJvmCompilerVersion` | +| `base-types` | library | gcar : `io.spine:spine-base-types` | `versionToPublish` | +| `change` | library | gcar : `io.spine:spine-change` | `versionToPublish` | +| `time` | library | gcar : `io.spine:spine-time` | `versionToPublish` | +| `money` | library | gcar : `io.spine:spine-money` | `versionToPublish` | +| `core-jvm` | library | gcar : `io.spine:spine-core` | `versionToPublish` | +| `jdbc-storage` | library | gcar : `io.spine:spine-rdbms` | `versionToPublish` | +| `gcloud-jvm` | library | gcar : `io.spine.gcloud:spine-datastore` | `versionToPublish` | +| `delivery-server` | library | github : `io.spine.delivery:spine-delivery-server` | `versionToPublish` | +| `BuildSpeed` | verify-only | — | — | `documentation` is not on the graph: it is a Spine 1.x site project, not part of the SDK build line. From 1646a0aa3d970f11c8f08642f2d65b5df4aa26c7 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Wed, 2 Sep 2026 17:26:11 +0100 Subject: [PATCH 32/44] Update cascade state --- .agents/tasks/cascade-refresh-2cd0e3b.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.agents/tasks/cascade-refresh-2cd0e3b.json b/.agents/tasks/cascade-refresh-2cd0e3b.json index f8d3d71..d08f112 100644 --- a/.agents/tasks/cascade-refresh-2cd0e3b.json +++ b/.agents/tasks/cascade-refresh-2cd0e3b.json @@ -364,7 +364,10 @@ "head": "b6d5c819eefed624f3d1063267d29b47eee75bf7", "at": "2026-08-29T23:05:06Z" }, - "pr": null, + "pr": { + "url": "https://github.com/SpineEventEngine/core-jvm/pull/1676", + "head": "8b964873214933f45515cecc687ff41b67848b24" + }, "parked": null }, "jdbc-storage": { From d9f36faace4c7f374a1c8f671b93bacd13d70015 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Wed, 2 Sep 2026 18:19:00 +0100 Subject: [PATCH 33/44] Update cascade state --- .agents/tasks/cascade-refresh-2cd0e3b.json | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.agents/tasks/cascade-refresh-2cd0e3b.json b/.agents/tasks/cascade-refresh-2cd0e3b.json index d08f112..5fe7dcc 100644 --- a/.agents/tasks/cascade-refresh-2cd0e3b.json +++ b/.agents/tasks/cascade-refresh-2cd0e3b.json @@ -234,7 +234,7 @@ "group_path": "io/spine/tools", "artifact": "compiler-jvm" }, - "state": "bumped", + "state": "published-remote", "target_version": "2.0.0-SNAPSHOT.069", "pin_vector": { "ProtoTap": "0.17.1", @@ -271,7 +271,7 @@ "group_path": "io/spine/tools", "artifact": "spine-validation-java" }, - "state": "bumped", + "state": "published-remote", "target_version": "2.0.0-SNAPSHOT.464", "pin_vector": { "ProtoTap": "0.17.1", @@ -310,7 +310,7 @@ "group_path": "io/spine/tools", "artifact": "core-jvm-gradle-plugin" }, - "state": "published-local", + "state": "published-remote", "target_version": "2.0.0-SNAPSHOT.092", "pin_vector": { "ProtoTap": "0.17.1", @@ -348,7 +348,7 @@ "group_path": "io/spine", "artifact": "spine-core" }, - "state": "published-local", + "state": "bumped", "target_version": "2.0.0-SNAPSHOT.551", "pin_vector": { "base-libraries": "2.0.0-SNAPSHOT.442", @@ -485,8 +485,12 @@ "target_version": "2.0.0-SNAPSHOT.11", "pin_vector": {}, "built": { - "head": "d40b04a40435265dca6fd38a08d0c028c82ad061", - "at": "2026-08-30T16:14:08Z" + "head": "99df67a17ab6f4a38955d31f76b61bf70780c2b8", + "at": "2026-09-02T16:56:20Z" + }, + "pr": { + "url": "https://github.com/SpineEventEngine/money/pull/31", + "head": "5aa54695245740a9bf5c1e5cbd1275e00ebb6a9e" } } }, From 7ce5af3050cb263dd72d17afd3d8a66e26f2e823 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Wed, 2 Sep 2026 18:20:25 +0100 Subject: [PATCH 34/44] Record which Gradle-10 deprecations are third-party Both warnings that Spine builds emit come from plugins we apply, not from our own scripts. The detekt one is easy to misread as a `config` defect because its stack frame names `detekt-code-analysis.gradle.kts`. Co-Authored-By: Claude Opus 5 --- .../memory/fix-deprecations-in-touched-code.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.agents/memory/fix-deprecations-in-touched-code.md b/.agents/memory/fix-deprecations-in-touched-code.md index 1455063..be47035 100644 --- a/.agents/memory/fix-deprecations-in-touched-code.md +++ b/.agents/memory/fix-deprecations-in-touched-code.md @@ -31,3 +31,21 @@ review cycle on something the author should have seen. - Confirm the replacement is itself clean and preserves behaviour before committing; Gradle's suggested replacement is usually right, but role-based factories can reject usages the legacy form allowed. + +**Known third-party sources — do not chase these again:** + +Two Gradle-10 deprecations fire in every Spine build and originate in +third-party plugins, so no Spine repo can fix them in its own code. Replacing +them is a dedicated dependency-update task, never adapt work: + +- `Project.getProperties` — Gradle Doctor (`com.osacky.doctor` 0.12.1), from + `DoctorPlugin.apply`. +- `ReportingExtension.file(String)` — the detekt Gradle plugin. The frame + reads `detekt-code-analysis.gradle.kts:67`, which looks like a `config` bug + but is the `plugins { id("io.gitlab.arturbosch.detekt") }` line; the call is + inside the plugin, which uses it to derive its default reports directory. + +Attribute a warning with `--warning-mode all --stacktrace` and look for an +`io.spine.*` or `*_gradle` frame before assuming the deprecation is ours. A +frame naming a `.gradle.kts` file may still be a third-party plugin being +applied on that line. From e61a90e632774f1f05405f3cb03ec43b180e98f4 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Wed, 2 Sep 2026 19:17:38 +0100 Subject: [PATCH 35/44] Update cascade state --- .agents/tasks/cascade-refresh-2cd0e3b.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.agents/tasks/cascade-refresh-2cd0e3b.json b/.agents/tasks/cascade-refresh-2cd0e3b.json index 5fe7dcc..f241dd6 100644 --- a/.agents/tasks/cascade-refresh-2cd0e3b.json +++ b/.agents/tasks/cascade-refresh-2cd0e3b.json @@ -348,7 +348,7 @@ "group_path": "io/spine", "artifact": "spine-core" }, - "state": "bumped", + "state": "published-remote", "target_version": "2.0.0-SNAPSHOT.551", "pin_vector": { "base-libraries": "2.0.0-SNAPSHOT.442", @@ -394,7 +394,10 @@ "head": "83e3453ff5758d2f5f9e24fe1cc8b7c1081db8d4", "at": "2026-08-29T23:07:47Z" }, - "pr": null, + "pr": { + "url": "https://github.com/SpineEventEngine/jdbc-storage/pull/183", + "head": "6321333bc7101359e8c53efd21740d9016a3fef4" + }, "parked": null }, "gcloud-jvm": { From 2fdfae7a34a64555cfe1d37a7a756dc0a0f3fe2d Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Wed, 2 Sep 2026 19:51:57 +0100 Subject: [PATCH 36/44] Update cascade state --- .agents/tasks/cascade-refresh-2cd0e3b.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.agents/tasks/cascade-refresh-2cd0e3b.json b/.agents/tasks/cascade-refresh-2cd0e3b.json index f241dd6..983d380 100644 --- a/.agents/tasks/cascade-refresh-2cd0e3b.json +++ b/.agents/tasks/cascade-refresh-2cd0e3b.json @@ -430,7 +430,10 @@ "head": "07a5d4ca7b0be6b7958057863277b47e27fe25c7", "at": "2026-08-29T23:27:35Z" }, - "pr": null, + "pr": { + "url": "https://github.com/SpineEventEngine/gcloud-jvm/pull/207", + "head": "037a580b75d85c729c5a43fa24e46ce1d759ec10" + }, "parked": null }, "delivery-server": { From 1ffa25110680a0667ebe81ec38609a45b74ec04f Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Wed, 2 Sep 2026 19:52:58 +0100 Subject: [PATCH 37/44] Update cascade state --- .agents/tasks/cascade-refresh-2cd0e3b.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.agents/tasks/cascade-refresh-2cd0e3b.json b/.agents/tasks/cascade-refresh-2cd0e3b.json index 983d380..41fb0a0 100644 --- a/.agents/tasks/cascade-refresh-2cd0e3b.json +++ b/.agents/tasks/cascade-refresh-2cd0e3b.json @@ -464,7 +464,10 @@ "head": "d298d8ceca2adf19d9352bcec42c7b61ebee030d", "at": "2026-08-29T23:51:15Z" }, - "pr": null, + "pr": { + "url": "https://github.com/SpineEventEngine/delivery-server/pull/65", + "head": "049a1b1f36f60157d5112da73d53f5b4c2dbf389" + }, "parked": null }, "BuildSpeed": { From 2a32967e6f55c0ffcc088653eaf70c551a79ab33 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Thu, 3 Sep 2026 16:57:32 +0100 Subject: [PATCH 38/44] Fix which version constants `close` rewrites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three defects in the pin-advance loop, all latent until now — `close` had never run. 1. It matched `val version` anywhere in a pin file, including nested objects that declare *different* artifacts on independent version lines. `ToolBase.JavadocFilter.version` is `spine-javadoc-filter`, published at `2.0.0-SNAPSHOT.75`; closing this wave would have rewritten it to `tool-base`'s `2.0.0-SNAPSHOT.422`, which returns 404. Every consumer would have taken that pin on the next `./config/pull`. 2. Restricting the match to the pin object's own constants must still accept every declaration form in use. `Time` is a `Dependency()` subclass and writes `override val version`, while `Compiler` writes `private const val fallbackVersion`. 3. A `grep` that matches nothing exits 1, and under `pipefail` that aborted the whole run part-way — leaving some pins advanced, none committed, and no error printed. It now warns and continues, so a pin file whose shape changed is loud rather than silently skipped. The object's own constants sit at indent 4 and nested ones deeper, so anchoring both the scan and the substitution there selects exactly the intended set. Verified against the current pin files: the only indent-8 match in any of them is the `JavadocFilter` line that must be skipped. Co-Authored-By: Claude Opus 5 --- cascade | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/cascade b/cascade index b2dc8ae..438bd80 100755 --- a/cascade +++ b/cascade @@ -1492,13 +1492,24 @@ cmd_close() { tv="$(mget "$wave" --arg r "$repo" '.repos[$r].target_version')" pf="buildSrc/src/main/kotlin/io/spine/dependency/local/$(repo_pin_file "$repo")" [ -f "$pf" ] || continue - grep -oE 'val (version|versionForBuildScript|dogfoodingVersion|fallbackVersion|fallbackDfVersion)(: String)? = "[^"]+"' "$pf" \ + # Only the pin object's OWN constants, which sit at indent 4. A nested + # object declares a different artifact on its own version line — e.g. + # `ToolBase.JavadocFilter.version` is `spine-javadoc-filter`, versioned + # independently of `tool-base`. Matching it would pin that artifact at a + # version it was never published under. + # A no-match `grep` exits 1, which under `pipefail` would abort the + # whole run; warn instead, so a pin file whose shape changed is loud + # rather than silently skipped. + pins="$(grep -oE '^ {4}(private )?(const |override )?val (version|versionForBuildScript|dogfoodingVersion|fallbackVersion|fallbackDfVersion)(: String)? = "[^"]+"' "$pf" || true)" + [ -n "$pins" ] || warn "$repo: no version constant matched in $(basename "$pf") — left untouched" + printf '%s\n' "$pins" \ | while read -r line; do - c="$(printf '%s' "$line" | sed -E 's/val ([A-Za-z]+).*/\1/')" + [ -n "$line" ] || continue + c="$(printf '%s' "$line" | sed -E 's/.*val ([A-Za-z]+).*/\1/')" cur="$(printf '%s' "$line" | sed -E 's/.*"([^"]+)".*/\1/')" [ "$cur" = "$tv" ] && continue cur_re="${cur//./\\.}" - sed_inplace -E "s/(val $c(: String)? = \")$cur_re(\")/\1$tv\3/" "$pf" + sed_inplace -E "s/^( {4}(private )?(const |override )?val $c(: String)? = \")$cur_re(\")/\1$tv\5/" "$pf" done done if [ -n "$(git status --porcelain buildSrc/src/main/kotlin/io/spine/dependency/local/)" ]; then From 2fb7f83ec15c13f515fed1f6b85f58f20779dbc1 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Thu, 3 Sep 2026 17:09:51 +0100 Subject: [PATCH 39/44] Update cascade state --- .agents/tasks/cascade-refresh-2cd0e3b.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.agents/tasks/cascade-refresh-2cd0e3b.json b/.agents/tasks/cascade-refresh-2cd0e3b.json index 41fb0a0..b382953 100644 --- a/.agents/tasks/cascade-refresh-2cd0e3b.json +++ b/.agents/tasks/cascade-refresh-2cd0e3b.json @@ -382,7 +382,7 @@ "group_path": "io/spine", "artifact": "spine-rdbms" }, - "state": "published-local", + "state": "published-remote", "target_version": "2.0.0-SNAPSHOT.121", "pin_vector": { "core-jvm": "2.0.0-SNAPSHOT.551", @@ -415,7 +415,7 @@ "group_path": "io/spine/gcloud", "artifact": "spine-datastore" }, - "state": "published-local", + "state": "published-remote", "target_version": "2.0.0-SNAPSHOT.231", "pin_vector": { "base-libraries": "2.0.0-SNAPSHOT.442", @@ -450,7 +450,7 @@ "group_path": "io/spine/delivery", "artifact": "spine-delivery-server" }, - "state": "published-local", + "state": "published-remote", "target_version": "0.17.1", "pin_vector": { "core-jvm": "2.0.0-SNAPSHOT.551", @@ -490,7 +490,7 @@ }, "money": { "adapt_rounds": 3, - "state": "bumped", + "state": "published-remote", "target_version": "2.0.0-SNAPSHOT.11", "pin_vector": {}, "built": { @@ -508,5 +508,5 @@ "testlib": "straggler: config submodule generations behind — first pull is a migration (opt in via --include)", "money": "straggler: config submodule generations behind — first pull is a migration (opt in via --include)" }, - "closing_pr": null + "closing_pr": "https://github.com/SpineEventEngine/config/pull/759" } From d7ca13649df875a5113be4d4faf3f9277bfc31d0 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Thu, 3 Sep 2026 17:22:34 +0100 Subject: [PATCH 40/44] Point submodules at the wave's merged masters The `cascade-refresh-2cd0e3b` wave is complete: every repository published, and `config` now carries the version vector recorded by config#759. Each submodule moves from its wave branch to the master that now contains it. `dokka-tools` moves off `fix-javadoc-publication`, merged during the same stretch. Co-Authored-By: Claude Opus 5 --- ProtoTap | 2 +- base-libraries | 2 +- base-types | 2 +- change | 2 +- compiler | 2 +- config | 2 +- core-jvm | 2 +- core-jvm-compiler | 2 +- delivery-server | 2 +- dokka-tools | 2 +- gcloud-jvm | 2 +- jdbc-storage | 2 +- logging | 2 +- money | 2 +- time | 2 +- tool-base | 2 +- validation | 2 +- 17 files changed, 17 insertions(+), 17 deletions(-) diff --git a/ProtoTap b/ProtoTap index 39d9103..8711b79 160000 --- a/ProtoTap +++ b/ProtoTap @@ -1 +1 @@ -Subproject commit 39d9103c676bc9a8e859c8c4dc9363fa8a4bc40b +Subproject commit 8711b79f3e20b017197b8c0a9054312b9e836764 diff --git a/base-libraries b/base-libraries index fa7d185..948b22b 160000 --- a/base-libraries +++ b/base-libraries @@ -1 +1 @@ -Subproject commit fa7d1854025d1d4e7b44a08b03f1c4cff4cd7235 +Subproject commit 948b22b81d37fbf2e18004b6b37578b046c42b04 diff --git a/base-types b/base-types index 0f2898b..f955c57 160000 --- a/base-types +++ b/base-types @@ -1 +1 @@ -Subproject commit 0f2898be841a4f0f92497dd597ab04e5216218ba +Subproject commit f955c573a5c9f2d785a3eb8c851fca731baadda9 diff --git a/change b/change index 4853940..57b5b85 160000 --- a/change +++ b/change @@ -1 +1 @@ -Subproject commit 4853940add7002d6b1c80915190e8a824f9149e3 +Subproject commit 57b5b85219de1990939a312d700c073b7436e12a diff --git a/compiler b/compiler index 85302f2..3a5a776 160000 --- a/compiler +++ b/compiler @@ -1 +1 @@ -Subproject commit 85302f29f473df13ea540aad1981211645d8232b +Subproject commit 3a5a77619846df12ed662a5321f429945b3a0c47 diff --git a/config b/config index b728a70..ea0d20d 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit b728a708f7f5f5a8d5a866b7c0a8d3a0f924b9e2 +Subproject commit ea0d20d951ae670294b63dcba1a0b02eccce1198 diff --git a/core-jvm b/core-jvm index 0ddca2c..8b3b049 160000 --- a/core-jvm +++ b/core-jvm @@ -1 +1 @@ -Subproject commit 0ddca2c79a4a4d7b893edf8be88158763a119a96 +Subproject commit 8b3b0498ac847dd33001b8eb94bcdd4123a2e726 diff --git a/core-jvm-compiler b/core-jvm-compiler index 2425d25..15b5c36 160000 --- a/core-jvm-compiler +++ b/core-jvm-compiler @@ -1 +1 @@ -Subproject commit 2425d257b3829f7531562ae673b845e972853620 +Subproject commit 15b5c36b6277b36fa7e68fcd648efda66b3781e2 diff --git a/delivery-server b/delivery-server index 0b79247..33d6c3c 160000 --- a/delivery-server +++ b/delivery-server @@ -1 +1 @@ -Subproject commit 0b79247951497e2764a13cbf326b8616255ccffc +Subproject commit 33d6c3c065ff67ad1dd03fd6468e6fc0b5f15360 diff --git a/dokka-tools b/dokka-tools index 489ece2..057439b 160000 --- a/dokka-tools +++ b/dokka-tools @@ -1 +1 @@ -Subproject commit 489ece28211171248c016495f897f62f7a69857d +Subproject commit 057439b62ea026a77f1ee3b39b8058642c656b05 diff --git a/gcloud-jvm b/gcloud-jvm index 13f6b45..3090e2a 160000 --- a/gcloud-jvm +++ b/gcloud-jvm @@ -1 +1 @@ -Subproject commit 13f6b45d15001cbd562e7ae54cdce42bff6600ba +Subproject commit 3090e2a2d81cc50bd92c2f3c1f8601716c6eefd8 diff --git a/jdbc-storage b/jdbc-storage index 0582f22..c747908 160000 --- a/jdbc-storage +++ b/jdbc-storage @@ -1 +1 @@ -Subproject commit 0582f2293f3925a11942daab8c8cf03a90b0585a +Subproject commit c747908403764eb9025826c0009d3928edd5882e diff --git a/logging b/logging index 1ca1eee..e4bcc0d 160000 --- a/logging +++ b/logging @@ -1 +1 @@ -Subproject commit 1ca1eee7500ba9d89bdac7c60db12c5ddfe99bfc +Subproject commit e4bcc0df8c991c6b7f49267b85fb11804f2ffebc diff --git a/money b/money index d8d5f6b..13e2f83 160000 --- a/money +++ b/money @@ -1 +1 @@ -Subproject commit d8d5f6ba522e21b690b68ac0401d393ee226204d +Subproject commit 13e2f83aed3c2686fbb95e5f4f8dfb7eb2a9c41c diff --git a/time b/time index 4daeedf..4c347eb 160000 --- a/time +++ b/time @@ -1 +1 @@ -Subproject commit 4daeedfb6a1f961c0fa8dc3692b330d370dbfbbc +Subproject commit 4c347ebde8b1d897af1ade29fa616d108e4257d7 diff --git a/tool-base b/tool-base index 7faea57..fb816d3 160000 --- a/tool-base +++ b/tool-base @@ -1 +1 @@ -Subproject commit 7faea573ce9bc5540f4a493817423c4c1160c27f +Subproject commit fb816d35829cabc0b15a422bca92f8e71003d061 diff --git a/validation b/validation index 15a7be4..6465029 160000 --- a/validation +++ b/validation @@ -1 +1 @@ -Subproject commit 15a7be44f24d4732588626248f43a6b1f6b922d6 +Subproject commit 6465029c83b9569610d39cea0b57faec5cce259c From 7c01d61c4aa8eb8891184fd4dd10d9bb29c7a23e Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Thu, 3 Sep 2026 17:54:18 +0100 Subject: [PATCH 41/44] Address the pre-PR review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Pin-file handling, factored into one place.** `cmd_close` had three defects fixed earlier in this branch; review found the same shape still live in `cmd_prep` and `derive_state`, which run far more often. In `cmd_prep` it was worse than at `close`: the match was unanchored, so on `ToolBase.kt` a `head -1` could take the nested `JavadocFilter.version` — a different artifact — and the verification step counted matches anywhere in the file, so a run that clobbered the wrong constant still reported success. All three now share `read_pin_constant`, `read_pin_constant_at` and `write_pin_constant`, anchored to the pin object's own indent-4 constants and accepting every declaration form in use: `const val`, `private const val`, and the `override val` of a `Dependency()` subclass. **Silent aborts.** A `grep` matching nothing exits 1, and under `pipefail` that killed the caller before its own diagnostic could run — so `cmd_prep` died without printing "constant not found", and `version_of` died before `die "cannot read $prop"`. Both now absorb the no-match and let the caller report. **Permissions.** `allow` carried `Bash(git push:*)`, which matches any arguments, while `deny` named no force variant — so `git push --force` was reachable. `safety-rules.md` is explicit that force-push, `git tag`, `git rebase`, `gh release create` and `gh pr merge` stay per-action even under a grant. Force-push is now denied and `git tag` asks; the other three were already covered. **Documentation accuracy.** Only `ship` performs the upstream drift check, not `status`. Not every exit-3 message names a skill. The prerequisites omitted the `read:packages` scope, without which `ship` and `close` cannot verify `delivery-server`, which publishes to GitHub Packages — a gap that blocked this very wave. One prose example used Mermaid's invalid single-dash arrow. Co-Authored-By: Claude Opus 5 --- .claude/settings.json | 4 ++++ cascade | 50 ++++++++++++++++++++++++++++++++-------- docs/dependency-graph.md | 2 +- docs/rollout/rebuild.md | 11 +++++---- 4 files changed, 52 insertions(+), 15 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index e31720a..7f8e7cc 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -47,6 +47,9 @@ "deny": [ "Bash(git reset --hard:*)", "Bash(git clean -fdx:*)", + "Bash(git push --force:*)", + "Bash(git push -f:*)", + "Bash(git push --force-with-lease:*)", "Bash(rm -rf /:*)", "Bash(rm -rf ~:*)", "Bash(gh pr merge:*)", @@ -54,6 +57,7 @@ ], "ask": [ "Bash(git rebase:*)", + "Bash(git tag:*)", "Bash(git merge:*)", "Bash(git cherry-pick:*)", "Bash(./gradlew publish:*)", diff --git a/cascade b/cascade index 438bd80..baee25f 100755 --- a/cascade +++ b/cascade @@ -53,6 +53,38 @@ sed_inplace() { if sed --version >/dev/null 2>&1; then sed -i "$@"; else sed -i '' "$@"; fi } +# --- Pin-file constants ------------------------------------------------------ +# A pin object's OWN constants sit at indent 4. A nested object declares a +# DIFFERENT artifact on its own version line — `ToolBase.JavadocFilter.version` +# is `spine-javadoc-filter`, versioned independently of `tool-base` — so a match +# must never reach deeper indentation. All declaration forms in use are +# accepted: `const val`, `private const val`, and the `override val` of a +# `Dependency()` subclass. +PIN_CONSTANTS='version|versionForBuildScript|dogfoodingVersion|fallbackVersion|fallbackDfVersion' + +pin_const_re() { # + printf '^ {4}(private )?(const |override )?val (%s)(: String)? = "' "$1" +} + +# Value of one constant, or empty. Never fails: a no-match `grep` exits 1, +# which under `pipefail` would abort the caller before it could report. +read_pin_constant() { # + grep -E "$(pin_const_re "$2")" "$1" 2>/dev/null | head -1 \ + | sed -E 's/.*"([^"]+)".*/\1/' || true +} + +# Same, reading the file out of a git ref rather than the work tree. +read_pin_constant_at() { # + git -C "$1" show "$2:$3" 2>/dev/null | grep -E "$(pin_const_re "$4")" | head -1 \ + | sed -E 's/.*"([^"]+)".*/\1/' || true +} + +# Rewrites one constant in place, anchored so a nested object cannot match. +write_pin_constant() { # + local old_re="${3//./\\.}" + sed_inplace -E "s/^( {4}(private )?(const |override )?val ($2)(: String)? = \")$old_re(\")/\1$4\6/" "$1" +} + # --------------------------------------------------------------------------- # Static knowledge. Everything here is verified at `plan` time (probe markers # against the registry; graph vs fresh derivation), so a wrong entry fails @@ -225,7 +257,7 @@ version_of() { # [] -> current version literal (empty if none) # covers `extra.set("prop", "X")`, `val prop = "X"`, and # `val prop: String by extra("X")` alike. v="$(printf '%s\n' "$content" | grep -E "(^|[^A-Za-z])$prop([^A-Za-z]|$)" \ - | grep -oE '"[0-9][0-9A-Za-z.-]*"' | head -1 | tr -d '"')" + | grep -oE '"[0-9][0-9A-Za-z.-]*"' | head -1 | tr -d '"' || true)" if [ -z "$v" ]; then # 2) Indirection: `extra.set("prop", alias)` — resolve the alias. alias="$(printf '%s\n' "$content" \ @@ -988,8 +1020,8 @@ derive_state() { # -> prints state [ -n "$pv" ] || { ok=0; break; } pf="$SUMMIT/$repo/buildSrc/src/main/kotlin/io/spine/dependency/local/$(repo_pin_file "$up")" for c in $(repo_track_constants "$up"); do - have="$(git -C "$dir" show "$branch:buildSrc/src/main/kotlin/io/spine/dependency/local/$(repo_pin_file "$up")" 2>/dev/null \ - | grep -E "val $c(: String)? = \"" | head -1 | sed -E 's/.*"([^"]+)".*/\1/')" + have="$(read_pin_constant_at "$dir" "$branch" \ + "buildSrc/src/main/kotlin/io/spine/dependency/local/$(repo_pin_file "$up")" "$c")" [ "$have" = "$pv" ] || { ok=0; break 2; } done done @@ -1187,12 +1219,11 @@ cmd_prep() { pf="buildSrc/src/main/kotlin/io/spine/dependency/local/$(repo_pin_file "$up")" [ -f "$pf" ] || die "$repo has no $pf — unexpected layout" for c in $(repo_track_constants "$up"); do - cur="$(grep -E "val $c(: String)? = \"" "$pf" | head -1 | sed -E 's/.*"([^"]+)".*/\1/')" + cur="$(read_pin_constant "$pf" "$c")" [ -n "$cur" ] || { warn "constant $c not found in $repo/$pf — edit by hand (cascade-adapt), then re-run: ./cascade prep $repo"; exit 3; } [ "$cur" = "$upver" ] && continue - cur_re="${cur//./\\.}" - sed_inplace -E "s/(val $c(: String)? = \")$cur_re(\")/\1$upver\3/" "$pf" - nfound="$(grep -cE "val $c(: String)? = \"$upver\"" "$pf" || true)" + write_pin_constant "$pf" "$c" "$cur" "$upver" + nfound="$(grep -cE "$(pin_const_re "$c")${upver//./\\.}\"" "$pf" || true)" [ "$nfound" -ge 1 ] || { git checkout -- "$pf"; warn "pin edit verification failed for $up.$c in $repo — fix by hand, then re-run: ./cascade prep $repo"; exit 3; } done mset "$wave" '.repos[$r].pin_vector[$u] = $v' --arg r "$repo" --arg u "$up" --arg v "$upver" @@ -1500,7 +1531,7 @@ cmd_close() { # A no-match `grep` exits 1, which under `pipefail` would abort the # whole run; warn instead, so a pin file whose shape changed is loud # rather than silently skipped. - pins="$(grep -oE '^ {4}(private )?(const |override )?val (version|versionForBuildScript|dogfoodingVersion|fallbackVersion|fallbackDfVersion)(: String)? = "[^"]+"' "$pf" || true)" + pins="$(grep -oE "$(pin_const_re "$PIN_CONSTANTS")[^\"]+\"" "$pf" || true)" [ -n "$pins" ] || warn "$repo: no version constant matched in $(basename "$pf") — left untouched" printf '%s\n' "$pins" \ | while read -r line; do @@ -1508,8 +1539,7 @@ cmd_close() { c="$(printf '%s' "$line" | sed -E 's/.*val ([A-Za-z]+).*/\1/')" cur="$(printf '%s' "$line" | sed -E 's/.*"([^"]+)".*/\1/')" [ "$cur" = "$tv" ] && continue - cur_re="${cur//./\\.}" - sed_inplace -E "s/^( {4}(private )?(const |override )?val $c(: String)? = \")$cur_re(\")/\1$tv\5/" "$pf" + write_pin_constant "$pf" "$c" "$cur" "$tv" done done if [ -n "$(git status --porcelain buildSrc/src/main/kotlin/io/spine/dependency/local/)" ]; then diff --git a/docs/dependency-graph.md b/docs/dependency-graph.md index 9c4c235..41808d8 100644 --- a/docs/dependency-graph.md +++ b/docs/dependency-graph.md @@ -71,7 +71,7 @@ distributes to all of them identically. It is therefore a property of the shared build configuration rather than of any repository, and the edges below — derived from repository-owned build files — do not show it. -The direction that *is* shown, `dokka-tools -> base-libraries`, is a real +The direction that *is* shown, `dokka-tools --> base-libraries`, is a real declared dependency: the plugin uses `io.spine.annotation.Internal`. The reverse direction is a documentation-time dependency on a published version, so it constrains nothing about build order. diff --git a/docs/rollout/rebuild.md b/docs/rollout/rebuild.md index 23849cd..802b0ce 100644 --- a/docs/rollout/rebuild.md +++ b/docs/rollout/rebuild.md @@ -7,8 +7,9 @@ repository plus a closing `config` PR that records the new version vector. The deterministic mechanics live in the repo-owned [`cascade`](../../cascade) script; the judgement steps — fixing build breakage, the pre-PR review, and the version edge cases — are agent-driven, requested by the script via **exit code 3** -(the message always names the skill and the resume command). State is derived, -never remembered: interrupt anything, re-run it, and the wave resumes. +(the message always names a resume command, and a skill when the fix needs +judgement). State is derived, never remembered: interrupt anything, re-run it, +and the wave resumes. How a wave uses [`../dependency-graph.md`](../dependency-graph.md): only dependencies on repositories *earlier* in that document's canonical build @@ -30,7 +31,9 @@ Wave kinds: - A durable `export JAVA_HOME="$(/usr/libexec/java_home -v 17)"` in your shell profile — agent-run builds (`pre-pr`, `bump-version`) do not inherit the script's own export. -- `gh auth status` shows a token with `repo` + `workflow` scope. +- `gh auth status` shows a token with `repo` + `workflow` scope, plus + `read:packages` — `delivery-server` publishes to GitHub Packages, and + `ship` and `close` cannot verify its artifacts without that scope. - Builds run **strictly sequentially** — never start a second Gradle build while a wave build runs. Maven Central consumption blocks (HTTP 429) are machine-wide and retries extend them up to 24 h; the script halts the wave on the first 429 @@ -96,7 +99,7 @@ acknowledge throttle halts. Everything else is the loop. ## 3. Drift If an upstream re-bumps while a downstream PR is in review (registry collision -or breaking-PR reclassification), `ship`/`status` detect it: +or breaking-PR reclassification), `ship` detects it: ```bash ./cascade refresh # re-pin, then rebuild + pre-pr + ship again From 44f170aad2668ede0c4099e40c7bdce3cf1c51d9 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Thu, 3 Sep 2026 18:03:41 +0100 Subject: [PATCH 42/44] Address the PR review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **A park did not hold the whole downstream.** `next` only looked at direct upstreams whose *stored* state was literally `parked`. With `A -> B -> C` and `A` parked, `B` was blocked but `C` was not, so `next` could schedule `prep` for `C` against an unbuilt `B`. Blocking is now computed in a pass over `.order`, which is topological, so it propagates. **A parked repo held the wave open forever.** `done_all` required `published-remote` from every repo, and a park is terminal — `close` skips parked repos. `BuildSpeed` is parked on every refresh, so `CLOSE` could never be emitted and `next` returned `WAIT` indefinitely. With this change `next` reports `DONE` for the completed wave, where before it reported `WAIT`. **Rebuilding at an unchanged version left stale artifacts.** `publishToMavenLocal` ran only when the marker directory was absent. `refresh` rebuilds a repo at the same version by design, so the directory existed and publication was skipped — downstream builds then resolved the previous HEAD's artifacts while the manifest recorded the new HEAD as built. Publication now follows every successful build. **A published ripple seed was scheduled for a version bump.** `plan` initialized every scoped repo as `pending`, including a seed named as `repo@version`, which by definition is already published. `next` then scheduled `prep` for it and bumped it a second time. Such a seed now starts terminal at the supplied version; downstream repos still read that version through `.seed.version`. **`close --ship` skipped the review gate.** It pushed and opened the config PR without checking the sentinel. The script's own `git push` and `gh pr create` never reach the PreToolUse hooks, so the documented close -> pre-pr -> close --ship step rested on operator memory. It now applies the same HEAD-bound gate as `ship`. Also: `floor_edges()` was defined twice with identical bodies; the duplicate is gone. And the `JAVA_HOME` prerequisite gave a macOS-only incantation without saying so. Co-Authored-By: Claude Opus 5 --- cascade | 56 ++++++++++++++++++++++++++++++----------- docs/rollout/rebuild.md | 8 +++--- 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/cascade b/cascade index baee25f..3828c62 100755 --- a/cascade +++ b/cascade @@ -532,7 +532,6 @@ filter_in_set() { done return 0 } -floor_edges() { while read -r a b; do [ "$(order_pos "$a")" -ge "$(order_pos "$b")" ] && echo "$a $b"; done || true; } # Edge pairs currently stored in the graph doc — the canonical machine list in # the ```edges fenced block (the mermaid diagram is its transitive reduction, @@ -928,7 +927,7 @@ cmd_plan() { # Manifest. mkdir -p "$TASKS_DIR" - local repos_json="{}" ups gp mk vp cls tv + local repos_json="{}" ups gp mk vp cls tv st0 for repo in $final; do ups="$(printf '%s\n' "$fwd" | awk -v r="$repo" '$2==r{print $1}' \ | filter_in_set "$final" | jq -R . | jq -s 'map(select(length > 0))')" @@ -936,12 +935,20 @@ cmd_plan() { vp="$(meta_vprop "$repo")"; cls="$(meta_class "$repo")" tv="null" [ "$kind" = retarget ] && [ "$cls" = library ] && tv="\"$target\"" + # A ripple seed given as `repo@version` is already published by hand: + # record it terminal at that version so `next` does not schedule a + # `prep` that would bump it a second time. Downstream repos still read + # the supplied version through `.seed.version`. + st0='"pending"' + if [ "$kind" = ripple ] && [ "$repo" = "$seed" ] && [ -n "$seed_ver" ]; then + st0='"published-remote"'; tv="\"$seed_ver\"" + fi repos_json="$(printf '%s' "$repos_json" | jq \ --arg r "$repo" --argjson ups "$ups" --arg gp "$gp" --arg mk "$mk" \ - --arg vp "$vp" --arg cls "$cls" --argjson tv "$tv" \ + --arg vp "$vp" --arg cls "$cls" --argjson tv "$tv" --argjson st0 "$st0" \ '. + {($r): {upstreams: $ups, class: $cls, version_property: $vp, probe: {group_path: $gp, artifact: $mk}, - state: "pending", target_version: $tv, pin_vector: {}, + state: $st0, target_version: $tv, pin_vector: {}, adapt_rounds: 0, built: null, pr: null, parked: null}}')" done jq -n \ @@ -1116,15 +1123,26 @@ cmd_next() { [ "$(mget "$wave" '.halted // empty')" != "" ] && { echo "HALTED $(mget "$wave" .halted)"; exit 5; } - local actions="" repo st up upok upst done_all=1 + # A park holds everything downstream of it, not just its direct consumers. + # `.order` is topological, so one pass propagates the block transitively. + local blocked_set=" " repo up upst for repo in $(mget "$wave" '.order[]'); do mget "$wave" --arg r "$repo" '.repos[$r] // empty' | grep -q . || continue - st="$(mget "$wave" --arg r "$repo" '.repos[$r].state')" - [ "$st" = published-remote ] || done_all=0 - # A parked transitive upstream blocks everything downstream. for up in $(upstreams_of "$wave" "$repo"); do - [ "$(mget "$wave" --arg r "$up" '.repos[$r].state // empty')" = parked ] && st=blocked + upst="$(mget "$wave" --arg r "$up" '.repos[$r].state // empty')" + case "$blocked_set" in *" $up "*) upst=parked ;; esac + [ "$upst" = parked ] && { blocked_set="$blocked_set$repo "; break; } done + done + + local actions="" st upok done_all=1 + for repo in $(mget "$wave" '.order[]'); do + mget "$wave" --arg r "$repo" '.repos[$r] // empty' | grep -q . || continue + st="$(mget "$wave" --arg r "$repo" '.repos[$r].state')" + # A parked repo is terminal: `close` skips it, so it must not hold the + # wave open forever (`BuildSpeed` is parked on every refresh). + case "$st" in published-remote|parked) ;; *) done_all=0 ;; esac + case "$blocked_set" in *" $repo "*) st=blocked ;; esac case "$st" in pending|pulled|pinned) actions="$actions PREP:$repo" ;; bumped) actions="$actions BUILD:$repo" ;; @@ -1314,15 +1332,16 @@ cmd_build() { exit 3 fi - # Publish to mavenLocal where `build` did not already chain it. + # Publish to mavenLocal after every successful build. Keying this on the + # marker directory would skip it whenever a repo is rebuilt at an unchanged + # version — exactly what `refresh` does — leaving downstream builds to + # resolve artifacts from the previous HEAD. if [ "$cls" = library ]; then local tv gp mk tv="$(mget "$wave" --arg r "$repo" '.repos[$r].target_version')" gp="$(meta_group "$repo")"; mk="$(meta_marker "$repo")" - if [ ! -d "$M2/$gp/$mk/$tv" ]; then - ./gradlew publishToMavenLocal --console=plain >>"$logf" 2>&1 \ - || { tail -20 "$logf" >&2; warn "publishToMavenLocal failed — inspect $logf, fix (cascade-adapt), then: ./cascade build $repo"; exit 3; } - fi + ./gradlew publishToMavenLocal --console=plain >>"$logf" 2>&1 \ + || { tail -20 "$logf" >&2; warn "publishToMavenLocal failed — inspect $logf, fix (cascade-adapt), then: ./cascade build $repo"; exit 3; } [ -d "$M2/$gp/$mk/$tv" ] || die "$mk $tv missing from mavenLocal after publish — check spinePublishing modules for $repo" fi @@ -1547,6 +1566,15 @@ cmd_close() { git commit -q -m "Update local dependency pins" fi if [ "$ship" = 1 ]; then + # Same gate as `ship`. The script's own `git push`/`gh pr create` never + # reach the PreToolUse hooks, so without this the documented + # close -> pre-pr -> close --ship review step rests on operator memory. + local gitdir chead s_head s_status + gitdir="$(git rev-parse --absolute-git-dir)"; chead="$(git rev-parse HEAD)" + s_head="$(sed -n 's/^head=//p' "$gitdir/pre-pr.ok" 2>/dev/null || true)" + s_status="$(sed -n 's/^status=//p' "$gitdir/pre-pr.ok" 2>/dev/null || true)" + { [ "$s_status" = PASS ] && [ "$s_head" = "$chead" ]; } \ + || { warn "no PASS pre-pr sentinel for HEAD in config — run the pre-pr skill there, then: ./cascade close --ship"; exit 3; } git push -u origin "$vbranch" gh pr create -R "$ORG/config" --base "$BASE" --head "$vbranch" \ --title "Record the \`$wave\` version vector" \ diff --git a/docs/rollout/rebuild.md b/docs/rollout/rebuild.md index 802b0ce..55425e8 100644 --- a/docs/rollout/rebuild.md +++ b/docs/rollout/rebuild.md @@ -28,9 +28,11 @@ Wave kinds: ## 0. Prerequisites (once per machine) -- A durable `export JAVA_HOME="$(/usr/libexec/java_home -v 17)"` in your shell - profile — agent-run builds (`pre-pr`, `bump-version`) do not inherit the - script's own export. +- A durable `JAVA_HOME` pointing at a JDK 17 in your shell profile — agent-run + builds (`pre-pr`, `bump-version`) do not inherit the script's own export. On + macOS that is `export JAVA_HOME="$(/usr/libexec/java_home -v 17)"`; elsewhere + set it to the JDK 17 path directly, or use your distribution's alternatives + mechanism. - `gh auth status` shows a token with `repo` + `workflow` scope, plus `read:packages` — `delivery-server` publishes to GitHub Packages, and `ship` and `close` cannot verify its artifacts without that scope. From e679d6ef6e7e67cfd14c5fee95bf4c6adf50086f Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Thu, 3 Sep 2026 18:20:51 +0100 Subject: [PATCH 43/44] Widen the deprecation rule to bumps, not just edits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rule keyed on the lines being edited, which misses the case that actually recurs: a wave propagates a new dependency version, that version deprecates API our untouched code uses, and nothing in the diff points at the warning. By the old wording nobody owned those lines, so the warning could ride through every repo the wave touches and surface only at the removal, far from the change that caused it. Restated around the warnings a change introduces, by either route — the line was edited, or the baseline moved under it. Bounded with the split `cascade-adapt` already uses for build breakage: a deprecation with a documented, behaviour-preserving successor is fixed in the same PR; one whose replacement changes behaviour, or whose surface needs design judgement, parks the repo with a diagnosis instead of being guessed at to keep a wave moving. Renamed accordingly; the third-party attribution notes are unchanged. Co-Authored-By: Claude Opus 5 --- .agents/memory/MEMORY.md | 2 +- ...wn-deprecations-your-change-introduces.md} | 44 +++++++++++++------ 2 files changed, 32 insertions(+), 14 deletions(-) rename .agents/memory/{fix-deprecations-in-touched-code.md => own-deprecations-your-change-introduces.md} (51%) diff --git a/.agents/memory/MEMORY.md b/.agents/memory/MEMORY.md index 39da0b6..9595ca3 100644 --- a/.agents/memory/MEMORY.md +++ b/.agents/memory/MEMORY.md @@ -3,4 +3,4 @@ One line per memory. Read this at the start of every session; open a file only when its hook is relevant. -- [Fix deprecations in touched code](fix-deprecations-in-touched-code.md) — deprecated API on a line being edited is in scope, and `-q` hides the warnings that reveal it. +- [Own the deprecations your change introduces](own-deprecations-your-change-introduces.md) — a bump deprecates untouched code too; fix mechanical, park semantic, and never use `-q`. diff --git a/.agents/memory/fix-deprecations-in-touched-code.md b/.agents/memory/own-deprecations-your-change-introduces.md similarity index 51% rename from .agents/memory/fix-deprecations-in-touched-code.md rename to .agents/memory/own-deprecations-your-change-introduces.md index be47035..3017874 100644 --- a/.agents/memory/fix-deprecations-in-touched-code.md +++ b/.agents/memory/own-deprecations-your-change-introduces.md @@ -1,30 +1,48 @@ --- -name: fix-deprecations-in-touched-code -description: Fix deprecated API usage in code being touched, without being asked +name: own-deprecations-your-change-introduces +description: Fix deprecation warnings a change introduces, even in untouched lines metadata: type: feedback --- -When editing a file, fix deprecated API usage on the lines being touched — -do not leave it for a follow-up or wait to be asked. This applies whether or -not the deprecation predates the change. +You own every deprecation warning your change introduces — whether or not +your diff touches the line that warns. A change introduces one in two ways: + +- **You edited the line.** A deprecation already sitting on a line you + rewrite is in scope, even though it predates your change. +- **You moved the baseline under it.** A dependency bump — a cascade wave + propagating a new version, a Gradle upgrade — can deprecate API that + untouched code uses. Nothing in the diff points at those lines, which is + exactly why nobody looks at them. + +**How far to go** — the same split [[cascade-adapt]] applies to build +breakage: + +- **Mechanical** — the warning names a successor, or one is documented, and + it preserves behaviour: fix it in the same PR. A bump is not finished + while it emits warnings a successor would silence. +- **Semantic** — the replacement changes behaviour, or the surface is large + enough to need design judgement: park the repo, write the diagnosis into + the task file, and stop. Never guess at a replacement's semantics to keep + a wave moving. **Why:** in `config#754` a fixture line was rewritten while `val name by configurations.creating { }` was left in place. That syntax is deprecated *and scheduled for removal in Gradle 10*, so the file would have -stopped compiling on that upgrade. The reviewer had to catch it. Shipping a -line one has just edited, still carrying a removal-scheduled API, wastes a -review cycle on something the author should have seen. +stopped compiling on that upgrade, and a reviewer had to catch it. The bump +case is worse: no line in the diff points at the problem, so a warning can +ride along through every repo a wave touches and surface only at removal, +far from the change that caused it. **How to apply:** -- Treat a deprecation on a touched line as in scope, and say in the commit - message that the form predated the change. -- Verify with `./gradlew --warning-mode all`. Do **not** use `-q` when - checking for deprecations: it suppresses the nagging summary, which is - exactly how the case above was missed. +- Verify with `./gradlew --warning-mode all`. Do **not** use `-q`: + it suppresses the nagging summary, which is exactly how the case above was + missed. - A failed build aborts before the deprecation summary prints — re-check after the build is green, not only on the failing run. +- **After a dependency bump, read that summary even when nothing in the diff + looks related.** That is the case this rule exists for. - Ask about the whole construct, not one half of it. Asking a reviewer whether `isCanBeResolved` was deprecated returned a correct "no" while the `by ... creating` delegate wrapping it was the deprecated part. From b69f1ad2baf38893829222b6f4531306b5923788 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Thu, 3 Sep 2026 18:31:04 +0100 Subject: [PATCH 44/44] Archive the completed wave's task files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `cascade-refresh-2cd0e3b` wave is finished — every repository published and the closing vector merged — so its manifest and task file move to `.agents/tasks/archive/`. `active_wave` globs `$TASKS_DIR/cascade-*.json` without recursing, so an archived wave stops being auto-selected, which is what completion should mean. The move surfaced two defects in the script: `cmd_status` declared and assigned in one statement: local wave="${1:-$(active_wave)}" `local` returns its own exit status, so a failing substitution was masked, `errexit` never fired, and `active_wave`'s `die` did not stop the script — it carried on with an empty wave and printed a second, contradictory error. Splitting the declaration lets the failure surface once. This was unreachable while a manifest always existed. `manifest_path` looked only in the top level, so archiving a wave made it unreadable by the tool that wrote it, even when named. It now falls back to `archive/`, keeping a completed wave inspectable via `CASCADE_WAVE= ./cascade status` while staying out of the glob. Co-Authored-By: Claude Opus 5 --- .../{ => archive}/cascade-refresh-2cd0e3b.json | 0 .../tasks/{ => archive}/cascade-refresh-2cd0e3b.md | 0 cascade | 14 ++++++++++++-- 3 files changed, 12 insertions(+), 2 deletions(-) rename .agents/tasks/{ => archive}/cascade-refresh-2cd0e3b.json (100%) rename .agents/tasks/{ => archive}/cascade-refresh-2cd0e3b.md (100%) diff --git a/.agents/tasks/cascade-refresh-2cd0e3b.json b/.agents/tasks/archive/cascade-refresh-2cd0e3b.json similarity index 100% rename from .agents/tasks/cascade-refresh-2cd0e3b.json rename to .agents/tasks/archive/cascade-refresh-2cd0e3b.json diff --git a/.agents/tasks/cascade-refresh-2cd0e3b.md b/.agents/tasks/archive/cascade-refresh-2cd0e3b.md similarity index 100% rename from .agents/tasks/cascade-refresh-2cd0e3b.md rename to .agents/tasks/archive/cascade-refresh-2cd0e3b.md diff --git a/cascade b/cascade index 3828c62..df51d34 100755 --- a/cascade +++ b/cascade @@ -775,7 +775,14 @@ cmd_graph() { # Manifest access # --------------------------------------------------------------------------- -manifest_path() { echo "$TASKS_DIR/$1.json"; } +# A completed wave's manifest is moved to `archive/`, which keeps it out of +# `active_wave`'s glob while leaving it readable when named explicitly: +# CASCADE_WAVE= ./cascade status +manifest_path() { + local p="$TASKS_DIR/$1.json" + [ -f "$p" ] || p="$TASKS_DIR/archive/$1.json" + echo "$p" +} # Resolve the active wave: explicit $CASCADE_WAVE / --wave beats a lone manifest. active_wave() { @@ -1085,7 +1092,10 @@ derive_state() { # -> prints state } cmd_status() { - local wave="${1:-$(active_wave)}" + # Declared before assignment: `local x="$(cmd)"` returns `local`'s status, + # masking a failing substitution, so `active_wave`'s `die` would not stop + # the script and a second, confusing error would follow. + local wave; wave="${1:-$(active_wave)}" [ -f "$(manifest_path "$wave")" ] || die "no manifest for wave '$wave'" local repo st blocked up upst wbranch wbranch="$(mget "$wave" .branch)"