Skip to content

fix(demo-data): the example data no longer creates apps that cannot be opened - #853

Merged
rubenvdlinde merged 3 commits into
developmentfrom
fix/demo-data-excludes-control-plane
Sep 18, 2026
Merged

rubenvdlinde merged 3 commits into
developmentfrom
fix/demo-data-excludes-control-plane

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Found while recording a demo. The setup walkthrough's "Load example data" step imported three objects for every schema in the buildiq register, and that register holds the schemas buildiq keeps about itself. The import therefore created:

  • three built-app rows ("Voorbeeld Name 1..3") that appear in the Apps list and on the dashboard and cannot be opened. Their applicationVersion rows carry application: "00000000-0000-4000-8000-000000000000", the placeholder a format: uuid property gets, and a manifest with zero pages, so the detail page renders empty and /apps/buildiq/builder/<slug>/ never loads.
  • three application-template rows standing in the Store beside the four real built-in templates. Cloning one produces an equally empty app.
  • three export-job rows reading as finished exports nobody ran.

Measured on the demo instance on 2026-09-18 before they were deleted by hand: app c5a2e155-432b-4ea8-a965-56661b991463, slug ccdc, status archived, opened to an empty detail page, and its version had manifest.pages: [].

Why nothing caught it

Every one of those objects satisfies its schema, so the generator was right and --check was green. Conformance is about an object's shape. Nothing in a schema says whether its rows are content somebody authors or bookkeeping the app writes, so no check could tell the difference and ADR-111 rule 1 asked for three of each.

What changed

Eight schemas now declare x-openregister-demo-data with the reason, and the generator honours it. The marker is added in ConductionNL/.github#781.

Schema Why it carries no demo data
built-app buildiq's own register of the apps it has built
applicationVersion written by publish and promote
built-app-route an index the Application lifecycle upserts on publish and deletes on archive
application-template the store's starter packs, whose manifest has to be a working app
export-job written by the export pipeline
rule-execution-log written by the rule engine, and a GDPR article 22 explainability record
agent-run written by AgentRunLogger
formDraft one person's half-filled form, owner-only, so a generated draft belongs to a user who does not exist

The first five are the ones the generator actually wrote objects for. The last three generate nothing today, because the register lists its schemas by slug while their definitions are keyed by name, so the resolver never pairs them. They are marked anyway: that resolution is a plausible thing to fix, and the day it is fixed is the day this bug comes back.

Kept on purpose. hello-message keeps its three objects: it is the app's content schema and the reason the dataset exists. So do the schemas an administrator authors by hand, automation, buildAgent, component-block, pageLayout, registrationForm and the rule-set family. Their rows are content, not bookkeeping. They do reference an Application that the demo data no longer creates, but a dangling reference in generated data is a different defect, and the fix for it is reference-aware generation rather than exclusion.

The dataset goes from 18 objects to 3.

The import now defends itself

The dataset is generated, so it can regress without anybody editing it. A generator run against a schema that has lost its declaration writes the control-plane objects straight back, and they import in silence: they satisfy their schemas, OpenRegister has no opinion about what they mean, and the wizard reports a cheerful count over three apps that cannot be opened.

DemoDataService::install() now reads the x-openregister-demo-data declarations from the app's own descriptors, the same file the generator reads, and refuses before anything is written when the dataset addresses one of them. It throws rather than filtering, because half a dataset is not what the operator asked for, and SetupController already surfaces the message.

How this was verified

Mutation first, because a guard that cannot fail reports the same green as one that passed.

  • DemoDatasetExcludesControlPlaneTest against the dataset shipped before this change: red, naming all five schemas and three objects each.
    -Array &0 []
    +Array &0 ['built-app' => 3, 'application-template' => 3, 'applicationVersion' => 3,
    +           'built-app-route' => 3, 'export-job' => 3]
    
  • With the refusal removed from install(): two of the three service arms red, and the control arm (an ordinary content schema still imports) green, which is what separates "the guard works" from "the guard refuses everything".
  • The generator's own --check on the old dataset with the new marker: exit 1, five FAIL lines. On the new dataset: exit 0, five SKIP lines that each say why.

