Market records: delete the dropped legacy set; defuse the rename map

- vitec:align-market-records gains --delete-dropped: soft-deletes the
  records the agency dropped from the taxonomy, including their MM rows
  (DataHandler does not clean the foreign side on delete). Decision:
  all nine legacy records go, including Healthcare #42 - the Overlake
  story loses its badge on purpose and reports as unresolved on future
  importer runs
- the run exposed a regression: the RENAME_MAP entries Government ->
  Government & Agencies and Education -> Education Communications are
  not idempotent because both old names double as NEW main-market
  names. The re-run hijacked the main records (stories and teaser
  travelled along), created empty replacements and deleted the real
  sub-market records as unclaimed
- repair: migrations/fix_market_rename_regression.php (dry-run default,
  guarded) swaps the pairs back - main records keep their stories and
  workbook teasers, the fresh empties take over the sub-market roles;
  applied and verified live (marketlist 11/11 with main detail pages,
  badge distribution back to 20/9/9/8/6/4/1, no legacy names left)
- the two non-idempotent entries are removed from RENAME_MAP with a
  comment: a rename mapping whose old name is reused by the new
  taxonomy must not survive its one-time run

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
orascheandClaude Fable 5 committed 2026-10-27 11:28:26 +01:00
1 parent 3b3ef470b0
commit 90c2f97c5b
2 files changed
+156 -7

No files matched your search

@@ -62,8 +62,13 @@ final class AlignMarketRecordsCommand extends Command
// sub-markets
'Aviation' => 'Airports',
'Casinos & Sports Books' => 'Casinos',
'Education' => 'Education Communications',
'Government' => 'Government & Agencies',
// 'Education' => 'Education Communications' and
// 'Government' => 'Government & Agencies' were REMOVED 2026-10-27:
// both old names double as NEW main-market names, so a re-run
// hijacks the main record (rename is not idempotent when the old
// title is reused by the new taxonomy). The one-time renames ran on
// 2026-10-26; the collateral of the 10-27 re-run was repaired by
// migrations/fix_market_rename_regression.php.
'Defense/ Military' => 'Defense & Security',
'Defense Military' => 'Defense & Security',
'Emergency Services' => 'Emergency Operation Centres',
@@ -81,12 +86,14 @@ final class AlignMarketRecordsCommand extends Command
protected function configure(): void
{
$this->addOption('apply', null, InputOption::VALUE_NONE, 'Write changes (default: dry run)');
$this->addOption('delete-dropped', null, InputOption::VALUE_NONE, 'Soft-delete the records the agency dropped (incl. their MM rows)');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
Bootstrap::initializeBackendAuthentication();
$apply = (bool)$input->getOption('apply');
$deleteDropped = (bool)$input->getOption('delete-dropped');
$pool = GeneralUtility::makeInstance(ConnectionPool::class);
$pages = $pool->getConnectionForTable('pages');
$markets = $pool->getConnectionForTable(self::TABLE);
@@ -190,36 +197,57 @@ final class AlignMarketRecordsCommand extends Command
$output->writeln(sprintf(' + CREATE "%s" (level %d, detail_page %d)', $target['title'], $target['level'], $target['pageUid']));
}
// pass 4: records without a target - the editorial decision list
// pass 4: records without a target. Default: the editorial decision
// list. With --delete-dropped (decision Olli 2026-10-27): soft-delete
// them, including their MM rows - a story whose only badge was such a
// record simply loses it and reports as unresolved on future
// importer runs (that WAS the decision for Overlake / Healthcare #42).
$cmdmap = [];
$droppedUids = [];
foreach ($records as $record) {
if (isset($claimedRecords[(int)$record['uid']])) {
continue;
}
$stories = $this->stories($pool, (int)$record['uid']);
$output->writeln(sprintf(' <comment>! DROPPED by the agency: #%d "%s"%s - untouched, %d story relation(s)%s</comment>',
$verb = $deleteDropped ? ($apply ? 'DELETING' : 'would DELETE') : 'untouched';
$output->writeln(sprintf(' <comment>! DROPPED by the agency: #%d "%s"%s - %s, %d story relation(s)%s</comment>',
(int)$record['uid'], (string)$record['title'],
((int)$record['hidden'] === 1) ? ' (hidden)' : '',
count($stories), $stories === [] ? '' : ':'));
$verb, count($stories), $stories === [] ? '' : ' (badge disappears, story reports unresolved on re-runs):'));
foreach ($stories as $story) {
$output->writeln(' - ' . $story);
}
if ($deleteDropped) {
$cmdmap[self::TABLE][(int)$record['uid']]['delete'] = 1;
$droppedUids[] = (int)$record['uid'];
}
}
if (!$apply) {
$output->writeln("\nDRY RUN - nothing written. Add --apply to write.");
return Command::SUCCESS;
}
if ($datamap === []) {
if ($datamap === [] && $cmdmap === []) {
$output->writeln('Nothing to change.');
return Command::SUCCESS;
}
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
$dataHandler->start($datamap, []);
$dataHandler->start($datamap, $cmdmap);
$dataHandler->process_datamap();
$dataHandler->process_cmdmap();
if ($dataHandler->errorLog !== []) {
$output->writeln('<error>DataHandler: ' . implode(' | ', $dataHandler->errorLog) . '</error>');
return Command::FAILURE;
}
if ($droppedUids !== []) {
// DataHandler does not clean foreign-side MM rows on delete
$mmConn = $pool->getConnectionForTable(self::MM);
$removed = 0;
foreach ($droppedUids as $uid) {
$removed += (int)$mmConn->delete(self::MM, ['uid_foreign' => $uid]);
}
$output->writeln(sprintf('<info>%d record(s) soft-deleted, %d MM row(s) removed.</info>', count($droppedUids), $removed));
}
$output->writeln('<info>Done. Reindex Solr and flush the cache afterwards.</info>');
return Command::SUCCESS;
}