diff --git a/appinfo/info.xml b/appinfo/info.xml
index bbdf12a8..9327fb3b 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -99,6 +99,14 @@ Vrij en open source onder de EUPL-licentie.
OCA\SoftwareCatalog\Repair\InitializeSettings
OCA\SoftwareCatalog\Repair\MigrateContactsToNc
OCA\SoftwareCatalog\Repair\BackfillContractApprovalState
+
+ OCA\SoftwareCatalog\Repair\RenameDutchCatalogColumns
diff --git a/lib/Repair/RenameDutchCatalogColumns.php b/lib/Repair/RenameDutchCatalogColumns.php
new file mode 100644
index 00000000..548130b5
--- /dev/null
+++ b/lib/Repair/RenameDutchCatalogColumns.php
@@ -0,0 +1,457 @@
+
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * @version GIT:
+ *
+ * @link https://conduction.nl
+ */
+
+declare(strict_types=1);
+
+namespace OCA\SoftwareCatalog\Repair;
+
+use OCP\DB\Exception;
+use OCP\IDBConnection;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Rename the catalog's Dutch columns to their English equivalents.
+ *
+ * @spec openspec/specs/english-vocabulary-migration/spec.md
+ */
+class RenameDutchCatalogColumns implements IRepairStep
+{
+ /**
+ * The register slug whose shard tables are in scope.
+ *
+ * @var string
+ */
+ private const REGISTER_SLUG = 'softwarecatalog';
+
+ /**
+ * Schema slugs holding externally-standardised field names, which are
+ * exempt from the vocabulary rule and must NOT be migrated.
+ *
+ * `element`, `relation` and `view` carry the GEMMA/GGM architecture model
+ * imported from VNG. Their property names are the import's wire format:
+ * `view` alone holds gemma_status, gemma_thema, gemma_type, gemma_url,
+ * detailniveau, publiceren and titel_view_swc.
+ *
+ * `model` and `property-definition` are the ArchiMate Open Exchange File
+ * Format containers — `model` carries xmlns, xsi, schema_location and
+ * identifier straight off the exchange root element. Neither holds a column
+ * this step's map targets today, so listing them changes nothing now; they
+ * are here so that a property added later is exempt by default rather than
+ * migrated by omission.
+ *
+ * @var array
+ */
+ private const WIRE_SCHEMA_SLUGS = [
+ 'element',
+ 'relation',
+ 'view',
+ 'model',
+ 'property-definition',
+ ];
+
+ /**
+ * Old snake_case column name => new snake_case column name.
+ *
+ * Snake_case, not camelCase: MagicMapper stores `shortDescription` as
+ * `short_description`, and a camelCase column is exactly what its
+ * de-duplication path then drops.
+ *
+ * @var array
+ */
+ private const COLUMN_MAP = [
+ 'naam' => 'name',
+ 'beschrijving' => 'description',
+ 'beschrijving_kort' => 'short_description',
+ 'beschrijving_lang' => 'description',
+ 'omschrijving' => 'description',
+ 'contactpersoon' => 'contact_person',
+ 'publicatiedatum' => 'publication_date',
+ 'depublicatiedatum' => 'depublication_date',
+ ];
+
+ /**
+ * Constructor.
+ *
+ * @param IDBConnection $db Database connection.
+ * @param LoggerInterface $logger Logger.
+ */
+ public function __construct(
+ private readonly IDBConnection $db,
+ private readonly LoggerInterface $logger,
+ ) {
+ }//end __construct()
+
+ /**
+ * Human-readable step name.
+ *
+ * @return string
+ *
+ * @spec openspec/specs/english-vocabulary-migration/spec.md
+ */
+ public function getName(): string
+ {
+ return 'Move catalog data from the Dutch columns to the English ones';
+
+ }//end getName()
+
+ /**
+ * Run the column migration across every in-scope shard table.
+ *
+ * @param IOutput $output Repair output.
+ *
+ * @return void
+ *
+ * @spec openspec/specs/english-vocabulary-migration/spec.md
+ */
+ public function run(IOutput $output): void
+ {
+ $tables = $this->inScopeShardTables();
+ if ($tables === []) {
+ $output->info('RenameDutchCatalogColumns: no in-scope shard tables on this install; nothing to do.');
+ return;
+ }
+
+ $renamed = 0;
+ $copied = 0;
+ $refused = 0;
+
+ foreach ($tables as $table) {
+ $columns = $this->columnsOf(table: $table);
+ $qTable = $this->quote(identifier: $table);
+
+ foreach (self::COLUMN_MAP as $old => $new) {
+ if (in_array($old, $columns, true) === false) {
+ // Already migrated, or this schema never had the property.
+ continue;
+ }
+
+ if ($this->hasCollision(table: $table, columns: $columns, target: $new) === true) {
+ $refused++;
+ continue;
+ }
+
+ $qOld = $this->quote(identifier: $old);
+ $qNew = $this->quote(identifier: $new);
+
+ if (in_array($new, $columns, true) === false) {
+ $sql = 'ALTER TABLE '.$qTable.' RENAME COLUMN '.$qOld.' TO '.$qNew;
+ if ($this->exec(sql: $sql) === true) {
+ $renamed++;
+ }
+
+ continue;
+ }
+
+ // The mapper already added an empty English column: back-fill and
+ // leave the Dutch one, so this stays reversible.
+ $sql = 'UPDATE '.$qTable.' SET '.$qNew.' = '.$qOld
+ .' WHERE '.$qNew.' IS NULL AND '.$qOld.' IS NOT NULL';
+ if ($this->exec(sql: $sql) === true) {
+ $copied++;
+ }
+ }//end foreach
+ }//end foreach
+
+ $output->info(
+ 'RenameDutchCatalogColumns: '.$renamed.' column(s) renamed, '
+ .$copied.' back-filled, '.$refused.' refused for ambiguity, across '
+ .count($tables).' shard table(s).'
+ );
+
+ }//end run()
+
+ /**
+ * Whether two Dutch columns in this table both target one English name.
+ *
+ * Merging them would silently destroy one of the two values, so the step
+ * refuses both and leaves a log line for a human to resolve.
+ *
+ * @param string $table Table name.
+ * @param array $columns Its column names.
+ * @param string $target The English destination name.
+ *
+ * @return bool True when the rename is ambiguous and must be skipped.
+ */
+ private function hasCollision(string $table, array $columns, string $target): bool
+ {
+ $sources = [];
+ foreach (self::COLUMN_MAP as $old => $new) {
+ if ($new === $target && in_array($old, $columns, true) === true) {
+ $sources[] = $old;
+ }
+ }
+
+ if (count($sources) < 2) {
+ return false;
+ }
+
+ $this->logger->warning(
+ 'RenameDutchCatalogColumns: refusing an ambiguous rename; two source columns target one destination.',
+ ['table' => $table, 'sources' => $sources, 'target' => $target]
+ );
+
+ return true;
+
+ }//end hasCollision()
+
+ /**
+ * Resolve the shard tables in scope: this register, minus the wire schemas.
+ *
+ * Ids are looked up at runtime — both the register id and the schema ids
+ * differ per install.
+ *
+ * @return array
+ */
+ private function inScopeShardTables(): array
+ {
+ try {
+ $registerId = $this->db->executeQuery(
+ 'SELECT id FROM `*PREFIX*openregister_registers` WHERE slug = ?',
+ [self::REGISTER_SLUG]
+ )->fetchOne();
+ } catch (Exception $e) {
+ $this->logger->warning(
+ 'RenameDutchCatalogColumns: could not resolve the register; skipping.',
+ ['exception' => $e->getMessage()]
+ );
+ return [];
+ }
+
+ if ($registerId === false || $registerId === null) {
+ return [];
+ }
+
+ $excluded = $this->wireSchemaIds();
+
+ // Table discovery goes through information_schema, NOT IDBConnection.
+ // OCP\IDBConnection exposes neither getSchema() nor getPrefix(); both
+ // exist only on the concrete OC\DB\Connection. Calling them is a runtime
+ // fatal that `php -l` and phpcs both report as clean — only phpstan
+ // catches it. Pattern follows openregister's own RegisterService: anchor
+ // on the `openregister_table_` MARKER, never on a computed prefix.
+ try {
+ $stmt = $this->db->prepare(
+ 'SELECT table_name FROM information_schema.tables WHERE table_name LIKE :pattern'
+ );
+ $stmt->bindValue('pattern', '%openregister\_table\_%');
+ $stmt->execute();
+ } catch (\Throwable $e) {
+ $this->logger->warning(
+ 'RenameDutchCatalogColumns: could not list tables; skipping.',
+ ['exception' => $e->getMessage()]
+ );
+ return [];
+ }
+
+ $marker = 'openregister_table_'.((int) $registerId).'_';
+
+ $tables = [];
+ while (($row = $stmt->fetch(\PDO::FETCH_ASSOC)) !== false) {
+ $name = (string) ($row['table_name'] ?? '');
+ if ($this->isMigratableShard(table: $name, marker: $marker, excluded: $excluded) === true) {
+ $tables[] = $name;
+ }
+ }
+
+ return $tables;
+
+ }//end inScopeShardTables()
+
+ /**
+ * Whether a table is a shard of this register that is NOT wire-exempt.
+ *
+ * @param string $table Table name from information_schema.
+ * @param string $marker `openregister_table__`.
+ * @param array $excluded Schema ids exempt as external wire formats.
+ *
+ * @return bool
+ */
+ private function isMigratableShard(string $table, string $marker, array $excluded): bool
+ {
+ $offset = strpos($table, $marker);
+ if ($offset === false) {
+ return false;
+ }
+
+ // Everything after the marker must be the numeric schema id, so a
+ // derived table (…_13_50_backup) or a non-shard (…_13_audit) is left
+ // alone. Note this is NOT what stops register 13 matching register
+ // 130's tables — the marker already ends in '_', so `…_table_13_` is
+ // not a substring of `…_table_130_50` in the first place.
+ $schemaId = substr($table, ($offset + strlen($marker)));
+ if (ctype_digit($schemaId) === false) {
+ return false;
+ }
+
+ // GEMMA/ArchiMate schemas carry an external wire format and are exempt.
+ return in_array((int) $schemaId, $excluded, true) === false;
+
+ }//end isMigratableShard()
+
+ /**
+ * Resolve the schema ids of the externally-standardised schemas.
+ *
+ * @return array
+ */
+ private function wireSchemaIds(): array
+ {
+ $placeholders = implode(',', array_fill(0, count(self::WIRE_SCHEMA_SLUGS), '?'));
+
+ try {
+ $ids = $this->db->executeQuery(
+ 'SELECT id FROM `*PREFIX*openregister_schemas` WHERE slug IN ('.$placeholders.')',
+ self::WIRE_SCHEMA_SLUGS
+ )->fetchAll(\PDO::FETCH_COLUMN);
+ } catch (Exception $e) {
+ // Fail CLOSED: if the exempt set cannot be resolved, migrate nothing
+ // rather than risk rewriting the GEMMA import contract.
+ $this->logger->error(
+ 'RenameDutchCatalogColumns: could not resolve the exempt GEMMA schemas; refusing to migrate anything.',
+ ['exception' => $e->getMessage()]
+ );
+ throw $e;
+ }
+
+ return array_map('intval', $ids);
+
+ }//end wireSchemaIds()
+
+ /**
+ * List the column names of a table.
+ *
+ * @param string $table Table name.
+ *
+ * @return array
+ */
+ private function columnsOf(string $table): array
+ {
+ // Queried from information_schema — IDBConnection has no getSchema().
+ try {
+ $stmt = $this->db->prepare(
+ 'SELECT column_name FROM information_schema.columns WHERE table_name = :table'
+ );
+ $stmt->bindValue('table', $table);
+ $stmt->execute();
+ } catch (\Throwable $e) {
+ $this->logger->warning(
+ 'RenameDutchCatalogColumns: could not read columns; skipping table.',
+ ['table' => $table, 'exception' => $e->getMessage()]
+ );
+ return [];
+ }
+
+ $columns = [];
+ while (($row = $stmt->fetch(\PDO::FETCH_ASSOC)) !== false) {
+ $name = (string) ($row['column_name'] ?? '');
+ if ($name !== '') {
+ $columns[] = $name;
+ }
+ }
+
+ return $columns;
+
+ }//end columnsOf()
+
+ /**
+ * Execute one DDL/DML statement, logging and swallowing failure.
+ *
+ * A failure must not abort the repair run: the remaining tables are
+ * independent, and an un-migrated column is still readable.
+ *
+ * @param string $sql The statement.
+ *
+ * @return bool Whether it succeeded.
+ */
+ private function exec(string $sql): bool
+ {
+ try {
+ $this->db->executeStatement($sql);
+ return true;
+ } catch (Exception $e) {
+ $this->logger->warning(
+ 'RenameDutchCatalogColumns: statement failed; leaving the column as it was.',
+ ['sql' => $sql, 'exception' => $e->getMessage()]
+ );
+ return false;
+ }
+
+ }//end exec()
+
+ /**
+ * Quote an identifier for the active platform.
+ *
+ * @param string $identifier Table or column name.
+ *
+ * @return string
+ */
+ private function quote(string $identifier): string
+ {
+ return $this->db->getDatabasePlatform()->quoteSingleIdentifier($identifier);
+
+ }//end quote()
+}//end class
diff --git a/openspec/specs/english-vocabulary-migration/spec.md b/openspec/specs/english-vocabulary-migration/spec.md
new file mode 100644
index 00000000..ebfb12c6
--- /dev/null
+++ b/openspec/specs/english-vocabulary-migration/spec.md
@@ -0,0 +1,82 @@
+# English Vocabulary Migration
+
+Adoption of the ratified fleet English-vocabulary decision for this app's stored
+data. The decision itself is owned by `hydra/openspec/changes/fleet-english-vocabulary`;
+this spec records only what SoftwareCatalog must do to its own rows.
+
+## Why a migration is required at all
+
+OpenRegister does not store an object as a JSON blob keyed by property name.
+Each schema property is a real, snake_cased **column** in the per-schema shard
+table `oc_openregister_table_{register}_{schema}`. On schema sync MagicMapper
+ADDS a column when the snake_cased name is absent and it never renames — there
+is no `RENAME COLUMN` anywhere in openregister.
+
+A register-only rename therefore leaves the data in the Dutch column while every
+read looks at the English one and finds null: no error, no data loss, and
+invisible to a suite that asserts against fixtures rather than migrated rows.
+
+## Requirements
+
+### Requirement: Renaming a stored property ships a data migration
+
+Every rename of a property that OpenRegister materialises as a column SHALL ship
+in the same change as a repair step that moves the stored values, or with
+evidence recorded in the change that no rows exist.
+
+#### Scenario: A property rename lands without its migration
+
+
+
+- **WHEN** a register fragment renames a property that has a materialised column
+- **THEN** the change SHALL NOT be merged until a repair step covers that column
+ or the change records a measured zero-row count for it
+
+### Requirement: Externally-standardised schemas are exempt
+
+The migration SHALL NOT touch schemas whose property names are the wire format
+of an external standard, and SHALL resolve that exempt set at runtime rather
+than assuming per-install ids.
+
+The `element`, `relation` and `view` schemas carry the GEMMA/GGM architecture
+model imported from VNG. Their `toelichting`, `bron`, `ggm-*` and `gemma-*`
+properties are that import's field names, exempt under the fleet rule as
+external standard names inside the adapter layer.
+
+#### Scenario: The exempt set cannot be resolved
+
+
+
+- **WHEN** the repair step cannot resolve the ids of the exempt schemas
+- **THEN** it SHALL fail closed and migrate nothing, rather than risk rewriting
+ the import contract
+
+### Requirement: Ambiguous renames are refused, never merged
+
+The migration SHALL refuse to migrate a column when two source columns in one
+table target the same destination name, and SHALL log the refusal.
+
+`beschrijving`, `beschrijving_lang` and `omschrijving` all mean `description`.
+They do not co-occur in any schema today, but a later fragment could introduce a
+pair, and a silent merge would destroy one of the two values.
+
+#### Scenario: Two Dutch columns target one English name
+
+
+
+- **WHEN** a shard table holds both `beschrijving` and `beschrijving_lang`
+- **THEN** the step SHALL migrate neither and SHALL log the table, both sources,
+ and the destination
+
+### Requirement: The migration is non-destructive and idempotent
+
+The migration SHALL leave every original column readable and SHALL be safe to
+re-run.
+
+#### Scenario: The English column already exists and is empty
+
+
+
+- **WHEN** MagicMapper has already added the English column before the step runs
+- **THEN** the step SHALL copy the values across and SHALL leave the Dutch column
+ in place, so the change remains reversible and a second run is a no-op
diff --git a/phpunit.xml b/phpunit.xml
index cd93e7d9..20af447a 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -26,6 +26,32 @@
lib/
+
+
+ lib/Repair/RenameDutchCatalogColumns.php
+
diff --git a/tests/Unit/Repair/RenameDutchCatalogColumnsTest.php b/tests/Unit/Repair/RenameDutchCatalogColumnsTest.php
new file mode 100644
index 00000000..d30ab79f
--- /dev/null
+++ b/tests/Unit/Repair/RenameDutchCatalogColumnsTest.php
@@ -0,0 +1,250 @@
+
+ * @copyright 2026 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * @version GIT:
+ *
+ * @link https://conduction.nl
+ *
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
+ * SPDX-License-Identifier: EUPL-1.2
+ */
+
+declare(strict_types=1);
+
+namespace OCA\SoftwareCatalog\Tests\Unit\Repair;
+
+use OCA\SoftwareCatalog\Repair\RenameDutchCatalogColumns;
+use PHPUnit\Framework\TestCase;
+use Psr\Log\NullLogger;
+use ReflectionClass;
+use ReflectionMethod;
+
+/**
+ * @covers \OCA\SoftwareCatalog\Repair\RenameDutchCatalogColumns
+ */
+class RenameDutchCatalogColumnsTest extends TestCase
+{
+ /**
+ * The step under test.
+ *
+ * @var RenameDutchCatalogColumns
+ */
+ private RenameDutchCatalogColumns $step;
+
+ /**
+ * Build the step WITHOUT running its constructor, then inject a logger.
+ *
+ * The constructor is skipped because mocking IDBConnection drags in
+ * Doctrine types this app's unit environment does not install.
+ *
+ * $logger IS still required, though: hasCollision() logs when it refuses an
+ * ambiguous rename, and a readonly promoted property left uninitialised
+ * throws "must not be accessed before initialization" the moment that path
+ * runs. An earlier version of this file skipped the constructor and set
+ * nothing; the collision test then errored in CI for exactly that reason,
+ * while the local standalone check of the same logic passed — because it
+ * exercised the algorithm as a free function, with no object state at all.
+ *
+ * @return void
+ */
+ protected function setUp(): void
+ {
+ $class = new ReflectionClass(RenameDutchCatalogColumns::class);
+ $this->step = $class->newInstanceWithoutConstructor();
+
+ $logger = $class->getProperty('logger');
+ $logger->setAccessible(true);
+ $logger->setValue($this->step, new NullLogger());
+
+ }//end setUp()
+
+ /**
+ * Invoke a private method on the step.
+ *
+ * @param string $name Method name.
+ * @param array $args Positional arguments.
+ *
+ * @return mixed
+ */
+ private function call(string $name, array $args)
+ {
+ $m = new ReflectionMethod(RenameDutchCatalogColumns::class, $name);
+ $m->setAccessible(true);
+ return $m->invokeArgs($this->step, $args);
+
+ }//end call()
+
+ /**
+ * Read a private constant off the step.
+ *
+ * @param string $name Constant name.
+ *
+ * @return mixed
+ */
+ private function constant(string $name)
+ {
+ return (new ReflectionClass(RenameDutchCatalogColumns::class))->getConstant($name);
+
+ }//end constant()
+
+ /**
+ * A shard of this register with a non-exempt schema is migrated.
+ *
+ * @return void
+ */
+ public function testMatchesAnOrdinaryShard(): void
+ {
+ self::assertTrue(
+ $this->call('isMigratableShard', ['oc_openregister_table_13_50', 'openregister_table_13_', []])
+ );
+
+ }//end testMatchesAnOrdinaryShard()
+
+ /**
+ * A wire-exempt schema is NOT migrated.
+ *
+ * This is the heart of the step. Schemas 44 (`element`), 49 (`relation`)
+ * and 45 (`view`) hold the GEMMA/GGM model imported from VNG, and 46
+ * (`model`) / 48 (`property-definition`) the ArchiMate Open Exchange
+ * containers. Their property names ARE the import's wire format. Migrating
+ * them would rewrite the import contract, and the symptom would be a GEMMA
+ * re-import silently writing nulls.
+ *
+ * @return void
+ */
+ public function testDoesNotMigrateWireExemptSchemas(): void
+ {
+ $marker = 'openregister_table_13_';
+ $excluded = [44, 45, 46, 48, 49];
+
+ foreach ([44, 45, 46, 48, 49] as $id) {
+ self::assertFalse(
+ $this->call('isMigratableShard', ["oc_openregister_table_13_$id", $marker, $excluded]),
+ "Schema $id is wire-exempt and must not be migrated"
+ );
+ }
+
+ // A non-exempt neighbour in the same register still is.
+ self::assertTrue(
+ $this->call('isMigratableShard', ['oc_openregister_table_13_50', $marker, $excluded])
+ );
+
+ }//end testDoesNotMigrateWireExemptSchemas()
+
+ /**
+ * A derived or non-shard table sharing the marker is left alone.
+ *
+ * This is what the digits-only suffix check guards. It is NOT what stops
+ * register 13 matching register 130 — the marker already ends in '_', so
+ * that collision cannot occur.
+ *
+ * @return void
+ */
+ public function testDoesNotMatchDerivedOrNonShardTables(): void
+ {
+ $marker = 'openregister_table_13_';
+ self::assertFalse($this->call('isMigratableShard', ['oc_openregister_table_13_50_backup', $marker, []]));
+ self::assertFalse($this->call('isMigratableShard', ['oc_openregister_table_13_audit', $marker, []]));
+ self::assertFalse($this->call('isMigratableShard', ['oc_openregister_registers', $marker, []]));
+
+ }//end testDoesNotMatchDerivedOrNonShardTables()
+
+ /**
+ * Two Dutch columns targeting one English name are refused, not merged.
+ *
+ * `beschrijving`, `beschrijving_lang` and `omschrijving` all mean
+ * `description`. They do not co-occur today, but a silent merge would
+ * destroy one of two values, so the step must migrate neither.
+ *
+ * @return void
+ */
+ public function testRefusesAmbiguousRename(): void
+ {
+ $columns = ['beschrijving', 'beschrijving_lang', 'naam'];
+ self::assertTrue($this->call('hasCollision', ['tbl', $columns, 'description']));
+
+ }//end testRefusesAmbiguousRename()
+
+ /**
+ * A single source for a destination is not a collision.
+ *
+ * @return void
+ */
+ public function testSingleSourceIsNotACollision(): void
+ {
+ $columns = ['beschrijving_kort', 'naam'];
+ self::assertFalse($this->call('hasCollision', ['tbl', $columns, 'short_description']));
+ self::assertFalse($this->call('hasCollision', ['tbl', $columns, 'name']));
+
+ }//end testSingleSourceIsNotACollision()
+
+ /**
+ * The GEMMA and ArchiMate schemas are all listed as exempt.
+ *
+ * Listing `model` and `property-definition` is deliberate even though
+ * neither carries a column the map targets today: it makes a property added
+ * later exempt by default rather than migrated by omission.
+ *
+ * @return void
+ */
+ public function testWireSchemasAreExempt(): void
+ {
+ $slugs = $this->constant('WIRE_SCHEMA_SLUGS');
+ self::assertIsArray($slugs);
+ foreach (['element', 'relation', 'view', 'model', 'property-definition'] as $slug) {
+ self::assertContains($slug, $slugs, "$slug carries an external wire format and must be exempt");
+ }
+
+ }//end testWireSchemasAreExempt()
+
+ /**
+ * Every destination is snake_case, never camelCase.
+ *
+ * MagicMapper stores `shortDescription` as `short_description`, and its
+ * de-duplication path DROPS a camelCase column whose snake_case twin
+ * exists — so a camelCase destination would be deleted on the next sync.
+ *
+ * @return void
+ */
+ public function testEveryDestinationIsSnakeCase(): void
+ {
+ $map = $this->constant('COLUMN_MAP');
+ self::assertIsArray($map);
+ foreach ($map as $old => $new) {
+ self::assertSame(
+ strtolower($new),
+ $new,
+ "Destination '$new' (from '$old') must be snake_case, not camelCase"
+ );
+ }
+
+ }//end testEveryDestinationIsSnakeCase()
+
+ /**
+ * The step reports a human-readable name.
+ *
+ * @return void
+ */
+ public function testGetName(): void
+ {
+ self::assertNotSame('', $this->step->getName());
+
+ }//end testGetName()
+}//end class