You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tinkerpop.apache.org by sp...@apache.org on 2025/11/05 11:50:15 UTC

(tinkerpop) 01/01: Coding agent prompt for 3.8.0 risk assessment

This is an automated email from the ASF dual-hosted git repository.

spmallette pushed a commit to branch prompt
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit 1e63faca4e9f3e968fcc197d2b35320e2c6ddc82
Author: Stephen Mallette <st...@amazon.com>
AuthorDate: Thu Oct 30 15:34:19 2025 -0400

    Coding agent prompt for 3.8.0 risk assessment
    
    Included test project.
---
 docs/src/dev/developer/release.asciidoc            |   4 +
 docs/src/upgrade/index.asciidoc                    |   8 +-
 ...lease-3.8.x.asciidoc => release-3.8.0.asciidoc} |  96 +++--
 docs/upgrade/java/README.md                        |  80 ++++
 docs/upgrade/java/bin/gremlin-dev.sh               | 204 ++++++++++
 docs/upgrade/java/pom.xml                          | 154 ++++++++
 .../apache/tinkerpop/example/cmdb/Application.java |  27 ++
 .../example/cmdb/api/AlertController.java          |  41 ++
 .../example/cmdb/api/DeploymentController.java     |  41 ++
 .../example/cmdb/api/HealthController.java         |  42 ++
 .../example/cmdb/api/IncidentController.java       |  41 ++
 .../example/cmdb/api/ServiceController.java        |  48 +++
 .../example/cmdb/config/GremlinClientConfig.java   |  81 ++++
 .../example/cmdb/config/GremlinClientFactory.java  |  64 ++++
 .../tinkerpop/example/cmdb/domain/PathDto.java     |  26 ++
 .../example/cmdb/errors/ApiException.java          |  30 ++
 .../example/cmdb/errors/ErrorHandler.java          |  38 ++
 .../example/cmdb/graph/GremlinGateway.java         |  51 +++
 .../tinkerpop/example/cmdb/graph/MappingUtils.java |  53 +++
 .../example/cmdb/graph/TraversalBuilders.java      |  99 +++++
 .../example/cmdb/service/AlertService.java         |  55 +++
 .../example/cmdb/service/DeploymentService.java    |  38 ++
 .../example/cmdb/service/IncidentService.java      |  41 ++
 .../example/cmdb/service/ServiceGraphService.java  |  42 ++
 .../java/src/main/resources/application.yml        |  28 ++
 .../tinkerpop/example/cmdb/it/DataLoader.java      |  39 ++
 .../tinkerpop/example/cmdb/it/ServiceIT.java       | 254 ++++++++++++
 .../example/cmdb/unit/AlertServiceTest.java        |  66 ++++
 .../example/cmdb/unit/DeploymentServiceTest.java   |  43 +++
 .../example/cmdb/unit/ErrorHandlerTest.java        |  47 +++
 .../example/cmdb/unit/HealthControllerTest.java    |  54 +++
 .../example/cmdb/unit/IncidentServiceTest.java     |  45 +++
 .../example/cmdb/unit/ServiceGraphServiceTest.java |  54 +++
 .../java/src/test/resources/application-test.yml   |  23 ++
 docs/upgrade/prompts/upgrade-3.8.0-helper.md       |  66 ++++
 docs/upgrade/spec/cmdb-data.gremlin                | 167 ++++++++
 docs/upgrade/spec/spec.md                          | 424 +++++++++++++++++++++
 gremlin-python/src/main/python/radish/gremlin.py   |  16 +-
 pom.xml                                            |   2 +
 39 files changed, 2691 insertions(+), 41 deletions(-)

diff --git a/docs/src/dev/developer/release.asciidoc b/docs/src/dev/developer/release.asciidoc
index c529fe8509..290a4a12e3 100644
--- a/docs/src/dev/developer/release.asciidoc
+++ b/docs/src/dev/developer/release.asciidoc
@@ -132,6 +132,8 @@ during this period.
 ... All tickets not marked "Fixed", "Done", or "Implemented" for their Resolution should not have a Fix Version
 assigned (use common sense when reviewing these tickets before removing the Fix Version as it is possible the incorrect
 Resolution may have been assigned).
+. Check the state of `docs/upgrade/prompts` to ensure there is an `upgrade-x.y.z-helper.md` for the current version and
+that it is up to date. **This process needs to be refined.**
 . When all documentation changes are in place, use `bin/publish-docs.sh` to deploy a final `SNAPSHOT` representation
 of the docs and thus validate that there are no issues with the documentation generation process. Request review
 of the published documentation on the dev mailing list.
@@ -215,6 +217,8 @@ current release was under development as this new release will have those change
 . `git diff` and review the updated files
 . `mvn clean install` - need to build first so that the right version of the console is used with `bin/publish-docs.sh`
 .. This step should update the Gremlin.Net project file and Gremlin Javascript package file with the newly bumped version.
+. Check the state of `docs/upgrade/prompts` to ensure there is an `upgrade-x.y.z-helper.md` for the current version and
+that it is up to date. **This process needs to be refined.**
 . `bin/process-docs.sh` and validate the generated documentation locally. Don't rely on "BUILD SUCCESS" - scroll up
 through logs to ensure there were no errors and view the HTML directly. Code blocks that did not execute properly have
 a gray background and do not show the results of the commands. In the event of failure, especially with `GraphComputer`
diff --git a/docs/src/upgrade/index.asciidoc b/docs/src/upgrade/index.asciidoc
index b70afe57fb..2b0e041eaa 100644
--- a/docs/src/upgrade/index.asciidoc
+++ b/docs/src/upgrade/index.asciidoc
@@ -28,7 +28,13 @@ outlines new features, how to resolve breaking changes and other information spe
 equally useful to TinkerPop providers, who build libraries and other systems on the core APIs and protocols that
 TinkerPop exposes.
 
-include::release-3.8.x.asciidoc[]
+= TinkerPop 3.8.0
+
+image::gremlin-67.png[width=185]
+
+*Grix Greven*
+
+include::release-3.8.0.asciidoc[]
 
 include::release-3.7.x.asciidoc[]
 
diff --git a/docs/src/upgrade/release-3.8.x.asciidoc b/docs/src/upgrade/release-3.8.0.asciidoc
similarity index 96%
rename from docs/src/upgrade/release-3.8.x.asciidoc
rename to docs/src/upgrade/release-3.8.0.asciidoc
index 7af103dcd3..5e5e2d0007 100644
--- a/docs/src/upgrade/release-3.8.x.asciidoc
+++ b/docs/src/upgrade/release-3.8.0.asciidoc
@@ -14,13 +14,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
 ////
-
-= TinkerPop 3.8.0
-
-image::gremlin-67.png[width=185]
-
-*Grix Greven*
-
 == TinkerPop 3.8.0
 
 *Release Date: NOT OFFICIALLY RELEASED YET*
@@ -89,6 +82,7 @@ how breaking changes may affect your traversals.
 
 ==== Removed and Renamed Steps
 
+[[removal-of-aggregate-with-scope-and-store]]
 ===== Removal of `aggregate()` with `Scope` and `store()`
 
 The meaning of `Scope` parameters in `aggregate()` have always been unique compared to all other "scopable" steps.
@@ -104,7 +98,7 @@ and reserves the `Scope` parameter exclusively for "traverser-local" application
 its contextual meanings.
 
 This makes the `AggregateStep` globally scoped by default with eager evaluation. Lazy evaluation with `aggregate()` is
-achieved by wrapping the step in `local()`. 
+achieved by wrapping the step in `local()`.
 
 Similarly, `store()` is an eqivalent step to `aggregate(local)` and has been deprecated since 3.4.3. It is also removed
 along with `aggregate(local)`.
@@ -166,6 +160,7 @@ gremlin> g.V().out().barrier().local(aggregate("x")).select("x")
 
 See: link:https://github.com/apache/tinkerpop/blob/master/docs/src/dev/future/proposal-scoping-5.asciidoc[Lazy vs. Eager Evaluation]
 
+[[removal-of-has-key-traversal]]
 ===== Removal of has(key, traversal)
 
 The has(key, traversal) API has been removed in version 3.8.0 due to its confusing behavior that differed from other
@@ -198,6 +193,7 @@ gremlin> g.V().has("age", P.gt(30))
 
 See: link:https://issues.apache.org/jira/browse/TINKERPOP-1463[TINKERPOP-1463]
 
+[[none-and-discard]]
 ===== none() and discard()
 
 There is a complicated relationship with the `none()` and `discard()` steps that begs some discussion. Prior to this
@@ -207,6 +203,7 @@ it also helped make room for `none()` to be repurposed as `none(P)` which is a c
 
 ==== Type System Changes
 
+[[new-default-datetime-type]]
 ===== New Default DateTime Type
 
 The default implementation for date type in Gremlin is now changed from the `java.util.Date` to the more encompassing
@@ -232,6 +229,7 @@ For Java GLV, this change would impact users who are expecting the old `Date` ob
 application, in this case the recommendation is to update code to expect `OffsetDateTime` as part of the version
 upgrade.
 
+[[float-defaults-to-double]]
 ===== Float Defaults to Double
 
 The `GremlinLangScriptEngine` has been modified to treat float literals without explicit type suffixes (like 'm', 'f',
@@ -242,6 +240,7 @@ or 'd') as Double by default. Users who need `BigDecimal` precision can still us
 
 ===== Changes to `repeat()`
 
+[[repeat-step-global-children-semantics-change]]
 ====== repeat() Step Global Children Semantics Change
 
 The `repeat()` step has been updated to treat the repeat traversal as a global child in all cases. Previously, the
@@ -288,7 +287,7 @@ gremlin> g.withoutStrategies(RepeatUnrollStrategy).V(1, 2, 3).
 
 This change may affect traversals that relied on the previous hybrid behavior, particularly those using side effects
 or barrier steps within `repeat()`. Review any traversals using `repeat()` with steps like `aggregate()`, `store()`,
-or other barrier steps to ensure they produce the expected results. 
+or other barrier steps to ensure they produce the expected results.
 
 If you would like `repeat()` to behave similarly to how it did in 3.7.x, then you should wrap the repeat inside a
 `local()`. The following example demonstrates this:
@@ -340,6 +339,7 @@ gremlin> g.V().local(repeat(both().simplePath().order().by("name")).times(2)).pa
 
 See: link:https://issues.apache.org/jira/browse/TINKERPOP-3200[TINKERPOP-3200]
 
+[[modified-limit-skip-range-semantics-in-repeat]]
 ====== Modified limit() skip() range() Semantics in repeat()
 
 The semantics of `limit()`, `skip()`, and `range()` steps called with default `Scope` or explicit `Scope.global` inside
