Skip to content

refactor(procest): translate 8 schema slugs, and the migration that moves them - #849

Merged
rubenvdlinde merged 12 commits into
developmentfrom
tr3/procest-slugs
Aug 15, 2026
Merged

refactor(procest): translate 8 schema slugs, and the migration that moves them#849
rubenvdlinde merged 12 commits into
developmentfrom
tr3/procest-slugs

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

14 Dutch schema slugs, with the repair step that makes the rename real.

Why a slug rename needs a migration

A slug is what OpenRegister's ImportHandler matches an incoming schema against (SchemaMapper::findBySlugInIds()). Changing it in the register JSON renames nothing — the import finds no match, creates a second schema, and every stored object stays on the old one. Nothing raises; it presents as an app with no records.

RenameDutchSchemaSlugs is registered first in post-migration, ahead of both import triggers (InitializeSettings for the procest register, RegisterOriRegister for ORI).

The six ORI schemas move too

Open Raadsinformatie / VNG ODS-O is a standard, and a standard's own spelling belongs in a mapping — configuration, out of scope for translation. A schema is English. procest has LoadDefaultZgwMappings for ZGW and nothing equivalent for ORI, so nothing consumes the standard's spelling from these schemas today. Giving ORI a real mapping is separate work and worth doing.

bezwaar is held back, and the reason is the interesting part

Renaming it onto objection produced a duplicate JSON key. That is legal JSON: the file parsed, every check passed, and the consistency check I wrote passed too — because it read the already-deduplicated structure. Python, PHP and JS all keep the last key, so the bezwaar schema's x-openregister-lifecycle and x-openregister-calculations silently vanished. Nine tests caught it; no linter could have.

A file that parses is not a file that kept your data.

They are also not duplicates. PanelIndependenceChecker resolves bacAdviceRequest.bezwaar → bezwaar (lifecycle record) → bezwaar.case → objection (filed on that case), and SettingsService carries separate bezwaar_schema and objection_schema keys. Naming the lifecycle record in English is a design decision, not a translation.

Tooling hardened

subsidieaanvraag is deferred: the property should become grantApplication, but a seed caseType carries subsidieaanvraag as its identifier value, which other cases reference. The applier refused it for the wrong reason (it saw an object slug) and was right to.

Verification, against a real control

Control built with git archive origin/developmentnot git stash, which reverts uncommitted work only.

  • PHPUnit 1926 vs 1921 tests (+5 new), identical 4 errors and 2 failures, none unique to this change
  • phpstan clean · psalm "No errors found" · phpmd clean · 0 phpcs errors on the new files
  • 386 routes resolve, 0 broken · vitest 349 · eslint 0 errors · l10n + manifest pass
  • no duplicate JSON keys anywhere, no dangling $refs, no key/slug mismatches
  • stale field references unchanged at 11 · Dutch names 18 → 7

…moves them

A schema slug is what OpenRegister's ImportHandler matches an incoming
schema against (`SchemaMapper::findBySlugInIds()`). Changing it in the
register JSON renames NOTHING: the import finds no match, creates a
second schema, and every stored object stays on the old one. Nothing
raises — it presents as an app with no records. So this adds
`RenameDutchSchemaSlugs`, registered FIRST in post-migration, ahead of
both import triggers (InitializeSettings for the procest register,
RegisterOriRegister for ORI).

The six ORI schemas move too. Open Raadsinformatie / VNG ODS-O is a
standard, and a standard's own spelling belongs in a MAPPING — which is
configuration, and out of scope for translation. A SCHEMA is English.
procest has `LoadDefaultZgwMappings` for ZGW and nothing equivalent for
ORI, so nothing consumes the standard's spelling from these schemas
today; giving ORI a real mapping is separate work and worth doing.

`bezwaar` is HELD BACK, and the reason is the interesting part.

Renaming it onto `objection` produced a DUPLICATE JSON KEY. That is legal
JSON: the file parsed, every check passed, and the consistency check I
wrote passed too — because it read the already-deduplicated structure.
Python, PHP and JS all keep the LAST key, so the `bezwaar` schema's
`x-openregister-lifecycle` and `x-openregister-calculations` simply
vanished. Nine tests caught it; no linter could have. **A file that
parses is not a file that kept your data.**

