SpringPhoenix — Modernization Pipeline

Module: com.example.legacy (UserController · UserService · UserRepository · User)
Orchestrator: IBM Bob 2.0 · Code Tool: Qwen 2.5 Coder
Pipeline runtime: 59 seconds
✓ All 72 tests passing — 0 surviving mutants
Line Coverage
0%
44 / 46 lines · click to explore
Branch Coverage
0%
Gate: ≥ 80%
Mutation Score
0%
26 / 26 killed · click to explore
Tests Passing
0
0 failures · click for breakdown
Self-Heal Cycles
—
Not triggered
Logic Gaps Flagged
0
Pending review · click to inspect

Coverage Progress

Line Coverage0%
80% gate threshold
Branch Coverage0%
Mutation Score0%

Pipeline Stages

Test Suite — At a Glance

High-Level Orchestration Flow

Trigger
Developer CLI
mvn / bob run
→
Orchestrator
IBM Bob 2.0
Doc Understanding
→
Subagent 1
Baseline & Gate
Qwen + JaCoCo
→
Gate
Human Approval
Bob CLI
→
Subagent 2
Modernize
Qwen + Containers
→
Subagent 3
Mutation Guard
PITest + Qwen
→
Output
Reports
MD + HTML

Bob-Native Layer

Master Workflow Orchestrator
Coordinates stage transitions, scoped context packets, and subagent lifecycles.
Document Understanding
Parses README, specs, and domain wikis into contextual embeddings for business-logic discovery.
Human Approval Gate
Suspends execution before refactoring. Presents diff previews and risk matrices for explicit sign-off.

Custom Tooling Layer

JaCoCo Coverage Auditor
Deterministic line/branch coverage check. 80% gate. No LLM subjectivity.
Testcontainers Engine
Docker-native PostgreSQL, MySQL, MariaDB instances for real SQL dialect regression testing.
PITest Mutation Engine
Injects bytecode mutants (conditional inversions, return mutations). Measures assertion strength.
Impact Report Generator
Python pipeline comparing baseline vs. modernized artifacts. Renders MD + HTML reports.

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.

SubagentContext BudgetTools PermittedExit Condition
SA-1 BaselineLegacy source + JaCoCo XMLQwen, JaCoCo, Maven≥ 80% line & branch coverage
SA-2 ModernizeApproved source + error logsQwen, Docker, MavenClean mvn test, 0 failures
SA-3 MutationModern source + PITest XMLPITest, Qwen, Maven0 surviving mutants

What Runs Here

Shell Baseline Capture
mvn clean test executed on the unmodified legacy codebase. Result (0 tests, 0% coverage) serialized to reports/baseline.json.
Bob Document Understanding
Bob ingests README.md, docs/architecture.md, docs/prd.md, and .bob/SKILL.md rules. Builds contextual embeddings for downstream subagents.
Target Version Confirmation
Bob reads 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);
    }
}
Line Coverage
95.65%
44 / 46 lines — Gate: ≥ 80% ✓
Branch Coverage
100.0%
Gate passed on first attempt

Coverage Gate — Line Coverage

0%95.65%Gate: 80%

Gate threshold marker at 80%. Pipeline would have looped back to Qwen for additional tests if below threshold.

Generated Test Files

FileTypeTests Generated
UserControllerTestUnit (MockMvc)7
UserControllerIntegrationTestIntegration (Testcontainers)15
UserServiceTestUnit (Mockito)6
UserServiceIntegrationTestIntegration12
UserRepositoryTestUnit5
UserRepositoryIntegrationTestIntegration (Testcontainers)13
UserTestUnit (Model)12
SpringPhoenixApplicationTestsApplication Context2
Total tests locked in72

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);
    }
}

🔒 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.

✓ Approve & Continue ⤫ Reject & Stop

Proposed Modernization Plan

ChangeScopeRisk
Java 8 → Java 21 (records, text blocks, switch expressions)Build + sourceLow
Spring Boot 2.7 → 3.2 (Jakarta namespace migration)pom.xml + importsLow
javax.* → jakarta.* in all importsAll source filesLow
Add @Transactional(readOnly=true) to read methodsUserServiceLow
Add updateUser(Long, User) methodUserService + UserControllerLow
POST /api/users returns HTTP 201UserControllerLow

Flagged Items for Review

⚠ UserService.deleteUser(Long id)
src/main/java/com/example/legacy/service/UserService.java:63
Calls deleteById 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.java
Surrogate-key equality means two transient (unpersisted) User instances with identical fields are not equal. hashCode() returns 0 for all transient objects. Documented JPA pattern but worth confirming intent.
Build Status
72/72
Tests passing — 0 failures
Self-Heal Cycles
0
Max allowed: 3
Integration DB
✓
PostgreSQL via Testcontainers

Modernization Changes Applied

pom.xml — Spring Boot 2.7 → 3.2 + Java 21
Parent POM version bumped, Java source/target set to 21, Hibernate 5 → 6 pulled transitively.
Namespace Migration: javax.* → jakarta.*
All imports across UserController, User model, and repository updated. Verified with mvn compile.
UserService — @Transactional Hardening
Class-level @Transactional(readOnly=true) added. Write methods annotated with @Transactional. updateUser() method added.
UserController — POST returns 201, PUT endpoint added
createUser now returns ResponseEntity.status(201).body(saved). New PUT /{id} delegates to updateUser().
Testcontainers Integration Tests Pass
Real PostgreSQL container started for 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); }
}
Mutation Score
100%
26 / 26 killed
Surviving Mutants
0
Target achieved
Strengthening Cycles
0
Max allowed: 3

Mutation Score Progress

0%100% — 26/26 killed100%

Mutation Categories Tested

Mutation TypeMutants InjectedKilledSurvived
Conditional boundary inversion (< → <=)880
Return value mutation (null / empty)770
Void method call removal660
Negated conditionals550
Total26260

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());
}

Before vs. After — Three-State Comparison

MetricBefore PipelineAfter Baseline GenerationAfter 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
Pipeline Runtime
59s
Manual Estimate
~8h
Net Time Saved
≈479 min

Test Suite Breakdown

Test ClassTypeTestsPassFailTime (s)
SpringPhoenixApplicationTestsUnit2200.70
UserControllerIntegrationTestIntegration151502.63
UserControllerTestUnit7700.15
UserTestUnit121200.01
UserRepositoryIntegrationTestIntegration131308.15
UserRepositoryTestUnit5500.80
UserServiceIntegrationTestIntegration121200.40
UserServiceTestUnit6600.27
Total7272013.1

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.

1. User.equals(Object o)
src/main/java/com/example/legacy/model/User.java
▼
Uses surrogate-key-only equality: two entities with the same field values but null 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?
JPA Pattern Review Needed
2. User.hashCode()
src/main/java/com/example/legacy/model/User.java
▼
Returns Objects.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?
JPA Constraint Review Needed
3. UserService.deleteUser(Long id)
src/main/java/com/example/legacy/service/UserService.java:63
▼
Calls deleteById 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?
Silent No-op Review Needed

Full details in reports/knowledge_gaps.md ↗