refactor(procest): translate 8 schema slugs, and the migration that moves them - #849
Conversation
…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.
|
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 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 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, Eight slugs remain, all procest's own vocabulary: 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. |
Quality Report — ConductionNL/procest @
|
| 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.
Quality Report — ConductionNL/procest @
|
| 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.
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.
Quality Report — ConductionNL/procest @
|
| 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.
Quality Report — ConductionNL/procest @
|
| 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.
Quality Report — ConductionNL/procest @
|
| 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.
Quality Report — ConductionNL/procest @
|
| 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.
Quality Report — ConductionNL/procest @
|
| 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.
Quality Report — ConductionNL/procest @
|
| 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.
Quality Report — ConductionNL/procest @
|
| 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.
Quality Report — ConductionNL/procest @
|
| 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.
Quality Report — ConductionNL/procest @
|
| 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.
Quality Report — ConductionNL/procest @
|
| 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.
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
ImportHandlermatches 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.RenameDutchSchemaSlugsis registered first in post-migration, ahead of both import triggers (InitializeSettingsfor the procest register,RegisterOriRegisterfor 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
LoadDefaultZgwMappingsfor 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.bezwaaris held back, and the reason is the interesting partRenaming it onto
objectionproduced 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 thebezwaarschema'sx-openregister-lifecycleandx-openregister-calculationssilently vanished. Nine tests caught it; no linter could have.They are also not duplicates.
PanelIndependenceCheckerresolvesbacAdviceRequest.bezwaar → bezwaar (lifecycle record) → bezwaar.case → objection (filed on that case), andSettingsServicecarries separatebezwaar_schemaandobjection_schemakeys. Naming the lifecycle record in English is a design decision, not a translation.Tooling hardened
rename-slugs.jsnow refuses when the target is already a schemaslugvalue, the/components/schemas/key, and the register'sconfiguration.schemaskey. softwarecatalog build(deps): Bump the npm_and_yarn group across 2 directories with 18 updates #518 had to fix the last two by hand.subsidieaanvraagis deferred: the property should becomegrantApplication, but a seedcaseTypecarriessubsidieaanvraagas itsidentifiervalue, 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/development— notgit stash, which reverts uncommitted work only.$refs, no key/slug mismatches