@@ -385,6 +385,7 @@ traversals use `limit()`, skip()`, or `range()` with default `Scope` or explicit
 is desired that the limit or range should apply across all loops then the `limit()`, `skip()`, or `range()` step should
 be moved out of the `repeat()` step.
 
+[[prevented-using-cap-inject-inside-repeat]]
 ====== Prevented using cap(), inject() inside repeat()
 
 `cap()` inside `repeat()` is now disallowed by the `StandardVerificationStrategy`. Using `cap()` inside `repeat()` would
@@ -393,10 +394,10 @@ the number of traversers to one, its use inside `repeat()` is limited.
 
 See: link:https://issues.apache.org/jira/browse/TINKERPOP-3195[TINKERPOP-3195]
 
-`inject()` inside `repeat()` is now also disallowed by the `StandardVerificationStrategy`. The usefulness of `inject()` 
-inside `repeat()` is questionable as the injections are exhausted after one iteration. Consider the following examples, 
-noting that the examples for version 3.7.4 demonstrate the effect of `RepeatUnrollStrategy` on `inject()` semantics, 
-which is problematic as strategies should not affect results. 3.8.0 examples do not disable the `RepeatUnrollStrategy` 
+`inject()` inside `repeat()` is now also disallowed by the `StandardVerificationStrategy`. The usefulness of `inject()`
+inside `repeat()` is questionable as the injections are exhausted after one iteration. Consider the following examples,
+noting that the examples for version 3.7.4 demonstrate the effect of `RepeatUnrollStrategy` on `inject()` semantics,
+which is problematic as strategies should not affect results. 3.8.0 examples do not disable the `RepeatUnrollStrategy`
 as the strategy was modified to be more restrictive in this version.
 
 [source,text]
@@ -420,7 +421,7 @@ gremlin> g.inject('x').repeat(inject('a')).times(5)
 The parent of inject()-step can not be repeat()-step: InjectStep(java.util.ArrayList$Itr@543da15)
 ----
 
-Before upgrading, users should look for usages of `inject()` inside `repeat()` and if it is determined that per-loop 
+Before upgrading, users should look for usages of `inject()` inside `repeat()` and if it is determined that per-loop
 injections are desired, it is possible to use `union()` and `constant()` instead.
 
 [source,text]
@@ -445,19 +446,20 @@ gremlin> g.inject('x').repeat(union(constant(['a','b']).limit(1).unfold(),identi
 ==>x
 ----
 
+[[stricter-repeatunrollstrategy]]
 ====== Stricter RepeatUnrollStrategy
 
-The `RepeatUnrollStrategy` has been updated to use a more conservative approach for determining which repeat traversals 
-are safe to unroll. Previously, the strategy would attempt to unroll most usages of `repeat()` used with `times()` 
-without `emit()`. This caused unintentional traversal semantic changes when some steps were unrolled (especially barrier 
-steps). 
+The `RepeatUnrollStrategy` has been updated to use a more conservative approach for determining which repeat traversals
+are safe to unroll. Previously, the strategy would attempt to unroll most usages of `repeat()` used with `times()`
+without `emit()`. This caused unintentional traversal semantic changes when some steps were unrolled (especially barrier
+steps).
 
-As of 3.8.0, the strategy will still only be applied if `repeat()` is used with `times()` without `emit()` but now only 
-applies to repeat traversals that contain exclusively safe, well-understood steps: `out()`, `in()`, `both()`, `inV()`, 
-`outV()`, `otherV()`, `has(key, value)`. 
+As of 3.8.0, the strategy will still only be applied if `repeat()` is used with `times()` without `emit()` but now only
+applies to repeat traversals that contain exclusively safe, well-understood steps: `out()`, `in()`, `both()`, `inV()`,
+`outV()`, `otherV()`, `has(key, value)`.
 
-Repeat traversals containing other steps will no longer be unrolled. There may be some performance differences for 
-traversals that previously benefited from automatic unrolling but the consistency of semantics outweighs the performance 
+Repeat traversals containing other steps will no longer be unrolled. There may be some performance differences for
+traversals that previously benefited from automatic unrolling but the consistency of semantics outweighs the performance
 impact.
 
 Examples of affected traversals include (but are not limited to):
@@ -474,7 +476,7 @@ g.V().repeat(both().sample(1)).times(2)
 *Migration Strategies*
 
 Before upgrading, analyze existing traversals which use `repeat()` with any steps other than `out()`, `in()`, `both()`,
-`inV()`, `outV()`, `otherV()`, `has(key, value)` and determine if the semantics of these traversals are as expected when 
+`inV()`, `outV()`, `otherV()`, `has(key, value)` and determine if the semantics of these traversals are as expected when
 the `RepeatUnrollStrategy` is disabled using `withoutStrategies(RepeatUnrollStrategy)`. If the semantics are unexpected
 the traversal should be restructured to no longer use `repeat()` by manually unrolling the steps inside `repeat()` or by
 moving affected steps outside the `repeat()`.
@@ -493,6 +495,7 @@ g.V().repeat(both()).times(2).dedup()
 
 See: link:https://issues.apache.org/jira/browse/TINKERPOP-3192[TINKERPOP-3192]
 
+[[split-on-empty-string]]
 ===== split() on Empty String
 
 The `split()` step will now split a string into a list of its characters if the given separator is an empty string.
@@ -510,6 +513,7 @@ g.inject("Hello").split("")
 
 See: link:https://issues.apache.org/jira/browse/TINKERPOP-3083[TINKERPOP-3083]
 
+[[asstring-no-longer-allow-nulls]]
 ===== asString() No Longer Allow Nulls
 
 The `asString()` step will no longer allow `null` input. An `IllegalArgumentException` will be thrown for consistency
@@ -517,6 +521,7 @@ with all other parsing steps (i.e. `asDate()`, `asBool()`, `asNumber()`).
 
 See: link:https://lists.apache.org/thread/q76pgrvhprosb4lty63bnsnbw2ljyl7m[DISCUSS] thread
 
+[[split-bulked-traversers-for-local]]
 ===== Split bulked traversers for `local()`
 
 Prior to 3.8.0, local() exhibited "traverser-local" semantics, where the local traversal would apply independently to
@@ -545,6 +550,7 @@ gremlin> g.V().out().barrier().local(count())
 
 See: link:https://issues.apache.org/jira/browse/TINKERPOP-3196[TINKERPOP-3196]
 
+[[add-barrier-to-most-sideeffect-steps]]
 ===== Add barrier to most SideEffect steps
 
 Prior to 3.8.0, the `group(String)`, `groupCount(String)`, `tree(String)` and `subgraph(String)` steps were non-blocking,
@@ -588,6 +594,7 @@ gremlin> g.V().local(groupCount("x")).select("x")
 ==>[v[1]:1,v[2]:1,v[3]:1,v[4]:1,v[5]:1,v[6]:1]
 ----
 
+[[choose-semantics]]
 ===== choose() Semantics
 
 Several enhancements and clarifications have been made to the `choose()` step in TinkerPop 3.8.0 to improve its behavior
@@ -720,11 +727,12 @@ gremlin> g.V().hasLabel("person").union(
 See: link:https://issues.apache.org/jira/browse/TINKERPOP-3178[TINKERPOP-3178],
 link:https://tinkerpop.apache.org/docs/3.8.0/reference/#choose-step[Reference Documentation - choose()]
 
+[[consistent-output-for-range-limit-tail]]
 ===== Consistent Output for range(), limit(), tail()
 
-The `range(local)`, `limit(local)`, and `tail(local)` steps now consistently return collections rather than automatically 
-unfolding single-element results when operating on iterable collections (List, Set, etc.). Previously, when these steps 
-operated on collections and the result contained only one element, the step would return the single element directly 
+The `range(local)`, `limit(local)`, and `tail(local)` steps now consistently return collections rather than automatically
+unfolding single-element results when operating on iterable collections (List, Set, etc.). Previously, when these steps
+operated on collections and the result contained only one element, the step would return the single element directly
 instead of a collection containing that element.
 
 This change ensures predictable return types based on the input type, making the behavior more consistent and intuitive.
@@ -732,9 +740,9 @@ Note that this change only affects iterable collections - Map objects continue t
 
 [WARNING]
 ====
-This is a breaking change that may require modifications to existing queries. If your queries relied on the previous 
-behavior of receiving single elements directly from `range(local)`, `limit(local)`, or `tail(local)` steps, you will 
-need to add `.unfold()` after these steps to maintain the same functionality. Without this update, some existing queries 
+This is a breaking change that may require modifications to existing queries. If your queries relied on the previous
+behavior of receiving single elements directly from `range(local)`, `limit(local)`, or `tail(local)` steps, you will
+need to add `.unfold()` after these steps to maintain the same functionality. Without this update, some existing queries
 may throw a `ClassCastException` while others may return unexpected results.
 ====
 
@@ -744,7 +752,7 @@ may throw a `ClassCastException` while others may return unexpected results.
 gremlin> g.inject([1, 2, 3]).limit(local, 1)
 ==>1  // single element returned directly
 
-gremlin> g.inject([1, 2, 3]).limit(local, 2) 
+gremlin> g.inject([1, 2, 3]).limit(local, 2)
 ==>[1,2]  // collection returned
 
 // 3.8.0 - consistent collection output for collections
@@ -768,12 +776,13 @@ gremlin> g.inject([1, 2, 3]).limit(local, 1).unfold()
 ----
 
 This change affects all three local collection manipulation steps when operating on iterable collections:
-- `range(local, low, high)` 
+- `range(local, low, high)`
 - `limit(local, count)`
 - `tail(local, count)`
 
 See: link:https://issues.apache.org/jira/browse/TINKERPOP-2491[TINKERPOP-2491]
 
+[[group-value-traversal-semantics]]
 ===== group() Value Traversal Semantics
 
 The `group()` step takes two `by()` modulators. The first defines the key for the grouping, and the second acts upon the
@@ -804,6 +813,7 @@ gremlin> g.V().has("person","name",P.within("vadas","peter")).group().by().by(__
 
 See: link:https://issues.apache.org/jira/browse/TINKERPOP-2971[TINKERPOP-2971]
 
+[[remove-undocumented-with-modulation]]
 ===== Remove Undocumented `with()` modulation
 
 There has long been a connection between the `with()` modulator, and mutating steps due to the design of
@@ -813,6 +823,7 @@ some of the interfaces in the gremlin traversal engine. This has led to several
 As of 3.8.0 `with()` modulation of the following steps will no longer work: `addV()`, `addE()`, `property()`, `drop()`,
 `mergeV()`, and `mergeE()`.
 
+[[by-modulation-semantics]]
 ===== By Modulation Semantics
 
 *valueMap() and propertyMap() Semantics*
@@ -833,6 +844,7 @@ link:https://issues.apache.org/jira/browse/TINKERPOP-2974[TINKERPOP-2974]
 
 A number of changes have been introduced to the Gremlin grammar to help make it be more consistent and easier to use.
 
+[[removed-structurevertex-from-grammar]]
 *Removed StructureVertex from Grammar*
 
 The grammar allowed the construction of a `Vertex` by way of syntax like `new Vertex(1,'person')` (or with similar
@@ -873,6 +885,7 @@ gremlin> client.submit(script).all().get().get(0).getEdge()
 ==>e[0][1-knows->6]
 ----
 
+[[new-keyword-is-now-optional]]
 *`new` keyword is now optional*
 
 The `new` keyword is now optional in all cases where it was previously used. Both of the following examples are now
@@ -887,6 +900,7 @@ g.V().withStrategies(SubgraphStrategy(vertices: __.hasLabel('person')))
 
 In a future version, it is likely that the `new` keyword will be removed entirely from the grammar.
 
+[[supports-withoutstrategies]]
 *Supports withoutStrategies()*
 
 The `withoutStrategies()` configuration step is now supported syntax for the grammar. While this option is not commonly
@@ -898,6 +912,7 @@ control over how a traversal works.
 g.V().withoutStrategies(CountStrategy)
 ----
 
+[[map-keys-restrictions]]
 *`Map` keys restrictions*
 
 Earlier versions of the grammar allowed a wide range of values for the keys. In many cases, these didn't really make
@@ -960,6 +975,7 @@ In this next example, the `Map` keys are defined in a way that changes will be n
 g.mergeE([label:'Sibling',created:'2022-02-07',from:1,to:2])
 ----
 
+[[restriction-of-step-arguments]]
 *Restriction of Step Arguments*
 
 Prior to 3.7.0, the grammar did not allow for any parameters in gremlin scripts. In 3.7, the grammar rules
@@ -980,15 +996,18 @@ link:https://issues.apache.org/jira/browse/TINKERPOP-3023[TINKERPOP-3023]
 
 ==== Gremlin Language Variant Changes
 
+[[set-minimum-java-version-to-11]]
 ===== Set minimum Java version to 11
 
 TinkerPop 3.8 requires a minimum of Java 11 for building and running. Support for Java 1.8 has been dropped.
 
+[[seedstrategy-construction]]
 ===== SeedStrategy Construction
 
 The `SeedStrategy` public constructor has been removed for Java and has been replaced by the builder pattern common
 to all strategies. This change was made to ensure that the `SeedStrategy` could be constructed consistently.
 
+[[optionsstrategy-in-python]]
 ===== OptionsStrategy in Python
 
 The `\\__init__()` syntax has been updated to be both more Pythonic and more aligned to the `gremlin-lang` syntax.
@@ -1013,6 +1032,7 @@ g.with_strategies(OptionsStrategy(**myOptions))
 
 ===== Serialization Changes
 
+[[properties-on-element-serialization-in-glvs]]
 *Properties on Element Serialization in GLVs*
 
 Element properties handling has been inconsistent across GLVs. Previously,`gremlin-python` deserialized empty properties
@@ -1072,11 +1092,13 @@ formats and server-side behavior remain unchanged.
 
 See: link:https://issues.apache.org/jira/browse/TINKERPOP-3186[TINKERPOP-3186]
 
+[[javascript-set-deserialization]]
 *Javascript Set Deserialization*
 
 Starting from this version, `gremlin-javascript` will deserialize `Set` data into a ECMAScript 2015 Set. Previously,
 these were deserialized into arrays.
 
+[[net-byte-serialization-change]]
 *.NET Byte Serialization Change*
 
 The Gremlin .NET serializers has been updated to correctly handle byte values as signed integers to align with the IO
@@ -1088,12 +1110,14 @@ compatibility.
 
 See: link:https://issues.apache.org/jira/browse/TINKERPOP-3161[TINKERPOP-3161]
 
+[[removal-of-p-getoriginalvalue]]
 ==== Removal of P.getOriginalValue()
 
 `P.getOriginalValue()` has been removed as it was not offering much value and was often confused with `P.getValue()`.
 Usage of `P.getOriginalValue()` often leads to unexpected results if called on a predicate which has had its value reset
 after construction. All usages of `P.getOriginalValue()` should be replaced with `P.getValue()`.
 
+[[improved-translators]]
 ==== Improved Translators
 
 The various Java `Translator` implementations allowing conversion of Gremlin traversals to string forms in various
@@ -1117,6 +1141,7 @@ gremlin> GremlinTranslator.translate("g.V().out('knows')", Translator.GO)
 
 See: link:https://issues.apache.org/jira/browse/TINKERPOP-3028[TINKERPOP-3028]
 
+[[simplified-comparability-semantics]]
 ==== Simplified Comparability Semantics
 
 The previous system of ternary boolean semantics has been replaced with simplified binary semantics. The triggers for
@@ -1133,6 +1158,7 @@ See: link:https://tinkerpop.apache.org/docs/3.8.0/dev/provider/#gremlin-semantic
 
 See: link:https://issues.apache.org/jira/browse/TINKERPOP-3173[TINKERPOP-3173]
 
+[[gremlin-mcp-server]]
 ==== Gremlin MCP Server
 
 Gremlin MCP Server is an experimental application that implements the link:https://modelcontextprotocol.io/[Model Context Protocol]
@@ -1143,6 +1169,7 @@ health checks are included to validate connectivity.
 A running Gremlin Server that fronts the target TinkerPop graph is required. An MCP client can be configured to connect
 to the Gremlin MCP Server endpoint.
 
+[[air-routes-dataset]]
 ==== Air Routes Dataset
 
 The Air Routes sample dataset has long been used to help showcase and teach Gremlin. Popularized by the first edition
@@ -1167,6 +1194,7 @@ TinkerPop distributes the 1.0 version of the dataset.
 
 ==== Type Conversion And Comparison
 
+[[type-predicate]]
 ===== Type Predicate
 
 The new `P.typeOf()` predicate allows filtering traversers based on their runtime type. It accepts either a `GType`
@@ -1202,6 +1230,7 @@ throw an exception at execution time.
 See: link:https://tinkerpop.apache.org/docs/3.8.0/reference/#a-note-on-predicates[Predicates],
 link:https://issues.apache.org/jira/browse/TINKERPOP-2234[TINKERPOP-2234]
 
+[[number-conversion-step]]
 ===== Number Conversion Step
 
 The new `asNumber()` step provides type casting functionality to Gremlin. It serves as an umbrella step that parses
@@ -1267,6 +1296,7 @@ gremlin> g.inject([1, 2, 3, 4]).asNumber()
 See: link:https://tinkerpop.apache.org/docs/3.8.0/reference/#asNumber-step[asNumber()-step],
 link:https://issues.apache.org/jira/browse/TINKERPOP-3166[TINKERPOP-3166]
 
+[[boolean-conversion-step]]
 ===== Boolean Conversion Step
 
 The `asBool()` step bridges another gap in Gremlin's casting functionalities. Users now have the ability to parse
@@ -1295,6 +1325,7 @@ gremlin> g.V().sack(assign).by(__.hasLabel('person').count().asBool()).sack(and)
 See: link:https://tinkerpop.apache.org/docs/3.8.0/reference/#asBool-step[asBool()-step],
 link:https://issues.apache.org/jira/browse/TINKERPOP-3175[TINKERPOP-3175]
 
+[[auto-promotion-of-numbers]]
 ===== Auto-promotion of Numbers
 
 Previously, operations like `sum` or `sack` that involved mathematical calculations did not automatically promote the
@@ -1337,6 +1368,7 @@ gremlin> g.inject([Float.MAX_VALUE, Float.MAX_VALUE], [Double.MAX_VALUE, Double.
 
 See link:https://issues.apache.org/jira/browse/TINKERPOP-3115[TINKERPOP-3115]
 
+[[deprecated-unifiedchannelizer]]
 ==== Deprecated UnifiedChannelizer
 
 The `UnifiedChannelizer` was added in 3.5.0 in any attempt to streamline Gremlin Server code paths and resource usage.
diff --git a/docs/upgrade/java/README.md b/docs/upgrade/java/README.md
new file mode 100644
index 0000000000..e5c803bb6d
--- /dev/null
+++ b/docs/upgrade/java/README.md
@@ -0,0 +1,80 @@
+////
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+////
+### CMDB Graph API (Java, Micronaut 4, Gremlin Server)
+
+This is a language/framework example project implementing the DevOps CMDB + Incident Knowledge Graph API described in `../spec/spec.md`. It connects remotely to a Gremlin Server and does not use any embedded graph.
+
+Key facts:
+- Java 17
+- Micronaut 4
+- Apache TinkerPop 3.7.4 (gremlin-driver)
+- Deterministic dataset: `../spec/cmdb-data.gremlin`
+
+Quick start (Gremlin Server via Docker):
+- From this directory (`docs/upgrade/java`):
+  - Make helper executable (first time only): `chmod +x ./bin/gremlin-dev.sh`
+  - Start server: `./bin/gremlin-dev.sh up`
+  - Wait until ready: `./bin/gremlin-dev.sh wait`
+  - Load dataset: `./bin/gremlin-dev.sh seed`
+  - Or all-in-one reset: `./bin/gremlin-dev.sh reset`
+  - Stop server: `./bin/gremlin-dev.sh down`
+
+TEMPORARY QUICK START STEPS ON STARTUP:
+```
+# connect to the running container after: ./bin/gremlin-dev.sh up`
+docker exec -it cmdb-gremlin /bin/bash
+
+# run the following in the container  
+sed -i "s#org.apache.tinkerpop.gremlin.server.channel.WebSocketChannelizer#org.apache.tinkerpop.gremlin.server.channel.WsAndHttpChannelizer#" "conf/gremlin-server.yaml"
+exit
+
+# restart the container
+docker container restart cmdb-gremlin
+```
+
+The helper uses the Docker image `tinkerpop/gremlin-server:3.7.4` and loads data by posting each line of `../spec/cmdb-data.gremlin` to the Gremlin Server HTTP endpoint. No Gremlin Console is required.
+
+Manual alternative (without the helper script):
+- Start server: `docker run -d --name cmdb-gremlin -p 8182:8182 tinkerpop/gremlin-server:3.7.4`
+- Wait for readiness (simple probe): `curl -sS -H 'Content-Type: application/json' -X POST http://localhost:8182/gremlin --data '{"gremlin":"g.V().limit(1)"}'`
+- Load dataset lines over HTTP (bash example):
+  ```bash
+  while IFS= read -r line; do \
+    [[ -z "$line" || "$line" =~ ^// || "$line" =~ ^# ]] && continue; \
+    json=$(printf '%s' "$line" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'); \
+    curl -sS -H 'Content-Type: application/json' -X POST http://localhost:8182/gremlin --data "{\"gremlin\":\"$json\"}" >/dev/null; \
+  done < ../spec/cmdb-data.gremlin
+  ```
+
+Build & Run API:
+- Build: `mvn -q -f ./pom.xml clean package`
+- Run: `mvn -q -f ./pom.xml mn:run`
+- Run Integration Tests: `mvn test -Dtest=ServiceIT`
+
+Endpoints (examples):
+- `GET http://localhost:8080/health/ready`
+- `GET http://localhost:8080/incidents/{incidentId}/blastRadius?depth=2`
+- `GET http://localhost:8080/services/{serviceId}/dependencies?depth=2`
+- `GET http://localhost:8080/services/{serviceId}/owners`
+- `GET http://localhost:8080/deployments/{deployId}/timeline`
+- `POST http://localhost:8080/alerts` with JSON `{ "alertId": "a1", "source": "prom", "metric": "cpu", "firedAt": 1700000000000 }`
+
+Configuration:
+- See `src/main/resources/application.yml` for Gremlin connection settings.
+
+License:
+- All files in this example are licensed under the Apache License, Version 2.0.
diff --git a/docs/upgrade/java/bin/gremlin-dev.sh b/docs/upgrade/java/bin/gremlin-dev.sh
new file mode 100755
index 0000000000..a7a3a04a15
--- /dev/null
+++ b/docs/upgrade/java/bin/gremlin-dev.sh
@@ -0,0 +1,204 @@
+#!/usr/bin/env bash
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -euo pipefail
+
+IMAGE="tinkerpop/gremlin-server:3.7.4"
+CONTAINER_NAME="cmdb-gremlin"
+HOST="localhost"
+PORT="8182"
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+# Dataset lives at docs/upgrade/spec/cmdb-data.gremlin relative to this script
+DATASET_PATH="${SCRIPT_DIR}/../../spec/cmdb-data.gremlin"
+
+usage() {
+  cat <<EOF
+Usage: $(basename "$0") <command>
+
+Commands:
+  up           Start Gremlin Server Docker container (${IMAGE}) on port ${PORT}
+  wait         Wait until Gremlin Server is ready to accept requests
+  seed         Load dataset from ${DATASET_PATH} into the running server
+  reset        Recreate container (down + up) and seed dataset
+  down         Stop and remove the Gremlin Server container
+  logs         Tail container logs
+  status       Show container status
+
+Environment overrides:
+  IMAGE            Docker image (default: ${IMAGE})
+  CONTAINER_NAME   Container name (default: ${CONTAINER_NAME})
+  HOST             Hostname for HTTP (default: ${HOST})
+  PORT             Host port for HTTP (default: ${PORT})
+EOF
+}
+
+container_exists() { docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; }
+container_running() { docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; }
+
+cmd_up() {
+  if container_running; then
+    echo "[INFO] Container ${CONTAINER_NAME} already running"
+    return 0
+  fi
+  if container_exists; then
+    echo "[INFO] Starting existing container ${CONTAINER_NAME}"
+    docker start "${CONTAINER_NAME}" >/dev/null
+  else
+    echo "[INFO] Pulling image ${IMAGE} (if needed)"
+    docker pull "${IMAGE}" >/dev/null || true
+    echo "[INFO] Creating and starting container ${CONTAINER_NAME}"
+    docker run -d --name "${CONTAINER_NAME}" -p "${PORT}:8182" "${IMAGE}" conf/gremlin-server.yaml >/dev/null
+  fi
+  echo "[INFO] Container is starting..."
+}
+
+cmd_wait() {
+  local timeout=90
+  local start_ts
+  start_ts=$(date +%s)
+  echo "[INFO] Waiting for Gremlin Server at http://${HOST}:${PORT}/gremlin (timeout ${timeout}s)"
+  while true; do
+    # Require explicit HTTP 200 from the /gremlin endpoint
+    local resp
+    resp=$(curl -sS -w "%{http_code}" -H 'Content-Type: application/json' \
+      -X POST "http://${HOST}:${PORT}/gremlin" \
+      --data '{"gremlin":"g.inject(1).count()"}')
+    local http_code
+    http_code="${resp: -3}"
+    if [[ "$http_code" == "200" ]]; then
+      break
+    fi
+    sleep 2
+    local now
+    now=$(date +%s)
+    if (( now - start_ts > timeout )); then
+      echo "[ERROR] Timed out waiting for Gremlin Server to become ready (last HTTP code: ${http_code})" >&2
+      exit 1
+    fi
+  done
+  echo "[INFO] Gremlin Server is ready"
+}
+
+cmd_seed() {
+  if ! container_running; then
+    echo "[ERROR] Container ${CONTAINER_NAME} is not running; start it with: $0 up && $0 wait" >&2
+    exit 1
+  fi
+  if [[ ! -f "${DATASET_PATH}" ]]; then
+    echo "[ERROR] Dataset not found at ${DATASET_PATH}" >&2
+    exit 1
+  fi
+  echo "[INFO] Loading dataset from ${DATASET_PATH}"
+
+  # Clear existing graph first to ensure a clean, deterministic state
+  clear_payload=$(printf '%s' "g.V().drop().iterate()" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g')
+  clear_resp=$(curl -sS -w "%{http_code}" -H 'Content-Type: application/json' \
+    -X POST "http://${HOST}:${PORT}/gremlin" \
+    --data "{\"gremlin\":\"${clear_payload}\"}")
+  clear_code="${clear_resp: -3}"
+  clear_body="${clear_resp::-3}"
+  if [[ "$clear_code" != "200" ]]; then
+    echo "[ERROR] Failed to clear graph with g.V().drop().iterate()" >&2
+    echo "[ERROR] HTTP ${clear_code} Response: ${clear_body}" >&2
+    exit 1
+  fi
+
+  # Post each non-empty, non-comment line to the /gremlin endpoint
+  local line_no=0
+  while IFS= read -r raw_line || [[ -n "$raw_line" ]]; do
+    line_no=$((line_no + 1))
+    # Trim leading/trailing whitespace
+    line="${raw_line%%[[:space:]]*}"
+    line="${raw_line%$'\r'}" # strip CR if present
+    trimmed="$(echo "$raw_line" | sed -e 's/^\s\+//' -e 's/\s\+$//')"
+
+    # Skip empty lines and comments (// ... or # ...)
+    if [[ -z "${trimmed}" ]] || [[ "${trimmed}" =~ ^// ]] || [[ "${trimmed}" =~ ^# ]]; then
+      continue
+    fi
+
+    # Escape JSON special chars in the Gremlin line: backslash and quotes
+    esc_line=$(printf '%s' "$trimmed" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g')
+
+    # Submit
+    resp=$(curl -sS -w "%{http_code}" -H 'Content-Type: application/json' \
+      -X POST "http://${HOST}:${PORT}/gremlin" \
+      --data "{\"gremlin\":\"${esc_line}\"}")
+
+    http_code="${resp: -3}"
+    body="${resp::-3}"
+
+    if [[ "$http_code" != "200" ]]; then
+      echo "[ERROR] Failed at line ${line_no}: ${trimmed}" >&2
+      echo "[ERROR] HTTP ${http_code} Response: ${body}" >&2
+      exit 1
+    fi
+  done < "${DATASET_PATH}"
+
+  echo "[INFO] Dataset load complete"
+}
+
+cmd_reset() {
+  cmd_down || true
+  cmd_up
+  cmd_wait
+  cmd_seed
+}
+
+cmd_down() {
+  if container_exists; then
+    echo "[INFO] Stopping and removing ${CONTAINER_NAME}"
+    docker rm -f "${CONTAINER_NAME}" >/dev/null || true
+  else
+    echo "[INFO] No existing container named ${CONTAINER_NAME}"
+  fi
+}
+
+cmd_logs() {
+  if container_exists; then
+    docker logs -f "${CONTAINER_NAME}"
+  else
+    echo "[INFO] No existing container named ${CONTAINER_NAME}"
+  fi
+}
+
+cmd_status() {
+  if container_running; then
+    echo "running"
+  elif container_exists; then
+    echo "stopped"
+  else
+    echo "absent"
+  fi
+}
+
+main() {
+  local cmd="${1:-}" || true
+  case "${cmd}" in
+    up) cmd_up ;;
+    wait) cmd_wait ;;
+    seed) cmd_seed ;;
+    reset) cmd_reset ;;
+    down) cmd_down ;;
+    logs) cmd_logs ;;
+    status) cmd_status ;;
+    -h|--help|help|"") usage ;;
+    *) echo "Unknown command: ${cmd}"; usage; exit 1 ;;
+  esac
+}
+
+main "$@"
diff --git a/docs/upgrade/java/pom.xml b/docs/upgrade/java/pom.xml
new file mode 100644
index 0000000000..22b5a84d08
--- /dev/null
+++ b/docs/upgrade/java/pom.xml
@@ -0,0 +1,154 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>org.apache.tinkerpop.example</groupId>
+    <artifactId>cmdb-gremlin</artifactId>
+    <version>0.1.0-SNAPSHOT</version>
+    <name>CMDB Graph API (Java, Micronaut 4, Gremlin Server)</name>
+    <description>Example Micronaut 4 REST API using Apache TinkerPop Gremlin Driver against Gremlin Server
+    </description>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <java.version>17</java.version>
+        <micronaut.version>4.6.2</micronaut.version>
+        <tinkerpop.version>3.7.4</tinkerpop.version>
+    </properties>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>io.micronaut.platform</groupId>
+                <artifactId>micronaut-platform</artifactId>
+                <version>${micronaut.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <dependencies>
+        <!-- Micronaut core -->
+        <dependency>
+            <groupId>io.micronaut</groupId>
+            <artifactId>micronaut-runtime</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.micronaut</groupId>
+            <artifactId>micronaut-http-server-netty</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.micronaut.validation</groupId>
+            <artifactId>micronaut-validation</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.micronaut.serde</groupId>
+            <artifactId>micronaut-serde-jackson</artifactId>
+        </dependency>
+
+        <!-- Gremlin driver (remote to Gremlin Server) -->
+        <dependency>
+            <groupId>org.apache.tinkerpop</groupId>
+            <artifactId>gremlin-driver</artifactId>
+            <version>${tinkerpop.version}</version>
+        </dependency>
+
+        <!-- Logging -->
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+            <scope>runtime</scope>
+        </dependency>
+
+        <!-- Testing (integration oriented) -->
+        <dependency>
+            <groupId>io.micronaut.test</groupId>
+            <artifactId>micronaut-test-junit5</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>io.micronaut</groupId>
+            <artifactId>micronaut-http-client</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.assertj</groupId>
+            <artifactId>assertj-core</artifactId>
+            <version>3.26.0</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.testcontainers</groupId>
+            <artifactId>junit-jupiter</artifactId>
+            <version>1.20.2</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <version>5.14.2</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.11.0</version>
+                <configuration>
+                    <release>${java.version}</release>
+                    <annotationProcessorPaths>
+                        <path>
+                            <groupId>io.micronaut</groupId>
+                            <artifactId>micronaut-inject-java</artifactId>
+                            <version>${micronaut.version}</version>
+                        </path>
+                        <path>
+                            <groupId>io.micronaut</groupId>
+                            <artifactId>micronaut-graal</artifactId>
+                            <version>${micronaut.version}</version>
+                        </path>
+                        <path>
+                            <groupId>io.micronaut</groupId>
+                            <artifactId>micronaut-http-validation</artifactId>
+                            <version>${micronaut.version}</version>
+                        </path>
+                    </annotationProcessorPaths>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <version>3.2.5</version>
+                <configuration>
+                    <useSystemClassLoader>true</useSystemClassLoader>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/Application.java b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/Application.java
new file mode 100644
index 0000000000..4dc42f74d4
--- /dev/null
+++ b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/Application.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tinkerpop.example.cmdb;
+
+import io.micronaut.runtime.Micronaut;
+
+public class Application {
+    public static void main(String[] args) {
+        Micronaut.build(args)
+                .packages("com.example.cmdb")
+                .start();
+    }
+}
diff --git a/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/api/AlertController.java b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/api/AlertController.java
new file mode 100644
index 0000000000..295a9ac5fa
--- /dev/null
+++ b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/api/AlertController.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tinkerpop.example.cmdb.api;
+
+import io.micronaut.http.HttpResponse;
+import io.micronaut.http.annotation.Body;
+import io.micronaut.http.annotation.Controller;
+import io.micronaut.http.annotation.Post;
+import org.apache.tinkerpop.example.cmdb.service.AlertService;
+
+import java.util.Map;
+
+@Controller("/alerts")
+public class AlertController {
+
+    private final AlertService service;
+
+    public AlertController(AlertService service) {
+        this.service = service;
+    }
+
+    @Post
+    public HttpResponse<String> upsert(@Body Map<String, Object> payload) {
+        service.upsertAlert(payload);
+        return HttpResponse.accepted().body("ok");
+    }
+}
diff --git a/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/api/DeploymentController.java b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/api/DeploymentController.java
new file mode 100644
index 0000000000..3ab8e85271
--- /dev/null
+++ b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/api/DeploymentController.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tinkerpop.example.cmdb.api;
+
+import io.micronaut.http.HttpResponse;
+import io.micronaut.http.annotation.Controller;
+import io.micronaut.http.annotation.Get;
+import io.micronaut.http.annotation.PathVariable;
+import org.apache.tinkerpop.example.cmdb.service.DeploymentService;
+
+import java.util.List;
+import java.util.Map;
+
+@Controller("/deployments")
+public class DeploymentController {
+
+    private final DeploymentService service;
+
+    public DeploymentController(DeploymentService service) {
+        this.service = service;
+    }
+
+    @Get("/{id}/timeline")
+    public HttpResponse<List<Map<Object, Object>>> timeline(@PathVariable String id) {
+        return HttpResponse.ok(service.timeline(id));
+    }
+}
diff --git a/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/api/HealthController.java b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/api/HealthController.java
new file mode 100644
index 0000000000..0def90d1e8
--- /dev/null
+++ b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/api/HealthController.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tinkerpop.example.cmdb.api;
+
+import io.micronaut.http.HttpResponse;
+import io.micronaut.http.annotation.Controller;
+import io.micronaut.http.annotation.Get;
+import org.apache.tinkerpop.example.cmdb.graph.GremlinGateway;
+
+@Controller("/health")
+public class HealthController {
+
+    private final GremlinGateway gateway;
+
+    public HealthController(GremlinGateway gateway) {
+        this.gateway = gateway;
+    }
+
+    @Get("/ready")
+    public HttpResponse<String> ready() {
+        try {
+            gateway.fetch(g -> g.V().limit(1));
+            return HttpResponse.ok("ready");
+        } catch (Exception e) {
+            return HttpResponse.serverError("not-ready: " + e.getMessage());
+        }
+    }
+}
diff --git a/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/api/IncidentController.java b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/api/IncidentController.java
new file mode 100644
index 0000000000..da2f98fe0e
--- /dev/null
+++ b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/api/IncidentController.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tinkerpop.example.cmdb.api;
+
+import io.micronaut.http.HttpResponse;
+import io.micronaut.http.annotation.Controller;
+import io.micronaut.http.annotation.Get;
+import io.micronaut.http.annotation.PathVariable;
+import io.micronaut.http.annotation.QueryValue;
+import org.apache.tinkerpop.example.cmdb.domain.PathDto;
+import org.apache.tinkerpop.example.cmdb.service.IncidentService;
+
+@Controller("/incidents")
+public class IncidentController {
+
+    private final IncidentService service;
+
+    public IncidentController(IncidentService service) {
+        this.service = service;
+    }
+
+    @Get("/{id}/blastRadius{?depth}")
+    public HttpResponse<PathDto> blastRadius(@PathVariable String id,
+                                             @QueryValue(defaultValue = "2") int depth) {
+        return HttpResponse.ok(service.blastRadius(id, depth));
+    }
+}
diff --git a/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/api/ServiceController.java b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/api/ServiceController.java
new file mode 100644
index 0000000000..d1190bd2ad
--- /dev/null
+++ b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/api/ServiceController.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tinkerpop.example.cmdb.api;
+
+import io.micronaut.http.HttpResponse;
+import io.micronaut.http.annotation.Controller;
+import io.micronaut.http.annotation.Get;
+import io.micronaut.http.annotation.PathVariable;
+import io.micronaut.http.annotation.QueryValue;
+import org.apache.tinkerpop.example.cmdb.service.ServiceGraphService;
+
+import java.util.List;
+import java.util.Map;
+
+@Controller("/services")
+public class ServiceController {
+
+    private final ServiceGraphService service;
+
+    public ServiceController(ServiceGraphService service) {
+        this.service = service;
+    }
+
+    @Get("/{id}/dependencies{?depth}")
+    public HttpResponse<List<Map<Object, Object>>> dependencies(@PathVariable String id,
+                                                                @QueryValue(defaultValue = "2") int depth) {
+        return HttpResponse.ok(service.dependencies(id, depth));
+    }
+
+    @Get("/{id}/owners")
+    public HttpResponse<List<Map<Object, Object>>> owners(@PathVariable String id) {
+        return HttpResponse.ok(service.owners(id));
+    }
+}
diff --git a/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/config/GremlinClientConfig.java b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/config/GremlinClientConfig.java
new file mode 100644
index 0000000000..3e563d5abb
--- /dev/null
+++ b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/config/GremlinClientConfig.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tinkerpop.example.cmdb.config;
+
+import io.micronaut.context.annotation.ConfigurationProperties;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@ConfigurationProperties("cmdb.gremlin")
+public class GremlinClientConfig {
+
+    private List<String> hosts = new ArrayList<>(List.of("localhost"));
+    private int port = 8182;
+    private String traversalSource = "g";
+    private String username;
+    private String password;
+    private boolean enableSsl = false;
+
+    public List<String> getHosts() {
+        return hosts;
+    }
+
+    public void setHosts(List<String> hosts) {
+        this.hosts = hosts;
+    }
+
+    public int getPort() {
+        return port;
+    }
+
+    public void setPort(int port) {
+        this.port = port;
+    }
+
+    public String getTraversalSource() {
+        return traversalSource;
+    }
+
+    public void setTraversalSource(String traversalSource) {
+        this.traversalSource = traversalSource;
+    }
+
+    public String getUsername() {
+        return username;
+    }
+
+    public void setUsername(String username) {
+        this.username = username;
+    }
+
+    public String getPassword() {
+        return password;
+    }
+
+    public void setPassword(String password) {
+        this.password = password;
+    }
+
+    public boolean isEnableSsl() {
+        return enableSsl;
+    }
+
+    public void setEnableSsl(boolean enableSsl) {
+        this.enableSsl = enableSsl;
+    }
+}
diff --git a/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/config/GremlinClientFactory.java b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/config/GremlinClientFactory.java
new file mode 100644
index 0000000000..6ea5dd69be
--- /dev/null
+++ b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/config/GremlinClientFactory.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tinkerpop.example.cmdb.config;
+
+import io.micronaut.context.annotation.Factory;
+import jakarta.annotation.PreDestroy;
+import jakarta.inject.Singleton;
+import org.apache.tinkerpop.gremlin.driver.Client;
+import org.apache.tinkerpop.gremlin.driver.Cluster;
+import org.apache.tinkerpop.gremlin.driver.remote.DriverRemoteConnection;
+
+@Factory
+public class GremlinClientFactory {
+
+    private Cluster cluster;
+    private Client client;
+    private DriverRemoteConnection connection;
+
+    @Singleton
+    public Cluster cluster(GremlinClientConfig config) {
+        Cluster.Builder builder = Cluster.build();
+        for (String host : config.getHosts()) builder.addContactPoint(host);
+        builder.port(config.getPort());
+        if (config.getUsername() != null && !config.getUsername().isBlank()) {
+            builder.credentials(config.getUsername(), config.getPassword());
+        }
+        if (config.isEnableSsl()) builder.enableSsl(true);
+        this.cluster = builder.create();
+        return this.cluster;
+    }
+
+    @Singleton
+    public Client client(Cluster cluster) {
+        this.client = cluster.connect();
+        return this.client;
+    }
+
+    @Singleton
+    public DriverRemoteConnection driverRemoteConnection(GremlinClientConfig config, Cluster cluster) {
+        this.connection = DriverRemoteConnection.using(cluster, config.getTraversalSource());
+        return this.connection;
+    }
+
+    @PreDestroy
+    void shutdown() throws Exception {
+        if (connection != null) connection.close();
+        if (client != null) client.close();
+        if (cluster != null) cluster.close();
+    }
+}
diff --git a/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/domain/PathDto.java b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/domain/PathDto.java
new file mode 100644
index 0000000000..4cd1052e12
--- /dev/null
+++ b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/domain/PathDto.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tinkerpop.example.cmdb.domain;
+
+import io.micronaut.serde.annotation.Serdeable;
+
+import java.util.List;
+import java.util.Map;
+
+@Serdeable
+public record PathDto(List<List<Map<String, Object>>> paths) {
+}
diff --git a/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/errors/ApiException.java b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/errors/ApiException.java
new file mode 100644
index 0000000000..0fae7f7614
--- /dev/null
+++ b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/errors/ApiException.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tinkerpop.example.cmdb.errors;
+
+public class ApiException extends RuntimeException {
+    private final int status;
+
+    public ApiException(int status, String message) {
+        super(message);
+        this.status = status;
+    }
+
+    public int getStatus() {
+        return status;
+    }
+}
diff --git a/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/errors/ErrorHandler.java b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/errors/ErrorHandler.java
new file mode 100644
index 0000000000..6335f5c26c
--- /dev/null
+++ b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/errors/ErrorHandler.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tinkerpop.example.cmdb.errors;
+
+import io.micronaut.http.HttpRequest;
+import io.micronaut.http.HttpResponse;
+import io.micronaut.http.MediaType;
+import io.micronaut.http.hateoas.JsonError;
+import io.micronaut.http.hateoas.Link;
+import io.micronaut.http.server.exceptions.ExceptionHandler;
+import jakarta.inject.Singleton;
+
+@Singleton
+public class ErrorHandler implements ExceptionHandler<ApiException, HttpResponse<JsonError>> {
+
+    @Override
+    public HttpResponse<JsonError> handle(HttpRequest request, ApiException exception) {
+        JsonError error = new JsonError(exception.getMessage());
+        error.link(Link.SELF, Link.of(String.valueOf(request.getUri())));
+        return HttpResponse.<JsonError>status(io.micronaut.http.HttpStatus.valueOf(exception.getStatus()))
+                .contentType(MediaType.APPLICATION_JSON_TYPE)
+                .body(error);
+    }
+}
diff --git a/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/graph/GremlinGateway.java b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/graph/GremlinGateway.java
new file mode 100644
index 0000000000..a9a077b5e2
--- /dev/null
+++ b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/graph/GremlinGateway.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tinkerpop.example.cmdb.graph;
+
+import jakarta.inject.Singleton;
+import org.apache.tinkerpop.gremlin.driver.Client;
+import org.apache.tinkerpop.gremlin.driver.remote.DriverRemoteConnection;
+import org.apache.tinkerpop.gremlin.process.traversal.AnonymousTraversalSource;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
+
+import java.util.List;
+import java.util.function.Function;
+
+@Singleton
+public class GremlinGateway {
+
+    private final GraphTraversalSource g;
+    private final Client client;
+
+    public GremlinGateway(DriverRemoteConnection connection, Client client) {
+        this.g = AnonymousTraversalSource.traversal().withRemote(connection);
+        this.client = client;
+    }
+
+    public <T> List<T> fetch(Function<GraphTraversalSource, GraphTraversal<?, T>> fn) {
+        return fn.apply(g).toList();
+    }
+
+    public void iterate(Function<GraphTraversalSource, GraphTraversal<?, ?>> fn) {
+        fn.apply(g).iterate();
+    }
+
+    public Client getClient() {
+        return client;
+    }
+}
diff --git a/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/graph/MappingUtils.java b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/graph/MappingUtils.java
new file mode 100644
index 0000000000..feb7063965
--- /dev/null
+++ b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/graph/MappingUtils.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tinkerpop.example.cmdb.graph;
+
+import org.apache.tinkerpop.gremlin.process.traversal.Path;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public final class MappingUtils {
+    private MappingUtils() {
+    }
+
+    @SuppressWarnings("unchecked")
+    public static List<List<Map<String, Object>>> toPathOfValueMaps(List<Path> paths) {
+        List<List<Map<String, Object>>> out = new ArrayList<>();
+        for (Path p : paths) {
+            List<Map<String, Object>> row = new ArrayList<>();
+            for (Object o : p.objects()) {
+                if (o instanceof Map) {
+                    Map<?, ?> m = (Map<?, ?>) o;
+                    Map<String, Object> as = new HashMap<>();
+                    for (Map.Entry<?, ?> e : m.entrySet()) {
+                        as.put(String.valueOf(e.getKey()), e.getValue());
+                    }
+                    row.add(as);
+                } else {
+                    Map<String, Object> wrap = new HashMap<>();
+                    wrap.put("value", o);
+                    row.add(wrap);
+                }
+            }
+            out.add(row);
+        }
+        return out;
+    }
+}
diff --git a/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/graph/TraversalBuilders.java b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/graph/TraversalBuilders.java
new file mode 100644
index 0000000000..83fc33866f
--- /dev/null
+++ b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/graph/TraversalBuilders.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tinkerpop.example.cmdb.graph;
+
+import org.apache.tinkerpop.gremlin.process.traversal.Order;
+import org.apache.tinkerpop.gremlin.process.traversal.Path;
+import org.apache.tinkerpop.gremlin.process.traversal.Scope;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
+import org.apache.tinkerpop.gremlin.structure.Vertex;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import static org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__.tail;
+
+public final class TraversalBuilders {
+    private TraversalBuilders() {
+    }
+
+    /**
+     * Blast radius paths starting from an incident
+     */
+    public static Function<GraphTraversalSource, GraphTraversal<?, Path>> incidentBlastRadius(String incidentId, int depth) {
+        int d = Math.max(0, depth);
+        return g -> g.V().has("incident", "incidentId", incidentId)
+                .out("affects")
+                .emit()
+                .repeat(
+                        __.union(
+                                __.out("dependsOn"),
+                                __.out("deployedAs").in("deploymentOf")
+                        )
+                )
+                .times(d)
+                .path().by(__.valueMap(true));
+    }
+
+    /**
+     * Service dependencies to a given depth (valueMap for DTO-friendly mapping)
+     */
+    public static Function<GraphTraversalSource, GraphTraversal<?, Map<Object, Object>>> serviceDependencies(String serviceId, int depth) {
+        int d = Math.max(1, depth);
+        return g -> g.V().has("service", "serviceId", serviceId)
+                .emit()
+                .repeat(__.out("dependsOn")).times(d)
+                .valueMap(true);
+    }
+
+    /**
+     * Deployment to incident correlation timeline (ordered incidents)
+     */
+    public static Function<GraphTraversalSource, GraphTraversal<?, Map<Object, Object>>> deploymentTimeline(String deployId) {
+        return g -> g.V().has("deployment", "deployId", deployId)
+                .coalesce(
+                        __.out("deployedTo").in("runsOn").in("deployedAs"),   // via hosts
+                        __.out("deploymentOf").in("deployedAs")                 // via application when no deployedTo edges
+                )
+                .dedup()
+                .in("affects").hasLabel("incident")
+                .order().by("openedAt", Order.desc)
+                .valueMap(true);
+    }
+
+    /**
+     * Gets Service owners.
+     */
+    public static Function<GraphTraversalSource, GraphTraversal<?, Map<Object, Object>>> serviceOwners(String serviceId) {
+        return g -> g.V().has("service", "serviceId", serviceId)
+                .out("ownedBy")
+                .valueMap(true);
+    }
+
+    public static Function<GraphTraversalSource, GraphTraversal<?, Vertex>> noneAndDiscard() {
+        return g -> g.V().none();
+    }
+
+    public static Function<GraphTraversalSource, GraphTraversal<?, List<Object>>> firstAndLast() {
+        return g -> g.V().hasLabel("team").values("name").
+                split("").map(__.union(__.limit(Scope.local,1), tail(Scope.local,1)).fold());
+    }
+
+}
diff --git a/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/service/AlertService.java b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/service/AlertService.java
new file mode 100644
index 0000000000..4729349aa9
--- /dev/null
+++ b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/service/AlertService.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tinkerpop.example.cmdb.service;
+
+import jakarta.inject.Singleton;
+import org.apache.tinkerpop.example.cmdb.errors.ApiException;
+import org.apache.tinkerpop.example.cmdb.graph.GremlinGateway;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
+
+import java.util.Map;
+
+@Singleton
+public class AlertService {
+
+    private final GremlinGateway gateway;
+
+    public AlertService(GremlinGateway gateway) {
+        this.gateway = gateway;
+    }
+
+    public void upsertAlert(Map<String, Object> body) {
+        String alertId = getString(body, "alertId");
+        if (alertId == null || alertId.isBlank()) {
+            throw new ApiException(400, "alertId is required");
+        }
+        String source = getString(body, "source");
+        String metric = getString(body, "metric");
+        Object firedAt = body.get("firedAt");
+
+        gateway.iterate(g -> g.V().has("alert", "alertId", alertId).fold()
+                .coalesce(__.unfold(), __.addV("alert").property("alertId", alertId))
+                .property("source", source)
+                .property("metric", metric)
+                .property("firedAt", firedAt));
+    }
+
+    private static String getString(Map<String, Object> m, String k) {
+        Object v = m.get(k);
+        return v == null ? null : String.valueOf(v);
+    }
+}
diff --git a/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/service/DeploymentService.java b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/service/DeploymentService.java
new file mode 100644
index 0000000000..aa780338b1
--- /dev/null
+++ b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/service/DeploymentService.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tinkerpop.example.cmdb.service;
+
+import jakarta.inject.Singleton;
+import org.apache.tinkerpop.example.cmdb.graph.GremlinGateway;
+import org.apache.tinkerpop.example.cmdb.graph.TraversalBuilders;
+
+import java.util.List;
+import java.util.Map;
+
+@Singleton
+public class DeploymentService {
+
+    private final GremlinGateway gateway;
+
+    public DeploymentService(GremlinGateway gateway) {
+        this.gateway = gateway;
+    }
+
+    public List<Map<Object, Object>> timeline(String deployId) {
+        return gateway.fetch(TraversalBuilders.deploymentTimeline(deployId));
+    }
+}
diff --git a/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/service/IncidentService.java b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/service/IncidentService.java
new file mode 100644
index 0000000000..5d55ed1364
--- /dev/null
+++ b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/service/IncidentService.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tinkerpop.example.cmdb.service;
+
+import jakarta.inject.Singleton;
+import org.apache.tinkerpop.example.cmdb.domain.PathDto;
+import org.apache.tinkerpop.example.cmdb.graph.GremlinGateway;
+import org.apache.tinkerpop.example.cmdb.graph.MappingUtils;
+import org.apache.tinkerpop.example.cmdb.graph.TraversalBuilders;
+import org.apache.tinkerpop.gremlin.process.traversal.Path;
+
+import java.util.List;
+
+@Singleton
+public class IncidentService {
+
+    private final GremlinGateway gateway;
+
+    public IncidentService(GremlinGateway gateway) {
+        this.gateway = gateway;
+    }
+
+    public PathDto blastRadius(String incidentId, int depth) {
+        List<Path> paths = gateway.fetch(TraversalBuilders.incidentBlastRadius(incidentId, depth));
+        return new PathDto(MappingUtils.toPathOfValueMaps(paths));
+    }
+}
diff --git a/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/service/ServiceGraphService.java b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/service/ServiceGraphService.java
new file mode 100644
index 0000000000..fc43ef0921
--- /dev/null
+++ b/docs/upgrade/java/src/main/java/org/apache/tinkerpop/example/cmdb/service/ServiceGraphService.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tinkerpop.example.cmdb.service;
+
+import jakarta.inject.Singleton;
+import org.apache.tinkerpop.example.cmdb.graph.GremlinGateway;
+import org.apache.tinkerpop.example.cmdb.graph.TraversalBuilders;
+
+import java.util.List;
+import java.util.Map;
+
+@Singleton
+public class ServiceGraphService {
+
+    private final GremlinGateway gateway;
+
+    public ServiceGraphService(GremlinGateway gateway) {
+        this.gateway = gateway;
+    }
+
+    public List<Map<Object, Object>> dependencies(String serviceId, int depth) {
+        return gateway.fetch(TraversalBuilders.serviceDependencies(serviceId, depth));
+    }
+
+    public List<Map<Object, Object>> owners(String serviceId) {
+        return gateway.fetch(TraversalBuilders.serviceOwners(serviceId));
+    }
+}
diff --git a/docs/upgrade/java/src/main/resources/application.yml b/docs/upgrade/java/src/main/resources/application.yml
new file mode 100644
index 0000000000..189ed414ae
--- /dev/null
+++ b/docs/upgrade/java/src/main/resources/application.yml
@@ -0,0 +1,28 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+micronaut:
+  application:
+    name: cmdb-graph-api
+  server:
+    port: 8080
+
+cmdb:
+  gremlin:
+    hosts: [ "localhost" ]
+    port: 8182
+    traversalSource: g
+    # username: ""
+    # password: ""
+    # enableSsl: false
diff --git a/docs/upgrade/java/src/test/java/org/apache/tinkerpop/example/cmdb/it/DataLoader.java b/docs/upgrade/java/src/test/java/org/apache/tinkerpop/example/cmdb/it/DataLoader.java
new file mode 100644
index 0000000000..d557ebad3c
--- /dev/null
+++ b/docs/upgrade/java/src/test/java/org/apache/tinkerpop/example/cmdb/it/DataLoader.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tinkerpop.example.cmdb.it;
+
+import org.apache.tinkerpop.gremlin.driver.Client;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+public final class DataLoader {
+    private DataLoader() {
+    }
+
+    public static void loadScript(Client client, Path scriptPath) throws IOException {
+        List<String> lines = Files.readAllLines(scriptPath, StandardCharsets.UTF_8);
+        for (String raw : lines) {
+            String line = raw.trim();
+            if (line.isEmpty() || line.startsWith("//") || line.startsWith("#")) continue;
+            client.submit(line).all().join();
+        }
+    }
+}
diff --git a/docs/upgrade/java/src/test/java/org/apache/tinkerpop/example/cmdb/it/ServiceIT.java b/docs/upgrade/java/src/test/java/org/apache/tinkerpop/example/cmdb/it/ServiceIT.java
new file mode 100644
index 0000000000..1c70529fe3
--- /dev/null
+++ b/docs/upgrade/java/src/test/java/org/apache/tinkerpop/example/cmdb/it/ServiceIT.java
@@ -0,0 +1,254 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tinkerpop.example.cmdb.it;
+
+import io.micronaut.http.HttpRequest;
+import io.micronaut.http.HttpResponse;
+import io.micronaut.http.client.HttpClient;
+import io.micronaut.http.client.annotation.Client;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import io.micronaut.test.support.TestPropertyProvider;
+import jakarta.inject.Inject;
+import org.apache.tinkerpop.gremlin.driver.Cluster;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestMethodOrder;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@MicronautTest
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+public class ServiceIT implements TestPropertyProvider {
+
+    @Override
+    public Map<String, String> getProperties() {
+        // Allow overriding host/port via env vars; default to localhost:8182
+        String host = Optional.ofNullable(System.getenv("CMDB_GREMLIN_HOST")).orElse("localhost");
+        String portStr = Optional.ofNullable(System.getenv("CMDB_GREMLIN_PORT")).orElse("8182");
+
+        Map<String, String> m = new HashMap<>();
+        m.put("cmdb.gremlin.hosts[0]", host);
+        m.put("cmdb.gremlin.port", portStr);
+        m.put("cmdb.gremlin.traversalSource", "g");
+        return m;
+    }
+
+    @Inject
+    @Client("/")
+    HttpClient httpClient;
+
+    @BeforeAll
+    void checkGremlinServerAndDataset() {
+        String host = Optional.ofNullable(System.getenv("CMDB_GREMLIN_HOST")).orElse("localhost");
+        int port = Integer.parseInt(Optional.ofNullable(System.getenv("CMDB_GREMLIN_PORT")).orElse("8182"));
+        Cluster cluster = null;
+        org.apache.tinkerpop.gremlin.driver.Client client = null;
+        try {
+            cluster = Cluster.build().addContactPoint(host).port(port).create();
+            client = cluster.connect();
+            // basic readiness
+            client.submit("g.inject(1)").all().join();
+            // dataset presence check against a known vertex id from cmdb-data.gremlin
+            long count = client.submit("g.V().has('incident','incidentId','inc:sev1-payments-001').count()").all().join().get(0).getLong();
+            if (count < 1) {
+                throw new org.opentest4j.TestAbortedException(
+                        "Gremlin Server reachable at " + host + ":" + port + ", but sample dataset not found. " +
+                                "Please load docs/upgrade/spec/cmdb-data.gremlin before running tests.");
+            }
+        } catch (org.opentest4j.TestAbortedException tae) {
+            throw tae;
+        } catch (Exception e) {
+            throw new org.opentest4j.TestAbortedException(
+                    "Gremlin Server not reachable at " + host + ":" + port + ". Please start it and load docs/upgrade/spec/cmdb-data.gremlin.", e);
+        } finally {
+            if (client != null) try {
+                client.close();
+            } catch (Exception ignore) {
+            }
+            if (cluster != null) try {
+                cluster.close();
+            } catch (Exception ignore) {
+            }
+        }
+    }
+
+    // Helper to issue GET and decode as List of Maps
+    private List<Map<String, Object>> getList(String path) {
+        io.micronaut.core.type.Argument<List<Map<String, Object>>> arg = (io.micronaut.core.type.Argument<List<Map<String, Object>>>) (io.micronaut.core.type.Argument) io.micronaut.core.type.Argument.listOf(Map.class);
+        HttpResponse<List<Map<String, Object>>> resp = (HttpResponse) httpClient.toBlocking().exchange(HttpRequest.GET(path), arg);
+        assertThat(resp.code()).isEqualTo(200);
+        List<Map<String, Object>> list = resp.body();
+        return list == null ? Collections.emptyList() : list;
+    }
+
+    private Map<String, Object> getJson(String path) {
+        io.micronaut.core.type.Argument<Map<String, Object>> arg = (io.micronaut.core.type.Argument<Map<String, Object>>) (io.micronaut.core.type.Argument) io.micronaut.core.type.Argument.mapOf(String.class, Object.class);
+        HttpResponse<Map<String, Object>> resp = (HttpResponse) httpClient.toBlocking().exchange(HttpRequest.GET(path), arg);
+        assertThat(resp.code()).isEqualTo(200);
+        return resp.body();
+    }
+
+    private static boolean anyHas(List<Map<String, Object>> rows, String key, String expected) {
+        for (Map<String, Object> m : rows) {
+            Object v = m.get(key);
+            if (v instanceof Collection) {
+                Collection<?> c = (Collection<?>) v;
+                if (c.stream().anyMatch(o -> expected.equals(String.valueOf(o)))) return true;
+            } else if (v != null && expected.equals(String.valueOf(v))) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Test
+    @Order(1)
+    void smoke_ready() {
+        HttpResponse<String> resp = httpClient.toBlocking().exchange(HttpRequest.GET("/health/ready"), String.class);
+        assertThat(resp.code()).isEqualTo(200);
+        assertThat(resp.body()).contains("ready");
+    }
+
+    @Test
+    @Order(10)
+    void svc_orders_depth2() {
+        List<Map<String, Object>> rows = getList("/services/svc:orders/dependencies?depth=2");
+        assertThat(rows).hasSize(2);
+        assertThat(anyHas(rows, "serviceId", "svc:orders")).isTrue();
+        assertThat(anyHas(rows, "depId", "dep:orders-db")).isTrue();
+    }
+
+    @Test
+    @Order(11)
+    void svc_payments_depth1() {
+        List<Map<String, Object>> rows = getList("/services/svc:payments/dependencies?depth=1");
+        assertThat(rows).hasSize(3);
+        assertThat(anyHas(rows, "serviceId", "svc:payments")).isTrue();
+        assertThat(anyHas(rows, "depId", "dep:payments-db")).isTrue();
+        assertThat(anyHas(rows, "depId", "dep:payments-queue")).isTrue();
+    }
+
+    @Test
+    @Order(12)
+    void svc_inventory_depth3() {
+        List<Map<String, Object>> rows = getList("/services/svc:inventory/dependencies?depth=3");
+        assertThat(rows).hasSize(2);
+        assertThat(anyHas(rows, "serviceId", "svc:inventory")).isTrue();
+        assertThat(anyHas(rows, "depId", "dep:inventory-cache")).isTrue();
+    }
+
+    @Test
+    @Order(20)
+    void owners_payments() {
+        List<Map<String, Object>> rows = getList("/services/svc:payments/owners");
+        assertThat(rows).hasSize(1);
+        assertThat(anyHas(rows, "teamId", "team:payments")).isTrue();
+        assertThat(anyHas(rows, "name", "Payments")).isTrue();
+        // onCall is a boolean; valueMap(true) may return as a scalar or list depending on provider
+        assertThat(rows.stream().anyMatch(m -> {
+            Object v = m.get("onCall");
+            if (v instanceof Collection) return ((Collection<?>) v).contains(Boolean.TRUE);
+            return Boolean.TRUE.equals(v);
+        })).isTrue();
+    }
+
+    @Test
+    @Order(21)
+    void owners_api_gateway() {
+        List<Map<String, Object>> rows = getList("/services/svc:api-gateway/owners");
+        assertThat(rows).hasSize(1);
+        assertThat(anyHas(rows, "teamId", "team:platform")).isTrue();
+        assertThat(anyHas(rows, "name", "Platform Engineering")).isTrue();
+    }
+
+    @Test
+    @Order(30)
+    void timeline_payments_20131113_2() {
+        List<Map<String, Object>> rows = getList("/deployments/depoy:payments-20231113.2/timeline");
+        assertThat(rows).hasSize(1);
+        assertThat(anyHas(rows, "incidentId", "inc:sev1-payments-001")).isTrue();
+    }
+
+    @Test
+    @Order(31)
+    void timeline_payments_20131114_1() {
+        List<Map<String, Object>> rows = getList("/deployments/depoy:payments-20231114.1/timeline");
+        assertThat(rows).hasSize(1);
+        assertThat(anyHas(rows, "incidentId", "inc:sev1-payments-001")).isTrue();
+    }
+
+    @Test
+    @Order(32)
+    void timeline_api_gateway_20131114_1() {
+        List<Map<String, Object>> rows = getList("/deployments/depoy:api-gateway-20231114.1/timeline");
+        assertThat(rows).hasSize(1);
+        assertThat(anyHas(rows, "incidentId", "inc:sev1-api-001")).isTrue();
+    }
+
+    @Test
+    @Order(40)
+    void blast_radius_payments_depth1() {
+        Map<String, Object> body = getJson("/incidents/inc:sev1-payments-001/blastRadius?depth=1");
+        @SuppressWarnings("unchecked") List<List<Map<String, Object>>> paths = (List<List<Map<String, Object>>>) body.get("paths");
+        assertThat(paths).isNotNull();
+        assertThat(paths.size()).isGreaterThanOrEqualTo(2);
+        List<Map<String, Object>> terminals = paths.stream().map(p -> p.get(p.size() - 1)).collect(Collectors.toList());
+        assertThat(anyHas(terminals, "depId", "dep:payments-db")).isTrue();
+        assertThat(anyHas(terminals, "depId", "dep:payments-queue")).isTrue();
+        assertThat(anyHas(terminals, "deployId", "depoy:payments-20231113.2")).isTrue();
+        assertThat(anyHas(terminals, "deployId", "depoy:payments-20231114.1")).isTrue();
+    }
+
+    @Test
+    @Order(41)
+    void blast_radius_api_depth1() {
+        Map<String, Object> body = getJson("/incidents/inc:sev1-api-001/blastRadius?depth=1");
+        @SuppressWarnings("unchecked") List<List<Map<String, Object>>> paths = (List<List<Map<String, Object>>>) body.get("paths");
+        assertThat(paths).isNotNull();
+        List<Map<String, Object>> terminals = paths.stream().map(p -> p.get(p.size() - 1)).collect(Collectors.toList());
+        assertThat(anyHas(terminals, "deployId", "depoy:api-gateway-20231113.1")).isTrue();
+        assertThat(anyHas(terminals, "deployId", "depoy:api-gateway-20231114.1")).isTrue();
+    }
+
+    @Test
+    @Order(50)
+    void unknown_service_returns_empty() {
+        List<Map<String, Object>> rows = getList("/services/svc:does-not-exist/dependencies?depth=2");
+        assertThat(rows).isEmpty();
+    }
+
+    @Test
+    @Order(51)
+    void optional_dependency_inventory_depth1_exact_two() {
+        List<Map<String, Object>> rows = getList("/services/svc:inventory/dependencies?depth=1");
+        assertThat(rows).hasSize(2);
+        assertThat(anyHas(rows, "serviceId", "svc:inventory")).isTrue();
+        assertThat(anyHas(rows, "depId", "dep:inventory-cache")).isTrue();
+    }
+}
diff --git a/docs/upgrade/java/src/test/java/org/apache/tinkerpop/example/cmdb/unit/AlertServiceTest.java b/docs/upgrade/java/src/test/java/org/apache/tinkerpop/example/cmdb/unit/AlertServiceTest.java
new file mode 100644
index 0000000000..91810adeea
--- /dev/null
+++ b/docs/upgrade/java/src/test/java/org/apache/tinkerpop/example/cmdb/unit/AlertServiceTest.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tinkerpop.example.cmdb.unit;
+
+import org.apache.tinkerpop.example.cmdb.errors.ApiException;
+import org.apache.tinkerpop.example.cmdb.graph.GremlinGateway;
+import org.apache.tinkerpop.example.cmdb.service.AlertService;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+public class AlertServiceTest {
+
+    @Test
+    void missingAlertIdThrows() {
+        GremlinGateway gateway = mock(GremlinGateway.class);
+        AlertService svc = new AlertService(gateway);
+
+        Map<String, Object> payload = new HashMap<>();
+        assertThatThrownBy(() -> svc.upsertAlert(payload))
+                .isInstanceOf(ApiException.class)
+                .hasMessageContaining("alertId is required");
+
+        verifyNoInteractions(gateway);
+    }
+
+    @Test
+    void validPayloadCallsIterate() {
+        GremlinGateway gateway = mock(GremlinGateway.class);
+        AlertService svc = new AlertService(gateway);
+
+        Map<String, Object> payload = new HashMap<>();
+        payload.put("alertId", "a-1");
+        payload.put("source", "prom");
+        payload.put("metric", "cpu");
+        payload.put("firedAt", 1700000000000L);
+
+        svc.upsertAlert(payload);
+
+        verify(gateway, times(1)).iterate(any());
+        verifyNoMoreInteractions(gateway);
+    }
+}
diff --git a/docs/upgrade/java/src/test/java/org/apache/tinkerpop/example/cmdb/unit/DeploymentServiceTest.java b/docs/upgrade/java/src/test/java/org/apache/tinkerpop/example/cmdb/unit/DeploymentServiceTest.java
new file mode 100644
index 0000000000..6e366b6752
--- /dev/null
+++ b/docs/upgrade/java/src/test/java/org/apache/tinkerpop/example/cmdb/unit/DeploymentServiceTest.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tinkerpop.example.cmdb.unit;
+
+import org.apache.tinkerpop.example.cmdb.graph.GremlinGateway;
+import org.apache.tinkerpop.example.cmdb.service.DeploymentService;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class DeploymentServiceTest {
+
+    @Test
+    void timelineDelegatesToGateway() {
+        GremlinGateway gateway = mock(GremlinGateway.class);
+        List<Map<Object, Object>> expected = List.of(Map.of("event", "incident-linked"));
+        when(gateway.fetch(any())).thenReturn((List) expected);
+
+        DeploymentService svc = new DeploymentService(gateway);
+        List<Map<Object, Object>> result = svc.timeline("dep-1");
+        assertThat(result).isEqualTo(expected);
+    }
+}
diff --git a/docs/upgrade/java/src/test/java/org/apache/tinkerpop/example/cmdb/unit/ErrorHandlerTest.java b/docs/upgrade/java/src/test/java/org/apache/tinkerpop/example/cmdb/unit/ErrorHandlerTest.java
new file mode 100644
index 0000000000..a18f8e6499
--- /dev/null
+++ b/docs/upgrade/java/src/test/java/org/apache/tinkerpop/example/cmdb/unit/ErrorHandlerTest.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tinkerpop.example.cmdb.unit;
+
+import io.micronaut.http.HttpRequest;
+import io.micronaut.http.HttpResponse;
+import io.micronaut.http.hateoas.JsonError;
+import org.apache.tinkerpop.example.cmdb.errors.ApiException;
+import org.apache.tinkerpop.example.cmdb.errors.ErrorHandler;
+import org.junit.jupiter.api.Test;
+
+import java.net.URI;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ErrorHandlerTest {
+
+    @Test
+    void mapsApiExceptionToHttpResponse() {
+        ErrorHandler handler = new ErrorHandler();
+        ApiException ex = new ApiException(422, "bad input");
+
+        HttpRequest<?> request = mock(HttpRequest.class);
+        when(request.getUri()).thenReturn(URI.create("/test"));
+
+        HttpResponse<JsonError> resp = handler.handle(request, ex);
+        assertThat(resp.getStatus().getCode()).isEqualTo(422);
+        assertThat(resp.getBody()).isPresent();
+        assertThat(resp.getBody().get().getMessage()).contains("bad input");
+    }
+}
diff --git a/docs/upgrade/java/src/test/java/org/apache/tinkerpop/example/cmdb/unit/HealthControllerTest.java b/docs/upgrade/java/src/test/java/org/apache/tinkerpop/example/cmdb/unit/HealthControllerTest.java
new file mode 100644
index 0000000000..c55200f6ea
--- /dev/null
+++ b/docs/upgrade/java/src/test/java/org/apache/tinkerpop/example/cmdb/unit/HealthControllerTest.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tinkerpop.example.cmdb.unit;
+
+import io.micronaut.http.HttpResponse;
+import org.apache.tinkerpop.example.cmdb.api.HealthController;
+import org.apache.tinkerpop.example.cmdb.graph.GremlinGateway;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class HealthControllerTest {
+
+    @Test
+    void readyOkWhenFetchSucceeds() {
+        GremlinGateway gateway = mock(GremlinGateway.class);
+        when(gateway.fetch(any())).thenReturn(Collections.emptyList());
+
+        HealthController controller = new HealthController(gateway);
+        HttpResponse<String> resp = controller.ready();
+        assertThat(resp.getStatus().getCode()).isEqualTo(200);
+        assertThat(resp.body()).isEqualTo("ready");
+    }
+
+    @Test
+    void readyServerErrorOnException() {
+        GremlinGateway gateway = mock(GremlinGateway.class);
+        when(gateway.fetch(any())).thenThrow(new RuntimeException("boom"));
+
+        HealthController controller = new HealthController(gateway);
+        HttpResponse<String> resp = controller.ready();
+        assertThat(resp.getStatus().getCode()).isEqualTo(500);
+        assertThat(resp.body()).contains("not-ready");
+    }
+}
diff --git a/docs/upgrade/java/src/test/java/org/apache/tinkerpop/example/cmdb/unit/IncidentServiceTest.java b/docs/upgrade/java/src/test/java/org/apache/tinkerpop/example/cmdb/unit/IncidentServiceTest.java
new file mode 100644
index 0000000000..901c5203d4
--- /dev/null
+++ b/docs/upgrade/java/src/test/java/org/apache/tinkerpop/example/cmdb/unit/IncidentServiceTest.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tinkerpop.example.cmdb.unit;
+
+import org.apache.tinkerpop.example.cmdb.domain.PathDto;
+import org.apache.tinkerpop.example.cmdb.graph.GremlinGateway;
+import org.apache.tinkerpop.example.cmdb.service.IncidentService;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class IncidentServiceTest {
+
+    @Test
+    void blastRadiusReturnsEmptyWhenNoPaths() {
+        GremlinGateway gateway = mock(GremlinGateway.class);
+        when(gateway.fetch(any())).thenReturn((List) Collections.emptyList());
+
+        IncidentService svc = new IncidentService(gateway);
+        PathDto dto = svc.blastRadius("inc-1", 2);
+
+        assertThat(dto).isNotNull();
+        assertThat(dto.paths()).isNotNull().isEmpty();
+    }
+}
diff --git a/docs/upgrade/java/src/test/java/org/apache/tinkerpop/example/cmdb/unit/ServiceGraphServiceTest.java b/docs/upgrade/java/src/test/java/org/apache/tinkerpop/example/cmdb/unit/ServiceGraphServiceTest.java
new file mode 100644
index 0000000000..c5652470e8
--- /dev/null
+++ b/docs/upgrade/java/src/test/java/org/apache/tinkerpop/example/cmdb/unit/ServiceGraphServiceTest.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tinkerpop.example.cmdb.unit;
+
+import org.apache.tinkerpop.example.cmdb.graph.GremlinGateway;
+import org.apache.tinkerpop.example.cmdb.service.ServiceGraphService;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ServiceGraphServiceTest {
+
+    @Test
+    void dependenciesDelegatesToGateway() {
+        GremlinGateway gateway = mock(GremlinGateway.class);
+        List<Map<Object, Object>> expected = List.of(Map.of("k", "v"));
+        when(gateway.fetch(any())).thenReturn((List) expected);
+
+        ServiceGraphService svc = new ServiceGraphService(gateway);
+        List<Map<Object, Object>> result = svc.dependencies("svc-1", 2);
+        assertThat(result).isEqualTo(expected);
+    }
+
+    @Test
+    void ownersDelegatesToGateway() {
+        GremlinGateway gateway = mock(GremlinGateway.class);
+        List<Map<Object, Object>> expected = List.of(Map.of("team", "platform"));
+        when(gateway.fetch(any())).thenReturn((List) expected);
+
+        ServiceGraphService svc = new ServiceGraphService(gateway);
+        List<Map<Object, Object>> result = svc.owners("svc-1");
+        assertThat(result).isEqualTo(expected);
+    }
+}
diff --git a/docs/upgrade/java/src/test/resources/application-test.yml b/docs/upgrade/java/src/test/resources/application-test.yml
new file mode 100644
index 0000000000..fd62b56cb3
--- /dev/null
+++ b/docs/upgrade/java/src/test/resources/application-test.yml
@@ -0,0 +1,23 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+micronaut:
+  application:
+    name: cmdb-graph-api-test
+
+cmdb:
+  gremlin:
+    hosts: [ "localhost" ]
+    port: 8182
+    traversalSource: g
diff --git a/docs/upgrade/prompts/upgrade-3.8.0-helper.md b/docs/upgrade/prompts/upgrade-3.8.0-helper.md
new file mode 100644
index 0000000000..dd0e73eef5
--- /dev/null
+++ b/docs/upgrade/prompts/upgrade-3.8.0-helper.md
@@ -0,0 +1,66 @@
+////
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+////
+# TinkerPop 3.8.0 Upgrade Risk Assessment Guide
+
+## Overview
+
+Given the range of changes in Upgrading to TinkerPop 3.8.0 can be a complex process. While TinkerPop provides 
+[Upgrade Documentation](xxx), which outlines all the changes introduced, that still leaves the reader with the heavy job
+of determining where in their code bases This documentation introduces a purpose-built prompt designed for use with 
+advanced coding agents, such as Claude, GitHub Copilot, and GPT-5, which can intelligently analyze your project for 
+areas that may break or behave unexpectedly after the upgrade. By leveraging AI, you minimize time spent on manual 
+review, reduce the likelihood of missing subtle incompatibilities, and gain actionable advice for adapting your code to 
+the new version. This approach empowers you to upgrade with greater confidence, efficiency, and accuracy, ensuring that 
+all significant compatibility issues are surfaced and addressed early in the migration process.
+
+## Usage
+
+It is advisable that you **update your code to version 3.7.4**, before using this prompt. The report produced can be 
+used as a guide to fix problems yourself or potentially as a specification for a coding agent to attempt fixes on your 
+behalf. In either case, it may be helpful to run this prompt after fixes have been put in place to see if it can uncover
+any further problems. This prompt is not a replacement for personal understanding the 3.8.0 changes themselves in their 
+entirety as coding agents may not always correctly note subtle aspects of what your Gremlin is doing.
+
+Here are a few additional tips to consider when using this prompt:
+
+* The prompt is written in such a way as to "analyze the entire codebase" which may not be advisable depending on that 
+codebase's size. Consider adjusting that text to work within a smaller context (e.g. specific files or modules that use 
+Gremlin) if the "entire codebase" is not effective.
+* The prompt references a link to the full 3.8.0 Upgrade Documentation. Better results may be achieved by providing 
+a subset of the documentation at a time to better isolate fixes.
+* 
+
+```text
+You are an expert software migration advisor. The task is to assist a development team in upgrading a codebase from 
+Apache TinkerPop 3.7.4 to 3.8.0. The 3.8.0 release introduces significant breaking changes to APIs as well as Gremlin 
+language semantics.
+
+Carefully review the official Upgrade Documentation for 3.8.0 here: https://raw.githubusercontent.com/apache/tinkerpop/refs/heads/3.8-dev/docs/src/upgrade/release-3.8.0.asciidoc
+
+Your tasks are:
+1. Thoroughly analyze the entire codebase for usages of APIs, methods, or Gremlin syntax patterns that are impacted by the breaking changes outlined in the 3.8.0 upgrade documentation.
+2. Label each occurrence of an incompatibility or area of risk found with the section header from Upgrade Documentation to which it applies. We will refer to these groupings of occurrences as findings.  
+3. Generate a "Apache TinkerPop 3.8.0 Risk Assessment" report using Markdown formatting that organizes each Finding by section header that can be used as a specification to fix the problems.
+  a. In the introductory section of the report, include the following warning verbatim: "This report is only intended as a general guide for identifying possible upgrade issues between 3.7.4 and 3.8.0. There may be other problems to resolve. Be familiar with the [Upgrade Documentation](https://tinkerpop.apache.org/docs/3.8.0/upgrade/#_tinkerpop_3_8_0_2) and review your code carefully."
+  b. For each finding, include a link that references a specific section of the Upgrade Documentation that helped form the basis for the problem found. The format of this link is `https://tinkerpop.apache.org/docs/3.8.0/upgrade/#<anchor>` where "<anchor>" refers to the asciidoc anchor associated with the section header in the Upgrade Documentation.
+  c. For each occurrence, provide a confidence level for it as high, medium or low. In assigning this confidence level when Gremlin is involved, determine how well you understand the Gremlin semantics of the code in question while forming the recommendation.
+
+Refer to the 3.8.0 upgrade documentation for all findings and recommendations. Do not provide other advice for upgrade
+that is not specific to Apache TinkerPop or that is related to anything outside of the 3.8.0 upgrade itself. Do not 
+include any information about risks or incompatibilities that were not found. A final summary of changes is not 
+required.
+```
\ No newline at end of file
diff --git a/docs/upgrade/spec/cmdb-data.gremlin b/docs/upgrade/spec/cmdb-data.gremlin
new file mode 100644
index 0000000000..3521578be6
--- /dev/null
+++ b/docs/upgrade/spec/cmdb-data.gremlin
@@ -0,0 +1,167 @@
+// DevOps CMDB + Incident Knowledge Graph — Deterministic Sample Dataset
+// Assumptions:
+// - Executed against a Gremlin Server–compliant system with traversal source bound as 'g'
+// - Uses natural keys for lookups (serviceId, appId, hostId, depId, teamId, deployId, incidentId, alertId)
+// - All timestamps are epochMillis (UTC) for portability
+// - One line per vertex/edge; each line terminates with iterate()
+
+// === Teams (3) ===
+g.addV('team').property('teamId','team:platform').property('name','Platform Engineering').property('onCall',true).property('pager','pagerduty:plat').property('slack','#team-platform').property('createdAt',1700000000000).property('updatedAt',1700000000000).iterate()
+g.addV('team').property('teamId','team:payments').property('name','Payments').property('onCall',true).property('pager','pagerduty:pay').property('slack','#team-payments').property('createdAt',1700000000000).property('updatedAt',1700000000000).iterate()
+g.addV('team').property('teamId','team:orders').property('name','Orders').property('onCall',false).property('pager','pagerduty:ord').property('slack','#team-orders').property('createdAt',1700000000000).property('updatedAt',1700000000000).iterate()
+
+// === Services (6) ===
+g.addV('service').property('serviceId','svc:api-gateway').property('name','API Gateway').property('tier','critical').property('ownerTeamId','team:platform').property('sla','99.95%/30d').property('env','prod').property('tags',['edge','ingress']).property('criticality',5).property('createdAt',1700001000000).property('updatedAt',1700001000000).iterate()
+g.addV('service').property('serviceId','svc:payments').property('name','Payments Service').property('tier','critical').property('ownerTeamId','team:payments').property('sla','99.9%/30d').property('env','prod').property('tags',['finance','pci']).property('criticality',5).property('createdAt',1700001001000).property('updatedAt',1700001001000).iterate()
+g.addV('service').property('serviceId','svc:orders').property('name','Orders Service').property('tier','high').property('ownerTeamId','team:orders').property('sla','99.9%/30d').property('env','prod').property('tags',['commerce']).property('criticality',4).property('createdAt',1700001002000).property('updatedAt',1700001002000).iterate()
+g.addV('service').property('serviceId','svc:inventory').property('name','Inventory Service').property('tier','high').property('ownerTeamId','team:orders').property('sla','99.9%/30d').property('env','prod').property('tags',['stock']).property('criticality',4).property('createdAt',1700001003000).property('updatedAt',1700001003000).iterate()
+g.addV('service').property('serviceId','svc:notifications').property('name','Notifications Service').property('tier','medium').property('ownerTeamId','team:platform').property('sla','99.5%/30d').property('env','prod').property('tags',['email','sms']).property('criticality',3).property('createdAt',1700001004000).property('updatedAt',1700001004000).iterate()
+g.addV('service').property('serviceId','svc:fraud').property('name','Fraud Detection Service').property('tier','high').property('ownerTeamId','team:payments').property('sla','99.5%/30d').property('env','prod').property('tags',['ml']).property('criticality',4).property('createdAt',1700001005000).property('updatedAt',1700001005000).iterate()
+
+// === Applications (6) ===
+g.addV('application').property('appId','app:api-gateway').property('name','api-gateway').property('language','java').property('repo','https://git.example.com/api-gateway').property('version','1.3.0').property('runtime','jvm').property('env','prod').property('createdAt',1700002000000).property('updatedAt',1700002000000).iterate()
+g.addV('application').property('appId','app:payments-api').property('name','payments-api').property('language','java').property('repo','https://git.example.com/payments').property('version','2.1.0').property('runtime','jvm').property('env','prod').property('createdAt',1700002001000).property('updatedAt',1700002001000).iterate()
+g.addV('application').property('appId','app:orders-api').property('name','orders-api').property('language','kotlin').property('repo','https://git.example.com/orders').property('version','1.8.2').property('runtime','jvm').property('env','prod').property('createdAt',1700002002000).property('updatedAt',1700002002000).iterate()
+g.addV('application').property('appId','app:inventory-api').property('name','inventory-api').property('language','go').property('repo','https://git.example.com/inventory').property('version','0.9.5').property('runtime','go').property('env','prod').property('createdAt',1700002003000).property('updatedAt',1700002003000).iterate()
+g.addV('application').property('appId','app:notifications-api').property('name','notifications-api').property('language','nodejs').property('repo','https://git.example.com/notifications').property('version','3.2.1').property('runtime','nodejs').property('env','prod').property('createdAt',1700002004000).property('updatedAt',1700002004000).iterate()
+g.addV('application').property('appId','app:fraud-api').property('name','fraud-api').property('language','python').property('repo','https://git.example.com/fraud').property('version','1.4.0').property('runtime','python').property('env','prod').property('createdAt',1700002005000).property('updatedAt',1700002005000).iterate()
+
+// === Hosts (8) ===
+g.addV('host').property('hostId','host:prod-use1a-01').property('hostname','ip-10-0-0-1').property('provider','aws').property('region','us-east-1').property('availabilityZone','us-east-1a').property('env','prod').property('os','linux').property('instanceType','m5.large').property('createdAt',1700003000000).property('updatedAt',1700003000000).iterate()
+g.addV('host').property('hostId','host:prod-use1a-02').property('hostname','ip-10-0-0-2').property('provider','aws').property('region','us-east-1').property('availabilityZone','us-east-1a').property('env','prod').property('os','linux').property('instanceType','m5.large').property('createdAt',1700003001000).property('updatedAt',1700003001000).iterate()
+g.addV('host').property('hostId','host:prod-use1b-01').property('hostname','ip-10-0-1-1').property('provider','aws').property('region','us-east-1').property('availabilityZone','us-east-1b').property('env','prod').property('os','linux').property('instanceType','m5.xlarge').property('createdAt',1700003002000).property('updatedAt',1700003002000).iterate()
+g.addV('host').property('hostId','host:prod-use1b-02').property('hostname','ip-10-0-1-2').property('provider','aws').property('region','us-east-1').property('availabilityZone','us-east-1b').property('env','prod').property('os','linux').property('instanceType','m5.xlarge').property('createdAt',1700003003000).property('updatedAt',1700003003000).iterate()
+g.addV('host').property('hostId','host:prod-use1c-01').property('hostname','ip-10-0-2-1').property('provider','aws').property('region','us-east-1').property('availabilityZone','us-east-1c').property('env','prod').property('os','linux').property('instanceType','c5.large').property('createdAt',1700003004000).property('updatedAt',1700003004000).iterate()
+g.addV('host').property('hostId','host:prod-use1c-02').property('hostname','ip-10-0-2-2').property('provider','aws').property('region','us-east-1').property('availabilityZone','us-east-1c').property('env','prod').property('os','linux').property('instanceType','c5.large').property('createdAt',1700003005000).property('updatedAt',1700003005000).iterate()
+g.addV('host').property('hostId','host:prod-use1d-01').property('hostname','ip-10-0-3-1').property('provider','aws').property('region','us-east-1').property('availabilityZone','us-east-1d').property('env','prod').property('os','linux').property('instanceType','c5.xlarge').property('createdAt',1700003006000).property('updatedAt',1700003006000).iterate()
+g.addV('host').property('hostId','host:prod-use1d-02').property('hostname','ip-10-0-3-2').property('provider','aws').property('region','us-east-1').property('availabilityZone','us-east-1d').property('env','prod').property('os','linux').property('instanceType','c5.xlarge').property('createdAt',1700003007000).property('updatedAt',1700003007000).iterate()
+
+// === Dependencies (6) ===
+g.addV('dependency').property('depId','dep:payments-db').property('type','db').property('name','payments-db').property('vendor','aurora-postgres').property('criticality',5).property('endpoint','postgres://payments-db.prod:5432/pay').property('env','prod').property('createdAt',1700004000000).property('updatedAt',1700004000000).iterate()
+g.addV('dependency').property('depId','dep:orders-db').property('type','db').property('name','orders-db').property('vendor','aurora-postgres').property('criticality',5).property('endpoint','postgres://orders-db.prod:5432/orders').property('env','prod').property('createdAt',1700004001000).property('updatedAt',1700004001000).iterate()
+g.addV('dependency').property('depId','dep:inventory-cache').property('type','cache').property('name','inventory-cache').property('vendor','redis').property('criticality',4).property('endpoint','redis://inventory-cache.prod:6379').property('env','prod').property('createdAt',1700004002000).property('updatedAt',1700004002000).iterate()
+g.addV('dependency').property('depId','dep:payments-queue').property('type','queue').property('name','payments-queue').property('vendor','sqs').property('criticality',3).property('endpoint','arn:aws:sqs:us-east-1:acct:payments').property('env','prod').property('createdAt',1700004003000).property('updatedAt',1700004003000).iterate()
+g.addV('dependency').property('depId','dep:notifications-svc').property('type','api').property('name','notifications').property('vendor','internal').property('criticality',3).property('endpoint','https://notifications.prod/api').property('env','prod').property('createdAt',1700004004000).property('updatedAt',1700004004000).iterate()
+g.addV('dependency').property('depId','dep:fraud-model').property('type','storage').property('name','fraud-model-store').property('vendor','s3').property('criticality',4).property('endpoint','s3://fraud-models/prod').property('env','prod').property('createdAt',1700004005000).property('updatedAt',1700004005000).iterate()
+
+// === Deployments (10) ===
+g.addV('deployment').property('deployId','depoy:api-gateway-20231113.1').property('version','1.3.0').property('initiatedBy','cd-bot').property('deployedAt',1700005000000).property('status','success').property('changeSet',['a1','a2']).property('env','prod').iterate()
+g.addV('deployment').property('deployId','depoy:payments-20231113.2').property('version','2.1.0').property('initiatedBy','cd-bot').property('deployedAt',1700005005000).property('status','partial').property('changeSet',['b1','b2','b3']).property('env','prod').iterate()
+g.addV('deployment').property('deployId','depoy:orders-20231113.1').property('version','1.8.2').property('initiatedBy','cd-bot').property('deployedAt',1700005010000).property('status','success').property('changeSet',['c1']).property('env','prod').iterate()
+g.addV('deployment').property('deployId','depoy:inventory-20231113.1').property('version','0.9.5').property('initiatedBy','cd-bot').property('deployedAt',1700005015000).property('status','success').property('changeSet',['d1','d2']).property('env','prod').iterate()
+g.addV('deployment').property('deployId','depoy:notifications-20231113.3').property('version','3.2.1').property('initiatedBy','cd-bot').property('deployedAt',1700005020000).property('status','success').property('changeSet',['e1']).property('env','prod').iterate()
+g.addV('deployment').property('deployId','depoy:fraud-20231113.1').property('version','1.4.0').property('initiatedBy','cd-bot').property('deployedAt',1700005025000).property('status','failed').property('changeSet',['f1','f2']).property('env','prod').iterate()
+g.addV('deployment').property('deployId','depoy:payments-20231114.1').property('version','2.1.1').property('initiatedBy','cd-bot').property('deployedAt',1700091400000).property('status','success').property('changeSet',['b4']).property('env','prod').iterate()
+g.addV('deployment').property('deployId','depoy:orders-20231114.1').property('version','1.8.3').property('initiatedBy','cd-bot').property('deployedAt',1700092400000).property('status','success').property('changeSet',['c2']).property('env','prod').iterate()
+g.addV('deployment').property('deployId','depoy:api-gateway-20231114.1').property('version','1.3.1').property('initiatedBy','cd-bot').property('deployedAt',1700093400000).property('status','success').property('changeSet',['a3']).property('env','prod').iterate()
+g.addV('deployment').property('deployId','depoy:fraud-20231114.1').property('version','1.4.1').property('initiatedBy','cd-bot').property('deployedAt',1700095400000).property('status','success').property('changeSet',['f3']).property('env','prod').iterate()
+
+// === Incidents (5) ===
+g.addV('incident').property('incidentId','inc:sev1-payments-001').property('severity','sev1').property('state','closed').property('openedAt',1700006000000).property('closedAt',1700007000000).property('summary','Spike in payment failures').property('description','Error rate spiked after deployment').property('env','prod').property('createdBy','sre').property('updatedAt',1700007000000).iterate()
+g.addV('incident').property('incidentId','inc:sev2-orders-001').property('severity','sev2').property('state','mitigated').property('openedAt',1700092000000).property('closedAt',1700095000000).property('summary','Order delays').property('description','Increased latency due to downstream cache').property('env','prod').property('createdBy','noc').property('updatedAt',1700095000000).iterate()
+g.addV('incident').property('incidentId','inc:sev3-inventory-001').property('severity','sev3').property('state','closed').property('openedAt',1700006200000).property('closedAt',1700007200000).property('summary','Inventory cache miss rate up').property('description','Redis node slow').property('env','prod').property('createdBy','sre').property('updatedAt',1700007200000).iterate()
+g.addV('incident').property('incidentId','inc:sev2-fraud-001').property('severity','sev2').property('state','closed').property('openedAt',1700006300000).property('closedAt',1700009300000).property('summary','Fraud model load failures').property('description','S3 throttling').property('env','prod').property('createdBy','sre').property('updatedAt',1700009300000).iterate()
+g.addV('incident').property('incidentId','inc:sev1-api-001').property('severity','sev1').property('state','closed').property('openedAt',1700093600000).property('closedAt',1700095600000).property('summary','Gateway 5xx surge').property('description','Upstream instability').property('env','prod').property('createdBy','sre').property('updatedAt',1700095600000).iterate()
+
+// === Alerts (15) ===
+g.addV('alert').property('alertId','alt:pay-err-001').property('source','prometheus').property('metric','errorRate').property('firedAt',1700006050000).property('clearedAt',1700007050000).property('severity','sev1').property('env','prod').property('fingerprint','pay-err').property('createdAt',1700006050000).property('updatedAt',1700007050000).iterate()
+g.addV('alert').property('alertId','alt:pay-lat-001').property('source','prometheus').property('metric','latency').property('firedAt',1700006060000).property('clearedAt',1700007060000).property('severity','sev2').property('env','prod').property('fingerprint','pay-lat').property('createdAt',1700006060000).property('updatedAt',1700007060000).iterate()
+g.addV('alert').property('alertId','alt:orders-lat-001').property('source','prometheus').property('metric','latency').property('firedAt',1700092100000).property('clearedAt',1700095100000).property('severity','sev2').property('env','prod').property('fingerprint','ord-lat').property('createdAt',1700092100000).property('updatedAt',1700095100000).iterate()
+g.addV('alert').property('alertId','alt:inv-cache-001').property('source','redis').property('metric','cacheMiss').property('firedAt',1700006210000).property('clearedAt',1700007210000).property('severity','sev3').property('env','prod').property('fingerprint','inv-cache').property('createdAt',1700006210000).property('updatedAt',1700007210000).iterate()
+g.addV('alert').property('alertId','alt:fraud-model-001').property('source','app').property('metric','modelLoadFail').property('firedAt',1700006310000).property('clearedAt',1700009310000).property('severity','sev2').property('env','prod').property('fingerprint','fraud-model').property('createdAt',1700006310000).property('updatedAt',1700009310000).iterate()
+g.addV('alert').property('alertId','alt:gateway-5xx-001').property('source','nginx').property('metric','5xx').property('firedAt',1700093610000).property('clearedAt',1700095610000).property('severity','sev1').property('env','prod').property('fingerprint','gw-5xx').property('createdAt',1700093610000).property('updatedAt',1700095610000).iterate()
+g.addV('alert').property('alertId','alt:payments-db-conn').property('source','rds').property('metric','connections').property('firedAt',1700006040000).property('clearedAt',1700007040000).property('severity','sev2').property('env','prod').property('fingerprint','rds-conns').property('createdAt',1700006040000).property('updatedAt',1700007040000).iterate()
+g.addV('alert').property('alertId','alt:orders-db-lag').property('source','rds').property('metric','replicaLag').property('firedAt',1700092200000).property('clearedAt',1700095200000).property('severity','sev3').property('env','prod').property('fingerprint','rds-lag').property('createdAt',1700092200000).property('updatedAt',1700095200000).iterate()
+g.addV('alert').property('alertId','alt:inv-redis-cpu').property('source','redis').property('metric','cpu').property('firedAt',1700006220000).property('clearedAt',1700007220000).property('severity','sev3').property('env','prod').property('fingerprint','redis-cpu').property('createdAt',1700006220000).property('updatedAt',1700007220000).iterate()
+g.addV('alert').property('alertId','alt:fraud-s3-429').property('source','s3').property('metric','throttle').property('firedAt',1700006320000).property('clearedAt',1700009320000).property('severity','sev2').property('env','prod').property('fingerprint','s3-429').property('createdAt',1700006320000).property('updatedAt',1700009320000).iterate()
+g.addV('alert').property('alertId','alt:pay-queue-depth').property('source','sqs').property('metric','queueDepth').property('firedAt',1700006070000).property('clearedAt',1700007070000).property('severity','sev2').property('env','prod').property('fingerprint','sqs-depth').property('createdAt',1700006070000).property('updatedAt',1700007070000).iterate()
+g.addV('alert').property('alertId','alt:api-rt-90p').property('source','prometheus').property('metric','latency').property('firedAt',1700093620000).property('clearedAt',1700095620000).property('severity','sev2').property('env','prod').property('fingerprint','api-rt').property('createdAt',1700093620000).property('updatedAt',1700095620000).iterate()
+g.addV('alert').property('alertId','alt:notify-bounce').property('source','app').property('metric','bounceRate').property('firedAt',1700006400000).property('clearedAt',1700007400000).property('severity','sev3').property('env','prod').property('fingerprint','notif-bounce').property('createdAt',1700006400000).property('updatedAt',1700007400000).iterate()
+g.addV('alert').property('alertId','alt:gateway-conn').property('source','nginx').property('metric','connections').property('firedAt',1700093630000).property('clearedAt',1700095630000).property('severity','sev3').property('env','prod').property('fingerprint','gw-conns').property('createdAt',1700093630000).property('updatedAt',1700095630000).iterate()
+g.addV('alert').property('alertId','alt:orders-backlog').property('source','app').property('metric','backlog').property('firedAt',1700092300000).property('clearedAt',1700095300000).property('severity','sev2').property('env','prod').property('fingerprint','ord-backlog').property('createdAt',1700092300000).property('updatedAt',1700095300000).iterate()
+
+// === Ownership (ownedBy) ===
+g.V().has('service','serviceId','svc:api-gateway').as('s').V().has('team','teamId','team:platform').addE('ownedBy').from('s').property('since',1699999000000).property('role','primary').property('escalationPolicy','24x7').iterate()
+g.V().has('service','serviceId','svc:payments').as('s').V().has('team','teamId','team:payments').addE('ownedBy').from('s').property('since',1699999001000).property('role','primary').property('escalationPolicy','24x7').iterate()
+g.V().has('service','serviceId','svc:orders').as('s').V().has('team','teamId','team:orders').addE('ownedBy').from('s').property('since',1699999002000).property('role','primary').property('escalationPolicy','business-hours').iterate()
+g.V().has('service','serviceId','svc:inventory').as('s').V().has('team','teamId','team:orders').addE('ownedBy').from('s').property('since',1699999003000).property('role','secondary').property('escalationPolicy','business-hours').iterate()
+g.V().has('service','serviceId','svc:notifications').as('s').V().has('team','teamId','team:platform').addE('ownedBy').from('s').property('since',1699999004000).property('role','primary').property('escalationPolicy','best-effort').iterate()
+g.V().has('service','serviceId','svc:fraud').as('s').V().has('team','teamId','team:payments').addE('ownedBy').from('s').property('since',1699999005000).property('role','primary').property('escalationPolicy','24x7').iterate()
+
+// === Service ↔ Application (deployedAs) ===
+g.V().has('service','serviceId','svc:api-gateway').as('s').V().has('application','appId','app:api-gateway').addE('deployedAs').from('s').property('since',1699999900000).property('strategy','rolling').property('trafficPct',100.0).property('status','active').property('notes','prod stable').iterate()
+g.V().has('service','serviceId','svc:payments').as('s').V().has('application','appId','app:payments-api').addE('deployedAs').from('s').property('since',1699999901000).property('strategy','canary').property('trafficPct',90.0).property('status','active').property('notes','10% older').iterate()
+g.V().has('service','serviceId','svc:orders').as('s').V().has('application','appId','app:orders-api').addE('deployedAs').from('s').property('since',1699999902000).property('strategy','rolling').property('trafficPct',100.0).property('status','active').property('notes','').iterate()
+g.V().has('service','serviceId','svc:inventory').as('s').V().has('application','appId','app:inventory-api').addE('deployedAs').from('s').property('since',1699999903000).property('strategy','rolling').property('trafficPct',100.0).property('status','active').property('notes','').iterate()
+g.V().has('service','serviceId','svc:notifications').as('s').V().has('application','appId','app:notifications-api').addE('deployedAs').from('s').property('since',1699999904000).property('strategy','blueGreen').property('trafficPct',100.0).property('status','active').property('notes','').iterate()
+g.V().has('service','serviceId','svc:fraud').as('s').V().has('application','appId','app:fraud-api').addE('deployedAs').from('s').property('since',1699999905000).property('strategy','recreate').property('trafficPct',100.0).property('status','active').property('notes','').iterate()
+
+// === Application ↔ Host (runsOn) ===
+g.V().has('application','appId','app:api-gateway').as('a').V().has('host','hostId','host:prod-use1a-01').addE('runsOn').from('a').property('since',1700002100000).property('status','active').property('instanceId','gw-1').property('lastSeen',1700005600000).property('cpuPct',35.2).property('memPct',62.1).property('confidence',1.0).iterate()
+g.V().has('application','appId','app:api-gateway').as('a').V().has('host','hostId','host:prod-use1b-01').addE('runsOn').from('a').property('since',1700002105000).property('status','active').property('instanceId','gw-2').property('lastSeen',1700005600000).property('cpuPct',28.5).property('memPct',54.3).property('confidence',1.0).iterate()
+g.V().has('application','appId','app:payments-api').as('a').V().has('host','hostId','host:prod-use1a-02').addE('runsOn').from('a').property('since',1700002110000).property('status','active').property('instanceId','pay-1').property('lastSeen',1700005600000).property('cpuPct',41.0).property('memPct',70.0).property('confidence',1.0).iterate()
+g.V().has('application','appId','app:payments-api').as('a').V().has('host','hostId','host:prod-use1c-01').addE('runsOn').from('a').property('since',1700002115000).property('status','active').property('instanceId','pay-2').property('lastSeen',1700005600000).property('cpuPct',39.0).property('memPct',68.0).property('confidence',1.0).iterate()
+g.V().has('application','appId','app:orders-api').as('a').V().has('host','hostId','host:prod-use1b-02').addE('runsOn').from('a').property('since',1700002120000).property('status','active').property('instanceId','ord-1').property('lastSeen',1700005600000).property('cpuPct',22.0).property('memPct',49.0).property('confidence',1.0).iterate()
+g.V().has('application','appId','app:inventory-api').as('a').V().has('host','hostId','host:prod-use1c-02').addE('runsOn').from('a').property('since',1700002125000).property('status','active').property('instanceId','inv-1').property('lastSeen',1700005600000).property('cpuPct',19.0).property('memPct',45.0).property('confidence',1.0).iterate()
+g.V().has('application','appId','app:notifications-api').as('a').V().has('host','hostId','host:prod-use1d-01').addE('runsOn').from('a').property('since',1700002130000).property('status','active').property('instanceId','ntf-1').property('lastSeen',1700005600000).property('cpuPct',12.0).property('memPct',35.0).property('confidence',1.0).iterate()
+g.V().has('application','appId','app:fraud-api').as('a').V().has('host','hostId','host:prod-use1d-02').addE('runsOn').from('a').property('since',1700002135000).property('status','active').property('instanceId','frd-1').property('lastSeen',1700005600000).property('cpuPct',55.0).property('memPct',75.0).property('confidence',0.9).iterate()
+
+// === Service ↔ Dependency (dependsOn) ===
+g.V().has('service','serviceId','svc:payments').as('s').V().has('dependency','depId','dep:payments-db').addE('dependsOn').from('s').property('since',1699999800000).property('required',true).property('direction','read-write').property('latencyP50Ms',12).property('errorRatePct',1.5).property('weight',1.0).property('lastVerifiedAt',1700005500000).iterate()
+g.V().has('service','serviceId','svc:orders').as('s').V().has('dependency','depId','dep:orders-db').addE('dependsOn').from('s').property('since',1699999801000).property('required',true).property('direction','read-write').property('latencyP50Ms',15).property('errorRatePct',0.9).property('weight',1.0).property('lastVerifiedAt',1700005500000).iterate()
+g.V().has('service','serviceId','svc:inventory').as('s').V().has('dependency','depId','dep:inventory-cache').addE('dependsOn').from('s').property('since',1699999802000).property('required',false).property('direction','read').property('latencyP50Ms',3).property('errorRatePct',2.5).property('weight',0.7).property('lastVerifiedAt',1700005500000).iterate()
+g.V().has('service','serviceId','svc:payments').as('s').V().has('dependency','depId','dep:payments-queue').addE('dependsOn').from('s').property('since',1699999803000).property('required',true).property('direction','write').property('latencyP50Ms',8).property('errorRatePct',0.5).property('weight',0.6).property('lastVerifiedAt',1700005500000).iterate()
+g.V().has('service','serviceId','svc:notifications').as('s').V().has('dependency','depId','dep:notifications-svc').addE('dependsOn').from('s').property('since',1699999804000).property('required',true).property('direction','http').property('latencyP50Ms',45).property('errorRatePct',0.2).property('weight',0.5).property('lastVerifiedAt',1700005500000).iterate()
+g.V().has('service','serviceId','svc:fraud').as('s').V().has('dependency','depId','dep:fraud-model').addE('dependsOn').from('s').property('since',1699999805000).property('required',true).property('direction','read').property('latencyP50Ms',30).property('errorRatePct',4.0).property('weight',0.9).property('lastVerifiedAt',1700005500000).iterate()
+
+// === Deployment ↔ Application (deploymentOf) ===
+g.V().has('deployment','deployId','depoy:api-gateway-20231113.1').as('d').V().has('application','appId','app:api-gateway').addE('deploymentOf').from('d').property('relationCreatedAt',1700005001000).property('targetVersion','1.3.0').property('result','success').property('durationMs',600000).property('changeTicket','CHG-1001').iterate()
+g.V().has('deployment','deployId','depoy:payments-20231113.2').as('d').V().has('application','appId','app:payments-api').addE('deploymentOf').from('d').property('relationCreatedAt',1700005006000).property('targetVersion','2.1.0').property('result','partial').property('durationMs',900000).property('changeTicket','CHG-1002').iterate()
+g.V().has('deployment','deployId','depoy:orders-20231113.1').as('d').V().has('application','appId','app:orders-api').addE('deploymentOf').from('d').property('relationCreatedAt',1700005011000).property('targetVersion','1.8.2').property('result','success').property('durationMs',420000).property('changeTicket','CHG-1003').iterate()
+g.V().has('deployment','deployId','depoy:inventory-20231113.1').as('d').V().has('application','appId','app:inventory-api').addE('deploymentOf').from('d').property('relationCreatedAt',1700005016000).property('targetVersion','0.9.5').property('result','success').property('durationMs',300000).property('changeTicket','CHG-1004').iterate()
+g.V().has('deployment','deployId','depoy:notifications-20231113.3').as('d').V().has('application','appId','app:notifications-api').addE('deploymentOf').from('d').property('relationCreatedAt',1700005021000).property('targetVersion','3.2.1').property('result','success').property('durationMs',300000).property('changeTicket','CHG-1005').iterate()
+g.V().has('deployment','deployId','depoy:fraud-20231113.1').as('d').V().has('application','appId','app:fraud-api').addE('deploymentOf').from('d').property('relationCreatedAt',1700005026000).property('targetVersion','1.4.0').property('result','failed').property('durationMs',780000).property('changeTicket','CHG-1006').iterate()
+g.V().has('deployment','deployId','depoy:payments-20231114.1').as('d').V().has('application','appId','app:payments-api').addE('deploymentOf').from('d').property('relationCreatedAt',1700091401000).property('targetVersion','2.1.1').property('result','success').property('durationMs',480000).property('changeTicket','CHG-1007').iterate()
+g.V().has('deployment','deployId','depoy:orders-20231114.1').as('d').V().has('application','appId','app:orders-api').addE('deploymentOf').from('d').property('relationCreatedAt',1700092401000).property('targetVersion','1.8.3').property('result','success').property('durationMs',450000).property('changeTicket','CHG-1008').iterate()
+g.V().has('deployment','deployId','depoy:api-gateway-20231114.1').as('d').V().has('application','appId','app:api-gateway').addE('deploymentOf').from('d').property('relationCreatedAt',1700093401000).property('targetVersion','1.3.1').property('result','success').property('durationMs',420000).property('changeTicket','CHG-1009').iterate()
+g.V().has('deployment','deployId','depoy:fraud-20231114.1').as('d').V().has('application','appId','app:fraud-api').addE('deploymentOf').from('d').property('relationCreatedAt',1700095401000).property('targetVersion','1.4.1').property('result','success').property('durationMs',600000).property('changeTicket','CHG-1010').iterate()
+
+// === Deployment ↔ Host (deployedTo) ===
+g.V().has('deployment','deployId','depoy:api-gateway-20231113.1').as('d').V().has('host','hostId','host:prod-use1a-01').addE('deployedTo').from('d').property('at',1700005002000).property('result','success').property('attempts',1).property('durationMs',120000).property('rollback',false).iterate()
+g.V().has('deployment','deployId','depoy:api-gateway-20231113.1').as('d').V().has('host','hostId','host:prod-use1b-01').addE('deployedTo').from('d').property('at',1700005003000).property('result','success').property('attempts',1).property('durationMs',120000).property('rollback',false).iterate()
+g.V().has('deployment','deployId','depoy:payments-20231113.2').as('d').V().has('host','hostId','host:prod-use1a-02').addE('deployedTo').from('d').property('at',1700005007000).property('result','failed').property('attempts',2).property('durationMs',300000).property('rollback',true).iterate()
+g.V().has('deployment','deployId','depoy:payments-20231114.1').as('d').V().has('host','hostId','host:prod-use1c-01').addE('deployedTo').from('d').property('at',1700091402000).property('result','success').property('attempts',1).property('durationMs',150000).property('rollback',false).iterate()
+g.V().has('deployment','deployId','depoy:orders-20231113.1').as('d').V().has('host','hostId','host:prod-use1b-02').addE('deployedTo').from('d').property('at',1700005012000).property('result','success').property('attempts',1).property('durationMs',90000).property('rollback',false).iterate()
+g.V().has('deployment','deployId','depoy:inventory-20231113.1').as('d').V().has('host','hostId','host:prod-use1c-02').addE('deployedTo').from('d').property('at',1700005017000).property('result','success').property('attempts',1).property('durationMs',80000).property('rollback',false).iterate()
+g.V().has('deployment','deployId','depoy:notifications-20231113.3').as('d').V().has('host','hostId','host:prod-use1d-01').addE('deployedTo').from('d').property('at',1700005022000).property('result','success').property('attempts',1).property('durationMs',85000).property('rollback',false).iterate()
+g.V().has('deployment','deployId','depoy:fraud-20231114.1').as('d').V().has('host','hostId','host:prod-use1d-02').addE('deployedTo').from('d').property('at',1700095402000).property('result','success').property('attempts',1).property('durationMs',140000).property('rollback',false).iterate()
+
+// === Application ↔ Application (calls) ===
+g.V().has('application','appId','app:api-gateway').as('a').V().has('application','appId','app:orders-api').addE('calls').from('a').property('since',1699999950000).property('protocol','http').property('method','GET /orders').property('avgLatencyMs',120).property('errorRatePct',1.1).property('sampleRatePct',50.0).property('lastSeen',1700005600000).iterate()
+g.V().has('application','appId','app:api-gateway').as('a').V().has('application','appId','app:payments-api').addE('calls').from('a').property('since',1699999951000).property('protocol','http').property('method','POST /payments').property('avgLatencyMs',180).property('errorRatePct',2.2).property('sampleRatePct',60.0).property('lastSeen',1700005600000).iterate()
+g.V().has('application','appId','app:orders-api').as('a').V().has('application','appId','app:inventory-api').addE('calls').from('a').property('since',1699999952000).property('protocol','grpc').property('method','GetStock').property('avgLatencyMs',45).property('errorRatePct',0.5).property('sampleRatePct',40.0).property('lastSeen',1700005600000).iterate()
+g.V().has('application','appId','app:payments-api').as('a').V().has('application','appId','app:fraud-api').addE('calls').from('a').property('since',1699999953000).property('protocol','http').property('method','POST /score').property('avgLatencyMs',95).property('errorRatePct',3.8).property('sampleRatePct',70.0).property('lastSeen',1700005600000).iterate()
+g.V().has('application','appId','app:fraud-api').as('a').V().has('application','appId','app:notifications-api').addE('calls').from('a').property('since',1699999954000).property('protocol','mq').property('method','publish:fraud-alert').property('avgLatencyMs',15).property('errorRatePct',0.2).property('sampleRatePct',20.0).property('lastSeen',1700005600000).iterate()
+
+// === Incidents ↔ Services (affects) ===
+g.V().has('incident','incidentId','inc:sev1-payments-001').as('i').V().has('service','serviceId','svc:payments').addE('affects').from('i').property('at',1700006055000).property('impactType','degradation').property('scope','25% traffic').property('slaImpactPct',10.0).property('confidence',0.9).iterate()
+g.V().has('incident','incidentId','inc:sev2-orders-001').as('i').V().has('service','serviceId','svc:orders').addE('affects').from('i').property('at',1700092105000).property('impactType','degradation').property('scope','region us-east-1').property('slaImpactPct',5.0).property('confidence',0.8).iterate()
+g.V().has('incident','incidentId','inc:sev3-inventory-001').as('i').V().has('service','serviceId','svc:inventory').addE('affects').from('i').property('at',1700006215000).property('impactType','degradation').property('scope','cache tier').property('slaImpactPct',2.0).property('confidence',0.7).iterate()
+g.V().has('incident','incidentId','inc:sev2-fraud-001').as('i').V().has('service','serviceId','svc:fraud').addE('affects').from('i').property('at',1700006315000).property('impactType','degradation').property('scope','model fetch').property('slaImpactPct',6.0).property('confidence',0.85).iterate()
+g.V().has('incident','incidentId','inc:sev1-api-001').as('i').V().has('service','serviceId','svc:api-gateway').addE('affects').from('i').property('at',1700093615000).property('impactType','outage').property('scope','global').property('slaImpactPct',20.0).property('confidence',0.95).iterate()
+
+// === Alerts ↔ Incidents (triggered) ===
+g.V().has('alert','alertId','alt:pay-err-001').as('a').V().has('incident','incidentId','inc:sev1-payments-001').addE('triggered').from('a').property('at',1700006065000).property('correlationScore',0.9).property('reason','rule:pay-err>2%').property('evidenceId','evt:123').iterate()
+g.V().has('alert','alertId','alt:payments-db-conn').as('a').V().has('incident','incidentId','inc:sev1-payments-001').addE('triggered').from('a').property('at',1700006068000).property('correlationScore',0.7).property('reason','db connections high').property('evidenceId','evt:124').iterate()
+g.V().has('alert','alertId','alt:orders-lat-001').as('a').V().has('incident','incidentId','inc:sev2-orders-001').addE('triggered').from('a').property('at',1700092110000).property('correlationScore',0.8).property('reason','p90 latency').property('evidenceId','evt:220').iterate()
+g.V().has('alert','alertId','alt:orders-db-lag').as('a').V().has('incident','incidentId','inc:sev2-orders-001').addE('triggered').from('a').property('at',1700092210000).property('correlationScore',0.6).property('reason','replica lag').property('evidenceId','evt:221').iterate()
+g.V().has('alert','alertId','alt:inv-cache-001').as('a').V().has('incident','incidentId','inc:sev3-inventory-001').addE('triggered').from('a').property('at',1700006225000).property('correlationScore',0.75).property('reason','cache miss').property('evidenceId','evt:320').iterate()
+g.V().has('alert','alertId','alt:inv-redis-cpu').as('a').V().has('incident','incidentId','inc:sev3-inventory-001').addE('triggered').from('a').property('at',1700006228000).property('correlationScore',0.65).property('reason','cpu').property('evidenceId','evt:321').iterate()
+g.V().has('alert','alertId','alt:fraud-model-001').as('a').V().has('incident','incidentId','inc:sev2-fraud-001').addE('triggered').from('a').property('at',1700006325000).property('correlationScore',0.85).property('reason','model load fail').property('evidenceId','evt:420').iterate()
+g.V().has('alert','alertId','alt:fraud-s3-429').as('a').V().has('incident','incidentId','inc:sev2-fraud-001').addE('triggered').from('a').property('at',1700006328000).property('correlationScore',0.7).property('reason','s3 throttle').property('evidenceId','evt:421').iterate()
+g.V().has('alert','alertId','alt:gateway-5xx-001').as('a').V().has('incident','incidentId','inc:sev1-api-001').addE('triggered').from('a').property('at',1700093625000).property('correlationScore',0.9).property('reason','5xx surge').property('evidenceId','evt:520').iterate()
+g.V().has('alert','alertId','alt:api-rt-90p').as('a').V().has('incident','incidentId','inc:sev1-api-001').addE('triggered').from('a').property('at',1700093628000).property('correlationScore',0.6).property('reason','latency p90').property('evidenceId','evt:521').iterate()
+
+// === Incidents ↔ Incidents (relatedTo) ===
+g.V().has('incident','incidentId','inc:sev1-payments-001').as('a').V().has('incident','incidentId','inc:sev3-inventory-001').addE('relatedTo').from('a').property('correlationScore',0.5).property('windowStart',1700006000000).property('windowEnd',1700007300000).property('relationType','sharedDependency').iterate()
+g.V().has('incident','incidentId','inc:sev2-orders-001').as('a').V().has('incident','incidentId','inc:sev1-api-001').addE('relatedTo').from('a').property('correlationScore',0.6).property('windowStart',1700092000000).property('windowEnd',1700095700000).property('relationType','coOccurrence').iterate()
diff --git a/docs/upgrade/spec/spec.md b/docs/upgrade/spec/spec.md
new file mode 100644
index 0000000000..17b8857a35
--- /dev/null
+++ b/docs/upgrade/spec/spec.md
@@ -0,0 +1,424 @@
+Title: DevOps CMDB and Incident Knowledge Graph — Language-Agnostic Specification
+
+Summary
+A vendor/framework/language-neutral specification for a graph-based Configuration Management Database (CMDB) and Incident Knowledge Graph suitable for building example applications and upgrade exercises on Apache TinkerPop. This document defines the domain model (vertices, edges, and properties), recommended identifiers and indexing guidelines, data quality constraints, example workloads and traversals, and non-functional requirements. The goal is to make it easy for an LLM (or a develop [...]
+
+Goals
+- Provide a realistic, moderately complex domain to exercise graph modeling, traversals, and API design.
+- Encourage use of rich edge properties (temporal, metrics, provenance) to demonstrate graph advantages over relational models.
+- Keep the spec portable across programming languages, application frameworks, and TinkerPop versions/providers.
+- Support common operational queries: blast radius, dependency discovery, root-cause hints, deployment/incident correlation, ownership, and timelines.
+
+Non-Goals
+- Prescribe a specific programming language, framework, or persistence backend.
+- Require a particular TinkerPop version or feature outside the broadly available core.
+- Provide exhaustive production hardening (security/compliance) beyond reasonable examples and guidance.
+
+Conceptual Overview
+The graph models services, applications, infrastructure, deployments, dependencies, teams, alerts, and incidents. It enables:
+- Traversal across service/application/runtime dependencies.
+- Temporal reasoning about deployments, alerts, and incidents.
+- Impact analysis (blast radius) and ownership discovery.
+- Correlation between runtime signals and change events.
+
+Canonical Labels and Properties
+Notes
+- Use explicit property names; no import wildcards in generated code.
+- Prefer stable string identifiers (natural keys) over auto-increment integers.
+- Property types are suggestions; adapt to your graph provider’s supported types.
+- Cardinality defaults to single unless specified as set or list.
+
+Vertex Labels
+1) service
+- serviceId: String (unique, stable) — e.g., svc:payments
+- name: String
+- tier: String (e.g., critical, high, medium, low)
+- ownerTeamId: String (FK to team.teamId)
+- sla: String (e.g., "99.9%/30d")
+- env: String (e.g., prod, staging, dev)
+- tags: Set<String>
+- criticality: Int (1..5)
+- createdAt: Instant
+- updatedAt: Instant
+
+2) application
+- appId: String (unique) — e.g., app:payments-api
+- name: String
+- language: String (free-form; do not assume a particular runtime)
+- repo: String (URL or VCS path)
+- version: String (semantic version or build hash)
+- runtime: String (e.g., jvm, dotnet, nodejs, python)
+- env: String
+- createdAt: Instant
+- updatedAt: Instant
+
+3) host
+- hostId: String (unique) — hostname, node id, or cloud instance id
+- hostname: String
+- provider: String (e.g., aws, gcp, onprem)
+- region: String
+- availabilityZone: String
+- env: String
+- os: String
+- instanceType: String
+- createdAt: Instant
+- updatedAt: Instant
+
+4) deployment
+- deployId: String (unique)
+- version: String
+- initiatedBy: String (user/bot)
+- deployedAt: Instant
+- status: String (e.g., success, failed, partial, inProgress)
+- changeSet: List<String> (commit ids/PRs/tickets)
+- env: String
+
+5) dependency (external resource)
+- depId: String (unique)
+- type: String (db, cache, queue, api, storage, dns, secret, other)
+- name: String
+- vendor: String
+- criticality: Int (1..5)
+- endpoint: String (URL, ARN, etc.)
+- env: String
+- createdAt: Instant
+- updatedAt: Instant
+
+6) team
+- teamId: String (unique)
+- name: String
+- onCall: Boolean
+- pager: String (optional)
+- slack: String (optional)
+- createdAt: Instant
+- updatedAt: Instant
+
+7) incident
+- incidentId: String (unique)
+- severity: String (sev0, sev1, sev2, sev3)
+- state: String (open, investigating, mitigated, closed)
+- openedAt: Instant
+- closedAt: Instant? (nullable)
+- summary: String
+- description: String
+- env: String
+- createdBy: String
+- updatedAt: Instant
+
+8) alert
+- alertId: String (unique)
+- source: String (monitoring system)
+- metric: String (e.g., errorRate, latency)
+- firedAt: Instant
+- clearedAt: Instant? (nullable)
+- severity: String
+- env: String
+- fingerprint: String (grouping key)
+- createdAt: Instant
+- updatedAt: Instant
+
+Edge Labels (camelCase) with Rich Properties
+Note: Edge properties are central to this model; use them to encode time, confidence, quantitative metrics, and provenance.
+
+A) runsOn (application → host)
+- since: Instant — when the app started running on the host
+- status: String (active, draining, retired)
+- instanceId: String (container/pod id, process id)
+- lastSeen: Instant — last heartbeat
+- cpuPct: Double
+- memPct: Double
+- confidence: Double (0..1) — if relationship inferred
+
+B) deployedAs (service → application)
+- since: Instant — when the service started being represented by the app
+- strategy: String (blueGreen, canary, rolling, recreate)
+- trafficPct: Double — percent traffic routed
+- status: String (active, deprecated)
+- notes: String
+
+C) deploymentOf (deployment → application)
+- relationCreatedAt: Instant — record creation time
+- targetVersion: String — duplicate for quick reads
+- result: String (success, failed, partial)
+- durationMs: Long (elapsed time)
+- changeTicket: String (optional external id)
+
+D) deployedTo (deployment → host)
+- at: Instant — when the deployment reached this host
+- result: String (success, failed, skipped)
+- attempts: Int
+- durationMs: Long
+- rollback: Boolean
+
+E) dependsOn (service → dependency)
+- since: Instant — when the dependency was added
+- required: Boolean — hard vs soft dependency
+- direction: String (read, write, both)
+- latencyP50Ms: Long (observed median)
+- errorRatePct: Double
+- weight: Double (relative importance, 0..1)
+- lastVerifiedAt: Instant
+
+F) ownedBy (service → team)
+- since: Instant
+- role: String (primary, secondary, consulting)
+- escalationPolicy: String
+
+G) triggered (alert → incident)
+- at: Instant — correlation time
+- correlationScore: Double (0..1)
+- reason: String (short explanation or rule id)
+- evidenceId: String (pointer to logs/trace)
+
+H) affects (incident → service)
+- at: Instant — first known impact
+- impactType: String (degradation, outage, dataLoss, security)
+- scope: String (percent traffic affected, region subset)
+- slaImpactPct: Double
+- confidence: Double (0..1)
+
+I) relatedTo (incident ↔ incident) [undirected semantics; model with two directed edges if needed]
+- correlationScore: Double
+- windowStart: Instant
+- windowEnd: Instant
+- relationType: String (duplicateCause, followOn, sharedDependency, coOccurrence)
+
+J) calls (application → application)
+- since: Instant
+- protocol: String (http, grpc, mq, rpc)
+- method: String (GET/POST or operation name)
+- avgLatencyMs: Long
+- errorRatePct: Double
+- sampleRatePct: Double
+- lastSeen: Instant
+
+Identifiers and Cardinality
+- Vertex IDs: Use stable string keys (e.g., serviceId) as custom IDs where supported; otherwise store as a dedicated property and map internal IDs as needed.
+- Edge multiplicity: Allow multiple edges between the same endpoints if they differ by context/time (e.g., calls over protocols). Consider a uniqueness policy per (outV, inV, label, keyPropertiesTuple).
+- Timestamps: Use Instant (UTC). Where Instant unsupported, use epochMillis (Long).
+- Sets vs Lists: tags are Set; changeSet is List to preserve order.
+
+Indexing Guidance (Provider-Agnostic)
+- Equality/lookup indices:
+  - Vertex: service.serviceId, application.appId, host.hostId, deployment.deployId, dependency.depId, team.teamId, incident.incidentId, alert.alertId.
+  - Common filters: env, name, severity, state, version.
+- Edge-centric indices (where supported):
+  - dependsOn.since, dependsOn.required
+  - calls.protocol, calls.method
+  - affected edges by at, impactType
+- Full-text (optional): incident.summary/description for search.
+- Composite vs Mixed: choose based on provider; ensure exact match queries benefit.
+
+Data Quality Constraints (Logical)
+- Uniqueness: All *Id properties are globally unique per label.
+- Referential integrity: Edge endpoints must exist; deletion should consider cascading or soft-delete.
+- Enum validation: severity, state, protocol, strategy, impactType, result should match allowed sets.
+- Temporal consistency: openedAt ≤ closedAt; since ≤ lastSeen; windowStart ≤ windowEnd.
+- Env consistency: Cross-env edges generally discouraged except where intended (documented exceptions only).
+- Confidence bounds: 0 ≤ confidence ≤ 1.0; 0 ≤ errorRatePct ≤ 100; 0 ≤ trafficPct ≤ 100.
+
+Minimum Viable Dataset (for examples/tests)
+A deterministic sample dataset is provided as a Gremlin script at docs/upgrade/spec/cmdb-data.gremlin. Each line is a single mutation (one vertex or one edge) terminated with iterate() for stable, idempotent loading via a Gremlin Server–compliant system.
+
+Example Traversal Patterns (Gremlin-like Pseudocode)
+Note: The following are conceptual and avoid version-specific constructs. Adapt to your provider’s traversal DSL. Variable names are illustrative.
+
+1) Blast radius from an incident (impacted services and upstream relations)
+- Input: incidentId
+- Idea: From an incident, traverse affects to services, then explore transitive dependencies and deployments.
+  g.V().has('incident','incidentId', iid)
+    .out('affects')
+    .emit()
+    .repeat(out('dependsOn','deployedAs').in('deploymentOf'))
+    .path()
+
+2) Root-cause candidates (upstream first failing dependency)
+- Input: incidentId
+  g.V().has('incident','incidentId', iid)
+    .out('affects')
+    .repeat(out('dependsOn')).emit()
+    .where(outE('dependsOn').values('errorRatePct').is(gt(5.0)))
+    .limit(50)
+
+3) Deployment → incident correlation
+- Input: deployId
+  g.V().has('deployment','deployId', did)
+    .out('deployedTo').in('runsOn').in('deployedAs').dedup()
+    .in('affects').hasLabel('incident')
+    .order().by('openedAt', decr).limit(20)
+
+4) Ownership and on-call for an impacted service
+- Input: incidentId
+  g.V().has('incident','incidentId', iid)
+    .out('affects')
+    .out('ownedBy')
+    .valueMap(true)
+
+5) Hot paths between applications (runtime call graph)
+- Input: appId
+  g.V().has('application','appId', aid)
+    .repeat(outE('calls').order().by('avgLatencyMs', decr).inV()).times(3)
+    .path()
+
+6) Services at risk due to a failing dependency
+- Input: depId
+  g.V().has('dependency','depId', did)
+    .inE('dependsOn').has('required', true).outV()
+    .dedup()
+
+7) Time-bounded alert to incident correlation
+- Inputs: fingerprint, windowStart, windowEnd
+  g.V().has('alert','fingerprint', fp)
+    .has('firedAt', between(windowStart, windowEnd))
+    .outE('triggered')
+    .has('correlationScore', gt(0.6))
+    .inV()
+
+Suggested API Surfaces (Language-Neutral)
+These are example resources for a REST/RPC/GraphQL interface. They are intentionally generic.
+- GET /incidents/{id}/blastRadius — returns affected services and transitive dependencies with paths.
+- GET /services/{id}/dependencies?depth=N — returns direct and transitive dependencies.
+- GET /deployments/{id}/timeline — returns path from deployment to impacted services/incidents.
+- GET /services/{id}/owners — returns team ownership and escalation info.
+- POST /alerts — upsert alert vertices and (optionally) triggered edges.
+- POST /incidents — create/modify incident vertices and affects edges.
+- POST /deployments — create deployment vertices and link to application/host.
+
+Operational Concerns (General)
+- Transactions: Group related vertex/edge mutations atomically when supported; otherwise implement idempotency keys on the write path.
+- Idempotency: Upserts keyed by natural IDs; avoid accidental duplication.
+- Pagination: Use limit/offset or cursor; cap server-side to protect the graph.
+- Validation: Enforce enum bounds, temporal checks, and numeric ranges before writes.
+- Timestamps: Use UTC; prefer Instant or epochMillis consistently.
+- Observability: Consider storing minimal audit metadata on edges (createdBy, updatedAt) if provider supports meta-properties.
+
+Testing Guidance
+This section specifies exact black-box tests to run against an implementation that follows this spec, assuming the deterministic dataset at docs/upgrade/spec/cmdb-data.gremlin has been loaded into a Gremlin Server–compliant system (traversal source g). Tests are language- and framework-agnostic. For REST, substitute your base URL; for other transports the inputs/outputs are equivalent.
+
+General setup
+- Precondition: Seed the dataset by submitting each non-empty, non-comment line from docs/upgrade/spec/cmdb-data.gremlin. Ensure a clean graph (e.g., drop all vertices first) so counts match exactly.
+- Conventions: All timestamps are epochMillis; string IDs are exact and case sensitive.
+
+Smoke and readiness
+- Call: GET /health/ready
+- Expect: HTTP 200 with body containing "ready" (or equivalent readiness signal).
+
+Service dependencies (GET /services/{id}/dependencies?depth=N)
+1) svc:orders, depth=2
+- Call: GET /services/svc:orders/dependencies?depth=2
+- Expect: HTTP 200; an ordered or unordered list of vertex value maps including exactly these two vertices:
+  - service svc:orders (properties include serviceId: "svc:orders")
+  - dependency dep:orders-db (properties include depId: "dep:orders-db")
+- Count: 2 total elements. No other services or dependencies should appear for this service in the provided dataset.
+
+2) svc:payments, depth=1
+- Call: GET /services/svc:payments/dependencies?depth=1
+- Expect: HTTP 200; list of value maps for:
+  - service svc:payments
+  - dependency dep:payments-db
+  - dependency dep:payments-queue
+- Count: 3 total elements.
+
+3) svc:inventory, depth=3
+- Call: GET /services/svc:inventory/dependencies?depth=3
+- Expect: HTTP 200; list includes:
+  - service svc:inventory
+  - dependency dep:inventory-cache
+- Count: 2 total elements (no additional transitive dependencies in the sample data).
+
+Service owners (GET /services/{id}/owners)
+4) svc:payments
+- Call: GET /services/svc:payments/owners
+- Expect: HTTP 200; list includes exactly one team vertex value map with:
+  - teamId: "team:payments"
+  - name: "Payments"
+  - onCall: true
+- Count: 1 total element.
+
+5) svc:api-gateway
+- Call: GET /services/svc:api-gateway/owners
+- Expect: HTTP 200; list includes exactly one team with teamId: "team:platform" and name: "Platform Engineering"
+- Count: 1 total element.
+
+Deployment timeline (GET /deployments/{id}/timeline)
+6) depoy:payments-20231113.2 (partial deployment that failed on at least one host)
+- Call: GET /deployments/depoy:payments-20231113.2/timeline
+- Expect: HTTP 200; list of incident vertex value maps related via the traversal (deployedTo -> runsOn -> deployedAs -> affects). Must include exactly:
+  - incidentId: "inc:sev1-payments-001" (severity sev1)
+- Count: 1 total element.
+
+7) depoy:payments-20231114.1 (successful follow-up)
+- Call: GET /deployments/depoy:payments-20231114.1/timeline
+- Expect: HTTP 200; list includes the same incident as above:
+  - incidentId: "inc:sev1-payments-001"
+- Count: 1 total element.
+
+8) depoy:api-gateway-20231114.1
+- Call: GET /deployments/depoy:api-gateway-20231114.1/timeline
+- Expect: HTTP 200; list includes:
+  - incidentId: "inc:sev1-api-001"
+- Count: 1 total element.
+
+Incident blast radius paths (GET /incidents/{id}/blastRadius?depth=N)
+Notes: Responses are typically represented as a collection of paths where each object is a valueMap(true) of the vertex at that step. Order within a path follows traversal order. Exact path ordering across the response may vary by provider, but membership assertions below must hold.
+
+9) inc:sev1-payments-001, depth=1
+- Call: GET /incidents/inc:sev1-payments-001/blastRadius?depth=1
+- Expect: HTTP 200; at least two paths starting with the incident then the affected service svc:payments and then one hop of the repeat step. The set of terminal vertices across all returned paths must contain:
+  - dependency dep:payments-db
+  - dependency dep:payments-queue
+  - deployment vertices reachable via deployedAs/deploymentOf for the payments service: depoy:payments-20231113.2 and depoy:payments-20231114.1
+- Minimal membership checks:
+  - There exists a path whose last vertex has depId = "dep:payments-db"
+  - There exists a path whose last vertex has depId = "dep:payments-queue"
+  - There exists a path whose last vertex has deployId = "depoy:payments-20231113.2"
+  - There exists a path whose last vertex has deployId = "depoy:payments-20231114.1"
+
+10) inc:sev1-api-001, depth=1
+- Call: GET /incidents/inc:sev1-api-001/blastRadius?depth=1
+- Expect: HTTP 200; paths must begin with incident inc:sev1-api-001 then service svc:api-gateway. The set of terminal vertices across all returned paths must include:
+  - deployment depoy:api-gateway-20231113.1
+  - deployment depoy:api-gateway-20231114.1
+- Minimal membership checks (as above) for both deployIds.
+
+Negative and edge cases
+11) Unknown serviceId
+- Call: GET /services/svc:does-not-exist/dependencies?depth=2
+- Expect: HTTP 200 with an empty list (no results).
+
+12) Service with optional dependency (required=false)
+- Call: GET /services/svc:inventory/dependencies?depth=1
+- Expect: HTTP 200 with exactly two elements: svc:inventory and dep:inventory-cache. Verify that dependency existence is returned regardless of the required flag (edge property), as this endpoint enumerates topology not policy.
+
+13) Readiness under graph reset
+- Procedure: Drop all vertices, reload the dataset, then call GET /health/ready
+- Expect: HTTP 200 and subsequent endpoint tests pass identically to the above, demonstrating determinism.
+
+Determinism requirements
+- Use the exact IDs in this section. Counts and membership must match precisely for a conforming implementation using the provided dataset.
+- If your transport or framework changes ordering, assert on membership and counts rather than list order.
+
+Data Loading and Fixtures
+- Provide a deterministic data generator creating the Minimum Viable Dataset with:
+  - Multiple envs (prod, staging) and regions to exercise filters.
+  - Multi-hop dependsOn and calls chains (depth ≥ 3).
+  - Mixed success/failed deployments around incident windows for correlation.
+- Loading order (suggested): teams → services → applications → hosts → dependencies → deployments → incidents → alerts → edges linking them.
+
+Upgrade/Portability Considerations
+- Exercise a broad set of Gremlin steps and traversal patterns (e.g., aggregate, project, path, repeat/emit, barrier, dedup, select/sack, coalesce, optional, groupCount, order(local), by(), math, where with predicates, subgraph, mergeV/E, profile) to intentionally surface potential upgrade differences; document provider/version nuances and, where practical, supply optional fallbacks.
+- Avoid provider-specific features unless guarded with fallbacks.
+- Keep label and property names stable; version and evolve via additive changes.
+- When deprecating: retain old properties/edges, populate new ones in parallel, and document migration traversals.
+
+Glossary
+- CMDB: Configuration Management Database
+- Blast Radius: The set of entities affected by an incident/change
+- Root Cause Candidates: Upstream dependencies most likely responsible for impact
+- Provenance: Metadata indicating how/when a relation was inferred or observed
+
+Appendix: Quick Reference of Labels
+Vertices: service, application, host, deployment, dependency, team, incident, alert
+Edges (camelCase): runsOn, deployedAs, deploymentOf, deployedTo, dependsOn, ownedBy, triggered, affects, relatedTo, calls
+
+This specification is intentionally framework-agnostic and should be sufficient for an LLM to generate a complete example project in any language and on any version/provider compatible with Apache TinkerPop.
\ No newline at end of file
diff --git a/gremlin-python/src/main/python/radish/gremlin.py b/gremlin-python/src/main/python/radish/gremlin.py
index 961230d521..c2586db4ad 100644
--- a/gremlin-python/src/main/python/radish/gremlin.py
+++ b/gremlin-python/src/main/python/radish/gremlin.py
@@ -1827,14 +1827,14 @@ world.gremlins = {
     'InjectXInfX_ltXNegInfX': [(lambda g:g.inject(float('inf')).is_(P.lt(float('-inf'))))], 
     'InjectXNegInfX_ltXInfX': [(lambda g:g.inject(float('-inf')).is_(P.lt(float('inf'))))], 
     'InjectXNegInfX_gtXInfX': [(lambda g:g.inject(float('-inf')).is_(P.gt(float('inf'))))], 
-    'Primitives_Number_eqXbyteX': [(lambda g, xx1=None:g.inject([1, 1, 1, long(1), 1, 1, 1000, bigdecimal(1), bigint(1)]).unfold().where(__.is_(xx1)))], 
-    'Primitives_Number_eqXshortX': [(lambda g, xx1=None:g.inject([1, 1, 1, long(1), 1, 1, 1000, bigdecimal(1), bigint(1)]).unfold().where(__.is_(xx1)))], 
-    'Primitives_Number_eqXintX': [(lambda g, xx1=None:g.inject([1, 1, 1, long(1), 1, 1, 1000, bigdecimal(1), bigint(1)]).unfold().where(__.is_(xx1)))], 
-    'Primitives_Number_eqXlongX': [(lambda g, xx1=None:g.inject([1, 1, 1, long(1), 1, 1, 1000, bigdecimal(1), bigint(1)]).unfold().where(__.is_(xx1)))], 
-    'Primitives_Number_eqXbigintX': [(lambda g, xx1=None:g.inject([1, 1, 1, long(1), 1, 1, 1000, bigdecimal(1), bigint(1)]).unfold().where(__.is_(xx1)))], 
-    'Primitives_Number_eqXfloatX': [(lambda g, xx1=None:g.inject([1, 1, 1, long(1), 1, 1, 1000, bigdecimal(1), bigint(1)]).unfold().where(__.is_(xx1)))], 
-    'Primitives_Number_eqXdoubleX': [(lambda g, xx1=None:g.inject([1, 1, 1, long(1), 1, 1, 1000, bigdecimal(1), bigint(1)]).unfold().where(__.is_(xx1)))], 
-    'Primitives_Number_eqXbigdecimalX': [(lambda g, xx1=None:g.inject([1, 1, 1, long(1), 1, 1, 1000, bigdecimal(1), bigint(1)]).unfold().where(__.is_(xx1)))], 
+    'Primitives_Number_eqXbyteX': [(lambda g, xx1=None:g.inject([1, 1, 1, long(1), 1, 1, 1000, 1, bigint(1)]).unfold().where(__.is_(xx1)))], 
+    'Primitives_Number_eqXshortX': [(lambda g, xx1=None:g.inject([1, 1, 1, long(1), 1, 1, 1000, 1, bigint(1)]).unfold().where(__.is_(xx1)))], 
+    'Primitives_Number_eqXintX': [(lambda g, xx1=None:g.inject([1, 1, 1, long(1), 1, 1, 1000, 1, bigint(1)]).unfold().where(__.is_(xx1)))], 
+    'Primitives_Number_eqXlongX': [(lambda g, xx1=None:g.inject([1, 1, 1, long(1), 1, 1, 1000, 1, bigint(1)]).unfold().where(__.is_(xx1)))], 
+    'Primitives_Number_eqXbigintX': [(lambda g, xx1=None:g.inject([1, 1, 1, long(1), 1, 1, 1000, 1, bigint(1)]).unfold().where(__.is_(xx1)))], 
+    'Primitives_Number_eqXfloatX': [(lambda g, xx1=None:g.inject([1, 1, 1, long(1), 1, 1, 1000, 1, bigint(1)]).unfold().where(__.is_(xx1)))], 
+    'Primitives_Number_eqXdoubleX': [(lambda g, xx1=None:g.inject([1, 1, 1, long(1), 1, 1, 1000, 1, bigint(1)]).unfold().where(__.is_(xx1)))], 
+    'Primitives_Number_eqXbigdecimalX': [(lambda g, xx1=None:g.inject([1, 1, 1, long(1), 1, 1, 1000, 1, bigint(1)]).unfold().where(__.is_(xx1)))], 
     'g_V_values_order': [(lambda g:g.V().values().order())], 
     'g_V_properties_order': [(lambda g:g.V().properties().order())], 
     'g_V_properties_order_id': [(lambda g:g.V().properties().order().id_())], 
diff --git a/pom.xml b/pom.xml
index 615b2c8a76..d97642f884 100644
--- a/pom.xml
+++ b/pom.xml
@@ -532,6 +532,8 @@ limitations under the License.
                         <exclude>**/npm-debug.log</exclude>
                         <exclude>**/dist/**</exclude>
                         <exclude>**/build/**</exclude>
+                        <exclude>docs/upgrade/spec/cmdb-data.gremlin</exclude>
+                        <exclude>docs/upgrade/spec/spec.md</exclude>
                         <!-- javascript docs/lib -->
                         <exclude>**/doc/**</exclude>
                         <exclude>**/lib/**</exclude>