diff --git a/.agents/memory/MEMORY.md b/.agents/memory/MEMORY.md new file mode 100644 index 0000000..9595ca3 --- /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. + +- [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/own-deprecations-your-change-introduces.md b/.agents/memory/own-deprecations-your-change-introduces.md new file mode 100644 index 0000000..3017874 --- /dev/null +++ b/.agents/memory/own-deprecations-your-change-introduces.md @@ -0,0 +1,69 @@ +--- +name: own-deprecations-your-change-introduces +description: Fix deprecation warnings a change introduces, even in untouched lines +metadata: + type: feedback +--- + +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, 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:** + +- 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. +- 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. diff --git a/.agents/tasks/archive/cascade-refresh-2cd0e3b.json b/.agents/tasks/archive/cascade-refresh-2cd0e3b.json new file mode 100644 index 0000000..b382953 --- /dev/null +++ b/.agents/tasks/archive/cascade-refresh-2cd0e3b.json @@ -0,0 +1,512 @@ +{ + "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": "published-remote", + "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": "published-remote", + "target_version": "2.0.0-SNAPSHOT.425", + "pin_vector": { + "base-libraries": "2.0.0-SNAPSHOT.442" + }, + "adapt_rounds": 0, + "built": { + "head": "1d7a7ce5069e31a71a059919f8eea67e42d8008b", + "at": "2026-08-30T16:05:09Z" + }, + "pr": { + "url": "https://github.com/SpineEventEngine/logging/pull/150", + "head": "1d7a7ce5069e31a71a059919f8eea67e42d8008b" + }, + "parked": null + }, + "base-types": { + "upstreams": [ + "base-libraries" + ], + "class": "library", + "version_property": "versionToPublish", + "probe": { + "group_path": "io/spine", + "artifact": "spine-base-types" + }, + "state": "published-remote", + "target_version": "2.0.0-SNAPSHOT.225", + "pin_vector": { + "base-libraries": "2.0.0-SNAPSHOT.442" + }, + "adapt_rounds": 1, + "built": { + "head": "7860cc8500a6c185cf692cdd1922d57a53b16b6e", + "at": "2026-08-30T16:05:09Z" + }, + "pr": { + "url": "https://github.com/SpineEventEngine/base-types/pull/55", + "head": "7860cc8500a6c185cf692cdd1922d57a53b16b6e" + }, + "parked": null + }, + "change": { + "upstreams": [ + "base-libraries" + ], + "class": "library", + "version_property": "versionToPublish", + "probe": { + "group_path": "io/spine", + "artifact": "spine-change" + }, + "state": "published-remote", + "target_version": "2.0.0-SNAPSHOT.208", + "pin_vector": { + "base-libraries": "2.0.0-SNAPSHOT.442" + }, + "adapt_rounds": 2, + "built": { + "head": "b4dd302c539548b59440d7acf3d9db0b05023052", + "at": "2026-08-30T16:05:09Z" + }, + "pr": { + "url": "https://github.com/SpineEventEngine/change/pull/9", + "head": "b4dd302c539548b59440d7acf3d9db0b05023052" + }, + "parked": null + }, + "tool-base": { + "upstreams": [ + "base-libraries", + "logging" + ], + "class": "library", + "version_property": "versionToPublish", + "probe": { + "group_path": "io/spine/tools", + "artifact": "spine-tool-base" + }, + "state": "published-remote", + "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": { + "head": "c61bc84c80ca9889493ad6ac19cdecfe065ef472", + "at": "2026-08-29T18:26:05Z" + }, + "pr": { + "url": "https://github.com/SpineEventEngine/tool-base/pull/191", + "head": "c61bc84c80ca9889493ad6ac19cdecfe065ef472" + }, + "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": "published-remote", + "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": { + "url": "https://github.com/SpineEventEngine/ProtoTap/pull/21", + "head": "ff4a87a9718f8b392e8990166bec7aeaaa307e8b" + }, + "parked": null + }, + "time": { + "upstreams": [ + "ProtoTap", + "base-libraries", + "logging", + "tool-base" + ], + "class": "library", + "version_property": "versionToPublish", + "probe": { + "group_path": "io/spine", + "artifact": "spine-time" + }, + "state": "published-remote", + "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": { + "head": "04a90564452a4795472661528f3cf101a5d13e5f", + "at": "2026-09-01T14:44:36Z" + }, + "pr": { + "url": "https://github.com/SpineEventEngine/time/pull/200", + "head": "04a90564452a4795472661528f3cf101a5d13e5f" + }, + "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": "published-remote", + "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": { + "head": "81bac527015446a5b13c05ffd974955031376892", + "at": "2026-08-29T22:44:14Z" + }, + "pr": { + "url": "https://github.com/SpineEventEngine/compiler/pull/87", + "head": "2cc050c57dba3e743fca12918e3a1d6edb9e8ff6" + }, + "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": "published-remote", + "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": { + "url": "https://github.com/SpineEventEngine/validation/pull/328", + "head": "28e89a534d7ad32a5b1b69adac6445ad52594f53" + }, + "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": "published-remote", + "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": { + "head": "627c6f62f4bfc2d416c85724bc19e5e06e028d1c", + "at": "2026-08-29T22:57:23Z" + }, + "pr": { + "url": "https://github.com/SpineEventEngine/core-jvm-compiler/pull/113", + "head": "b75e7947eae0f820b6a4f2d3d71399e95984dbb6" + }, + "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": "published-remote", + "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": { + "url": "https://github.com/SpineEventEngine/core-jvm/pull/1676", + "head": "8b964873214933f45515cecc687ff41b67848b24" + }, + "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": "published-remote", + "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": { + "head": "83e3453ff5758d2f5f9e24fe1cc8b7c1081db8d4", + "at": "2026-08-29T23:07:47Z" + }, + "pr": { + "url": "https://github.com/SpineEventEngine/jdbc-storage/pull/183", + "head": "6321333bc7101359e8c53efd21740d9016a3fef4" + }, + "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": "published-remote", + "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": { + "url": "https://github.com/SpineEventEngine/gcloud-jvm/pull/207", + "head": "037a580b75d85c729c5a43fa24e46ce1d759ec10" + }, + "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": "published-remote", + "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": { + "url": "https://github.com/SpineEventEngine/delivery-server/pull/65", + "head": "049a1b1f36f60157d5112da73d53f5b4c2dbf389" + }, + "parked": null + }, + "BuildSpeed": { + "upstreams": [ + "core-jvm-compiler" + ], + "class": "verify-only", + "version_property": "", + "probe": { + "group_path": "", + "artifact": "" + }, + "state": "parked", + "target_version": null, + "pin_vector": {}, + "adapt_rounds": 0, + "built": null, + "pr": 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": "published-remote", + "target_version": "2.0.0-SNAPSHOT.11", + "pin_vector": {}, + "built": { + "head": "99df67a17ab6f4a38955d31f76b61bf70780c2b8", + "at": "2026-09-02T16:56:20Z" + }, + "pr": { + "url": "https://github.com/SpineEventEngine/money/pull/31", + "head": "5aa54695245740a9bf5c1e5cbd1275e00ebb6a9e" + } + } + }, + "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": "https://github.com/SpineEventEngine/config/pull/759" +} diff --git a/.agents/tasks/archive/cascade-refresh-2cd0e3b.md b/.agents/tasks/archive/cascade-refresh-2cd0e3b.md new file mode 100644 index 0000000..efdeab4 --- /dev/null +++ b/.agents/tasks/archive/cascade-refresh-2cd0e3b.md @@ -0,0 +1,82 @@ +--- +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`. +- 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`. + +- 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). +- 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`. +- 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-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 + `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. 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/.claude/settings.json b/.claude/settings.json index 3c7da68..7f8e7cc 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -37,19 +37,27 @@ "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:*)", "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:*)", "Bash(gh release create:*)" ], "ask": [ - "Bash(git commit:*)", "Bash(git rebase:*)", + "Bash(git tag:*)", "Bash(git merge:*)", "Bash(git cherry-pick:*)", "Bash(./gradlew publish:*)", diff --git a/.gitmodules b/.gitmodules index 7682bb6..6f34f85 100644 --- a/.gitmodules +++ b/.gitmodules @@ -58,3 +58,15 @@ [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 +[submodule "dokka-tools"] + path = dokka-tools + url = https://github.com/SpineEventEngine/dokka-tools.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/ProtoTap b/ProtoTap index 43ac1ac..8711b79 160000 --- a/ProtoTap +++ b/ProtoTap @@ -1 +1 @@ -Subproject commit 43ac1acaa282398920c1706230241660a3817041 +Subproject commit 8711b79f3e20b017197b8c0a9054312b9e836764 diff --git a/README.md b/README.md index 162016e..4d09e1c 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,15 @@ 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 -| 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 +30,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 +128,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/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..948b22b 160000 --- a/base-libraries +++ b/base-libraries @@ -1 +1 @@ -Subproject commit 7a05857b2adc68dc2f9b28a03ae3073e7c3e9df3 +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/cascade b/cascade new file mode 100755 index 0000000..df51d34 --- /dev/null +++ b/cascade @@ -0,0 +1,1653 @@ +#!/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 +} + +# --- 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 +# 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 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). +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|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" ;; + 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" ;; + 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() { + # 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: +# extra.set("prop", "X") | val prop ... by extra("X") +version_of() { # [] -> current version literal (empty if none) + 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 + # 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 '"' || true)" + 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. +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" +} + +# 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)" \ + "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 } + # 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" + 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 +} + +# 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. + +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 + 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 +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 +# --------------------------------------------------------------------------- + +# 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() { + 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 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))')" + gp="$(meta_group "$repo")"; mk="$(meta_marker "$repo")" + 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" --argjson st0 "$st0" \ + '. + {($r): {upstreams: $ups, class: $cls, version_property: $vp, + probe: {group_path: $gp, artifact: $mk}, + state: $st0, 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; } + + # 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)" + + # 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="$(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 + [ "$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() { + # 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)" + 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 // empty' \ + | 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)" + 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; } + + # 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 + for up in $(upstreams_of "$wave" "$repo"); do + 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" ;; + 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. 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 + 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="$(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 + 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" + 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 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")" + ./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 + + # 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 + + # 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" + 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 + # 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 "$(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 + [ -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 + write_pin_constant "$pf" "$c" "$cur" "$tv" + 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 + # 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" \ + --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). 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 ;; + esac +done +set -- ${ARGS[@]+"${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/change b/change index 53fe702..57b5b85 160000 --- a/change +++ b/change @@ -1 +1 @@ -Subproject commit 53fe7029a18920b8cd5d79c8636affb79f77ce84 +Subproject commit 57b5b85219de1990939a312d700c073b7436e12a diff --git a/compiler b/compiler index 313a647..3a5a776 160000 --- a/compiler +++ b/compiler @@ -1 +1 @@ -Subproject commit 313a647055faf29942d94047fcbc256386a8ebdc +Subproject commit 3a5a77619846df12ed662a5321f429945b3a0c47 diff --git a/config b/config index 6cee3cf..ea0d20d 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit 6cee3cf52f3044f08a8433cf838713c804ea1084 +Subproject commit ea0d20d951ae670294b63dcba1a0b02eccce1198 diff --git a/core-jvm b/core-jvm index a408b0d..8b3b049 160000 --- a/core-jvm +++ b/core-jvm @@ -1 +1 @@ -Subproject commit a408b0d70dafd603efc55b89c8b4b6f3e8c19d3b +Subproject commit 8b3b0498ac847dd33001b8eb94bcdd4123a2e726 diff --git a/core-jvm-compiler b/core-jvm-compiler index 0eb8ec3..15b5c36 160000 --- a/core-jvm-compiler +++ b/core-jvm-compiler @@ -1 +1 @@ -Subproject commit 0eb8ec332cbad5731193bea5b22a21759300e4f9 +Subproject commit 15b5c36b6277b36fa7e68fcd648efda66b3781e2 diff --git a/delivery-server b/delivery-server new file mode 160000 index 0000000..33d6c3c --- /dev/null +++ b/delivery-server @@ -0,0 +1 @@ +Subproject commit 33d6c3c065ff67ad1dd03fd6468e6fc0b5f15360 diff --git a/docs/dependency-graph.md b/docs/dependency-graph.md new file mode 100644 index 0000000..41808d8 --- /dev/null +++ b/docs/dependency-graph.md @@ -0,0 +1,327 @@ +# 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. + +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. `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) + +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 + dokka-tools --> base-libraries + logging --> base-libraries + 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 + base-types --> core-jvm-compiler + change --> core-jvm-compiler + money --> core-jvm-compiler + time --> core-jvm-compiler + testlib --> logging + logging --> reflect + tool-base --> testlib + core-jvm --> time + ProtoTap --> tool-base + compiler --> tool-base + core-jvm-compiler --> 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 + base-types -.-> validation + change -.-> validation + core-jvm -.-> compiler + core-jvm -.-> core-jvm-compiler + core-jvm -.-> validation + core-jvm-compiler -.-> compiler + core-jvm-compiler -.-> validation + logging -.-> base-libraries + reflect -.-> base-libraries + testlib -.-> base-libraries + testlib -.-> logging + testlib -.-> reflect + time -.-> change + time -.-> compiler + time -.-> core-jvm-compiler + time -.-> validation + tool-base -.-> base-libraries + tool-base -.-> testlib + validation -.-> compiler +``` + +
+ +## 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: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. + +## 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 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 +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 compiler +delivery-server core-jvm +delivery-server core-jvm-compiler +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 +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 compiler +money core-jvm-compiler +money testlib +money validation +reflect testlib +testlib logging +testlib tool-base +time ProtoTap +time base-libraries +time compiler +time core-jvm-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..55425e8 --- /dev/null +++ b/docs/rollout/rebuild.md @@ -0,0 +1,130 @@ +# 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 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 +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 `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. +- 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, 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. +- `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` detects 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). diff --git a/documentation b/documentation new file mode 160000 index 0000000..9422d07 --- /dev/null +++ b/documentation @@ -0,0 +1 @@ +Subproject commit 9422d07b54d4bee5753f4589bdd1c5ccf43e2b70 diff --git a/dokka-tools b/dokka-tools new file mode 160000 index 0000000..057439b --- /dev/null +++ b/dokka-tools @@ -0,0 +1 @@ +Subproject commit 057439b62ea026a77f1ee3b39b8058642c656b05 diff --git a/gcloud-jvm b/gcloud-jvm index f4ade19..3090e2a 160000 --- a/gcloud-jvm +++ b/gcloud-jvm @@ -1 +1 @@ -Subproject commit f4ade19d8bf7666447f068607426475cda485afe +Subproject commit 3090e2a2d81cc50bd92c2f3c1f8601716c6eefd8 diff --git a/jdbc-storage b/jdbc-storage index 1b77832..c747908 160000 --- a/jdbc-storage +++ b/jdbc-storage @@ -1 +1 @@ -Subproject commit 1b778327a6d24c9834102af86e18f9c532b8ac98 +Subproject commit c747908403764eb9025826c0009d3928edd5882e diff --git a/logging b/logging index b883c8f..e4bcc0d 160000 --- a/logging +++ b/logging @@ -1 +1 @@ -Subproject commit b883c8f9707c105f6dafceb94b192dfd8a082662 +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 57d3dd9..4c347eb 160000 --- a/time +++ b/time @@ -1 +1 @@ -Subproject commit 57d3dd98fea8efcdc4a3843f91143acc2dce87dc +Subproject commit 4c347ebde8b1d897af1ade29fa616d108e4257d7 diff --git a/tool-base b/tool-base index cca5d03..fb816d3 160000 --- a/tool-base +++ b/tool-base @@ -1 +1 @@ -Subproject commit cca5d03ff5f313b5c03beb37c798c1fc65543919 +Subproject commit fb816d35829cabc0b15a422bca92f8e71003d061 diff --git a/validation b/validation index c737fc1..6465029 160000 --- a/validation +++ b/validation @@ -1 +1 @@ -Subproject commit c737fc15ff3b11e72bf3d2540d763015d8cd5908 +Subproject commit 6465029c83b9569610d39cea0b57faec5cce259c