Local, on this branch, composer check:strict once and npm run lint once:

Check Exit Findings in this diff's files
npm run lint 0 none
test:all 0 1193 tests, 3756 assertions, including the 16 demo-data tests
phpcs 2 none. Errors in 7 files, all inherited: AppInfo/Application.php, Integration/PageLayoutLeafProvider.php, Integration/RegistrationFormLeafProvider.php, Listener/BuildiqLeafRegistrationListener.php, Service/LayoutDeltaService.php, Service/PageLayoutValidator.php, Service/RegistrationFormValidator.php
psalm 2 none. PageLayoutLeafProvider, RegistrationFormLeafProvider, BuildiqLeafRegistrationListener
phpstan 1 none. 13 errors, all in PageLayoutLeafProvider and RegistrationFormLeafProvider
phpmd 2 none, on a cold cache. See below
gates 2 gate-60 and gate-101, below
composer check:strict 1 the sum of the above

⚠️ phpmd reported three findings on DemoDataService that do not exist. ~/.pdepend is shared by every checkout on this machine, and it served the pre-0dbf2fd5 analysis of the file: complexity 15 and NPath 1188, the exact numbers from before the split. Running the same command with a private HOME, so pdepend builds a cold cache, returns zero findings on the file and 20 inherited ones across LayoutDeltaService, PageLayoutLeafProvider, RegistrationFormLeafProvider, PageLayoutValidator and RegistrationFormValidator. phpcs, phpmd and phpmd-unusedparams each exit 0 against the file directly.

gate-60 icon-vocabulary fails on src/icons.js: ContentSaveEditOutline and FormSelect are used by a manifest and not registered. Inherited. Both names sit in lib/Settings/register.d/50-registration-forms.json on development already, and src/icons.js is not in this diff. It belongs to the CI repair lane that owns that file.

gate-101 demo-data-coverage fails locally, and it will fail on this PR's CI too, until ConductionNL/.github#781 merges. The gate runs the generator from conduction/hydra-gates, and the version on main does not know the marker yet, so it asks buildiq for the three built-app rows this PR removes. Verified by running the patched generator by hand with the gate's own arguments (--check --only-changed over this diff's files): exit 0, five SKIP lines. Merge #781 first.

development is red on 10 jobs from other work, so the comparison above is against that branch rather than against green.

What an existing instance still needs

Nothing in this PR touches data that is already there. An instance where somebody ran the demo import keeps its fifteen rows until a person deletes them, and this change only stops the next import creating more. They are:

built-app             built-app-voorbeeld-name-1-1 .. -3-3
applicationVersion    applicationversion-voorbeeld-name-1-1 .. -3-3
built-app-route       built-app-route-builtapproute-1-1 .. -3-3
application-template  application-template-voorbeeld-title-1-1 .. -3-3
export-job            export-job-exportjob-1-1 .. -3-3

They were deleted by hand on the demo instance on 2026-09-18.

A repair step should not clean them up, and this PR does not add one. ADR-111 rule 3 keeps demo data out of Repair in one direction, and the same reasoning holds in the other: a Repair step runs on upgrade, unasked, on instances nobody is watching. The rows it would have to delete are built-app objects, which ARE buildiq's app registry, and the only thing separating a demo app from a real one is a slug pattern somebody could have typed. A heuristic delete that is wrong once has removed an app and its version history. Nothing in openspec/changes/openbuild-first-time-setup/ asks for a removal path, so there is no spec to implement against either.

The honest repair is the one an operator performs deliberately, after looking at the list. If that turns out to be common, the thing to build is an "uninstall example data" action on the walkthrough step that removes exactly the slugs the shipped dataset declares, as a decision the operator makes rather than one an upgrade makes for them. That needs a spec first.

🤖 Generated with Claude Code

The setup walkthrough's example data imported three objects for every schema
in the buildiq register. That register holds the schemas buildiq keeps about
itself, so the import created three apps that appear in the Apps list and on
the dashboard and cannot be opened: their applicationVersion rows carry
application: 00000000-0000-4000-8000-000000000000, the placeholder a
format: uuid property gets, and a manifest with no pages, so the detail page
renders empty and /apps/buildiq/builder/<slug>/ never resolves. Three more
stood in the template store beside the four real built-ins and would clone into
an equally empty app, and three export-job rows read as finished exports nobody
ran. Seen on the demo instance on 2026-09-18, app
c5a2e155-432b-4ea8-a965-56661b991463, slug ccdc.

Every one of those objects satisfies its schema, which is why the generator and
its --check were both green. Conformance is about an object's shape; nothing in
a schema says whether its rows are content somebody authors or bookkeeping the
app writes. Eight schemas now say so with x-openregister-demo-data, the marker
ConductionNL/.github#781 adds:

  built-app             the register of apps buildiq has built
  applicationVersion    written by publish and promote
  built-app-route       an index the Application lifecycle maintains
  application-template  the store's starter packs
  export-job            written by the export pipeline
  rule-execution-log    written by the rule engine, a GDPR art. 22 record
  agent-run             written by AgentRunLogger
  formDraft             one person's half-filled form, owner-only

hello-message keeps its three objects: it is the app's content schema and the
reason the dataset exists. So do the schemas an administrator authors by hand
(automation, buildAgent, component-block, pageLayout, registrationForm and the
rule-set family): their rows are content, not bookkeeping, and none of them
generates today anyway.

Regenerated with the patched generator. The dataset goes from 18 objects to 3,
and --check exits 0 with five SKIP lines that each say why.
…ookkeeping

The dataset is generated, so it can regress without anybody editing it: a
generator run against a schema that has lost its declaration writes the
control-plane objects straight back. They then import in silence, because they
satisfy their schemas, OpenRegister has no opinion about what they mean, and
the wizard reports a cheerful count over three apps that cannot be opened.

DemoDataService now reads the x-openregister-demo-data declarations from the
app's own descriptors, the same file the generator reads, and refuses the
import before anything is written when the dataset addresses one of them. It
throws rather than filtering: half a dataset is not what the operator asked
for, and SetupController already surfaces the message.

Three arms cover it, plus a guard on the shipped file:

- DemoDataServiceTest: the refusal, that the importer is never called, and the
  control that an ordinary content schema still imports.
- DemoDatasetExcludesControlPlaneTest: the shipped dataset carries no object
  for a declared schema, the five control-plane schemas still declare it with a
  reason, and hello-message still carries its three objects.

Verified by mutation. Against the dataset shipped before this change the guard
test reddens naming all five schemas and three objects each; with the refusal
removed from install(), two of the three service arms redden and the control
stays green.
… clean

The first version put path discovery, the mock filter and the per-schema
decision in one method: cyclomatic complexity 15 against a threshold of 10, an
NPath of 1188 against 200, and five phpcs errors from the `?:` on glob and an
unnamed argument. All NEW findings on lines this change wrote, so they are this
PR's to fix.

No behaviour change. phpcs, phpmd and phpmd-unusedparams all exit 0 on the
file, and the sixteen demo-data tests still pass.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/buildiq @ 7743de6

Check PHP Vue Security License Tests
lint ✅
phpcs ❌
phpmd ❌
psalm ❌
phpstan ❌
phpmetrics ✅
eslint ✅
stylelint ✅
build ✅
check-manifest ✅
test-l10n ✅
check-gitignore ✅
check-nc-floor ✅
format ❌
check-l10n-js ✅
check-schema-l10n ❌
composer ✅ ✅ 106/106
npm ✅ ✅ 752/752
app:check-code ⏭️
info.xml ✅
REUSE ❌
lockfile sync ✅
PHPUnit ✅
Newman ❌
Playwright ⏭️ deferred: E2E runs locally and on the promotion path only. This pull request targets development, so the suite is asked once per promotion into beta and main rather than once per push per open pull request. Run it on any branch from the Actions tab, or locally with npx playwright test.
Hydra gates ❌

Quality workflow — 2026-09-18 10:22 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde
rubenvdlinde merged commit 699ebae into development Sep 18, 2026
36 of 45 checks passed
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