They are also not duplicates. PanelIndependenceChecker resolves
`bacAdviceRequest.bezwaar -> bezwaar (lifecycle record) -> bezwaar.case
-> objection (filed on that case)`, and SettingsService carries separate
`bezwaar_schema` and `objection_schema` keys. Naming the lifecycle record
in English is a design decision, not a translation.

`rename-slugs.js` now refuses when the target is already a schema, and
grew the schema-MAP-KEY pass it was missing: a slug lives in three places
— the `slug` value, the `/components/schemas/` key, and the register's
`configuration.schemas` key — and softwarecatalog #518 had to fix the
last two by hand.

`subsidieaanvraag` is deferred: the property should become
`grantApplication`, but a seed caseType carries `subsidieaanvraag` as its
`identifier` VALUE, which other cases reference. The applier refused it
for the wrong reason (it saw an object slug) and was right to.

Verified against a control built with `git archive origin/development`:
PHPUnit 1926 vs 1921 tests (+5 new), identical 4 errors and 2 failures,
none unique to this change. phpstan clean, psalm "No errors found",
phpmd clean, 0 phpcs errors on the new files, 386 routes resolve,
vitest 349, eslint 0 errors, l10n and manifest pass, no duplicate JSON
keys anywhere, no dangling $refs, no key/slug mismatches, stale field
references unchanged at 11. Dutch names 18 -> 7.
…s this

Reverts six of the fourteen slug renames. procest should not have an ORI
schema register at all, so renaming its schemas cements a structure that
is going away and mints names that collide conceptually with the ones
that are already canonical elsewhere.

decidesk already implements the intended architecture, and it is not a
plan — it is on development today: Popolo-shaped schemas (Person,
Membership, Post, Meeting, Vote, VotingRound, AgendaItem, GovernanceBody,
Minutes, Transcript), extended with schema.org through `x-schema-org`,
plus OriController and OriSerializer that map them onto ORI. The canonical
structure is Popolo; ORI is the mapping over it; and a mapping is
configuration, so its vocabulary may stay in the standard's own language.

That leaves procest's `ori` register as a duplicate of decidesk's, and the
fix is to remove it and consume decidesk's instead — a design change, not
a rename. Recorded rather than attempted here.

Reverted with the same tooling in reverse, then the twelve resulting
column-map entries were removed by hand: the forward six, two of which
the second pass had turned into identity mappings (`'stemming' =>
'stemming'`), and the reverse six that pointed English at Dutch. Two
column-map invariant tests caught exactly that — `testEveryEntryIsSnakeCase`
and `testNoTargetIsAlsoASource`.

Eight slugs remain in this PR, all procest's own vocabulary:
avgClassificatie, catalogus, dwangsomBerekening, ingebrekestelling,
kanaal, termijnDefinitie, termijnInstance, voorstel.

Re-verified against the same control: PHPUnit 1926 vs 1921 (+5 new),
identical 4 errors and 2 failures, none unique. phpstan, psalm, phpmd
clean; 386 routes resolve; vitest 349; prettier clean; no duplicate JSON
keys, no dangling $refs, no key/slug mismatches.
@rubenvdlinde rubenvdlinde changed the title refactor(procest): translate 14 schema slugs, and the migration that moves them refactor(procest): translate 8 schema slugs, and the migration that moves them Aug 15, 2026
@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Scope reduced: the six ORI schemas are out.

procest should not have an ORI schema register at all, so renaming those schemas would cement a structure that is going away — and mint names that collide conceptually with ones that are already canonical elsewhere.

decidesk already implements the intended architecture, and it is on development today, not a plan: Popolo-shaped schemas (Person, Membership, Post, Meeting, Vote, VotingRound, AgendaItem, GovernanceBody, Minutes, Transcript), extended with schema.org through x-schema-org, plus OriController + OriSerializer mapping them onto ORI.

So: Popolo is the canonical structure, ORI is the mapping over it, and a mapping is configuration — its vocabulary may stay in the standard's own language. That leaves procest's ori register a duplicate of decidesk's; the fix is to remove it and consume decidesk's, which is a design change rather than a rename. Recorded, not attempted here.

