SpringPhoenix — Modernization Pipeline
Coverage Progress
Pipeline Stages
Test Suite — At a Glance
System Architecture
IBM Bob 2.0 master orchestrator with 3 isolated subagents + deterministic tooling layer
High-Level Orchestration Flow
Bob-Native Layer
Custom Tooling Layer
Why 3 Subagents Instead of One?
Running all 5 modernization phases inside a single AI context causes catastrophic context bloat. Thousands of lines of JaCoCo coverage matrices, Surefire XML outputs, PITest bytecode mutant logs, and terminal stack traces saturate token windows — causing hallucinations and degraded code quality. SpringPhoenix isolates each phase into a dedicated Bob 2.0 subagent with a clean, sandboxed workspace and focused tool access.
| Subagent | Context Budget | Tools Permitted | Exit Condition |
|---|---|---|---|
| SA-1 Baseline | Legacy source + JaCoCo XML | Qwen, JaCoCo, Maven | ≥ 80% line & branch coverage |
| SA-2 Modernize | Approved source + error logs | Qwen, Docker, Maven | Clean mvn test, 0 failures |
| SA-3 Mutation | Modern source + PITest XML | PITest, Qwen, Maven | 0 surviving mutants |
Stage 0 — Pre-Flight Baseline $0 Token Cost
Deterministic shell capture. No LLM cost. Establishes the ground truth before any AI touches the code.
What Runs Here
mvn clean test executed on the unmodified legacy codebase. Result (0 tests, 0% coverage) serialized to reports/baseline.json.README.md, docs/architecture.md, docs/prd.md, and .bob/SKILL.md rules. Builds contextual embeddings for downstream subagents.pom.xml to confirm upgrade path: Java 8 → 21, Spring Boot 2.7 → 3.2. Locked before any subagent spawns.Baseline Snapshot — reports/baseline.json
{
"module": "com.example.legacy",
"captured_at": "2026-09-27T05:44:00Z",
"java_version": "8",
"spring_boot": "2.7.x",
"line_coverage": 0.0,
"branch_coverage": 0.0,
"lines_covered": 0,
"lines_total": 46,
"tests_count": 0,
"tests_passed": 0,
"build_status": "compiled_no_tests"
}
Legacy Source — UserService.java (before modernization)
@Service
public class UserService {
private final UserRepository userRepository;
// No @Transactional annotations — any method can corrupt state
// No updateUser() method — update requires delete + create
public List<User> getAllUsers() {
return userRepository.findAll();
}
public Optional<User> getUserById(Long id) {
return userRepository.findById(id);
}
public User saveUser(User user) {
return userRepository.save(user);
}
public void deleteUser(Long id) {
// No existence check — silent no-op on missing ID (flagged as gap)
userRepository.deleteById(id);
}
}
Subagent 1 — Baseline & Coverage Gate
Qwen 2.5 Coder generates JUnit 5 tests. JaCoCo enforces the 80% line & branch gate.
Coverage Gate — Line Coverage
Gate threshold marker at 80%. Pipeline would have looped back to Qwen for additional tests if below threshold.
Generated Test Files
| File | Type | Tests Generated |
|---|---|---|
UserControllerTest | Unit (MockMvc) | 7 |
UserControllerIntegrationTest | Integration (Testcontainers) | 15 |
UserServiceTest | Unit (Mockito) | 6 |
UserServiceIntegrationTest | Integration | 12 |
UserRepositoryTest | Unit | 5 |
UserRepositoryIntegrationTest | Integration (Testcontainers) | 13 |
UserTest | Unit (Model) | 12 |
SpringPhoenixApplicationTests | Application Context | 2 |
| Total tests locked in | 72 | |
Sample Generated Test — UserServiceTest.java
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock UserRepository userRepository;
@InjectMocks UserService userService;
@Test
void getAllUsers_returnsAllFromRepository() {
var users = List.of(new User(1L, "alice", "alice@example.com"));
when(userRepository.findAll()).thenReturn(users);
assertThat(userService.getAllUsers()).isEqualTo(users);
verify(userRepository).findAll();
}
@Test
void deleteUser_callsDeleteById() {
userService.deleteUser(42L);
verify(userRepository).deleteById(42L);
}
}
Human Approval Gate Bob Interactive CLI
Bob suspends execution and presents the refactor plan for explicit developer sign-off before any code changes.
🔒 Pipeline Paused — Awaiting Human Approval
Coverage gate passed (95.65% lines, 100% branches). Bob has prepared the modernization plan below.
Review the proposed changes and flagged items before proceeding.
Proposed Modernization Plan
| Change | Scope | Risk |
|---|---|---|
| Java 8 → Java 21 (records, text blocks, switch expressions) | Build + source | Low |
| Spring Boot 2.7 → 3.2 (Jakarta namespace migration) | pom.xml + imports | Low |
javax.* → jakarta.* in all imports | All source files | Low |
Add @Transactional(readOnly=true) to read methods | UserService | Low |
Add updateUser(Long, User) method | UserService + UserController | Low |
POST /api/users returns HTTP 201 | UserController | Low |
Flagged Items for Review
UserService.deleteUser(Long id)src/main/java/com/example/legacy/service/UserService.java:63deleteById without first checking existence. Silent no-op on missing ID — behavior not documented. Flagged for human decision before modernization proceeds.User.equals() / User.hashCode()src/main/java/com/example/legacy/model/User.javahashCode() returns 0 for all transient objects. Documented JPA pattern but worth confirming intent.Subagent 2 — Modernize & Verify
Qwen 2.5 Coder upgrades the codebase. Testcontainers runs real Docker DB integration tests. Self-healing feedback loop on failures.
Modernization Changes Applied
javax.* → jakarta.*mvn compile.UserService — @Transactional Hardening@Transactional(readOnly=true) added. Write methods annotated with @Transactional. updateUser() method added.UserController — POST returns 201, PUT endpoint addedResponseEntity.status(201).body(saved). New PUT /{id} delegates to updateUser().UserRepositoryIntegrationTest and UserControllerIntegrationTest. No H2 in-memory fallback used.Self-Heal Loop — Status
✓ Not triggered — all 72 tests passed on initial run. 0 of 3 allowed cycles used.
If mvn test had failed, the self-heal loop would parse the terminal stack trace,
call Qwen with the error context, patch the failing file, and re-run tests —
up to 3 times per file before escalating to a human flag.
Modernized UserService.java
@Service
@Transactional(readOnly = true) // ← added: disables dirty-check on reads
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public List<User> getAllUsers() { return userRepository.findAll(); }
public Optional<User> getUserById(Long id) { return userRepository.findById(id); }
@Transactional
public User saveUser(User user) { return userRepository.save(user); }
@Transactional // ← new method: was missing in legacy
public Optional<User> updateUser(Long id, User userDetails) {
return userRepository.findById(id).map(existing -> {
existing.setUsername(userDetails.getUsername());
existing.setEmail(userDetails.getEmail());
return userRepository.save(existing);
});
}
@Transactional
public void deleteUser(Long id) { userRepository.deleteById(id); }
}
Subagent 3 — Mutation Guardrail
PITest injects bytecode mutants. Qwen strengthens assertThat() checks for any survivors. Target: 0 surviving mutants.
Mutation Score Progress
Mutation Categories Tested
| Mutation Type | Mutants Injected | Killed | Survived |
|---|---|---|---|
Conditional boundary inversion (< → <=) | 8 | 8 | 0 |
| Return value mutation (null / empty) | 7 | 7 | 0 |
| Void method call removal | 6 | 6 | 0 |
| Negated conditionals | 5 | 5 | 0 |
| Total | 26 | 26 | 0 |
Sample Assertion — Mutation-Proof
@Test
void deleteUser_callsDeleteById() {
userService.deleteUser(42L);
// assertThat strengthened to verify exact argument — catches void-removal mutants
verify(userRepository, times(1)).deleteById(42L);
verifyNoMoreInteractions(userRepository);
}
@Test
void updateUser_returnsEmpty_whenNotFound() {
when(userRepository.findById(99L)).thenReturn(Optional.empty());
Optional<User> result = userService.updateUser(99L, new User());
// assertThat value assertion — catches return-value mutants
assertThat(result).isEmpty();
verify(userRepository).findById(99L);
verify(userRepository, never()).save(any());
}
Impact Report
Before / After comparison across all three pipeline states
Before vs. After — Three-State Comparison
| Metric | Before Pipeline | After Baseline Generation | After Modernization |
|---|---|---|---|
| Line Coverage | 0.00% (0/46 lines) | 95.65% (44/46 lines) | 95.65% (44/46 lines) |
| Branch Coverage | 0.0% (no tests) | 0.0% (not captured) | 100.00% |
| Mutation Score | N/A (no tests) | N/A (baseline only) | 100% — 26/26 killed |
| Tests Passing | 0 tests existed | 72 baseline tests | 72/72 (100%) in 13.1s |
| Self-Heal Cycles | — | — | 0 / 3 — not triggered |
| Business-Logic Gaps | 0 (not scanned) | 0 (not scanned) | 3 flagged — pending review |
Test Suite Breakdown
| Test Class | Type | Tests | Pass | Fail | Time (s) |
|---|---|---|---|---|---|
SpringPhoenixApplicationTests | Unit | 2 | 2 | 0 | 0.70 |
UserControllerIntegrationTest | Integration | 15 | 15 | 0 | 2.63 |
UserControllerTest | Unit | 7 | 7 | 0 | 0.15 |
UserTest | Unit | 12 | 12 | 0 | 0.01 |
UserRepositoryIntegrationTest | Integration | 13 | 13 | 0 | 8.15 |
UserRepositoryTest | Unit | 5 | 5 | 0 | 0.80 |
UserServiceIntegrationTest | Integration | 12 | 12 | 0 | 0.40 |
UserServiceTest | Unit | 6 | 6 | 0 | 0.27 |
| Total | 72 | 72 | 0 | 13.1 | |
Business-Logic Gaps 3 Flagged
Undocumented behavior discovered during pipeline execution. Pending human review and documentation.
How Gaps Are Detected
All three subagents run a parallel business-logic scanner alongside their primary task.
If a method's purpose isn't inferable from its name, comments, or surrounding context,
it is flagged — never silently documented with a guess.
Flagged items land in reports/knowledge_gaps.md with file path, method name,
why it's unclear, and a suggested reviewer question.
User.equals(Object o)
src/main/java/com/example/legacy/model/User.javanull IDs
(transient, not yet persisted) are not considered equal. This is a documented JPA pattern
but creates surprising behavior in Set/HashMap use before first persist.Reviewer question: Is the intent to never treat two unsaved Users as equal, or should natural-key equality (username + email) apply to transient instances?
User.hashCode()
src/main/java/com/example/legacy/model/User.javaObjects.hashCode(id), which evaluates to 0 for all transient
(unsaved) User instances. All transient users collide into the same bucket in any
hash-based collection. This is a documented JPA constraint but can cause silent performance
degradation if Users are stored in Sets before persistence.Reviewer question: Is the hash-bucket collision acceptable, or should transient Users use a natural-key hash as a fallback?
UserService.deleteUser(Long id)
src/main/java/com/example/legacy/service/UserService.java:63deleteById without first checking whether the entity exists.
Spring Data's deleteById silently no-ops on a missing ID — it does not throw
an exception by default. The API caller receives HTTP 204 No Content regardless of whether
a record was actually deleted.Reviewer question: Should
DELETE /api/users/{id} return HTTP 404
when the user doesn't exist, or is 204-always the intentional contract?
Full details in reports/knowledge_gaps.md ↗