fix(gates): a mock descriptor is data, so strip the blocks that name PHP code - #612
Merged
Merged
Conversation
…PHP code gate-56 (register-handler-resolution) failed on shillinq with 6 unresolved handler references, every one of them inside the generated mock descriptor. Measured as a control: 6 findings with the mock in the gate's file list, 0 without. The app's real descriptor is clean. The references were STALE, and this generator wrote them. The mock named `OCA\Shillinq\Consolidation\ConsolidationGuard`, a namespace no class on disk uses; the real descriptor names `OCA\Shillinq\Service\ConsolidationGuard`, which exists. Definitions are merged across every component file, so a lifecycle block from a non-canonical file is unioned into the copy alongside the canonical one -- and the merge has no way to tell which of two same-named schemas holds the reference that still resolves. Rather than teach the merge to prefer one, the blocks are dropped: a mock descriptor exists so demo objects have a register to live in and a schema to be validated against. Carrying the state machine across makes DATA assert things about CODE, which is what broke. `x-openregister-lifecycle` is not a JSON Schema keyword, so validation of the demo objects is unaffected -- `--check` still passes 499 of 499 schemas on shillinq. `class` is deliberately NOT stripped: it is a plausible name for an ordinary schema property, and removing it would silently change the shape the demo objects are validated against. Measured across all seven apps carrying demo data: gate-56 clean everywhere, and the apps already merged are clean as committed, so no follow-up is needed for them.
rubenvdlinde
added a commit
to ConductionNL/shillinq
that referenced
this pull request
Aug 28, 2026
gate-56 (register-handler-resolution) resolves `handler`, `guard`, `requires`, `save`, `fallbackGuard` and `preconditions` to real classes, and it reads `lib/Settings/*register*.json` — which matches the generated mock. shillinq failed with 6 unresolved references, all inside the mock: it named `OCA\Shillinq\Consolidation\ConsolidationGuard`, a namespace no class uses, while the real descriptor names `OCA\Shillinq\Service\ConsolidationGuard`, which exists. The generator merges definitions across every component file, so a lifecycle block from a non-canonical file was unioned into the copy. The generator now strips those blocks (ConductionNL/.github#612): a mock exists so demo objects have a register to live in and a schema to validate against, not to describe a state machine. Regenerated here. Verified: gate-56 0 findings, `--check` still passes every schema, and shillinq's manifest budget still PASSes.
rubenvdlinde
pushed a commit
to ConductionNL/buildiq
that referenced
this pull request
Aug 28, 2026
gate-56 (register-handler-resolution) resolves `handler`, `guard`, `requires`, `save`, `fallbackGuard` and `preconditions` to real classes, and it reads `lib/Settings/*register*.json` — which matches the generated mock. shillinq failed with 6 unresolved references, all inside the mock: it named `OCA\Shillinq\Consolidation\ConsolidationGuard`, a namespace no class uses, while the real descriptor names `OCA\Shillinq\Service\ConsolidationGuard`, which exists. The generator merges definitions across every component file, so a lifecycle block from a non-canonical file was unioned into the copy. The generator now strips those blocks (ConductionNL/.github#612): a mock exists so demo objects have a register to live in and a schema to validate against, not to describe a state machine. Regenerated here. Verified: gate-56 0 findings, `--check` still passes every schema, and shillinq's manifest budget still PASSes.
rubenvdlinde
added a commit
to ConductionNL/opencatalogi
that referenced
this pull request
Aug 28, 2026
gate-56 (register-handler-resolution) resolves `handler`, `guard`, `requires`, `save`, `fallbackGuard` and `preconditions` to real classes, and it reads `lib/Settings/*register*.json` — which matches the generated mock. shillinq failed with 6 unresolved references, all inside the mock: it named `OCA\Shillinq\Consolidation\ConsolidationGuard`, a namespace no class uses, while the real descriptor names `OCA\Shillinq\Service\ConsolidationGuard`, which exists. The generator merges definitions across every component file, so a lifecycle block from a non-canonical file was unioned into the copy. The generator now strips those blocks (ConductionNL/.github#612): a mock exists so demo objects have a register to live in and a schema to validate against, not to describe a state machine. Regenerated here. Verified: gate-56 0 findings, `--check` still passes every schema, and shillinq's manifest budget still PASSes.
added 2 commits
August 28, 2026 10:04
…bered 99
`Package invariants` failed with three failures in the demo-data suite, every
one of them reading a line about a different gate:
FAIL — expected FAIL on planted, got: [gate-99] manifest-l10n-coverage:
NOT APPLICABLE — no user-visible manifest string ...
TWO gates declared 99. `manifest-l10n-coverage` at line 10818 and
`demo-data-coverage` at line 10997, both calling `_pass 99` / `_fail 99` with
different names — so one gate's verdict overwrote the other's, the COVERAGE
tally counted one slot for two gates, and a suite asserting on demo-data
coverage was handed the l10n gate's answer.
The number is part of a gate's identity. `manifest-l10n-coverage` keeps 99 —
it was there first and is already reporting under it — and demo-data-coverage
takes 101, the next free number. All five of its call sites moved together;
`grep '99 "demo-data-coverage"'` is now 0.
The suite is renamed to match and records WHY, so the next reader does not
renumber it back. All four arms pass, including ARM 3 (inherited debt is not
this PR's problem) and ARM 4 (counting is not checking).
🔴 There is a gate-95 for ADR number collisions and nothing equivalent for GATE
numbers, which is why this reached main. Worth its own gate; noted rather than
built here.
shillinq and opencatalogi failed PHPUnit on this programme's own demo data.
shillinq named it exactly:
OrderPrimitiveSchemaTest::testExactlyOneOrderSchemaDefinitionExists
Failed asserting that actual size 2 matches expected size 1.
That test globs `lib/Settings/{*.json,register.d/*.json}` and counts schema
definitions by slug. The generated mock sits in `lib/Settings` and copied every
definition, so "exactly one" became two for every schema it carried. A third
test then failed with `$lifecycle` null, because the copy it happened to pick
was the mock's — the one whose behavioural blocks the previous commit strips.
The copy was never needed. The mock now declares its REGISTER (the descriptor
lookup keys on that, and the importer needs somewhere to put the objects) and
its OBJECTS, and nothing else:
- the importer resolves an object's schema by slug through
`schemaMapper->find()` when the descriptor omits it, and the real
descriptor has always imported first — ADR-005 seeds it from a Repair step
at install;
- `--check` reads definitions from the app's REAL descriptors, because
`_component_files` skips `type: mock`, so validation is untouched.
Verified on all seven apps carrying demo data: every object and every schema
still covered (shillinq 1497 objects / 499 schemas, unchanged), `--check`
passes everywhere, and shillinq's OrderPrimitiveSchemaTest goes from 4 failures
to OK (9 tests, 116 assertions).
🔴 The generator's own summary line read the block it no longer emits and
crashed with KeyError. Its suite caught that — three arms went red — which is
the whole reason the suite exists. Fixed to count distinct schemas from the
objects, which is the honest number anyway: it counts what was generated.
rubenvdlinde
added a commit
to ConductionNL/shillinq
that referenced
this pull request
Aug 28, 2026
…schema
The generated mock sits in `lib/Settings` and copied every schema definition,
so apps that assert their schemas are unique started counting two. shillinq
named it:
testExactlyOneOrderSchemaDefinitionExists
Failed asserting that actual size 2 matches expected size 1.
The copy was never needed: the importer resolves an object's schema by slug
when the descriptor omits it, and the real descriptor always imports first
(ADR-005 seeds it from a Repair step at install). Regenerated against
ConductionNL/.github#612 — the descriptor now declares its register and its
objects only.
Every object and schema still covered; `--check` passes unchanged.
rubenvdlinde
added a commit
to ConductionNL/opencatalogi
that referenced
this pull request
Aug 28, 2026
…schema
The generated mock sits in `lib/Settings` and copied every schema definition,
so apps that assert their schemas are unique started counting two. shillinq
named it:
testExactlyOneOrderSchemaDefinitionExists
Failed asserting that actual size 2 matches expected size 1.
The copy was never needed: the importer resolves an object's schema by slug
when the descriptor omits it, and the real descriptor always imports first
(ADR-005 seeds it from a Repair step at install). Regenerated against
ConductionNL/.github#612 — the descriptor now declares its register and its
objects only.
Every object and schema still covered; `--check` passes unchanged.
rubenvdlinde
pushed a commit
to ConductionNL/buildiq
that referenced
this pull request
Aug 28, 2026
…schema
The generated mock sits in `lib/Settings` and copied every schema definition,
so apps that assert their schemas are unique started counting two. shillinq
named it:
testExactlyOneOrderSchemaDefinitionExists
Failed asserting that actual size 2 matches expected size 1.
The copy was never needed: the importer resolves an object's schema by slug
when the descriptor omits it, and the real descriptor always imports first
(ADR-005 seeds it from a Repair step at install). Regenerated against
ConductionNL/.github#612 — the descriptor now declares its register and its
objects only.
Every object and schema still covered; `--check` passes unchanged.
rubenvdlinde
added a commit
to ConductionNL/pipelinq
that referenced
this pull request
Aug 28, 2026
…schema
The generated mock sits in `lib/Settings` and copied every schema definition,
so apps that assert their schemas are unique started counting two. shillinq
named it:
testExactlyOneOrderSchemaDefinitionExists
Failed asserting that actual size 2 matches expected size 1.
The copy was never needed: the importer resolves an object's schema by slug
when the descriptor omits it, and the real descriptor always imports first
(ADR-005 seeds it from a Repair step at install). Regenerated against
ConductionNL/.github#612 — the descriptor now declares its register and its
objects only.
Every object and schema still covered; `--check` passes unchanged.
rubenvdlinde
added a commit
to ConductionNL/pipelinq
that referenced
this pull request
Aug 28, 2026
* feat(demo): generated demo data for every schema (ADR-111 rules 1-2)
This app declares schemas and shipped no demo data, so it opened on an empty
list: the person evaluating it had to author objects by hand against a schema
they did not know yet. Fleet-wide, 562 of 598 schemas were in that state.
🔴 GENERATED, NOT WRITTEN. Every value is derived from the schema that will
validate it — `enum` picks from the enum, `pattern` is satisfied, `format`
drives the shape, `minimum`/`maxLength` are honoured, `required` is always
populated. Hand-written demo data is wrong in a way nobody sees until the demo
(a status outside its own enum, a required field omitted) and it fails at
import, in front of whoever asked for the demo.
Produced and validated by the single copy that gate-99 also runs:
`vendor/conduction/hydra-gates/scripts/lib/generate_mock_register.py`.
Regenerate with that file; `--keep` preserves curated objects and tops up only
what is short.
🔴 IT DOES NOT INSTALL ITSELF (ADR-111 rule 3). `x-openregister.type: mock` is
imported ON DEMAND — sample data appearing on a production instance because
somebody upgraded is a data-integrity incident, not a convenience. Install it
with:
occ openregister:descriptors:list --app=<app> --import=<register>
The setup-wizard step that offers this during first-run install (ADR-111 rule 4)
follows once OpenRegister's shared installer lands — deliberately not twenty-one
copies of the same logic.
Verified: `--check` re-validates every object against its own schema with
jsonschema and reports clean.
* fix(demo): emit real MDI icon names, so gate-60 stops rejecting the demo data
gate-60 (icon-vocabulary) failed the very PR that added demo data: the generator
emitted `Voorbeeld Icon 1` for properties named `icon`, and an icon outside the
canonical vocabulary "renders blank wherever it is not aliased locally"
(ADR-077).
🔴 THE FIX READS THE GATE'S OWN VOCABULARY FILE, never a copy of it. Carrying a
second list here is precisely how a producer and its judge drift apart, which
this package fixed once already today.
🔴 AND IT READS THE VALUES, NOT THE KEYS. The first attempt emitted the semantic
key — `documentation` — because that is what the map is keyed by. The map is
`"documentation": "BookOpenVariantOutline"`, and gate-60 accepts the MDI name:
it rejected `documentation` in the same breath as the invented name it replaced,
"a kebab-case or lowercase spelling of an MDI name resolves to nothing". Half
reading a vocabulary is not reading it.
Verified on portaliq: gate-60 PASS, zero gate failures. All seven apps
regenerate with zero validation findings.
* fix(demo-data): generate against the whole schema, not the file that sorts first
A schema defined across more than one file was truncated to whichever file
sorted first, so the demo data satisfied half a schema. pipelinq's `ticket`
is the clear case: a base definition plus a CTI overlay whose own comment
says the overlay must land there "or OpenRegister's magic-table columns for
these fields never exist". The generator kept one half and discarded the
other, and both outcomes produce valid-looking objects against a real
schema, so nothing complained.
Definitions are now merged across files (properties and `required` unioned),
which is the "an extended schema extends its base" semantics the descriptors
already assumed. Regenerated and re-validated: no schema lost, coverage
unchanged, every object still validates.
* feat(setup): offer the demo data first, before the welcome screen
ADR-111 rule 4. Pipelinq already had the whole mechanism — a `demo-data`
run-action backed by DemoSeedService, the same write path as
`occ pipelinq:demo:seed` — sitting at step 4, behind a welcome screen, a
currency choice and a provisioning run.
The first question a new administrator has is whether they can see the app
work, and answering it requires data they cannot author against a schema they
do not know yet. A welcome screen answers a question nobody asked; it can
still say hello from second place.
One line moved. The step set is byte-identical, the walkthrough tours are
untouched, and the setup spec iterates step ids rather than asserting their
order, so nothing downstream depends on the position.
* fix(demo-data): use only icons this app registers
gate-60 failed on the generated demo data: it offered canonical vocabulary
icons the app does not import in src/icons.js, and `CnAppNav` resolves an MDI
name only through the registry `registerIcons()` populates — with no fallback,
so an unregistered name renders nothing at all.
Regenerated with the generator scoping icons to what the app registers.
gate-60: 0 failures.
* fix(e2e): complete every optional setup step, not just the demo-data one
The xWiki sidebar spec failed on this branch and passed on every other. The
suite's own header names the mechanism: the optional setup wizard renders a
full modal mask over the shell, its dismissal lives in localStorage, and every
Playwright test runs in a fresh context — so it reopens in every single test
unless no optional step is outstanding.
Steps 1-3 cleared `provision`, `currency` and `demo-data` and left
`organisation` behind: SetupController marks it done only once
`receipt_company_name` is set, and nothing here ever set it. Step 3b does.
🔴 AND THE CHECK BELOW HAD GONE STALE. It ended with:
Deliberately NOT an error: ... since nextcloud-vue 2.1.0-vue3.17 the
server's `completed` flag suppresses the optional wizard regardless.
The library disagrees, in its own words. `CnAppRoot.optionalSetupPending`:
`completed` is still honoured as a NEGATIVE signal ...; it just no longer
overrides a step the server explicitly reported as not done.
So `completed: true` suppresses nothing now, and the tolerance built on it let
a wizard-parking condition through as "(fine — see below)". An unmet optional
step that is not `info`/`summary` is now a hard error naming the step, with
`info`/`summary` excluded for the library's own reason: the server has nothing
to persist for them, so they report done:false forever.
`integrations` is deliberately NOT forced: SetupController reports it done when
neither shillinq nor integriq is installed, which is the CI stack, and setting
`xwiki_direct_url` would point the integration at a host that does not exist —
breaking the very specs that assert on its degraded state.
Honest limit: I could not reproduce the exact trigger statically — both
branches lock nextcloud-vue 2.19.0 and resolve the same first-unmet step — so
this removes the overlay as a variable rather than claiming to have isolated
it. If the spec still fails, the confound is gone and the next run says so.
* fix(demo-data): the mock carries objects, not a second copy of every schema
The generated mock sits in `lib/Settings` and copied every schema definition,
so apps that assert their schemas are unique started counting two. shillinq
named it:
testExactlyOneOrderSchemaDefinitionExists
Failed asserting that actual size 2 matches expected size 1.
The copy was never needed: the importer resolves an object's schema by slug
when the descriptor omits it, and the real descriptor always imports first
(ADR-005 seeds it from a Repair step at install). Regenerated against
ConductionNL/.github#612 — the descriptor now declares its register and its
objects only.
Every object and schema still covered; `--check` passes unchanged.
rubenvdlinde
added a commit
to ConductionNL/opencatalogi
that referenced
this pull request
Aug 28, 2026
…s (ADR-111) (#1161) * feat(demo-data): ship demo data for every schema this app supplies (ADR-111) Three objects per (register, schema) pair, each generated from the schema itself and validated against it. Not installed automatically: a mock register is imported on demand, from the setup walkthrough or `occ openregister:descriptors:list --app=<id> --import=<slug>`. Attributed to the app id in appinfo/info.xml — `x-openregister.app` is what the descriptor inventory resolves a register to an app by, so the checkout directory name would name an app that does not exist. * feat(setup): offer the demo data as the first setup step (ADR-111 rule 4) An app installed from the App Store opens on an empty list, and the only question its first reader has is whether they can see it work. Answering it needs data they cannot author, against a schema they do not know yet. A welcome screen answers a question nobody asked; it can still say hello from second place. `DemoDataService` imports the generated `opencatalogi_mock_register.json` through the same OpenRegister importer the app already uses for its real configuration. Two decisions worth stating: - `force: true`. OpenRegister version-gates a non-forced import and SKIPS silently when the version has not moved. An operator who asks for demo data and is told it worked, on an instance where nothing was written, has been lied to by a version compare. The request is explicit, so the import is. - Its own config identity (`opencatalogi.demo`), so the demo import and the real configuration import cannot mask one another's version gate. `skip-demo-data` exists so "no thanks" is a decision the wizard can record — otherwise the only way past the step is to install demo data, which is wrong on a production instance. The status flag records that the step was DEALT WITH, not that demo objects exist. The cross-app getter returns `object`, not the OpenRegister class: naming a class from an optional app in a native return type makes PHP resolve it on every return, so an instance without OpenRegister fails with a TypeError about a class nobody mentioned instead of the RuntimeException that names the missing app. Found by writing the test. 7 unit tests, 13 assertions. phpcs clean, manifest validates against app-manifest-v2, gate-100 passes. * fix(tests): pass DemoDataService where the SetupController constructor expects it Adding `DemoDataService` to the constructor for ADR-111 rule 4 broke the existing SetupController tests, and I did not grep for callers when I changed the signature. buildiq's CI named it on all three NC versions: TypeError: SetupController::__construct(): Argument #6 ($demoDataService) must be of type OCA\Buildiq\Service\DemoDataService, MockObject_SettingsService_f805ce68 given A positional construction does not fail where the argument is missing — it fails one slot LATER, reported as a type error on whatever now lands in the wrong place. That is why it reads as a problem with $settings. Swept all five apps that gained the step: three construct the controller in tests. openbuild and opencatalogi positionally (opencatalogi twice — a match-count assertion caught the second, which a single blind replace would have missed), shillinq by named argument, where the new parameter is simply a missing required one rather than a shifted slot. Verified: openbuild 3 tests, shillinq 10, opencatalogi 16 — all green. * fix(manifest): drop the em-dashes from the demo-data step body gate-96 (manifest-copy-style) failed on larpinq. The Conduction voice bans em-dashes in user-visible copy — voice.md section 8 calls them and double dashes AI tells — and I wrote two of them into the ADR-111 step body. FAIL src/manifest.json.setup.steps[0].body: em-dash Rewritten with commas, same content and length. Swept every app that gained the step: larpinq and opencatalogi carried em-dashes, dossiq, shillinq and buildiq were already clean. pipelinq is clean too and stays untouched: its body contains `occ pipelinq:demo:seed --remove`, and the gate correctly exempts a double dash inside an identifier or command rather than treating a CLI flag as prose. Verified: gate-96 clean on both (225 and 120 manifest strings checked, so the check is measuring rather than finding nothing), gate-100 passes, manifests validate against app-manifest-v2 with 0 errors, l10n unaffected. * fix(l10n): translate the demo-data step, and rebuild the browser catalogues gate-99 (manifest-l10n-coverage) failed: the ADR-111 step's title and body had no `l10n/nl.json` key, so a Dutch user would have read English in the first step of the setup walkthrough. Both strings added to nl.json and en.json, taken verbatim from each app's own manifest rather than retyped, so the key always matches the string the gate looks up. Wording follows what dossiq already shipped ("Demodata (optioneel)"). The compiled catalogues are rebuilt too. The JSON is the source; the browser loads `l10n/*.js`, so translating only the JSON would have satisfied the gate while still showing English to every actual user — the same source-vs-artefact split that made dossiq's check:l10n-js go red earlier today. Verified locally before pushing, not after: manifest-l10n-coverage 0 findings, manifest-copy-style 0, setup-demo-data-first 0, manifest schema 0 errors, check:l10n-js PASS, and shillinq's manifest budget still PASS. * fix(l10n): keep the manifest strings in nl.json only Adding the ADR-111 step's strings to en.json as well as nl.json broke two checks in opposite directions, and neither is wrong: larpinq test:l10n — en.json defines the required key set, so two new keys obliged all 37 locale files to translate them. opencatalogi check:l10n — flags keys in en.js that no t() call references. A manifest string is not a t() call, so all three counted as UNUSED. One demands the key be everywhere, the other demands it be nowhere. Both are satisfied by putting manifest strings in nl.json ONLY, which is also what gate-99 actually asks for: it looks up `l10n/nl.json` and nothing else. en.json is the SOURCE catalogue governed by t() extraction and locale parity; a manifest string is neither extracted nor parity-tracked, so it does not belong there. Applied to all four apps rather than only the two that were red, so the rule is uniform and the next locale added to any of them cannot reopen this. Compiled catalogues rebuilt. Verified per app, each against its own scripts: larpinq test:l10n PASS, openbuild test:l10n PASS, opencatalogi check:l10n PASS, shillinq test:l10n + test:l10n-parity PASS, check:l10n-js PASS everywhere, and manifest-l10n-coverage 0 findings on all four. * fix(l10n): restore the manifest strings in en.json development's �[1m�[36mopencatalogi l10n check�[0m �[2mScanned 178 files (84 .vue), 979 keys in en.js; 75 strings also translated server-side�[0m �[32m�[1mMISSING from l10n/en.js (0)�[0m ✓ none �[32m�[1mUNUSED keys in l10n/en.js (0)�[0m ✓ none �[32m�[1mUNWRAPPED literals matching an l10n key (0)�[0m ✓ none �[2m�[1mUnanalyzable t() calls — dynamic args, skipped (3)�[0m �[2m•�[0m src/services/usageStats.js:106 �[2mt('opencatalogi', key, params). * @return {?string} The note, or null when ther...�[0m �[2m•�[0m src/views/dashboard/Dashboard.vue:946 �[2mt('opencatalogi', s.name), })), } } } catch (err) { consol...�[0m �[2m•�[0m src/views/dashboard/Dashboard.vue:984 �[2mt('opencatalogi', s.name), })), } } } catch (err) { con...�[0m �[32m�[1m✓ all clean�[0m now understands manifest-declared strings and reports them as MISSING from l10n/en.js. It previously flagged them as UNUSED, which is why they were removed here. The requirement inverted under the merge, so the keys go back. This app is the only one whose checker changed: larpinq, buildiq and shillinq still hold the manifest strings in nl.json only, verified against each app's own scripts. check:l10n PASS, check:l10n-js PASS, manifest-l10n-coverage 0 findings. * fix(demo-data): the mock no longer carries blocks that name PHP code gate-56 (register-handler-resolution) resolves `handler`, `guard`, `requires`, `save`, `fallbackGuard` and `preconditions` to real classes, and it reads `lib/Settings/*register*.json` — which matches the generated mock. shillinq failed with 6 unresolved references, all inside the mock: it named `OCA\Shillinq\Consolidation\ConsolidationGuard`, a namespace no class uses, while the real descriptor names `OCA\Shillinq\Service\ConsolidationGuard`, which exists. The generator merges definitions across every component file, so a lifecycle block from a non-canonical file was unioned into the copy. The generator now strips those blocks (ConductionNL/.github#612): a mock exists so demo objects have a register to live in and a schema to validate against, not to describe a state machine. Regenerated here. Verified: gate-56 0 findings, `--check` still passes every schema, and shillinq's manifest budget still PASSes. * fix(demo-data): the mock carries objects, not a second copy of every schema The generated mock sits in `lib/Settings` and copied every schema definition, so apps that assert their schemas are unique started counting two. shillinq named it: testExactlyOneOrderSchemaDefinitionExists Failed asserting that actual size 2 matches expected size 1. The copy was never needed: the importer resolves an object's schema by slug when the descriptor omits it, and the real descriptor always imports first (ADR-005 seeds it from a Repair step at install). Regenerated against ConductionNL/.github#612 — the descriptor now declares its register and its objects only. Every object and schema still covered; `--check` passes unchanged. * test(setup): cover the demo-data action, which nothing exercised The coverage ratchet failed this branch: FAIL: coverage of the code this change KEEPS or ADDS dropped by 3.68%. base 170/218 -> head 211/284 statements Fair. `installDemoData()` and `skipDemoData()` were added to SetupController and no test called either — the suite had one incidental mention of "demo" in its whole length. 66 statements arrived, 25 of them unreached. Three tests, one per path: - the happy path asserts the COUNTS reach the operator. "Demo data installed" with no numbers cannot be told apart from an import that wrote nothing, which is the defect the openregister half of this programme shipped and had to fix; - a FAILED install must leave the step UNDECIDED. The controller records the decision only after the import returns, and the test pins that: marking it first would let a failed install present as a finished step, so the wizard would never offer it again and nobody would learn the data is absent; - skipping records `skipped` without importing — a decision, not an absence of one. The service mock is now a property rather than an inline `createMock`, because a mock nothing can configure cannot express any of the above. Verified with a control: delete the line that records the decision and the first test fails. Three tests, 12 assertions. (The six pre-existing errors in this file under a standalone worktree are environmental — they need OpenRegister classes that a checkout outside the server tree has no access to — and are unrelated to this change.) --------- Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
rubenvdlinde
added a commit
to ConductionNL/buildiq
that referenced
this pull request
Aug 28, 2026
* feat(demo): generated demo data for every schema (ADR-111 rules 1-2)
This app declares schemas and shipped no demo data, so it opened on an empty
list: the person evaluating it had to author objects by hand against a schema
they did not know yet. Fleet-wide, 562 of 598 schemas were in that state.
🔴 GENERATED, NOT WRITTEN. Every value is derived from the schema that will
validate it — `enum` picks from the enum, `pattern` is satisfied, `format`
drives the shape, `minimum`/`maxLength` are honoured, `required` is always
populated. Hand-written demo data is wrong in a way nobody sees until the demo
(a status outside its own enum, a required field omitted) and it fails at
import, in front of whoever asked for the demo.
Produced and validated by the single file gate-99 also runs:
`vendor/conduction/hydra-gates/scripts/lib/generate_mock_register.py`.
`--keep` preserves curated objects and tops up only what is short.
🔴 IT DOES NOT INSTALL ITSELF (ADR-111 rule 3). `x-openregister.type: mock` is
imported ON DEMAND — sample data appearing on a production instance because
somebody upgraded is a data-integrity incident, not a convenience:
occ openregister:descriptors:list --app=<app> --import=<register>
The setup-wizard step offering this on first run (ADR-111 rule 4, gate-100)
follows once OpenRegister's shared installer lands — deliberately not
twenty-one copies of the same logic.
Verified: `--check` re-validates every object against its own schema with
jsonschema and reports zero findings.
* fix(demo-data): generate against the whole schema, not the file that sorts first
A schema defined across more than one file was truncated to whichever file
sorted first, so the demo data satisfied half a schema. pipelinq's `ticket`
is the clear case: a base definition plus a CTI overlay whose own comment
says the overlay must land there "or OpenRegister's magic-table columns for
these fields never exist". The generator kept one half and discarded the
other, and both outcomes produce valid-looking objects against a real
schema, so nothing complained.
Definitions are now merged across files (properties and `required` unioned),
which is the "an extended schema extends its base" semantics the descriptors
already assumed. Regenerated and re-validated: no schema lost, coverage
unchanged, every object still validates.
* fix(demo-data): attribute the descriptor to the app id, not the directory
`x-openregister.app` is what the descriptor inventory resolves a register to
an app by, and the generator was writing the CHECKOUT DIRECTORY name into it.
Five of eight apps already ship an `<id>` that differs from their directory —
larpinq, learniq, humaniq, decidiq, buildiq — so those descriptors named an
app that does not exist, and the `occ openregister:descriptors:list --app=…`
command printed in their own description resolved nothing. A cross-app id is
a runtime lookup: it does not error, it silently finds nobody.
The id now comes from `<id>` in appinfo/info.xml, the stated authority,
falling back to the directory name only when there is no info.xml. The output
file is renamed to match and the directory-named one removed, so exactly one
mock descriptor remains per app — two would import demo data twice.
Register slugs are untouched: those are frozen per app.
* feat(setup): offer the demo data as the first setup step (ADR-111 rule 4)
An app installed from the App Store opens on an empty list, and the only
question its first reader has is whether they can see it work. Answering it
needs data they cannot author, against a schema they do not know yet. A
welcome screen answers a question nobody asked; it can still say hello from
second place.
`DemoDataService` imports this app's generated mock register through the same
OpenRegister importer the app already uses for its real configuration. Two
decisions worth stating:
- `force: true`. OpenRegister version-gates a non-forced import and SKIPS
silently when the version has not moved. An operator who asks for demo data
and is told it worked, on an instance where nothing was written, has been
lied to by a version compare. The request is explicit, so the import is.
- Its own config identity (`<app>.demo`), so the demo import and the real
configuration import cannot mask one another's version gate.
`skip-demo-data` exists so "no thanks" is a decision the wizard can record —
otherwise the only way past the step is to install demo data, which is wrong
on a production instance. The status flag records that the step was DEALT
WITH, not that demo objects exist.
The cross-app getter returns `object`, not the OpenRegister class: naming a
class from an optional app in a native return type makes PHP resolve it on
every return, so an instance without OpenRegister fails with a TypeError
about a class nobody mentioned instead of the RuntimeException that names the
missing app.
7 unit tests, 13 assertions. phpcs clean, manifest validates against
app-manifest-v2, gate-100 passes.
* fix(tests): pass DemoDataService where the SetupController constructor expects it
Adding `DemoDataService` to the constructor for ADR-111 rule 4 broke the
existing SetupController tests, and I did not grep for callers when I changed
the signature. buildiq's CI named it on all three NC versions:
TypeError: SetupController::__construct(): Argument #6 ($demoDataService)
must be of type OCA\Buildiq\Service\DemoDataService,
MockObject_SettingsService_f805ce68 given
A positional construction does not fail where the argument is missing — it
fails one slot LATER, reported as a type error on whatever now lands in the
wrong place. That is why it reads as a problem with $settings.
Swept all five apps that gained the step: three construct the controller in
tests. openbuild and opencatalogi positionally (opencatalogi twice — a
match-count assertion caught the second, which a single blind replace would
have missed), shillinq by named argument, where the new parameter is simply a
missing required one rather than a shifted slot.
Verified: openbuild 3 tests, shillinq 10, opencatalogi 16 — all green.
* fix(l10n): translate the demo-data step, and rebuild the browser catalogues
gate-99 (manifest-l10n-coverage) failed: the ADR-111 step's title and body had
no `l10n/nl.json` key, so a Dutch user would have read English in the first
step of the setup walkthrough.
Both strings added to nl.json and en.json, taken verbatim from each app's own
manifest rather than retyped, so the key always matches the string the gate
looks up. Wording follows what dossiq already shipped ("Demodata (optioneel)").
The compiled catalogues are rebuilt too. The JSON is the source; the browser
loads `l10n/*.js`, so translating only the JSON would have satisfied the gate
while still showing English to every actual user — the same source-vs-artefact
split that made dossiq's check:l10n-js go red earlier today.
Verified locally before pushing, not after: manifest-l10n-coverage 0 findings,
manifest-copy-style 0, setup-demo-data-first 0, manifest schema 0 errors,
check:l10n-js PASS, and shillinq's manifest budget still PASS.
* fix(l10n): keep the manifest strings in nl.json only
Adding the ADR-111 step's strings to en.json as well as nl.json broke two
checks in opposite directions, and neither is wrong:
larpinq test:l10n — en.json defines the required key set, so two new keys
obliged all 37 locale files to translate them.
opencatalogi check:l10n — flags keys in en.js that no t() call references.
A manifest string is not a t() call, so all three
counted as UNUSED.
One demands the key be everywhere, the other demands it be nowhere. Both are
satisfied by putting manifest strings in nl.json ONLY, which is also what
gate-99 actually asks for: it looks up `l10n/nl.json` and nothing else.
en.json is the SOURCE catalogue governed by t() extraction and locale parity;
a manifest string is neither extracted nor parity-tracked, so it does not
belong there.
Applied to all four apps rather than only the two that were red, so the rule
is uniform and the next locale added to any of them cannot reopen this.
Compiled catalogues rebuilt.
Verified per app, each against its own scripts: larpinq test:l10n PASS,
openbuild test:l10n PASS, opencatalogi check:l10n PASS, shillinq test:l10n +
test:l10n-parity PASS, check:l10n-js PASS everywhere, and
manifest-l10n-coverage 0 findings on all four.
* fix(demo-data): the mock no longer carries blocks that name PHP code
gate-56 (register-handler-resolution) resolves `handler`, `guard`, `requires`,
`save`, `fallbackGuard` and `preconditions` to real classes, and it reads
`lib/Settings/*register*.json` — which matches the generated mock.
shillinq failed with 6 unresolved references, all inside the mock: it named
`OCA\Shillinq\Consolidation\ConsolidationGuard`, a namespace no class uses,
while the real descriptor names `OCA\Shillinq\Service\ConsolidationGuard`,
which exists. The generator merges definitions across every component file, so
a lifecycle block from a non-canonical file was unioned into the copy.
The generator now strips those blocks (ConductionNL/.github#612): a mock exists
so demo objects have a register to live in and a schema to validate against,
not to describe a state machine. Regenerated here.
Verified: gate-56 0 findings, `--check` still passes every schema, and
shillinq's manifest budget still PASSes.
* fix(demo-data): the mock carries objects, not a second copy of every schema
The generated mock sits in `lib/Settings` and copied every schema definition,
so apps that assert their schemas are unique started counting two. shillinq
named it:
testExactlyOneOrderSchemaDefinitionExists
Failed asserting that actual size 2 matches expected size 1.
The copy was never needed: the importer resolves an object's schema by slug
when the descriptor omits it, and the real descriptor always imports first
(ADR-005 seeds it from a Repair step at install). Regenerated against
ConductionNL/.github#612 — the descriptor now declares its register and its
objects only.
Every object and schema still covered; `--check` passes unchanged.
---------
Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
rubenvdlinde
added a commit
that referenced
this pull request
Aug 28, 2026
… checked (#623) TWO SESSIONS FIXED ONE COLLISION AND MADE IT WORSE. gate-99 was claimed by `manifest-l10n-coverage` and `demo-data-coverage` at once. #612 moved demo-data-coverage to 101. #615 moved manifest-l10n-coverage to 101. Both diffs were correct in isolation; together they relocated the collision from 99 to 101 and left gate-99 orphaned. Nothing noticed until the same acceptance suite failed again with the number changed: FAIL — expected FAIL on planted, got: [gate-101] manifest-l10n-coverage: NOT APPLICABLE ... demo-data-coverage takes 102. manifest-l10n-coverage keeps 101, because #615 says so in its title and its own suite keys on it; mine is the newer claim in conflict, so mine moves. gate-99 stays vacant rather than being reused: its history now points at two different gates, and a number that has meant two things is worse than a gap. 🔴 MY OWN VERIFICATION MISSED IT. After the first renumbering I checked the `# GATE N — name` COMMENT headers and confirmed "no duplicates". The headers were distinct (99 and 101) while both gates called `_pass 101`. I verified the labels and not the behaviour. So the new check reads the CALL SITES, which are what the report, the COVERAGE tally and every acceptance suite actually key on. There is a gate-95 for ADR number collisions and nothing for GATE numbers; this is that check. Five arms, and ARM 2 reconstructs the exact state that reached main and asserts both gate names appear in the error. ARM 3 plants distinct headers over colliding calls — the precise thing my manual check waved through. ARM 4 pins that a runner with no gates exits 2, because a silent zero is how this class hides. 81 gates, every number claimed once. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
gate-56 (
register-handler-resolution) failed on shillinq with 6 unresolved handler references, every one inside the generated mock descriptor.Control: 6 findings with the mock in the gate's file list, 0 without. The app's real descriptor is clean.
The references were stale, and this generator wrote them
OCA\Shillinq\Service\ConsolidationGuardOCA\Shillinq\Consolidation\ConsolidationGuardDefinitions are merged across every component file, so a lifecycle block from a non-canonical file is unioned into the copy alongside the canonical one — and the merge cannot tell which of two same-named schemas holds the reference that still resolves.
Why strip rather than teach the merge to choose
A mock descriptor exists so demo objects have a register to live in and a schema to be validated against. Carrying the state machine across makes data assert things about code, which is what broke.
x-openregister-lifecycleis not a JSON Schema keyword, so object validation is unaffected —--checkstill passes 499 of 499 schemas on shillinq.classis deliberately not stripped: it is a plausible name for an ordinary schema property, and removing it would silently change the shape the demo objects are validated against.Measured across all seven apps carrying demo data: gate-56 clean everywhere, and the already-merged apps are clean as committed, so they need no follow-up.