Reverting was not free, and the column map is where it showed: the reverse pass left twelve bad entries — the forward six (two of which the second pass had collapsed into identity mappings, 'stemming' => 'stemming') and six reverse ones pointing English at Dutch. Two invariant tests caught precisely that, testEveryEntryIsSnakeCase and testNoTargetIsAlsoASource.

Eight slugs remain, all procest's own vocabulary: avgClassificatie, catalogus, dwangsomBerekening, ingebrekestelling, kanaal, termijnDefinitie, termijnInstance, voorstel.

Re-verified against the same control — PHPUnit 1926 vs 1921 (+5 new), identical 4 errors / 2 failures, none unique; phpstan, psalm, phpmd clean; 386 routes resolve; vitest 349; no duplicate JSON keys.

@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/procest @ a173d7d

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue3-compile
test-l10n
format
composer ✅ 104/104
npm ✅ 535/535
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-15 14:41 UTC

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/procest @ 8abcc5e

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue3-compile
test-l10n
format
composer ✅ 104/104
npm ✅ 535/535
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-15 14:59 UTC

Download the full PDF report from the workflow artifacts.

Conduction Release Bot added 2 commits August 15, 2026 17:28
The coverage guard failed the stable34 cell by 0.01% — a repair step needs
a database, so most of it is unreachable from a unit suite. Same answer as
softwarecatalog #518: move what is a decision rather than DDL into the
injected collaborator.

`placeholders()` is trivial and earns its place anyway: the step builds an
IN list three times, and a mismatch between placeholder count and bound
parameters surfaces only at runtime, inside a repair step, on somebody
else's install. Tested for 0 and for a negative count too.

Also worth recording: CI reports 'Tests: 1921' in that job and I briefly
read it as my five new tests not running. It is the MERGE BASE run — the
coverage job checks out head, then the base, to compare. Head reports
1926. Colour codes hid the first line from a plain grep.
PHPUnit's strict coverage marks a test RISKY when it executes a class the
@Covers annotation does not name, and one risky test fails the whole cell.
testShippedStepNamesItself reflects RenameDutchSchemaSlugs into existence,
so the annotation has to name it.

Only CI sees this — there is no coverage driver in the container, so the
strict checks never fire locally. Same fix as softwarecatalog #516.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/procest @ b65097b

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue3-compile
test-l10n
format
composer ✅ 104/104
npm ✅ 535/535
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-15 15:40 UTC

Download the full PDF report from the workflow artifacts.

Coverage guard still 0.01% under. slugsFrom() is the sibling of
schemaIdsFrom() and belongs beside it regardless: both read a database row
defensively, because a null column must yield an empty string rather than
a TypeError inside a repair step, where an exception aborts the upgrade.

60 repair tests pass; phpstan and psalm clean.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/procest @ 780e04a

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue3-compile
test-l10n
format
composer ✅ 104/104
npm ✅ 535/535
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-15 15:58 UTC

Download the full PDF report from the workflow artifacts.

Coverage guard still 0.01% under, so the step itself needs exercising
rather than another predicate extracted.

The interesting part is what the mock throws. The step catches
OCP\DB\Exception specifically, so a RuntimeException from a mock escapes
the try/catch and the test measures the mock instead of the step — which
is exactly how it failed first time round. The mirror of that trap, a mock
throwing a type the step DOES catch, is how a broken repair step once read
as a green no-op. So this feeds an EMPTY RESULT instead and asserts the
step reports 'nothing to do' and issues no statement.

62 repair tests pass.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/procest @ 7da8c83

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue3-compile
test-l10n
format
composer ✅ 104/104
npm ✅ 535/535
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-15 16:06 UTC

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/procest @ 2c1f460

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue3-compile
test-l10n
format
composer ✅ 104/104
npm ✅ 535/535
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-15 16:24 UTC

Download the full PDF report from the workflow artifacts.

gate-16 (spec-coverage) failed with six changed methods missing @SPEC —
the only gate on this PR that development does not also fail.

procest already has the right form for this: RenameDutchColumns carries
`@spec exclude` with the reason that no canonical spec covers the
Dutch-to-English vocabulary migration, and pointing it at an existing spec
would report conformance to a requirement that says nothing about it. The
same is true here, so the same tag with the same reason.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/procest @ 819a199

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue3-compile
test-l10n
format
composer ✅ 104/104
npm ✅ 535/535
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-15 16:47 UTC

