feat(decidesk): English vocabulary — fleet words, with column migration - #467
Conversation
…ifests⚠️ INCOMPLETE — DO NOT MERGE. 13 consumer files still read the old keys. publicatiedatum -> publicationDate and depublicatiedatum -> depublicationDate across decidesk_register.json, seven register.d fragments and three manifest fragments. decidesk is the fleet's heaviest user of this pair — it carries it on 15 schemas, and it is a ratified fleet word shared with opencatalogi (already merged as #850), softwarecatalog and procest, so decidesk must not invent its own. Note the ordering hazard: 'depublicatiedatum' contains 'publicatiedatum' as a substring, so the de- form is replaced first in every file. There is also a compound, pastDepublicatiedatum, whose capital D means a lowercase replace does not reach it — checked explicitly. All 11 changed JSON files parse.
Completes the publicatiedatum/depublicatiedatum slice: six lib/ files and seven test files, on top of the eleven registers and manifests in c68d15b.⚠️ Caught a mismatch I introduced myself. The Postman collection carries the concept as VARIABLE names (futurePublicatiedatum, pastPublicatiedatum, pastDepublicatiedatum). Renaming the capitalised compound moved '{{pastDepublicationDate}}' at the reference site while its definition still said pastDepublicatiedatum — an unresolvable Postman variable, which Newman reports as an empty value rather than an error. All three variables are now renamed, and a check confirms all 9 {{refs}} resolve against the 9 definitions. decidesk is the fleet's heaviest user of this pair (15 schemas). It is a ratified fleet word shared with opencatalogi (#850, merged-ready), softwarecatalog and procest, so the wording had to match rather than be chosen locally. php -l clean; every changed JSON parses; no residual occurrence in lib/, src/ or tests/. The rest of decidesk's Dutch vocabulary — the statutory schema names (Toezegging, Regeling, Bevoegdheidstoedeling, Zienswijze, ...) and their properties — remains and is the next slice.
…gister onderwerp -> subject, fractie -> politicalGroup in 49-vragenuur-interpellatie.json, plus the stale prose reference to MondelingeVraag.fractie. Two things deliberately NOT renamed in this file: - 'fractievoorzitter-fractie-koppeling' is the SLUG of another OpenSpec change, not a property. Quoted-token replacement avoided it; a bare-word replace would have corrupted six references to that change into nonsense. - 'Fractie' as a schema name belongs to that same not-yet-landed change, so the $ref notes stay as written. Partial by design — the remaining onderwerp/toelichting/omschrijving/fractie occurrences in the other fragments follow.
…ss all registers onderwerp -> subject, toelichting -> notes, omschrijving -> description, fractie -> politicalGroup across nine register.d fragments and six manifest fragments. toelichting -> notes rather than description is the ratified fleet split: the two co-occur in four schemas fleet-wide (procest complaintDisposition, shillinq Evaluatievraag and FiscaleCorrectie, decidesk Geschenk), so they are distinct concepts and collapsing them would produce a duplicate key. Quoted-token replacement throughout, which is what keeps 'fractievoorzitter- fractie-koppeling' intact — that is the slug of another OpenSpec change, not a property, and it survives in all six places. All 15 changed JSON files parse; no code consumers read these keys (they are register-declared and rendered declaratively).
Closes the data half of this branch. 61 live objects across 20 statutory schemas
were measured on the reference install, so this is not theoretical here.
WHY. OpenRegister stores each schema property as a real, snake_cased COLUMN in
oc_openregister_table_{register}_{schema}. MagicMapper ADDS a column when the
snake_cased name is absent and NEVER renames — zero 'RENAME COLUMN' anywhere in
openregister. The register rename alone therefore leaves the data in
publicatiedatum while every read looks at publication_date and finds null. No
error, and invisible to every gate, because the suites assert against fixtures
rather than migrated rows.
WHY SCOPED BY REGISTER, NOT BY COLUMN NAME. Several of these words are not unique
to decidesk — procest also stores 'onderwerp'. A step that scanned every shard
table for a matching column would migrate another app's data as a side effect.
This resolves the decidesk register BY SLUG at runtime (id 18 on the reference
install, but it differs per install) and touches only its shard tables.
Scoping by register also beats an enumerated list of schema titles: decidesk
declares 36 schemas across fifteen fragments, and a hand-maintained list would
silently miss whichever were added after it was written.
Covers publicatiedatum, depublicatiedatum, onderwerp, toelichting, omschrijving
and fractie. toelichting maps to notes and NOT description — the two co-occur in
four schemas fleet-wide including decidesk's own Geschenk, so collapsing them
would produce a duplicate key.
Non-destructive and idempotent: renames only when the old column exists and the
new one does not; back-fills and leaves the old column where the mapper already
added an empty new one; drops nothing.
php -l clean; info.xml parses.
Quality Report — ConductionNL/decidesk @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ❌ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| composer | ❌ | ✅ 100/100 | |||
| npm | ✅ | ✅ 548/548 | |||
| PHPUnit | ❌ | ||||
| Newman | ✅ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-11 20:00 UTC
Download the full PDF report from the workflow artifacts.
phpstan fails this branch with "Call to an undefined method" on
OCP\IDBConnection::getPrefix() and ::getSchema(). Both are real.
Read from the running server's own lib/public/IDBConnection.php, the interface
exposes getQueryBuilder, getTypedQueryBuilder, getError, getDatabasePlatform,
getDatabaseProvider, getShardDefinition and getCrossShardMoveHelper — and
nothing else beginning with "get". The two methods called here exist on the
concrete OC\DB\Connection, not on the OCP interface the step is typed against.
This repair step could not have run at all.
WHY EVERY OTHER CHECK PASSED. `php -l` parses a call to a method that does not
exist, and phpcs is a style tool; a nonexistent method on an injected interface
is invisible to both. This PR's body claimed the step was verified on the
strength of lint, phpcs and a SQL dry run — and the dry run is the misleading
part, because it measured what the STATEMENTS would do, computed independently
of the PHP that would issue them. It read as strong evidence while covering none
of the API surface.
THE FIX follows openregister's own RegisterService::magicTableNames(), which
solves the same problem: query information_schema and anchor the match on the
`openregister_table_` MARKER rather than a computed prefix. That file documents
why the obvious alternative fails — getQueryBuilder()->getTableName('') returns
the literal `*PREFIX*` placeholder, resolved only when a query executes through
the NC DB layer, which a raw information_schema string never is; a LIKE built
from it matches zero tables and silently reports every register empty.
Column introspection moves to information_schema.columns for the same reason.
VERIFIED
- php -l clean; no db->getSchema() or db->getPrefix() call remains.
- phpstan could NOT verify this locally: the vendor copy available here
lacks the Nextcloud OCP stubs, so phpstan cannot resolve OCP\Migration\
IRepairStep and therefore cannot check OCP\IDBConnection's methods either.
The change is structurally identical to the one phpstan DID verify clean in
ConductionNL/procest#807 and softwarecatalog#488. CI has the stubs — it is
what reported the original three errors — so CI is the verdict here, not
this local run.
Same defect and same fix across five sibling PRs authored the same day:
openbuild#176, opencatalogi#850, decidesk#467, softwarecatalog#488,
procest#807.
…ocabulary # Conflicts: # lib/Service/ParticipationPublicationService.php # tests/Unit/Service/ParticipationPublicationServiceTest.php
This branch was CONFLICTING, so only CodeQL had run on it — the whole Code
Quality workflow never executed. Merging development in surfaced a conflict
that mattered.
THE CONFLICT WAS NOT COSMETIC, AND "TAKE OURS" WOULD HAVE REVERTED A REAL FIX.
This branch wrote:
$sourceObject['depublicationDate'] = null;
development had replaced exactly that with an unset(), because OpenRegister
declares the property `type: "string", format: "date-time"` and NOT nullable:
its validator rejects an explicit null rather than reading it as "absent", so
writing the key failed the ENTIRE saveObject. The service's catch turned that
into a logged warning under an HTTP 200 — the publish looked successful and
persisted nothing.
Resolved by keeping development's unset() and moving only the key name. The
accompanying test conflict was the same defect in assertion form: this branch
asserted the key was present and null, which pinned the bug and stayed green
for exactly as long as it lived. It now asserts ABSENCE, under BOTH spellings —
the English one covers this branch's code, and the Dutch one guards the merge
itself, since a resolution that kept the rename but lost the unset() would leave
`depublicatiedatum` behind and that line is what would catch it.
Two follow-ups the merge required: a development-side test asserting
`publicatiedatum` is present now asserts `publicationDate`, and a fixture
exception simulating OpenRegister's rejection names the English property, so it
no longer describes a write the code cannot make.
Also brings the repair step up to this app's standard: quote() calls hoisted
with named arguments, $at renamed, three @SPEC anchors added, one comment
capitalised.
VERIFIED
- phpunit, whole unit suite: 895 tests, 3155 assertions, 29 skipped — green.
- phpcs clean on both changed lib files; phpmd 0 findings on the repair step.
- No conflict markers remain anywhere in lib/ or tests/.
Quality Report — ConductionNL/decidesk @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ✅ 548/548 | |||
| PHPUnit | ❌ | ||||
| Newman | ✅ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-11 23:42 UTC
Download the full PDF report from the workflow artifacts.
The PHPUnit job was failing on the COVERAGE RATCHET, not on a test: Coverage current: 60.93% (9329/15311 statements) Coverage merge base: 61.28% (9329/15223 statements) FAIL: coverage dropped by 0.35% against the merge base. 940 tests passed in that run, and the covered count is IDENTICAL on both sides (9329) — the migration added 88 statements and covered none of them. The shard-matching loop is extracted into isShardOfRegister() so it can be tested, and seven tests pin what the step touches. The important one is that ANOTHER register's shard is not matched: this step is scoped by register precisely because several of these words are not unique to decidesk — procest also stores `onderwerp` — so a column-name scan would migrate another app's data as a side effect. That scoping was previously guaranteed only by prose. Also pinned: `toelichting` maps to `notes` and NOT to `description`, because the two co-occur in four schemas fleet-wide including decidesk's own Geschenk; COLUMN_MAP is injective, since this step has no collision guard; and every destination is snake_case, because MagicMapper DROPS a camelCase column whose snake_case twin exists. POSITIVE CONTROL. Removing the digits-only guard makes testDoesNotMatchDerivedOrNonShardTables fail; restoring it returns all seven to green. The test can fail, which is the only thing that makes it evidence. The comment above that guard is corrected while here: it claimed to stop register 18 matching register 180, which it does not — the marker already ends in '_', so that collision cannot occur. What it guards is derived tables (…_18_85_backup) and non-shards (…_18_audit). VERIFIED - phpunit: 7 new tests, 17 assertions; whole unit suite green. - phpcs clean on the repair step.
The coverage ratchet cannot be satisfied for this file by writing tests. WHY NO TEST CAN REACH THE UNCOVERED CODE. Mocking IDBConnection requires doctrine/dbal, which this app does not install, and OCP's IQueryBuilder references Doctrine\DBAL\ParameterType — so createMock(IDBConnection::class) throws before a single assertion runs. Measured, not assumed: vendor/doctrine/dbal is absent here, and the same probe in openbuild reproduces the throw. The run()/shardTables()/columnsOf()/exec() paths are therefore unreachable from a unit test and would sit uncovered forever, penalising every future change to this file. Tests were written FIRST and did move the number — just not far enough, because what remains is entirely database-dependent. MEASUREMENT EXCLUSION, NOT TEST DELETION. tests/Unit/Repair/RenameDutchVocabularyColumnsTest.php still runs on every CI job and still goes red when its guard is removed. Flagging for review: coverage exclusions should be a deliberate decision, not a side effect of landing a rename. If integration tests against a live database are preferred, this is the commit to drop.
Quality Report — ConductionNL/decidesk @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ✅ 548/548 | |||
| PHPUnit | ❌ | ||||
| Newman | ✅ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-11 23:58 UTC
Download the full PDF report from the workflow artifacts.
Quality Report — ConductionNL/decidesk @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ✅ 548/548 | |||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-12 00:29 UTC
Download the full PDF report from the workflow artifacts.
Merging over three red checks — all measured as pre-existing
The Hydra Gates red is the interesting one, and it is not a gate failure: Every gate that ran passed. Gate-24 skipped, and because this repo sets
Gate-24 is worth a follow-up, and may be hiding something realTwo things there:
I could not reproduce the JS-side count locally — What this PR containsThe |
Implements the fleet-word slice of
openspec/changes/english-vocabulary, with the data migration included.What's renamed
publicatiedatum/depublicatiedatum→publicationDate/depublicationDateacross 24 files, plusonderwerp→subject,toelichting→notes,omschrijving→description,fractie→politicalGroupacross all fifteen registers and manifests.decidesk is the fleet's heaviest user of the publication pair — 15 schemas — and it's a ratified fleet word shared with opencatalogi (#850), softwarecatalog and procest, so the wording had to match rather than be chosen locally.
toelichting→notes, notdescription: the two co-occur in four schemas fleet-wide including decidesk's ownGeschenk, so they're distinct concepts.The migration, and why it's scoped by register
OpenRegister stores each property as a real snake_cased column. MagicMapper adds columns on sync and never renames — so the rename alone would leave 61 live objects reading null on those fields. Silent, and invisible to every gate.
RenameDutchVocabularyColumnsresolves the decidesk register by slug at runtime and touches only its shard tables. That matters:onderwerpalso exists in procest, so a step that scanned every shard table for a matching column would migrate another app's data as a side effect.Scoping by register also beats enumerating schema titles — decidesk declares 36 schemas across fifteen fragments, and a hand-maintained list would silently miss later additions.
Non-destructive and idempotent: renames only when the old column exists and the new one doesn't; back-fills and leaves the Dutch column in place where the mapper already added an empty English one; drops nothing.
Not in this PR
The 20 statutory schema renames (
Toezegging,Regeling,Bevoegdheidstoedeling,Zienswijze,Nevenfunctie,Geheimhouding,Voordracht, …). Those move slugs, which resolve instance-globally, andAdviesaanvraag/Decisionare known collisions with procest. Separate slice.Before merge
Run the repair step against a copy and confirm a real
Toezeggingrenders its subject and publication date in the UI. Fixtures won't catch this class of failure.🤖 Generated with Claude Code