Summary
src/Tasks/MintApiTokenTask.php and src/Tasks/SetupServiceAccountTask.php are the only permanent
divergence between branch 1 (SS6, BuildTask::execute(InputInterface, PolyOutput): int) and ss5
(SS5.2, legacy BuildTask::run($request): void). Both files on ss5 carry a docblock: "there is no
shared entry point between the two branches, so a business-logic fix here must be manually ported to
branch 1's copy and vice versa."
This already cost something: when ss5 was created, tests/Tasks/SetupServiceAccountTaskTest.php
had to be rewritten around the new signature, and in the process lost its Symfony Command::SUCCESS/
FAILURE/INVALID exit-code assertions — replaced with output-string + state assertions
(assertStringContainsString('refusing to guess', $output)), which are coupled to message wording
and would silently pass through a future reword that changed the actual behavior.
Proposal
Extract the provisioning/minting logic into branch-neutral service classes, e.g.:
src/Tasks/Support/ServiceAccountProvisioner.php — provision(string $groupTitle, bool $populate): TaskResult
src/Tasks/Support/ApiTokenMinter.php — mint(string $email): TaskResult
src/Tasks/Support/TaskResult.php — a small value object: Status $status (Success|Invalid|Failure
enum, identical on both branches) + array<int,string> $lines
Each branch's BuildTask subclass collapses to a thin (~25-line) adapter whose only per-branch
content is the entry-point signature and the render loop (looping $lines into $output->writeln()
- mapping
Status to Command::* on branch 1; echoing $lines on ss5). The test file becomes
byte-identical across branches, asserting on TaskResult::$status (a real enum) instead of parsing
output text, and every future sync touches ~25 lines of adapter instead of ~180 lines of duplicated
logic.
Considered and rejected: a single file with a class_exists(PolyOutput::class) conditional branch —
unanalyzable by PHPStan on whichever branch lacks the class, permanently-unreachable branches in
coverage, and costs more than the ~25 lines of divergence it would save.
🤖 Generated with Claude Code
Summary
src/Tasks/MintApiTokenTask.phpandsrc/Tasks/SetupServiceAccountTask.phpare the only permanentdivergence between branch
1(SS6,BuildTask::execute(InputInterface, PolyOutput): int) andss5(SS5.2, legacy
BuildTask::run($request): void). Both files onss5carry a docblock: "there is noshared entry point between the two branches, so a business-logic fix here must be manually ported to
branch
1's copy and vice versa."This already cost something: when
ss5was created,tests/Tasks/SetupServiceAccountTaskTest.phphad to be rewritten around the new signature, and in the process lost its Symfony
Command::SUCCESS/FAILURE/INVALIDexit-code assertions — replaced with output-string + state assertions(
assertStringContainsString('refusing to guess', $output)), which are coupled to message wordingand would silently pass through a future reword that changed the actual behavior.
Proposal
Extract the provisioning/minting logic into branch-neutral service classes, e.g.:
src/Tasks/Support/ServiceAccountProvisioner.php—provision(string $groupTitle, bool $populate): TaskResultsrc/Tasks/Support/ApiTokenMinter.php—mint(string $email): TaskResultsrc/Tasks/Support/TaskResult.php— a small value object:Status $status(Success|Invalid|Failureenum, identical on both branches) +
array<int,string> $linesEach branch's
BuildTasksubclass collapses to a thin (~25-line) adapter whose only per-branchcontent is the entry-point signature and the render loop (looping
$linesinto$output->writeln()StatustoCommand::*on branch1;echoing$linesonss5). The test file becomesbyte-identical across branches, asserting on
TaskResult::$status(a real enum) instead of parsingoutput text, and every future sync touches ~25 lines of adapter instead of ~180 lines of duplicated
logic.
Considered and rejected: a single file with a
class_exists(PolyOutput::class)conditional branch —unanalyzable by PHPStan on whichever branch lacks the class, permanently-unreachable branches in
coverage, and costs more than the ~25 lines of divergence it would save.
🤖 Generated with Claude Code