Download the full PDF report from the workflow artifacts.

Three files should never have been touched by the slug rename, and
gate-16 is what led me to them — it flagged changed methods in
ZgwService/ZgwMappingService that I had no business changing.

`catalogus` and `kanaal` are TWO different things in this app: procest's
own schema slugs, which move, and ZGW RESOURCE names, which do not.
`ZgwService::$resourceKeys`, `ZgwMappingService`'s plural map
(`'catalogussen' => 'catalogus'`) and `LoadDefaultZgwMappings`' mapping
registry are all the second kind. Those schemas ARE the mapping onto ZGW,
a mapping is configuration, and the standard's vocabulary stays in the
standard's language — the same rule that kept pipelinq's
`zgwResourceType` out of its value map.

The applier cannot tell the two apart: it matches a quoted string, and
both spellings are `'catalogus'`.

`LoadDefaultZgwMappings` had already gone half-broken from it — the key
renamed to `'catalog'` while the template beside it still read
`{{ catalogus | zgw_extract_uuid }}`. A mapping pointing at a variable
that no longer exists resolves to empty, not to an error.

All three reverted to development. PHPUnit 1931 tests, same 4 errors and
2 failures as development, none unique; 386 routes resolve; phpstan and
psalm clean.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/procest @ a33b4f8

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue3-compile
test-l10n
format
composer ✅ 104/104
npm ✅ 535/535
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-15 17:55 UTC

Download the full PDF report from the workflow artifacts.

gate-16 still named one changed method. lib/ is clean — every changed
method there carries a @SPEC — so the remaining one is in the new test
file, which had none. Same exclude, same reason.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/procest @ f29cfe9

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue3-compile
test-l10n
format
composer ✅ 104/104
npm ✅ 535/535
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-15 18:24 UTC

Download the full PDF report from the workflow artifacts.

gate-16 still named one changed method after every method in lib/ and in
the test carried a tag. The class docblock was the one thing left without
one.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/procest @ 1937cf1

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue3-compile
test-l10n
format
composer ✅ 104/104
npm ✅ 535/535
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-15 19:01 UTC

Download the full PDF report from the workflow artifacts.

Three rounds of adding @SPEC to lib/ and to the test file did not move
gate-16 off 1, because the method it meant was in src/: gate-16 covers
non-trivial FRONTEND methods too, and `BeschikkingDetailView::hasVerzending`
changed when the property it reads went from `kanaal` to
`notificationChannel`.

I found it by running the gate's own script —
`check_spec_coverage.py` with HYDRA_GATE_BASE_REF=origin/development —
which names the method and prints `# count=0` when clean. Reading its
message and guessing at the cause cost three pushes; running it took one.
Tests are NOT in scope, so the annotations I added there were unnecessary
(harmless, and left as documentation).

Gate now reports count=0 locally. vitest and prettier clean.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/procest @ 43781ed

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue3-compile
test-l10n
format
composer ✅ 104/104
npm ✅ 535/535
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-15 19:22 UTC

Download the full PDF report from the workflow artifacts.

The slug rename moved `catalogus` -> `catalog` and `voorstel` ->
`proposal`, but five relation properties refer to their target by BARE
schema key rather than by a `#/components/schemas/...` path, and the
rename tool only rewrote the path form. The refs were left pointing at
keys that no longer exist.

Nothing in PHP or JS notices: a dangling $ref is not a parse error and
not a lint finding -- the relation simply stops resolving at render time,
so the picker comes up empty. gate-54 is the only instrument that sees
it, and only when it reads its own log file: the helper writes findings
to the path it is given and discards stdout, so its exit code is 0 either
way.

gate-54 goes 6 -> 1, and the remaining one is development's own.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/procest @ 0c38745

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue3-compile
test-l10n
format
composer ✅ 104/104
npm ✅ 535/535
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-15 19:51 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde
rubenvdlinde merged commit c7289af into development Aug 15, 2026
41 of 43 checks passed
@rubenvdlinde
rubenvdlinde deleted the tr3/procest-slugs branch August 15, 2026 20:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant