Release: merge development into beta - #476
Merged
Merged
Conversation
Bumps phpstan/phpstan to ^2.0 and conduction/hydra-gates to ^1.8.2 (which carries the shared phpstan-base.neon fixes for `treatPhpDocTypesAsCertain` and PHPMD's `@SuppressWarnings` phpDoc.parseError), then removes the code PHPStan 2 correctly identifies as unreachable. `phpstan analyse` goes from 11 errors to 0; 880 PHPUnit tests stay green and phpcs reports 0 errors. Dead `method_exists()` back-compat probes (5) --------------------------------------------- `runAsSystem()`, `lockObject()` and `unlockObject()` are all declared on `OCA\OpenRegister\Contract\ObjectServiceInterface` — verified against both the canonical `openregister@development` contract and the copy vendored by hydra-gates v1.8.2, which is what static analysis resolves here. The probes were written for back-compat with an OpenRegister release that predates the elevation/locking API. That release cannot run this code at all: `openregister@main` ships no `Contract` namespace whatsoever, so the constructors that type-hint `ObjectServiceInterface` cannot have their dependencies resolved there. Wherever this code executes, the contract is the one that declares all three methods, and every fallback branch behind the probes was unreachable. Removing them cascades in MigrateToVersionedModel: `$hasSystemContext` was always true, so `ROW_BLOCKED` was never returned and `STATE_BLOCKED` was never written. Both constants and the retry prose go with them. This is backwards compatible — a `blocked` value persisted by an older install is still not `STATE_DONE`, so the step retries exactly as before. Other always-true conditions (6) -------------------------------- - AbstractToolHandler: the lock is acquired in a *separate* try/catch that throws on failure, so the later `finally` can only run with the lock held; the `$locked` flag could never be false and is now gone. - AppChannelApplier / ApplicationVersionService: `is_array()` on findAll()'s return and `is_object()` on saveObject()'s ObjectEntityInterface. - AgentsController / ManifestResolverService: `array_values()` on arrays only ever appended to with `[]=` (and usort()ed in place) — no-ops. - SettingsService: `registry_url` is one of CONFIG_KEYS and is always set by the loop above, so the `?? ''` was unreachable. Every removal is annotated in place so the next reader does not reintroduce the guard.
Bumps [diff](https://github.com/kpdecker/jsdiff) from 5.2.2 to 9.0.0. - [Changelog](https://github.com/kpdecker/jsdiff/blob/master/release-notes.md) - [Commits](kpdecker/jsdiff@v5.2.2...v9.0.0) --- updated-dependencies: - dependency-name: diff dependency-version: 9.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com>
Bumps [jsdom](https://github.com/jsdom/jsdom) from 24.1.3 to 30.0.1. - [Release notes](https://github.com/jsdom/jsdom/releases) - [Commits](jsdom/jsdom@v24.1.3...v30.0.1) --- updated-dependencies: - dependency-name: jsdom dependency-version: 30.0.1 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com>
chore(quality): migrate to PHPStan 2 and clear the 11 residual errors
…elopment/jsdom-30.0.1 build(deps-dev): bump jsdom from 24.1.3 to 30.0.1
…elopment/diff-9.0.0 build(deps): bump diff from 5.2.2 to 9.0.0
…al copy Verbatim copy of ConductionNL/.github@main quality-config/coverage-guard.php. The shipped 433-line copy predates --deletion-neutral, so deleting well-tested dead code reads as a coverage drop: on decidesk the same class of change gave 1782/2324 -> 1763/2305 statements, i.e. 19 deleted and all 19 covered, and the guard failed it as -0.19%. Nothing was less tested. All nine PHP apps ship this identical stale copy with no local edits, so the refresh is wholesale rather than patched. The ratchet is NOT disabled or baselined — a real coverage loss still fails.
…on-neutral chore(ci): refresh coverage-guard.php to the canonical deletion-neutral copy
…t mocks
The library upgrade is trivial -- openbuild uses exactly two of its exports,
`generateUrl` and `imagePath`, and v3 still exports both. Unlike @nextcloud/vue
v9 or @nextcloud/dialogs v7, v3 keeps `main` plus BOTH `import` and `require`
conditions, so there is no resolution work either.
What blocked this PR was 33 failing test files, and they were not the library's
fault.
Error: [vitest] No "imagePath" export is defined on the
"@nextcloud/router" mock. Did you forget to return it from "vi.mock"?
57 spec files mock @nextcloud/router, and most declared only what they
personally cared about:
vi.mock('@nextcloud/router', () => ({ generateUrl: (p) => p }))
But a `vi.mock` factory replaces the module for the WHOLE module graph of that
test, not just for the spec's own imports. Any component reachable from the test
that imports `imagePath` gets a module where it does not exist. src has been
importing `imagePath` all along -- so these mocks have been incomplete since they
were written.
Why it only surfaces now: v2 was resolved as CJS, and vitest models a mocked CJS
module as a proxy that answers `undefined` for anything the factory omitted.
v3 declares `"type": "module"` and is resolved as ESM, where a missing named
export is an error rather than an undefined. The upgrade did not break the
mocks; it stopped hiding that they were broken.
Fixed with `importOriginal` rather than by adding `imagePath`
-------------------------------------------------------------
Every one of the 33 now spreads the real module and overrides only what it
means to stub:
vi.mock('@nextcloud/router', async (importOriginal) => ({
...(await importOriginal()),
generateUrl: (p) => p,
}))
Adding `imagePath: () => ...` to each would have fixed today's failure and left
the same trap for the next export any component starts using. Spreading the
original cannot rot: the stub stays deliberate, everything else stays real.
Files that stub more than one function (ThemePickerDialog, the two
`{slug}`-expanding GitHub modals) keep their own overrides on top of the spread.
Measured, not assumed
---------------------
The test COUNT is the part worth reading, not the pass/fail:
before (router 3, old mocks): 33 files failed, 1125 tests ran
after: 141 files passed, 1378 tests ran
Those 33 files errored during collection, so their 253 tests never executed at
all. A file that fails to load reports as one red file, not 253 missing tests --
the count is the only place the difference is visible.
Verified with the commands CI runs
----------------------------------
npm ci rc=0
npm run build rc=0, 0 errors (3 pre-existing size warnings)
npx vitest run 141/141 files, 1378/1378 tests
npm run lint rc=0
npm run stylelint rc=0
npx prettier --check rc=0
check:manifest, check:gitignore, check:nc-floor, test:l10n all rc=0
One caveat, stated rather than smoothed over: immediately after the cold
`npm ci` reinstall, one run reported `1 failed | 1377 passed`. Four subsequent
full runs were clean (1378/1378), and I did not capture which test it was before
the log was overwritten, so I cannot attribute it. It is not a mock failure --
those fail at collection and take a whole file with them, which this did not. I
am flagging it as an unidentified one-off rather than calling the suite
deterministic on four green runs.
Closes #246
chore(deps): @nextcloud/router 2 -> 3, and complete 33 incomplete test mocks
#297) Dependabot opened #244 to bump @nextcloud/files 3.12.2 -> 4.0.0. The right answer is not a bump: openbuild does not use this package. grep -rn "@nextcloud/files" --include=*.js --include=*.vue \ --include=*.ts --include=*.json . # minus node_modules and the lockfile package.json:48: "@nextcloud/files": "^3.12.2" One hit, and it is the declaration itself. No import in src/, no import in tests/, no webpack external, no vitest alias. It is still in the tree, just not as ours ----------------------------------------- Two dependencies genuinely need it, and they disagree about the major: @conduction/nextcloud-vue@2.8.2 -> @nextcloud/files@3.12.2 @nextcloud/dialogs@7.4.1 -> @nextcloud/files@4.0.0 npm resolves both, nested, and it keeps doing so after this change -- verified by `npm ls @nextcloud/files` before and after. Removing OUR declaration removes a claim we were not making good on; it does not remove the package. That disagreement is also why bumping is the wrong move rather than merely an unnecessary one. Declaring ^4.0.0 would hoist v4 to the top level while @conduction/nextcloud-vue still expects v3, which is how an app ends up shipping a library a consumer was not built against -- the same class of failure the webpack config already documents at length for @vueuse/core. Declaring nothing lets each consumer keep the major it was compiled for. Verified with the commands CI runs ---------------------------------- npm ci rc=0 npm run build rc=0, 0 errors npx vitest run 141/141 files, 1378/1378 tests npm run lint rc=0 npm run stylelint rc=0 npm run check:manifest rc=0 1378 is the same total this repo reports on development, so nothing stopped being collected -- worth checking explicitly, because a module that fails to resolve takes its whole spec file out of the run and shows up as one red file rather than as missing tests. Closes #244 Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
… 6, coverage-v8 4) (#298) Combines dependabot #284 (vitest) and #288 (@vitest/coverage-v8). They cannot land separately: vitest 4.1.11 peers `@vitest/coverage-v8: 4.1.11` EXACTLY, so either PR alone produces a tree `npm ci` refuses. The peer chain drags in two more packages, so this is a four-package move: vitest ^1.6.1 -> ^4.1.11 @vitest/coverage-v8 ^1.6.1 -> ^4.1.11 (exact-pinned to vitest) vite ^5.4.0 -> ^7.3.6 (vitest 4 peers ^6 || ^7 || ^8) @vitejs/plugin-vue ^5.2.1 -> ^6.0.8 (needed for vite 7) vite 7, not vite 8, deliberately. vite 8.2.2 is the current latest, but this repo's `.npmrc` sets `min-release-age=2` — a supply-chain cooldown that makes the lockfile lag newly published versions ON PURPOSE. Pulling a package inside its cooldown window is how you get a lock that resolves differently in CI than locally. vite 7.3.6 is well outside it and satisfies vitest 4's peer range. Five tests failed, all the same latent race ------------------------------------------- Three in ManifestDiff.spec.js, one in PageDesignerHost.spec.js, one in ApplicationDetailHeader.spec.js. None is a vitest bug and none needed a production change — every one is a test seeding component state while that component's own mounted-hook fetch is still in flight. ManifestDiff is the clearest. The test mounts, then immediately does: await wrapper.setData({ fromBlob: sampleFrom, toBlob: sampleTo }) with a comment claiming this "skips the async fetch". It does not skip it, it RACES it. The mocked axios resolves `{from: null, to: null}`, and whichever settles second wins. Under vitest 1 setData won; under vitest 4 the fetch does, nulls `fromBlob` straight back out, and `diffParts` computes over two empty strings and returns []. Verified directly rather than guessed — a probe printed `fromBlob: null` immediately after an awaited setData, while `diffLines` itself was confirmed to be a working function returning correct hunks. PageDesignerHost is the same shape wearing different clothes: it arms `mockRejectedValueOnce` before the mount-time `load()` has settled, so the mount's own request eats the rejection and the explicit `load()` succeeds. ApplicationDetailHeader assigns `wrapper.vm.versions` while the mounted hook is still fetching and assigning that same field. Fixed by letting the mount settle first (`await flushPromises()` / `await flush(wrapper)`) before seeding, with a comment at each site saying what the ordering depends on. These tests were correct-by-accident for three major versions. before: 3 files failed, 5 tests failed, 1373 passed after: 141 files passed, 1378 tests passed The coverage baseline is recalibrated, and that number needs reading carefully ------------------------------------------------------------------------------- tests/.coverage-baseline.json vitest: 85.18 -> 64.72. This is NOT 20 points of lost coverage. The suite is identical — 1378 tests before and after. The instrument changed: coverage-v8 v1: src/App.vue = 235 lines (exactly its `wc -l`) coverage-v8 v4: src/App.vue = 24 lines across src/**: 59,333 -> 10,097 lines, over the same ~200 files v1 treated every physical line as coverable — blank lines, comments and Vue template markup included, nearly all of which score covered for free. v4 remaps through `ast-v8-to-istanbul` to executable lines only. 64.72% of real statements is a stricter bar than 89.63% of a file's line count, so the ratchet is being recalibrated, not relaxed. The baseline file carries a `_note` explaining this so nobody "restores" 85.18 — under v4 it is unreachable and meaningless. Flagging explicitly for review: this repo does NOT currently run the vitest ratchet in CI. `frontend-checks` is `["check:manifest", "test:l10n", "check:gitignore", "check:nc-floor", "format"]`, and `enable-coverage-guard: true` refers to the separate PHP clover guard (`.coverage-baseline` = 57.39). So no CI gate changes either way here — but leaving 85.18 in place would make the ratchet fail instantly the day someone wires it up, which is why it is corrected rather than left alone. Verified with the commands CI runs ---------------------------------- npm ci rc=0 npx vitest run 141/141 files, 1378/1378 tests npm run test:coverage rc=0 npm run test:coverage-ratchet rc=0 (holds at floor) npm run build rc=0 npm run lint rc=0 npm run stylelint rc=0 npx prettier --check rc=0 Closes #284 Closes #288 Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
hydra-gates v1.8.2 -> v1.8.2 nc-vue 2.8.2 -> 2.9.2 Lock-only: both packages are already declared with caret ranges that permit these versions, so nothing about what this app ACCEPTS changes - only what it currently resolves to. Opened by the weekly fleet shared-dependency bump, because a lock nobody re-resolves is a pin nobody chose. Merging is gated by this repository's own suite, deliberately: taking hydra-gates v1.8.1 added patchObject() to a published interface, which is a load-time fatal for any concrete double that implements it without the method. CI is the only thing that can tell a safe bump from that. Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
hydra-gates v1.8.2 -> v1.8.2 nc-vue 2.9.2 -> 2.10.1 Lock-only: both packages are already declared with caret ranges that permit these versions, so nothing about what this app ACCEPTS changes - only what it currently resolves to. Opened by the weekly fleet shared-dependency bump, because a lock nobody re-resolves is a pin nobody chose. Merging is gated by this repository's own suite, deliberately: taking hydra-gates v1.8.1 added patchObject() to a published interface, which is a load-time fatal for any concrete double that implements it without the method. CI is the only thing that can tell a safe bump from that. Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…a claimed prefix (#306) conduction/hydra-gates claims `OCA\OpenRegister\Contract\` as a RUNTIME psr-4 prefix, so every consumer gets these interfaces implicitly. That prefix is LONGER than openregister's own `OCA\OpenRegister\` -> `lib/`, and PSR-4 is longest-prefix-wins, so whichever app's autoloader registers first defines OpenRegister's contract for the whole process (ConductionNL/.github#531). This adds the order-independent opt-in so the prefix can be dropped from hydra-gates. It asks whether each interface is RESOLVABLE rather than who registered first — the distinction matters, because appending a fallback autoloader does NOT work: spl_autoload_register appends relative to registration order, and registration order across independently loaded apps is exactly what nobody controls. Both interfaces, and BEFORE tests/stubs/openregister-stubs.php, because that file declares class ObjectEntity ... implements \OCA\OpenRegister\Contract\ObjectEntityInterface so the interface must exist by then or PHP fatals inside the bootstrap rather than failing a test. MEASURED both directions on this app, with the prefix removed from the vendored package's entry in vendor/composer/installed.json (editing the vendored composer.json does nothing — Composer reads installed.json): prefix PRESENT (today) OK (839 tests, 2704 assertions) — guard no-ops prefix REMOVED (after #531) OK (839 tests, 2704 assertions) — guard supplies prefix REMOVED, no guard Error in bootstrap script: Interface "OCA\OpenRegister\Contract\ObjectEntityInterface" not found Safe to land now: while hydra-gates still declares the prefix this is a no-op. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
hydra-gates v1.8.2 -> v1.9.0 nc-vue 2.10.1 -> 2.11.1 Lock-only: both packages are already declared with caret ranges that permit these versions, so nothing about what this app ACCEPTS changes - only what it currently resolves to. Opened by the weekly fleet shared-dependency bump, because a lock nobody re-resolves is a pin nobody chose. Merging is gated by this repository's own suite, deliberately: taking hydra-gates v1.8.1 added patchObject() to a published interface, which is a load-time fatal for any concrete double that implements it without the method. CI is the only thing that can tell a safe bump from that. Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…#329) The drift sweep found buildiq red on two docudesk-template specs, and the seed log had already said why — phrased as success: [globalSetup] docudesk template "Bevestigingsbrief" create returned 500 [globalSetup] docudesk template "Besluit" create returned 500 [globalSetup] docudesk templates ready: Bevestigingsbrief, Besluit (already present) Nothing was created and nothing was present. The summary read `created.length ? '(created …)' : '(already present)'`, and `created` is empty in BOTH the nothing-to-do case and the everything-failed case, so the message picked the innocent reading of the ambiguity. The cost is not the wrong word. It is that the failure then resurfaces 30 minutes later as Test timeout of 30000ms exceeded. waiting for getByRole('option').filter({ hasText: /Bevestigingsbrief/i }) — a Playwright timeout on an empty picker, which reads as a flaky UI test and sent me looking at nc-vue, at openregister's register-scope change, and at the sibling app's branch before I re-read the seeding log I already had. Now a failed create is tracked and the summary says NOT ready, names the failures with their status codes, and says where to look. A run that seeded nothing can no longer announce that it seeded everything. The 500 itself is a separate, live defect in docudesk's template-create path against the current openregister development — reported on ConductionNL/openregister#2774. This change is only about the instrument. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
…r resolves (#328) Two of the four app templates declared page types that exist in neither the manifest v2 page-type enum nor CnPageRenderer's pageTypes registry: - employee-onboarding: OnboardingTaskChecklist type 'checklist' - permit-tracker: ApplicationKanban type 'kanban' CnPageRenderer looks the type up in the registry and, on a miss, logs a console warning and renders NOTHING. An app scaffolded from either template therefore shipped a blank page — and the manifest also fails validation against the canonical v2 schema, so gate-22 would reject it. Both pages list objects grouped by status, so both become 'index' over their schema. A genuine board/checklist page type would have to be implemented in nc-vue and added to the registry and the schema enum first; config.groupBy is left in place for whenever that lands. Adds a guard over every shipped template so a page type cannot drift from the renderer again. Refs #327 Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
…ender (#332) * fix(l10n): ship the browser catalogue, so the translations actually render This app has a complete Dutch catalogue that no user has ever seen. Nextcloud reads `l10n/<locale>.json` server-side for PHP `$l->t()`, but the browser only ever gets `l10n/<locale>.js` — the `OC.L10N.register()` file. Raw JSON is not served out of an app directory at all: GET /custom_apps/<app>/l10n/nl.json -> 404 (measured) With no `.js` half, `t('<app>', …)` has nothing registered, so it returns the key unchanged. Every string in the interface renders in English no matter what language the user picked, while every server-rendered string is translated. Nothing errors, nothing logs, and a catalogue check that only reads the JSON reports full coverage. Three parts: - `scripts/build-l10n-js.js` GENERATES the .js from the .json, so the pair cannot drift. It reads the app id from appinfo/info.xml rather than hardcoding it — a catalogue registered under a stale id after a rename is silently ignored, which is the same failure one level down. - `pluralForm` added to both catalogues. Core's shape is {translations, pluralForm}; without it plural strings fall back. - `check:l10n-js` in CI fails when the committed .js is stale and names the command that regenerates it. Verified must-fail: mutate one value in the JSON and it exits 1 naming the file. Generated, never hand-edited: run `npm run l10n:build` after touching a catalogue. * fix(l10n): generate EVERY locale, not just en/nl The first commit generated `en.js` and `nl.js`, which is what humaniq needed. This app ships far more than two catalogues, and all of the others were in the same position: present as JSON, absent as JS, therefore unreachable. larpinq 37 locale catalogues, .js for 0 of them keepiq .js present for all 37 — and STALE across the board: 595 keys and 17 corrected translations never reached a browser The generator now discovers locales from `l10n/*.json` instead of a hardcoded pair, so adding a language is a JSON file and nothing else. `pluralForm` is taken from the catalogue when it declares one. When it does not, the fallback is the two-form rule `nplurals=2; plural=(n != 1);` — which is what every generated catalogue in this fleet already carries, including for languages that genuinely have more forms (cs, pl, ru). That is a known simplification rather than a verified per-language rule, and it is documented as such in the script: a catalogue that starts using plural strings in one of those languages needs its real rule in the JSON, which the generator honours. Verified non-destructive: across keepiq's 37 regenerated catalogues, 0 keys lost, 595 added, 17 values corrected. Also formatted the script with this repo's prettier config. --------- Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
* feat: rename the app id from openbuild to buildiq
Moves the app id, PHP namespace (OCA\OpenBuild -> OCA\Buildiq), display name,
composer/package names, l10n domain, routes, bundle names and occ prefix.
29 files renamed with git mv; both lockfiles refreshed so npm ci and composer
validate.
FROZEN, each because renaming it fails SILENTLY rather than loudly:
- OpenRegister register slug 'openbuild', and every slug DERIVED from it:
`openbuild-${slug}`, 'openbuild-' . $slug, openbuild-app-staging,
openbuild-preview-*. These address existing registers; a renamed slug
creates a fresh EMPTY one and orphans every stored object.
- GitHub repository topic 'openbuild-app'. Repositories out on GitHub carry
that tag. Renaming the search term makes the template gallery return
NOTHING, with no error.
- openbuildEditable — a property name the ADR-024 manifest schema declares.
Renamed, it becomes an "additional property" and the manifest stops
validating.
- openbuild-app.json, the descriptor filename inside template repos.
- The DISCOVERY_TOPIC / entry-id / object-type / store-id / catch-all-route
constants ('openbuild-app', 'openbuild-app-', 'openbuild-application',
'openbuild-hidden', 'openbuild-schemas', 'openbuild-builder-catch-all').
- openbuild.conduction.nl (HTTP 200; buildiq.conduction.nl is 000).
- openspec/specs/openbuild-* and openspec/changes/openbuild-* and every
@SPEC path: capability ids are dereferenced by gate-46 and cited from the
frozen archive, so they cannot move.
- lib/Settings/openbuild_register.json, loaded by an explicit path.
The virtual app ids this builder MINTS (openbuild-{slug}) are the sharpest
case: credentials are registered against them, so they name existing records
even though they look like this app's own id.
Local: phpcs 0, phpstan OK, psalm clean, phpmd 0, eslint 0 errors,
1378/1378 vitest, l10n OK, l10n-js up to date, prettier 0.
* fix(spec): tag the 88 methods surfaced by gate-16, and repoint one anchor
gate-16 is diff-scoped, and a rename touches nearly every file, so the app's
whole legacy @SPEC debt landed in this PR: 88 changed methods with no anchor.
Each now cites the capability it implements. Where no requirement covers the
method, the citation is the capability spec WITHOUT an anchor -- gate-16
accepts that, and a citation to a requirement that says nothing about the
method would be worse than none.
gate-46: one anchor was left behind by the freeze rule. I deliberately froze
@SPEC PATHS (openspec/changes/openbuild-* directories cannot move), but the
sweep did rename the product name inside the spec PROSE -- so the heading
became 'Owner signal is derived from existing Buildiq primitives' while the
citation still pointed at ...-openbuild-primitives. The directory stays frozen;
only the anchor follows the heading.
Two self-inflicted problems fixed before pushing:
- The tagger inserted a docblock BETWEEN a method's PHP attributes and its
signature, so phpcs read that empty block as the method's docblock and
reported the real one's @param/@return as missing. Merged into the original.
- It also injected a docblock inside a Vue TEMPLATE expression rather than at
the method definition (fixed in the decidiq equivalent of this commit).
Local: phpcs 0, phpstan OK, psalm clean, eslint 0, 1378/1378 vitest, l10n OK,
gate-16 0, gate-46 0.
* fix: the register slug is openbuild everywhere; only the APP id is buildiq
CI's PHPUnit found the real cost of the bulk rename: the two identifiers look
alike and my sweep could not tell them apart.
register slug = openbuild (FROZEN -- it addresses existing registers)
app id = buildiq (moved)
The sweep had renamed the register in ~36 call sites -- register: 'buildiq',
searchObjectsBySlug('buildiq', ...), registerMapper->find('buildiq'),
REGISTER_SLUG constants, and positional saveObject() arguments -- while the
constants that spell it out (AgentChannelProvisioner::AGENT_REGISTER,
FlowAndAgentExportBundler::BUILDIQ_REGISTER) had stayed frozen. Production
therefore addressed a register that does not exist, and the mocks disagreed
with the calls. All reverted to openbuild.
It had also renamed the descriptor filename on ONE side: AppRepoParser looked
for buildiq-app.json while TemplateRepoSerializer still wrote openbuild-app.json.
That filename lives in template repositories out on GitHub, so both sides stay
frozen -- the parser now matches the serializer again.
Test fixtures that had been renamed the same way are reverted with the code, so
the expectations match what production actually passes:
- saveObject register arguments and filters['register']
- openbuild/built-app-route schema refs
- allowedApps: ['openbuild'] -- existing broker credentials carry that value;
a fixture claiming buildiq would misrepresent what is stored (production
only ever tests for hermiq here, so it did not drive the failure).
Deliberately still buildiq, because they ARE the app id: Capabilities' key,
NOTIFICATION_APP, getAppPath(), appDataFactory->get(), and the MCP provider id.
Local: phpcs 0, phpstan OK, psalm clean, 881/881 phpunit, eslint 0,
1378/1378 vitest, l10n OK, gate-16 0.
* fix(e2e): the seed's register check must name the frozen slug
CI failed before any test ran:
Buildiq registers missing after import: ['buildiq']
The e2e suite cannot create an application, schema, page or automation
without them; every UI spec would fail on an empty list.
ci-seed.sh verifies the imported registers by slug, and the sweep had rewritten
that expectation to 'buildiq'. The register is 'openbuild' -- src/manifest.json
still names it 13 times -- so the check manufactured an absence and aborted the
seed. Same for a page-editor spec's config.register comment.
The register SLUG stays openbuild; only the APP id moved. Noted inline so the
next reader does not 'fix' it back.
* fix: the URL segment after objects/ is the REGISTER slug, not the app id
Newman aborted in the seed:
The buildiq application collection is not readable (HTTP 404).
ci-seed.sh probes
/index.php/apps/openregister/api/objects/<REGISTER>/application, and the sweep
had rewritten that segment to buildiq. OpenRegister addresses objects by
register SLUG, which stays openbuild -- so the probe asked for a register that
does not exist and read the 404 as 'the collection is missing'.
133 occurrences across 66 files: postman raw URLs AND their path arrays (Postman
builds from the array), vitest expectations, docs and active openspec. The
archive is untouched, and CHANGELOG entries describing past behaviour keep the
URL they actually described.
Local: phpcs 0, 881/881 phpunit, 1378/1378 vitest.
* docs: same register segment in a routes.php comment
* fix(newman): the per-app register pattern is openbuild-<slug>, not buildiq-<slug>
The version-create request failed 422 'already exists'. The collection asked
for register buildiq-hello-world-newman-test-v1, but SeedHelloWorldFixture
writes ApplicationVersionService::REGISTER_SLUG . '-' . 'hello-world' --
openbuild-hello-world -- because the register slug did not move with the app id.
Four references corrected across the collections. Also fixed the fixture's own
comment, which the sweep had rewritten to claim the pattern is 'buildiq-<slug>'
while the code one line below produces openbuild-<slug>: a comment that
contradicts its code is how the next person re-breaks this.
Local: phpcs 0, 881/881 phpunit.
* fix(newman): register values inside request BODIES, not just URLs
Traced the last two failures to their source instead of guessing.
The version-create returned 422 because the request body asked for
"register": "buildiq-newman-roundtrip-production"
while ApplicationCreationService:269 mints per-app registers as
'openbuild-' . $appSlug . '-' . $versionSlug
so the body named a register that does not exist. The 'has no resolvable
manifest' warning was a CASCADE: it comes from the manifest GET later in the
same folder (and returns 404, not 422), which could only fail once the version
create had failed. Reading it as the cause would have sent me into
ManifestResolverService, which was already correct.
Also fixed the manifest page config.register values inside the same bodies --
"config": { "register": "buildiq", "schema": "hello-message" } -- which name
the SHARED data register, likewise still openbuild.
These sat in escaped JSON inside Postman "raw" bodies, which is why every
earlier sweep missed them: the URL fixes matched /objects/<slug>/ and the path
arrays, but a register named in a request BODY looks like ordinary JSON text.
Corrected the ManifestResolverService docblock, which still claimed
'register=buildiq' one line above code reading openbuild.
Local: phpcs 0, 881/881 phpunit.
* fix: stored lifecycle-guard FQCNs in the register descriptor
Newman's version-transition step logged:
Lifecycle guard tag "OCA\OpenBuild\Lifecycle\ApplicationVersionOwnerGuard"
could not be resolved: Class ... does not exist
lib/Settings/openbuild_register.json carries that guard's fully-qualified class
name in three places. It is a STORED reference OpenRegister resolves at
transition time, so the namespace rename orphaned it: the guard silently stopped
running and the transition failed.
This is the same class of defect the integriq work found with jobClass, and the
reason it matters is the failure mode: a guard that cannot be resolved does not
throw at boot or at deploy. It fails at the moment it is supposed to make an
authorisation decision — on a user-scoped delta, ApplicationVersionOwnerGuard is
what stops one user reading or rewriting another user's override.
Worth stating plainly: the register descriptor is DATA, so a grep over lib/*.php
never sees it. I found it only because Newman surfaced the resolution error in
the Nextcloud log, not because any sweep matched it.
Local: phpcs 0, 881/881 phpunit, register JSON valid.
* fix(e2e): the register segment inside an escaped REGEX
Three page-editor specs (map, roadmap, search) failed with
TimeoutError: page.waitForResponse: Timeout 20000ms exceeded
saveAndAwaitPersist() waits for the manifest write by matching
/\/api\/objects\/buildiq\/(applicationVersion|application)\/[^/]+$/
The register segment is the frozen 'openbuild', so the matcher waited 20s for a
URL that never occurs — the save itself was fine.
This one survived every earlier objects/<slug> sweep because the slashes are
BACKSLASH-ESCAPED for the regex literal: 'objects/buildiq' does not appear in
the file, only 'objects\/buildiq'. A plain-text grep for the URL shape cannot
see it.
That is the fourth distinct syntax this same value has hidden behind in this
app: a plain URL, a Postman path ARRAY, an escaped JSON request BODY, and now
an escaped regex.
* fix(e2e): the fixture's register slug constant, and the page-editor selects
Four workflow/CRUD specs failed with the fixture's own message:
at least one register must own slug "buildiq" (found 0)
tests/e2e/workflows/fixtures.ts declared
export const BUILDIQ_REGISTER_SLUG = 'buildiq'
but the register slug is frozen at 'openbuild', so every fixture that resolves a
register by that constant found nothing and the suites failed at setup — which
is why three of them failed in well under a second.
Also repointed page-editor-coverage's register <select> assertions, and the
comment block that reasons about which registers exist. That comment is worth
reading: it already says the hello-world manifest pages carry
config.register = 'openbuild', two lines after asserting the list contains
'buildiq'. The prose and the assertion had drifted apart in the sweep.
Same value, a fifth syntax: after a plain URL, a Postman path array, an escaped
JSON body and an escaped regex, here it is as an exported TypeScript constant.
* fix(e2e): register slug inside a CSS attribute selector
REQ-PEC-006 failed with the spec's own message:
the seeded app's register must be offered by the register picker
Locator: ... .locator('option[value="buildiq"]')
The register picker lists real registers, and the seeded one is 'openbuild'.
Sixth syntax for this same frozen value, after a plain URL, a Postman path
array, an escaped JSON body, an escaped regex and an exported TS constant: a CSS
ATTRIBUTE SELECTOR.
Swept every remaining 'buildiq' spelling in tests/ and confirmed the rest are
legitimately the APP id: loadState('buildiq', ...), the ['apps','buildiq',...]
URL path segments, OC_App::loadApp('buildiq'), and ExporterEndToEndTest's
assertion that an exported app must NOT depend on buildiq.
* fix(e2e): the docs host is frozen — the assertion had been renamed
'surfaces the documentation link to openbuild.conduction.nl' asserted
getByRole('link', { name: /buildiq\.conduction\.nl/i })
The docs host stays openbuild.conduction.nl (it is live; buildiq.conduction.nl
is not), and src/manifest.json correctly renders the frozen URL. The sweep had
rewritten the EXPECTATION while leaving the value frozen, so the test's own
title and its assertion contradicted each other — the title says openbuild, the
locator said buildiq.
I had written this failure off as 'depends on the live GitHub API'. That was
wrong, and the log said so plainly: 'element(s) not found' naming the locator.
Reading the assertion beats reasoning about the subject matter.
Seventh syntax for a frozen value in this app: inside a REGEX LITERAL in a
getByRole name matcher, where the dots are backslash-escaped.
---------
Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Sixth in the series. The app id moved to `buildiq` in #334; the register it addresses was still `openbuild`, together with `x-openregister.app` — which for a `type: application` configuration IS a register slug, not an attribution label (ImportHandler::autoCreateRegisterIfApplication reads `$xOpenregister['app'] ?? $appId`). MigrateRegisterSlug renames the register ROW ahead of InitializeSettings in both hooks. OpenRegister resolves a register by slug and its not-found branch CREATES a second, empty register rather than failing, so shipping a renamed slug without it would fork the register and strand every stored object, silently. The rename moves no data: an object is bound to its register by NUMERIC id. SCOPED TO THIS APP'S OWN REGISTER, and that boundary is the interesting part here. The per-generated-app registers `openbuild-{slug}[-{version}]` are deliberately NOT in the map. Those names are minted per generated app, stored on the app, resolved with `registerMapper->find('openbuild-' . $slug)`, and the credential broker holds `allowedApps` entries against the matching `openbuild-{slug}` virtual app ids — a strict in_array() that fails CLOSED. They move only in a pass that moves the minted credentials with them. Every sweep rule requires a closing quote immediately after the old name, so `'openbuild-' . $slug` was never a candidate; the boundary is enforced by construction, not by care. Two sweep rules earned their place here and are now applied fleet-wide: - Postman `path` ARRAYS (`"objects","openbuild","application"`, 25 of them). Postman builds the request from the array, not from `raw`, so rewriting only the raw URL leaves the request hitting the old one while the diff looks done. - Register constants under any name, not just REGISTER/REGISTER_SLUG. `AGENT_REGISTER` and `BUILDIQ_REGISTER` both held 'openbuild' and both were missed by a rule anchored on the exact constant name. Re-scanning the whole series on the broadened pattern found three more in integriq (SOURCE_REGISTER, SELF_REGISTER, ENDPOINT_REGISTER_SLUG). Still frozen: `allowedApps: ['openbuild']` (minted credentials carry it), `topic:openbuild-app` (a GitHub repository topic), `openbuildEditable` (declared by the ADR-024 manifest schema), the `openbuild-{slug}` virtual app ids, the docs host, and `lib/Settings/openbuild_register.json`'s FILENAME. PHPUnit 896 passed · vitest 1378 passed · PHPCS 0 errors · PHPMD clean · Psalm 0 errors · PHPStan 0 errors · gate-16 count=0 · gate-46 clean.
`OC_REGISTER` / `BUILDIQ_REGISTER_SLUG` held the old slug and drove every OpenRegister call in those specs. Same class as the PHP constants fixed earlier, found by the same broadened rule once it was applied to .ts as well as .php.
`buildiq` is three characters shorter than `openbuild`, so a generateUrl() call and a const initialiser now fit on one line and prettier wanted them there. Applied its output rather than hand-wrapping. This is the whole of `quality / Frontend Check (format)`. The six red PHPUnit cells in the same run are a different matter and are NOT caused by this branch: every one of them dies in the bootstrap with "Undefined constant Doctrine\DBAL\ArrayParameterType::BINARY", raised from server/lib/private/ AppConfig.php before a single line of app code runs, and on all three Nextcloud refs rather than one. This PR touches no composer file and no vendor directory.
CORRECTION. My previous commit said the six red PHPUnit cells were "NOT caused
by this branch". That was wrong, and I asserted it from the wrong evidence — I
compared against a development run from 14:46 and reasoned that the branch
changes no composer file. It does not, but it does change tests/bootstrap.php and
adds tests/stubs/DoctrineStubs.php, which I had not looked at. Dispatching
Code Quality on development just now settles it: all six cells pass there and
fail here, so this branch owns the failure.
WHAT IT IS. The stub declares Doctrine\DBAL\ArrayParameterType so
createMock(IDBConnection::class) can work where doctrine/dbal is not installed —
IQueryBuilder evaluates constants referencing it at parse time. The stub's own
docblock already records why class_exists() cannot protect this: at the moment
it runs, the genuine class is not yet reachable, the guard passes, and the STUB
WINS THE NAME for the rest of the process.
In a full-server leg that shadow is not harmless. Nextcloud's AppConfig reads
ArrayParameterType::BINARY while loading app versions, the stub does not declare
it, and every cell died with "Undefined constant
Doctrine\DBAL\ArrayParameterType::BINARY" — in the bootstrap, before a single
test ran and before any code this app owns. The stub was written to make mocking
possible and ended up breaking the leg that needs no mocking at all.
THE FIX is not to complete the constant list. That closes this symbol and leaves
the next one waiting for whoever adds a caller. The bootstrap now loads the stubs
only when lib/base.php is absent — where a real server exists it ships
doctrine/dbal in 3rdparty and the stub has nothing to add. BINARY is added as
well so the stub stays a faithful stand-in on the path where it IS used, rather
than a partial one that happens not to be asked yet.
The stub file's guidance ("stub a class nothing extends") was right as far as it
went; the missing half is that a class nothing extends can still be READ FROM,
and a stand-in that is complete enough for your callers is not complete enough
for someone else's.
…itor
The Doctrine fix cleared all six PHPUnit cells; what was left is the seed.
ci-seed.sh refused with "Buildiq registers missing after import: ['openbuild']"
while printing a register list that contains `buildiq`. Its required-registers
guard still named the old slug — and the note above it stated the OLD POLICY in
so many words: "The register SLUG stays 'openbuild' across the app-id rename …
Only the APP id moved." That was true while the rename was the app id alone.
This change renames the register too, and MigrateRegisterSlug renames the
existing row ahead of the import, so it is the same row answering to a new name.
The comment is replaced rather than deleted, because a stale rationale left in
place is worse than none — the next reader would have trusted it.
page-editor-coverage.spec.ts picked `openbuild` out of the register dropdown and
asserted the value round-trips. Its own long comment already explained that this
test had once been pinned to a fixture that existed only on the machine it was
written on; leaving the slug stale would have re-pinned it to a fixture that
exists nowhere.
DELIBERATELY NOT RENAMED. `openbuild_register.json` stays — the seed references
it by PATH, and a filename is not an identifier anything resolves.
`openbuild-preview-{slug}`, `openbuild-{slug}-{version}` and
`openbuild-application` are a wizard-generated appId convention and a type
constant. `openbuild-template-catalogue` and `openbuild-version-snapshots` are
spec file names. None of them is a register slug, and MigrateRegisterSlugTest's
`openbuild` occurrences are correct: they are the FROM side of the migration.
Seed script parses (bash -n) and its embedded python block still parses;
prettier clean.
…ster
`occ buildiq:seed-hello-world-fixture` failed its own schema:
Property 'register' should match pattern
'^openbuild-[a-z0-9][a-z0-9-]*[a-z0-9]$' but 'buildiq-hello-world' does not.
Two different identifiers had been sharing a spelling, and the rename separated
them. ApplicationVersionService::REGISTER_SLUG is the app's MAIN register and
correctly moved to `buildiq`. The applicationVersion `register` field names a
PER-VERSION register the creation wizard provisions, convention
`openbuild-{appSlug}-{versionSlug}` — and that one did NOT move: all five
producers still emit the `openbuild-` prefix (ApplicationsController,
ApplicationCreationService, AppRepoSerializer, GitHubAppSyncService,
UpsertSchemaHandler) and the schema pins it with a regex.
The fixture built the second from the first, so renaming the main register
dragged the per-version name with it and the value stopped matching the pattern
that guards it. This is the same shape as a validator and an executor each
holding their own copy of a grammar: here the copies agreed until one of them
was renamed.
Written out as a named constant rather than another literal. Its docblock is the
explanation, which also keeps it out of execute() — my first version put the
reasoning at the call site and pushed the method to 115 lines against phpmd's
100-line threshold, so the comment that explained the fix would have failed the
build on its own. It is now the seventh place this prefix appears; a shared
constant belongs in the change that touches those five producers, not in a
rename.
Verified the emitted value against the actual regex from the schema:
`openbuild-hello-world` matches, `buildiq-hello-world` does not — which is
exactly what CI reported. phpcs clean, phpmd exit 0.
The l10n rollout fixed a defect no existing check could see: `l10n/<locale>.js`
was missing, so `t('<app>', key)` had nothing registered and handed the key
back — the whole interface rendered English regardless of the user's language,
while every server-rendered string was translated. Nothing errored.
Six apps in the fleet ran an l10n check that passed the entire time, because
it reads the JSON — the half that was never broken. A check that validates the
SOURCE cannot see that the ARTEFACT the runtime loads does not exist, so this
test asserts from the browser instead:
1. GET l10n/<locale>.js returns 200, is an OC.L10N.register call, and names
the CURRENT app id. (Raw JSON out of an app directory is a 404, which is
what made every translation unreachable.)
2. The running app has that catalogue registered, and t() resolves a real
key through it rather than falling back to returning the key.
Must-fail verified: delete l10n/nl.js and both scenarios fail — the first on
404, the second on the missing registration.
Written to be identical in every app: the app id is read from
appinfo/info.xml at run time rather than hardcoded, so it survives a rename —
and a catalogue registered under a pre-rename id, which `t()` silently
ignores, fails scenario 1. No fixture strings either: the assertion picks a
translated key out of the app's own registered catalogue at run time, so it
does not need editing when copy changes.
The file is meant to be byte-identical in every app, so it has to satisfy the strictest formatter in the fleet. decidiq's format check objected; this is its prettier output, verified to also satisfy every other app that runs one.
… slug The seed failed a second time — 'buildiq-opencatalogi' against the same '^openbuild-…' pattern — and chasing that one call site turned up the real problem, which is not a test fixture. REGISTER_SLUG was doing TWO JOBS. On development it read 'openbuild' and served both as the app's main register slug AND as the prefix callers used to build per-version register names (`REGISTER_SLUG . '-' . $slug`). Renaming it to 'buildiq' for the first job silently changed the second, and the applicationVersion schema pins that one with "pattern": "^openbuild-[a-z0-9][a-z0-9-]*[a-z0-9]$". FOUR PRODUCTION PATHS were affected, not just the seeder: * AppOverrideService, twice — writes an ApplicationVersion on override * GitHubAppSyncService — the draft register for a synced app * MigrateToVersionedModel — a REPAIR STEP, so it would have failed on upgrade Each would have thrown "Property 'register' should match pattern … but 'buildiq-…' does not" the moment it ran. None of them is exercised by the unit suite; the seed is the only path CI walks, which is why one fixture failure was the whole visible surface of a four-site regression. Had the seed not existed, this would have shipped and surfaced on a user's upgrade. Added ApplicationVersionService::VERSION_REGISTER_PREFIX and pointed every derived site at it, including the seeder — which now ALIASES the canonical constant rather than declaring its own copy, since a second copy of this string is exactly the shape of the bug it exists to prevent. Deliberately NOT renaming the per-version registers themselves. They are real, populated registers; moving them needs a data migration and a change to that schema pattern, and neither belongs in an app-slug rename. phpcs 0 errors, phpmd exit 0, all five files parse. PHPUnit cannot run in this worktree (it needs OpenRegister on the include path); CI covers it.
…441) OpenRegister resolves a REGISTER by slug alone (RegisterMapper::find) but a SCHEMA by the PAIR (SchemaMapper::findByApplicationAndSlug). This app now passes appId: Application::APP_ID = buildiq while every schema it already owns still carries application = openbuild, so the pair matches nothing — and ImportHandler's not-found branch is not an error path, it is the "create a new one" path. The next import therefore builds a SECOND, EMPTY schema set under the new application id while every stored object stays bound to the old rows. Nothing errors; the app renders empty collections. Sibling of MigrateRegisterSlug, and neither covers the other. Registered in both <post-migration> and <install>, immediately after MigrateRegisterSlug and ahead of InitializeSettings, which triggers the import. Three properties the step keeps: - it REFUSES rather than merges when a slug already has a twin under the new application id. Two rows sharing (application, slug) means the capped lookup silently picks one and the other's objects become unreachable — a decision about data, not a migration. - a FAILED READ is not an empty result. An empty list says every move is safe; a failed read says nothing at all. There is a test for it. - it never deletes a schema and never throws — it runs under <install>, where an escaping exception aborts the install and the app never enables. Measured on a live install: 17 schemas still under openbuild, zero colliding slugs under buildiq — all 17 move. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
…anch (#443) * fix(docs): publish the documentation from development, not a stale branch This workflow triggered on `push: branches: [documentation]`. That branch exists, which is why nothing ever looked broken — but nobody updates it. Its last commit predates most of this year's work: larpinq 19 May (development is 594 commits ahead), buildiq 9 July (980 ahead). So the docs pipeline has been faithfully republishing a months-old snapshot, and reporting success every time. It is the reason larpinq.conduction.nl still says "LarpingApp" and decidiq.conduction.nl still says "Decidesk" while both repos' docs sources have said the new name since the app-id rename merged. The rename was never missed in the docs — the docs were publishing from somewhere else. Four of the twelve fleet apps (dossiq, integriq, stackiq, thematiq) already point at `development`. This brings the rest in line. Pairs with ConductionNL/.github#555, which made the reusable workflow publish to the Cloudflare Worker that actually serves the host — previously it wrote gh-pages and nothing read it. Both halves are needed: one fixes WHERE the docs go, this one fixes WHICH COMMIT they come from. * fix(docs): list both docs hosts, and name the worker that serves them The trigger fix in this PR makes the docs workflow run. This commit makes the run reach the edge without taking the new host down on the way. `docs-hosts` was MISSING. It defaults to `cname` alone, and wrangler reconciles the worker's triggers against whatever it is given rather than appending — so the first successful deploy would have REMOVED the new .conduction.nl hostname from the worker and taken it offline. It was attached as a second custom domain on 2026-08-23 and answers 200 today. A deploy is not the moment to discover that. `worker-name` was never passed either, so the callee derived it from `cname`. Today that derivation happens to land on the right worker, which is precisely why it is now pinned: the moment `cname` moves to the new subdomain the derived name silently becomes a worker that does not exist, and the deploy forks off a second one while both custom domains keep routing to the original — green, and reaching nobody. Measured today: the live host still serves the pre-rename title, which is the staleness this PR chain exists to end. * fix(docs): ship the og:image file the config already names The docs build FAILS, and has been failing — it was simply never run, because the workflow triggered on a branch nobody updates. Making the trigger correct surfaced it on the first run. The app-id rename updated `docusaurus.config.js` to point og:image at the new filename and left the actual PNG under its old name, so the AI-baseline validator's last check fails: ✗ og:image URL resolves to a file in the build and `npm run build` exits 1 via postbuild. Nothing could have published even with a correct trigger and a correct worker. Renames the asset to the name the config has been asking for. No references to the old filename remain. Verified locally: npm ci --legacy-peer-deps && npm run build now exits 0 with all 10 AI-baseline checks passing.
GitHub is the only host for this org — Codeberg was a mirror and is not used for anything, including issues. Every codeberg.org URL in this repo was therefore dead or pointing at an unused mirror. Many of the links also carried PRE-RENAME repo names. The slug had drifted on both hosts, so this repo contained both `Conduction/openbuild` and `Conduction/buildiq` links; both map to `ConductionNL/buildiq`. Cross-app links were remapped the same way: openconnector->integriq, procest->dossiq, docudesk->filinq, nldesign->thematiq. openregister, launchpad, nextcloud-vue, nextcloud-app-template and hydra keep their slugs. Issue/PR numbers were NOT mapped across hosts. Codeberg issue numbers do not correspond to GitHub issue numbers, so rewriting only the host would have silently pointed at a real but unrelated GitHub issue — worse than a dead link. Those three references (#69, #68 in the tutorials and #126 in the phpstan baseline) were de-linked and kept as plain text noting they are pre-migration and were not migrated to GitHub. Issue links with no number (the tutorial's "issue tracker") were remapped normally. Also converted: deep /src/branch/<b>/ links to /blob/<b>/, the raw screenshot link to /raw/main/, and the two shields.io/ci.codeberg badges to their GitHub equivalents (the code-quality badge points at the code-quality.yml workflow, which exists in both this repo and the app template it ships). The docusaurus editUrl is the one place the branch was NOT preserved verbatim: buildiq's `main` branch has no docs/ directory, so /blob/main/docs/ 404s. It points at `development`, where the docs live. Deliberately left untouched: the Conduction/concurrentie-analyse links (that repo does not exist on GitHub), the `codeberg` forge-type enum and its https://codeberg.org example placeholder in RoadmapPageEditor (a user-selectable forge, not a link to us), historical "Codeberg issue on X" prose in archived openspec changes, and .forgejo/ plus the generated docs/.docusaurus/ build cache. Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
… page (#439) * feat(app-page): show pages, menu, schemas and flows on the app detail page An app is navigation, pages, data and business logic. The app detail page showed none of those as lists, so building an app meant opening the app and using the in-page action menu. This puts the four structure tables on the page itself. Three of the four already existed and were mounted NOWHERE. PagesWidget, MenuWidget and SchemasWidget are implemented, carry REQ-OBADO-009 spec tags, and have passing unit tests, but `grep -rl` finds no importer. The dashboard's own docblock claims a "Structural grid — Register / Schemas / Groups / Pages / Menu", while its template mounted only three widgets. They were built, spec'd, tested, and never rendered for a single user. So this mounts them, fed by computed props the dashboard already had (activePages / activeMenu / activeSchemas), and adds the missing fourth: - FlowsWidget lists the OpenRegister flows bound to the app. Flows are a field on the Application record (ApplicationDetailActions.setFlows patches `{ flows }`), not part of the version manifest, so activeFlows reads the record. Rows deep-link into OpenRegister, which owns flows (ADR-022); the widget lists and hands off rather than wrapping OR's editor. - flowState() reports an absent `enabled` as "Unknown" rather than "Disabled". An unset field is not a value, and defaulting it to the innocent-looking answer is how a missing state becomes a wrong one. SchemasWidget's add button emitted an event nobody listened to, logging "schema-create dialog not yet registered — deferred to schema-designer spec". That designer shipped: SchemaDesignerList lives at /builder/:slug/schemas. The emit now routes there. Renamed that emit `add-schema` -> `addSchema` to satisfy vue/v-on-event-hyphenation, which is safe precisely because nothing consumed it, and declared two emits that were fired but never declared. Also corrects a docblock that contradicted its own code: registerSlugForApp returns `openbuild-{appSlug}` while its comment claimed `buildiq-{appSlug}`. The code is right. The app id moved to buildiq and the CONTROL register moved with it, but per-app register slugs are frozen once objects live under them; every per-app register on a live instance is named `openbuild-*`. Verified in a browser against a real instance, not just unit tests: created an app and confirmed all four tables render with real rows (Pages 2, Menu 2, Schemas 1, Flows empty-state). eslint clean, 1378/1378 vitest, phpcs clean, check-manifest PASS. * chore(lint): prune the suppression the addSchema rename made stale Renaming SchemasWidget's emit from `add-schema` to `addSchema` fixed the underlying vue/custom-event-name-casing warning, which left its entry in eslint-suppressions.json describing a problem that no longer occurs. ESLint 9 treats an unmatched suppression as a failure, so Lint Check exited 2 with "0 errors, 142 warnings" — the count was unchanged from development, and the exit code came entirely from the stale entry. `npx eslint src --prune-suppressions` removes exactly that one entry. eslint now exits 0. * feat(app-page): add Settings, Setup wizard and Walkthrough buttons These edit the app's chrome — its settings, its first-run wizard, its guided tour — rather than any one page, so they belong on the app page rather than in the in-page orange edit menu, which is being narrowed to page-local actions. Setup wizard needed no new editor. The Walkthrough Designer already hosts BOTH editors as tabs, so the work was making the tab reachable: `mode` now initialises from `?mode=setup`, and the button deep-links to it. Without that the button would land the user on the walkthrough tab and leave them to find the setup one, which is the kind of "technically it is there" that makes a feature feel missing. Optional chaining on `$route` because the designer is mounted in unit tests without a router. Not included: Edit support & donation. Its modal (CnEditSupportModal) exists in @conduction/nextcloud-vue but is not exported from the package index, so it cannot be imported, and `manifest.support` was rejected by the v2 schema's `additionalProperties: false`. Both are fixed in ConductionNL/nextcloud-vue#748; the button lands here once that reaches a published release. Shipping three working buttons now beats shipping four where one is wired to nothing. Browser-verified against a live instance: both buttons render on the app page, Walkthrough routes to /builder/pet-store/walkthrough, and Setup wizard routes to ?mode=setup and opens ON the Setup steps tab. eslint exit 0, 27 applicationDetail tests, 14 WalkthroughDesigner tests, 11 WalkthroughDesignerHost tests all pass. * feat(walkthrough): the tour now builds the pet store and ends on the docs The tour walked a user to the Store and had them clone a template, then stopped. Cloning is not building, so a new user finished the guided tour without having made a schema, a page, or anything of their own. It now builds the canonical pet store, which is the sample domain the academy tutorials already use, so the tour and the docs teach the same thing: welcome -> Apps -> create Pet Store -> add a Pet schema -> add an index page over Pet -> open the app and add a pet -> done Each step keeps a real `advanceOn`, anchored to routes that exist (VirtualApps, SchemaDesigner, PageDesigner, BuilderHost) rather than to invented ones, and the build steps carry `allowManualNext` so a user who does it slightly differently is never trapped. The final step now closes on a call to action that opens the documentation, per the fleet rule that a walkthrough's last step points somewhere. It targets the Documentation nav item, which already exists in the menu, so the CTA lands on a real destination rather than a URL invented for the copy. Voice-checked against the shared writing skill: no em-dashes, every sentence under 16 words, every task starts with a verb, no praise, and no step whose body restates its own task. check-manifest PASSES. * fix(l10n,format): register FlowsWidget's string and satisfy prettier Two CI gates caught real gaps in the FlowsWidget commit. `test:l10n` found "No flows bound to this app yet." used in the component but absent from l10n/en.json, so the string would have shipped untranslatable. Added via the repo's own extractor, plus the Dutch translation — the gate only checks en.json, but Dutch is required fleet-wide, and a key present only in English is a string Dutch users read in English. `format` found FlowsWidget.vue not prettier-clean. Fixed with prettier --write; the only change is wrapping one call's arguments. The extractor also picked up "This application has no flows yet.", a pre-existing missing key from another component that was already failing this gate. * fix(l10n): rebuild the compiled l10n artefacts the browser actually loads The previous commit added "No flows bound to this app yet." to l10n/en.json and l10n/nl.json, and `test:l10n` went green — that check reads the JSON. Nextcloud does not serve the JSON. It serves l10n/*.js, which is generated from it, and those still lacked the key. The string would have rendered untranslated in every language while two l10n gates reported success. `check:l10n-js` is the gate that can see the difference, and it is the one that failed. Regenerated via the repo's own `l10n:build`; en.js and nl.js now carry the key.
This repository's only issue forms lived under `.forgejo/issue_template/`. GitHub is the fleet's only host, so those forms are invisible to everyone filing an issue here. Two approved fleet changes make this urgent: 1. `.forgejo/` is being removed fleet-wide. Without this port that removal would delete the only issue forms this repo has, leaving contributors with a blank issue box. 2. The shared library's `DEFAULT_FORGE` moves from `codeberg` to `github`. The in-product "Request a feature" deep-link then targets a GitHub Issue Form named exactly `feature-request.yml`. If that file is absent GitHub silently drops every pre-filled field instead of erroring, so the app context (app, page, surface, object, spec-ref) would be lost without a single visible failure. Copies all four templates to `.github/ISSUE_TEMPLATE/`, keeping the filenames identical. `.forgejo/` is deliberately left untouched; its removal is a separate later change. Conversion is lossless: Forgejo's issue-template schema is derived from GitHub's, and every construct used here (markdown/input/textarea/dropdown blocks, `render: shell`, `validations.required`, `labels`, `assignees`, `title`) is valid GitHub issue-form syntax. Nothing was dropped or reworded. The top-level `type: "Feature"` in feature-request.yml was verified against the ConductionNL org issue types, where "Feature" exists and is enabled. No `config.yml` was added: `.forgejo/issue_template/` has no equivalent.
* fix(docs): pass secrets to the reusable documentation workflow A called workflow receives no secrets from its caller unless they are passed explicitly or inherited. Without `secrets: inherit` the callee sees an empty `secrets.CF_API_TOKEN`, its "Publish to the Cloudflare Worker" step skips itself on its own guard, and the run finishes green having written only gh-pages — which nothing serves. The live docs site never changes and no check goes red to say so. Measured on planninq run 32715324775: all three jobs green, GitHub Pages deploy success, Worker publish skipped, warn step reporting the Worker was not updated. * fix(docs): map the Cloudflare secrets explicitly instead of inheriting all `secrets: inherit` handed the reusable documentation workflow every secret this repo holds — the Nextcloud signing cert and key, the appstore token, the deploy keys — for the sake of two Cloudflare values. It also would not have worked. The org secrets are CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID and `inherit` passes secrets under their original names, while the callee reads CF_API_TOKEN / CF_ACCOUNT_ID — so the publish step would still have skipped itself and the run would still have gone green over an unchanged live site. Maps the two names explicitly instead, so nothing else crosses the boundary. Depends on ConductionNL/.github#568, which declares both as optional secrets on the callee: an explicit mapping only compiles for names the callee declares.
… Vue Flow canvas (#449) 2.15.0 is the first release that carries the four `@vue-flow/*` dependencies (`core`, `background`, `minimap`, `node-resizer`) required by `CnGraphCanvas`, `CnFlowDetail` and `CnFlowEditModal`. The declared range `^2.6.2` already *allowed* 2.15.0, and this repo's lockfile happens to have already floated up to 2.15.0 — so the canvas does currently ship here. But nothing enforced that: the range still permitted any 2.6.x build, i.e. a build with no `@vue-flow/*` deps, so any lockfile regeneration or resolution could silently drop the canvas out of the bundle again. This is exactly what happened in openregister, where the caret range allowed 2.15.0 while the lockfile pinned an older build and the canvas never reached the bundle. Raising the floor makes the requirement explicit. Verified after `npm run build`: - installed @conduction/nextcloud-vue: 2.15.0 (unchanged; range 2.6.2 -> 2.15.0) - node_modules/@vue-flow/{core,background,minimap,node-resizer} present - 47 distinct `vue-flow__*` classes in the built js/ + css/ output Only package.json and package-lock.json change; js/ build output is gitignored.
…ist (#454) The mapping read `secrets.CLOUDFLARE_API_TOKEN` / `secrets.CLOUDFLARE_ACCOUNT_ID`, which are not secrets anywhere in this org. Mapping from a non-existent secret is not an error - it yields an empty string - so the callee's "Publish to the Cloudflare Worker" step skipped itself on its own guard and the run stayed green while the live docs site kept its pre-rename build. The real org secrets are CF_API_TOKEN / CF_ACCOUNT_ID, the same names the callee declares and the same ones ConductionNL/.github deploy-docs.yml reads directly. Only the mapping values change; the keys stay. The comment justifying the explicit mapping claimed the names differ on each side. They do not, and that claim is what produced the bug. Replaced with the reason that still holds: `secrets: inherit` would hand the callee every secret this repo holds for the sake of two Cloudflare values.
GitHub is the only host this organisation publishes to. No local checkout has a Codeberg git remote, so nothing is pushed there and no workflow under .forgejo/ has ever run for this repository. Issue templates: the 4 templates under `.forgejo/issue_template/` were already ported to `.github/ISSUE_TEMPLATE/` and were verified present there before deletion (including `feature-request.yml`, which the in-product "Request a feature" deep-link targets by that exact filename). This repository had no `.forgejo` release workflow. Its release path, `.github/workflows/release.yml`, is untouched. .github/workflows/ is untouched — that is the live CI. Any CODEBERG_TOKEN reference lived only inside the deleted files and goes with them. Removes 6 file(s) under .forgejo/.
* feat(app-page): add the Support & donation editor to the app page Completes the app-level editor row. The app page now edits all four pieces of an app's chrome — its settings, its first-run wizard, its guided tour and its support note — rather than sending the user into the running app's orange edit menu for the last one. The button opens `CnEditSupportModal` on a CLONE of the resolved manifest, so cancelling costs nothing: the copy is simply dropped. The manifest is resolved from the active version through the app's own endpoint rather than read off the Application record, because an Application carries no manifest — it lives on the ApplicationVersion, and `obApp.manifest` is undefined. Requires @conduction/nextcloud-vue 2.15.1, bumped here. The modal shipped inside the library for months but was never exported from its package index, so no consumer could mount it; ConductionNL/nextcloud-vue#748 exports it and 2.15.1 is the first release that carries it. Verified against the published tarball rather than the repo: `CnEditSupportModal` is present in dist/esm/index.js and the v2 schema carries the `support` block. Also drops the stale comment that said these modals "cannot be imported" — that was true when it was written and is not any more. * fix(l10n): register the support-button strings, source and compiled `test:l10n` caught two strings the support button introduced that no catalogue carried: "Support & donation" and "Failed to load settings". Added to l10n/en.json via the repo's own extractor, translated on the Dutch side, and — the part that matters — regenerated l10n/*.js. Nextcloud does not serve the JSON. It serves the compiled l10n/*.js built from it, so a key present only in the JSON is a string the browser never receives. That is the same gap that made `test:l10n` pass and `check:l10n-js` fail earlier on this branch's predecessor: two l10n gates, only one of which can see the artefact the runtime loads. Both now pass, and the key is present in en.js and nl.js.
…461) * fix(e2e): a scheduled fixture flow must name the identity it runs as Two e2e tests are red on development, both refused at creation with 400: creating the fixture flow must succeed (got 400) seeding the definition must succeed (got 400) openregister's TriggerScheduleNode now refuses a schedule trigger whose config carries no 'runAs' (ADR-099). Nobody is present when a schedule fires, so there is no session to take an identity from, and the flow's owner is deliberately not used as a fallback — authoring a flow is not consent to unattended execution as its author. Both fixtures posted 'openregister.trigger-schedule' with an empty config, so both are refused. The value is validated against real accounts via userManager->get(), so it cannot be an arbitrary label; RUN_AS tracks the account the suite authenticates as, from the same environment variable playwright.config.ts reads. Not caused by #453, which is simply the commit these failures first appear on: the two commits between the last green e2e and this one are CI and docs config only, and the 400 comes from openregister's validator rather than from anything in this app. The same contract change is failing integriq's JobToFlowGenerator test. * fix(e2e): the schedule trigger needs cron AND runAs, not either The first commit added runAs alone, and CI still returned 400 — the error simply moved. Probed against a live instance to establish the whole contract rather than inferring it from one message: {cron, runAs} -> 201 {cron} -> 400 'must carry a "runAs" naming the user its runs act as' {runAs} -> 400 'must carry a "cron" expression' Both are mandatory and each is refused separately, so an error naming one says nothing about the other. These fixtures sent config: {}, so they were always missing both; fixing one at a time reads as 'the fix did not work' when it was half a fix. FIXTURE_CRON is a five-field expression because macros like @hourly are refused. Nothing here waits for the schedule to fire — the flows are created disabled or driven directly — so the time only has to be valid.
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
The committed sbom.cdx.json dates from this app's scaffold commit in May 2026 and has never been regenerated, while composer.lock and package-lock.json have moved many times underneath it. A stale SBOM asserts a dependency set that is no longer true while still looking authoritative. The SBOM is generated per run by the shared quality workflow, published as the sbom-<app> artifact and, as of ConductionNL/.github#572, attached to stable releases. It is never committed — see the hydra sbom-generation spec (ConductionNL/hydra#617). hermiq already ignores it; the app template shipped the file despite already carrying the rule, which is how this app inherited it. Refs ConductionNL/.github#572, ConductionNL/hydra#617 Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
…doned (#459) * spec(runtime): REQ-OBR-002/003 described an architecture the app abandoned Both requirements mandated a NESTED CnAppRoot inside the Buildiq shell, with the outer CnAppNav, header and chrome staying visible, mounted by BuilderHost.vue, and an OUTER router forwarding an opaque path suffix to an INNER one. The product deliberately does the opposite, and says so in its own source. appinfo/routes.php maps the bare `/builder/{slug}` to `dashboard#builder`, a standalone page booting the `src/builder.js` webpack entry, whose header states the reason: It is deliberately NOT the Buildiq SPA: rendering the app inside Buildiq's shell nests one NcContent in another (double chrome) and, worse, shares Buildiq's router — which has none of the app's page routes, so page content never resolves. BuilderHost.vue still exists and is still registered, but only ever mounts for builder sub-paths falling through to the SPA catch-all — never for the runtime route these scenarios navigate to. So `[data-testid="buildiq-builder-host"]` was genuinely absent while the app rendered correctly, and the two e2e tests could not pass however they were written. They were left permanently skipped with a note recommending exactly this rewrite. This is that rewrite. REQ-OBR-002 now describes the standalone shell, and its scenario asserts the property the old wording had BACKWARDS: one shell, not two. REQ-OBR-003 now describes the app's own router resolving its own routes, and drops the outer/inner forwarding machinery, which has no counterpart in the shipped design. Its scenario is written around opening a row rather than deep-linking `00000000-0000-0000-0000-000000000000` — a uuid that exists in no fixture, so the old scenario could not have been asserted even had the architecture matched. THE TEST IS REAL, NOT A STUB The REQ-OBR-002 test now opens `/builder/hello-world`, asserts the app's own seeded index content, and then COUNTS mounted CnAppRoot instances via the existing componentTree helper, requiring exactly one. Counting is the point: the abandoned design would show two, and a plain visibility check cannot tell those apart. It also asserts the SPA's builder-host wrapper has count 0 on this route. No test is written for REQ-OBR-003: "REQ-OBR-004 — the seeded index lists the three sample messages and opens one" already clicks a row, waits for the URL to move onto the manifest's `/messages/:id` route, and asserts the detail renders. That IS the rewritten scenario; a second test would add a passing assertion without adding coverage. Verified with the repo's own gate — `npm run format` (prettier --check "**/*.{js,ts,vue,css,scss}") passes across the tree. * fix(e2e): a schedule-trigger fixture must carry a cron and a runAs Two tests failed on flow creation, not on anything they assert: creating the fixture flow must succeed (got 400) seeding the definition must succeed (got 400) Both fixtures build `openregister.trigger-schedule` nodes with `config: {}`. OpenRegister validates that config on save now and refuses an empty one: the node requires a five-field `cron`, and TriggerScheduleNode::validateActingIdentity() requires a `runAs` naming an existing account — "nobody is present when a schedule fires, so there is no session to take an identity from, and the flow's owner is not used as a fallback". These fixtures exist to be exported and re-imported, never to fire, but they still have to be documents the platform accepts. `runAs` is the admin, which is the account the e2e session actually runs as, so the fixture names something true rather than a placeholder that happens to exist. THIRD APP HIT BY THE SAME UPSTREAM CHANGE integriq's JobToFlowGenerator emitted schedule triggers with no `runAs` (fixed in ConductionNL/integriq#1568) and its PHPUnit went red on the node's own validateConfig. decidiq and learniq were untouched. Worth noting for anyone else seeing a sudden 400 on flow-create: the request shape did not change, the node's contract did. Verified: `npm run format` clean across the tree. * test(e2e): two real runtime bodies, and the header's defect is fixed THE DEFECT THIS FILE'S HEADER DESCRIBED NO LONGER EXISTS It read: "DocumentActions filters attachments by object['@self'].schema, which OpenRegister returns as the NUMERIC schema id ("21"), while a runtime.documents[] entry declares a schema SLUG ("hello-message"). The two never match, so the surface renders nothing for every real object." The component now injects `cnObjectContext` — whose `schema` is the manifest slug — and resolves candidates through `objectSchemaKeys()`, which collects `[ctx.schema, obj.schema, self.schemaSlug, self.schema]` and matches any of them. Slug and numeric id both hit. A skip reason outliving its cause is how this file came to carry twelve tests asserting nothing, so it is corrected rather than left to age further. It is the second such case in this repo today: the `buildiq#41` quarantine cited across 16 files names a PR that MERGED on 2026-07-27. TWO STUBS BECOME REAL TESTS Both were `goto('/applications')` + `expect(main).toBeVisible()` — bodies that would pass without ever reaching a runtime object. REQ-DDT-004 "no attachments renders nothing" now opens a real object detail on the seeded `hello-world` runtime app (which declares no runtime.documents[] — exactly the fixture the scenario needs) and asserts `.ob-document-actions` has count 0. The component gates its whole root on `v-if="schemaAttachments .length"`, so "renders nothing" is an ABSENT element, not an empty one. REQ-DDT-005 "runtime surface degrades without requests" asserts what the DOM cannot show: that no request to /apps/docudesk/ is issued at all. A surface that renders empty while still calling Docudesk on every object detail is the failure this scenario exists to catch, and it is invisible in markup. THE REQ-DDT-003 STUBS ARE LEFT SKIPPED, DELIBERATELY They need a published app carrying an attachment — real fixture work, not a skip to remove. Their notes now say that instead of citing the retired defect. Enabling them as they stand would produce green tests asserting nothing and would start crediting their scenarios with coverage they do not have. Verified: `npm run format` clean across the tree. * test(e2e): a real nldesign degradation test, and 16 stale quarantine notes THE QUARANTINE CITED ACROSS THIS SUITE IS STALE Sixteen spec files deferred to "Conduction/buildiq#41: buildiq admin UI not functional in this build". #41 is a PULL REQUEST that MERGED on 2026-07-27, and 47 spec files in this suite already pass against that same UI — applicationDetailOverview.spec.ts alone has 9 passing tests. Every one of those notes now says what actually blocks its test: the body is a stub. Leaving them pointed at a merged PR is how a file ends up carrying twelve tests that assert nothing, which this repo has already done twice. ONE STUB BECOMES A REAL TEST REQ-NTS-005 "designer degrades when nldesign is missing" was `goto('/applications')` + `expect(main).toBeVisible()` — it asserts nothing about themes and passes on any page that renders. It now opens the page designer, scrolls to the Theme section, and asserts BOTH halves of degrading: that the absence is EXPLAINED (`.ob-theme-section__hint` visible) and that the control which cannot work is disabled rather than silently inert (`Change` is `:disabled="!nldesignAvailable"`). Deterministic, not hopeful: this app's CI installs openregister and docudesk and NOT nldesign (code-quality.yml `additional-apps`), so `nldesignAvailable` is false and the degraded branch is the one under test. WHAT IS DELIBERATELY LEFT SKIPPED The other stubs stay disabled with corrected notes. They need real fixtures — a published app carrying a theme, a seeded token-set catalogue — and enabling them as they stand would produce green tests asserting nothing AND start crediting their scenarios with coverage they do not have. gate-19 counts `skip`/`fixme` as off, so while skipped they credit nothing; that is the honest state until bodies exist. Verified: `npm run format` clean across the tree. The one eslint finding on the new import is `import-extensions/extensions`, which fires 71 times across tests/e2e already — no file in this repo uses the `.ts` extension, so the import follows the established convention rather than inventing a new one. * test(e2e): dismiss the walkthrough that was covering the runtime row Both new runtime tests timed out at 30s, and not for want of the element: locator.click: Test timeout of 30000ms exceeded waiting for getByText('Welcome to Buildiq').first() 52 x waiting for element to be visible, enabled and stable The page snapshot shows the runtime app rendered correctly — the "Messages" heading, the table, and the "Welcome to Buildiq" cell all present. The click spent its whole timeout on actionability, which is what a covering overlay looks like. The runtime app declares a walkthrough that pops a beat AFTER navigation settles. `dismissOverlays()` from appFixture checks instantaneously and races it; `dismissFirstVisitOverlays()` from support/overlays polls with `waitFor()`, which is why buildiq-runtime.spec.ts drives this same route successfully with it. Switched, and the row is scrolled into view before the click. `dismissOverlays` stays imported — the designer tests in this file still use it on a surface that has no walkthrough. --------- Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Part of the 2026-08-25 fleet structure audit (ADR-100 Decision 2: the repository root is a closed set; generated files are never tracked). Ignore rules added: .stale/ /.e2e-state/ .phpunit.result.cache Untracked (kept on disk, now ignored where a rule covers them): - "s*buildiq\"" `.stale/` was missing from ALL 19 fleet repos and is the one that matters most operationally: agent scratch there grew unbounded and filled the dev disk once already. Refs ConductionNL/hydra ADR-100. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
The docs site carried 58 npm advisories, 2 critical - the largest single
source of dependabot alerts in this repo (84 of 122). None are fixable by
upgrading: they sit in Docusaurus's own build toolchain, and npm audit
reports fixAvailable:false across the whole @docusaurus/* cascade because
no published release moves them.
Refreshing the lockfile changed nothing (58 -> 58); it was already at the
newest versions the declared ranges permit. So the remedy is overrides.
Added 22, each the newest release within the major the tree already
resolves, so no consumer sees a new API:
58 advisories -> 30, both criticals cleared
Three candidates were deliberately excluded. js-yaml, uuid and ws each
resolve to TWO majors in this tree, and forcing one version collapses the
older consumer. js-yaml proved it: overriding 3.14.2 to 4.3.1 removed
safeLoad, which gray-matter still calls, and the build died parsing front
matter. That was caught by building, not by auditing - npm audit was
perfectly happy with the broken tree.
Verified: npx docusaurus build exits 0 and emits 47 HTML pages.
Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
…lates (#466) * fix(templates): repoint four dead sourceUrl links to the renamed repo The four seeded application templates all carried https://codeberg.org/Conduction/concurrentie-analyse/src/branch/main/ app-builder/README.md#user-stories which returns HTTP 404. Confirmed with curl rather than assumed. The repository was not deleted, it was renamed: `concurrentie-analyse` is now `ConductionNL/market-intelligence` on GitHub. Found by reading the git remote of the local checkout that still uses the old directory name, since a GitHub search for the old name finds only unrelated repos. Verified before repointing, not after: app-builder/README.md .... exists on main (46,313 bytes) #user-stories anchor ..... `## User Stories` is a real heading (line 515) Left as-is deliberately: this repo is PRIVATE. That is the right target anyway. `sourceUrl` is defined in openbuild_register.json as "Link back to the originating user-story / RFP / blog post for traceability", and the only thing that reads it is EditTemplateMetadataDialog — an admin edit field, not a link rendered to end users. Traceability wants the true origin; pointing it at some public stand-in would make it accurate-looking and wrong. Three other Codeberg references in this app were checked and left alone: the forge-type dropdown in RoadmapPageEditor.vue, where `codeberg` is a valid option a user can pick, is not a stale link. * fix(templates): drop sourceUrl rather than point a public app at a private repo Correcting the previous commit on this branch. Repointing the dead Codeberg link to `ConductionNL/market-intelligence` fixed the 404 for us and left it broken for everyone else: buildiq is PUBLIC (visibility=PUBLIC), these four templates ship inside it, and market-intelligence is private. Every admin outside Conduction would click through to a login wall. My earlier reasoning — that this is admin-only traceability metadata so the private target is fine — does not survive the app being open source. The templates are public data; who reads the field does not change who can reach the URL. The alternative was pointing at https://openbuild.conduction.nl/docs/intro, the app's only live public docs page. That is worse: it resolves, so it looks right, while claiming these user stories came from a generic intro page they did not come from. False provenance beats no provenance only until someone follows it. So the field is removed. `sourceUrl` is optional — ApplicationTemplate.required is [slug, title, description, useCase, category, manifest, isSeeded, version] — and all four templates still carry every required key. If the origin material is ever published, the honest fix is to add the field back pointing at the real public URL.
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix(l10n): translate the 36 untranslated manifest strings The manifest is data the renderer walks, not source the l10n extractor scans, so CnAppNav's `menu[].label`, the setup wizard and CnWalkthrough's step copy looked up keys that were never in the catalogue. A missing key falls back to the English source and nothing reports it, so a Dutch user reads English. Thirty-six strings: the setup wizard, the whole pet-store tour, and the nav. Product names stay untranslated on purpose. "Pet Store" is the app the user is told to create BY NAME, and "Pet" is the schema they type, so translating either would make the instruction not match what they must enter. The prose around them is Dutch: "Noem die Pet Store", "Voeg een schema toe met de naam Pet". Same reasoning keeps Apps, App, Manifest, Exports and Agents as they are, and translates the ones that genuinely have Dutch: Winkel, Manifestlagen, Geschiedenis, Rondleidingontwerper. nl.json ONLY. Adding the same keys to en.json is the obvious move and it is wrong in this fleet: check-l10n-parity.js is a ratchet over every required locale (the official language of every European country, plus Russian and Turkish), so one new English source key demands a real translation in about thirty languages. keepiq#449 shows the failure mode — eleven new en.json keys produced "+568 more missing" across the other locales. Verified: 0 manifest strings missing Dutch. * fix(l10n): rebuild nl.js, the browser loads the artifact not the JSON Same omission as keepiq#449 on the same day: thirty-six keys added to l10n/nl.json, l10n/nl.js never regenerated. nl.js was 1,009 keys against nl.json's 1,045, which is exactly the additions sitting only in source. The .json is source; the .js is what the runtime loads via OC.L10N.register. A catalogue correct in JSON and stale in JS is a translation nobody receives, and every check that reads the JSON calls it done. That is why `check:l10n-js` exists, and it is what caught the keepiq case in CI. Checked this by comparing key counts across all three of my open l10n branches rather than waiting for CI to tell me twice. Ran `l10n:build`; did not hand-edit the generated file. Verified: l10n:build PASS, check:l10n-js PASS.
Those were the only conflicts. The merge keeps BETA's version string (`0.6.2-beta.20260820211351`) rather than development's (`0.6.1-unstable.20260826085824`). Development's is numerically lower, so taking it would have published a downgrade; the release job bumps from here anyway. Everything else takes development's content. Conflicts were resolved mechanically and each result was checked: the XML and JSON were re-parsed, no conflict markers remain, and the version substitution was asserted to have matched exactly once. Any repo whose conflicts extended beyond these files was left alone rather than auto-resolved.
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
… enables (#475) ## The upgrade `package.json` and `package-lock.json` both move to 2.19.0. The lockfile is the part that matters: `^2.15.1` already PERMITTED 2.19.0, so the range looked fine while `npm ci` kept installing 2.15.1. A range is not an installed version. Verified all three agree afterwards — declared `^2.19.0`, locked 2.19.0, installed 2.19.0 — and that the installed copy actually contains the feature being relied on (`hasError()` in CnStatsBlock), rather than trusting the version number. Nothing broke across the four-minor jump: 141 test files / 1378 tests pass, eslint and prettier are clean, and the webpack build compiles. ## What it unblocks `ApplicationDetailDashboard` sets `this.error` on a failed insights load and then never renders it — the property appears nowhere in the template. So a non-404 failure left the three data KPIs showing their initialised zeros: "0 active users, 0 objects, 0 audit events", with nothing on screen to say the read had failed. The three tiles now pass `:error`, so they show a dash and "Unavailable". Deliberately unchanged: - The 404 branch. It sets `versionNoLongerAccessible`, which drives a real banner ("This version is no longer accessible. Switch to production?"), and zeroes the KPIs on purpose. That state is already communicated, and `error` stays null there, so this change does not touch it. - The fourth CnStatsBlock. It is a hardcoded `loading` placeholder with `:count="0"` for Storage — a permanent spinner, not a tile bound to fetched data. Completes the fleet sweep started in decidiq#918, pipelinq#1462, learniq#643 and shillinq#1268. buildiq was the one app that had to wait, because its lockfile pinned a version predating the prop. Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
Contributor
Quality Report — ConductionNL/buildiq @
|
| 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 | ✅ | ✅ 642/642 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-27 09:13 UTC
Download the full PDF report from the workflow artifacts.
gate-16 failed the development->beta release PR (#476) on one changed method missing @SPEC: ApplicationDetailActions.vue::onSettingsOpen. It is real behaviour, not glue, so it gets a real @SPEC rather than an exclusion -- the same application-detail-ui spec its sibling setFlows already carries. The docblock records why the fetch is lazy (the flows are read only inside this modal, and most visits never open it) and what the three guards buy: the request happens exactly once, never on close, never when a previous open already filled the list, never while one is in flight. Verified with the real gate (ConductionNL/.github check_spec_coverage.py) against origin/beta: count=1 before, count=0 after. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Brings development's onSettingsOpen annotation across, which is what the release PR's Hydra Gates leg was failing on. The only conflict was the version line again: beta's 0.6.2-beta.20260820211351 is kept over development's lower 0.6.1-unstable, so the release is not a downgrade.
Contributor
Quality Report — ConductionNL/buildiq @
|
| 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 | ✅ | ✅ 642/642 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-27 11:59 UTC
Download the full PDF report from the workflow artifacts.
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.
Same development->beta release merge as the bot PR, with the version-stamped conflicts resolved.
Branch is named
hotfix/*because the beta branch-protection check only acceptsdevelopment,mainorhotfix/*as a source, and the sanctioneddevelopment -> betapath cannot be used here: resolving the conflict on that PR would merge beta INTO development, dragging beta-only release plumbing (re-trigger commits, semrel caller fixes) back into development.The merge keeps BETA's version string rather than development's, which is one patch lower in every repo -- taking development's would have published a downgrade. Everything else takes development's content, including the removal of the codeberg.org URLs still present in beta's info.xml.
Verified per repo: XML and JSON re-parse, no conflict markers remain, and the version substitution matched exactly once.