Release: merge development into beta - #923
Merged
Merged
Conversation
…60821043606 chore(release): 1.0.1-unstable.20260821043606
…60821045501 chore(release): 1.0.1-unstable.20260821045501
…60821051111 chore(release): 1.0.1-unstable.20260821051111
…60821052845 chore(release): 1.0.1-unstable.20260821052845
Supersedes the dependabot PR, which failed `PHP Quality (phpcs)` with:
Script ./vendor/bin/phpcs --standard=phpcs.xml ... returned with error code 3
Exit 3 is a phpcs PROCESSING failure, not a verdict on the code. Reading it as a
phpcs-4 policy change (warnings starting to fail the build) and reaching for
`ignore_warnings_on_exit` would have suppressed a real breakage and left the
sniffs half-running.
The lockfile, not the sniffer
-----------------------------
The bump itself is fine. What differed was everything around it:
dependabot branch: conduction/hydra-gates v1.8.0 + php_codesniffer 4.0.4
development: conduction/hydra-gates v1.8.2 + php_codesniffer 3.13.6
this branch: conduction/hydra-gates v1.8.2 + php_codesniffer 4.0.4
hydra-gates v1.8.0 predates phpcs 4 and its sniffs cannot load under it.
Dependabot branched before v1.8.2 landed, so its lockfile pinned the older gates
package and carried it forward -- the bump was being tested against a sniff
bundle that no longer matches the sniffer. pipelinq's dependabot bump failed the
same way, from the same v1.8.0 pin.
Rebuilding the same bump on current development is the whole fix. Nothing in
phpcs.xml or the composer scripts changes, and no warning is suppressed: the 116
`@spec` warnings are still reported, exactly as on development today, and still
do not fail the build.
Verified locally against the exact CI invocation
------------------------------------------------
`./vendor/bin/phpcs --standard=phpcs.xml`, not a summary report -- report format
changes what is printed, and it is easy to "confirm" a pass with the wrong one.
phpcs 3.13.6 on development: 0 errors / 116 warnings in 108 files, exit 0
phpcs 4.0.4 on this branch: 0 errors / 116 warnings in 108 files, exit 0
Same counts, same exit, different sniffer -- what a clean linter major should
look like.
phpstan No errors
psalm No errors
phpmd exit 0
phpunit 1153 tests, 4573 assertions, 0 failures
Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
#815) * style: apply php-cs-fixer across lib/ and tests/ (no behaviour change) `composer cs:check` was red on 119 files. The fixer is wired into no workflow, so this had drifted silently — running it now brings the tree to the standard the repo declares. The ruleset is exactly Nextcloud's: `Conduction\CodingStandard\Config` extends it and its ADDITIONS array is EMPTY by design, because every rule the fleet wants beyond Nextcloud's is semantic rather than typographic and lives in PHP_CodeSniffer instead. So this can only move whitespace and syntax, never meaning. Verified rather than assumed, because an autofix CAN change meaning: * `git diff -w` (whitespace-blind) is NOT empty — 110 files — so the run did make token-level changes, and they were inspected rather than waved through. Every one falls into three groups: `use` statements REORDERED (identical text, moved lines), trailing commas added to multi-line signatures (PHP 8.0+; CI runs 8.3 and 8.4), and promoted constructor properties split across lines. No comparison operators, no `declare(strict_types)` insertion, nothing semantic. * All 119 changed files parse (`php -l`). * Suite identical before and after: 1153 tests, 4573 assertions, 0 failures — the same counts, which is what a typographic change should produce. * phpmd, psalm, phpstan all exit 0. `cs:check` now exits 0. `composer phpcs` still exits 1, unchanged by this commit and expected: the 108 SPDX-header `InvalidEndChar` warnings are DELIBERATE. A full stop after `SPDX-License-Identifier: EUPL-1.2` makes it a different, invalid identifier and breaks REUSE, so hydra-gates' shared ruleset downgrades that one code to a warning on purpose. CI counts errors only. * fix(style): keep the @return prose out of the tag so phpcs and the fixer agree The sweep introduced ONE phpcs error, caught by CI and confirmed by measuring both branches: development has 0 files with errors, the sweep branch had 1. php-cs-fixer's docblock aligner indents a tag description to clear the longest type on the block. Behind HealthController::engineBody()'s 88-character `array{...}|null` shape that lands at column 95, producing a 162-character line — over phpcs's 150-character budget, which the shared ruleset adds deliberately because Nextcloud enforces no line length at all. The two tools genuinely disagree here, so the fix is to remove what they disagree about: the prose moves into the docblock body, leaving the tag with nothing to over-align. Verified against BOTH tools and against the baseline: phpcs errors 0 (same as development), php-cs-fixer clean (exit 0), warnings still 108 (unchanged — those are the deliberate SPDX ones REUSE requires), suite green. --------- Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
) `development` is red on one E2E test: "Display preferences: default view Meetings redirects the app root to the meetings list" timed out at 20s. It is mis-budgeted, not slow. The suite's 20s cap is calibrated in playwright.config.ts as "2.6× the slowest observed pass", which holds for a test that loads one page and asserts. This one cannot: proving a REDIRECT PREFERENCE needs the settings panel plus three full app navigations — save, app root, deep link — and those alone cost ~16s of the 20. The evidence that it is load and not defect: the same commit range passed at 05:54 and timed out at 07:08 with no code change between, and the test's own comment records an earlier round of exactly this, where the restore step was moved off the UI and onto the API to buy back a fourth page load. test.slow() triples the budget for THIS test only. The global cap is untouched, so every other failure still costs 20s rather than 60, and `retries: 0` stays — nothing here can convert a red into a green. 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 (#829) Prepares this app for ConductionNL/.github#531, which drops `OCA\OpenRegister\Contract\` from conduction/hydra-gates' RUNTIME psr-4 autoload. That prefix is LONGER than both openregister's own `OCA\OpenRegister\` -> `lib/` and the stub root this bootstrap registers, and PSR-4 is longest-prefix-wins, so whichever app's autoloader registers first defines OpenRegister's contract for the whole process. Without this block, once the prefix is gone the stub root resolves `...\Contract\ObjectServiceInterface` to tests/Stubs/Contract/, which this app does not ship. MEASURED: 662 errors, every one "Class or interface OCA\OpenRegister\Contract\ObjectServiceInterface does not exist" out of MockBuilder. interface_exists() is order-independent: it asks whether the interface is RESOLVABLE rather than who registered first. Appending a fallback autoloader does not work, because spl_autoload_register appends relative to registration order and that order across independently loaded apps is the thing nobody controls. Placed in tests/bootstrap-unit.php, which is what phpunit.xml actually loads — this app has BOTH bootstrap.php and bootstrap-unit.php, and the first edit went to the wrong one and changed nothing. MEASURED both directions, 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) Tests: 1108, Assertions: 4456, Skipped: 22 prefix REMOVED (after #531) Tests: 1108, Assertions: 4456, Skipped: 22 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>
* chore(deps): refresh the shared Conduction locks 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. * fix(psalm): stub OpenRegister's contract, which v1.9.0 stopped autoloading hydra-gates v1.9.0 removed `OCA\OpenRegister\Contract\` from its runtime psr-4 autoload (ConductionNL/.github#531). That removal was right — the prefix is longer than openregister's own, so a vendored copy in ANY app defined the contract for the whole process — but I verified it against PHPUnit only. PSALM NEVER RUNS THE TEST BOOTSTRAP. It resolves types through the composer autoload map, so the guarded require in tests/bootstrap-unit.php does nothing for it, and this app's lib/ typehints the interface in production code: lib/AppInfo/Application.php:100 UndefinedClass: OCA\OpenRegister\Contract\ObjectServiceInterface lib/BackgroundJob/MailReplyHandler.php:67 … 204 of them, all the same class. A stub is the right seam. It teaches the analyser the shape WITHOUT putting the class back into the runtime autoloader, which is exactly what caused the original defect. OpenRegister still supplies the real interface at runtime; the files stubbed here are the copies hydra-gates ships for this purpose. Measured in this checkout on the real v1.9.0: before 204 UndefinedClass errors after 0 — "No errors found!", psalm exit 0 --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
…tover Dutch default (#846) Two defects from the Dutch->English value rename, both silent. 1. actionOverdue could never fire. Its scheduled filter was {taskStatus: 'overdue'} — a strict-equality shortcut against a status nothing in the app ever writes. ActionItemWriter::mapStatus() has no 'overdue' entry, and overdue-ness is derived at read time in ActionItemAnalyticsService instead. The daily job ran, matched nothing, and notified nobody. Replaced with the condition the app actually means, in the dialect ScheduledFilterEvaluator implements (equals|notEquals|withinNext|olderThan, entries ANDed): dueDate olderThan PT0S AND taskStatus notEquals completed. Overdue is now derived by the filter rather than depending on a stored status that no writer maintains. 2. ProxyAuthorization.signatureStatus defaulted to 'ongetekend', which is not in its own enum (unsigned|signed|refused) and disagrees with the lifecycle's initial state 'unsigned'. The enum and lifecycle were migrated by RenameDutchDecideskValues; default, example and the prose were left behind. Refs #845 Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
…s (19 rules) (#848) * fix(notifications): reactionPendingModeration used the wrong filter shape The created-trigger path reads a single clause, {field, operator, value} (AnnotationNotificationDispatcher::createdFilterMatches). This rule passed a field=>value map — the shape the SCHEDULED path takes — so the dispatcher looked up $filter['field'], found nothing, and returned false for every reaction ever created. Moderators have never been told a reaction is waiting. Rewritten as a clause. Verified against the dispatcher's own semantics: a pending reaction notifies, an approved one does not, and a reaction with no moderationStatus does not. Found by a fleet sweep of created-trigger filters after the same class of defect turned up in 24 scheduled filters (ConductionNL/openregister#2787). * fix(notifications): updated triggers carried a filter the engine ignores 15 updated-trigger rules declared a `filter`. The dispatcher does not read `filter` on an updated trigger — it reads `condition`, and its own comment records the consequence: "condition-less `updated` rules match on type alone (back-compat)". `filter` is consulted only for `created` triggers. So every one of these fired on EVERY update to the object, not on the state change they name. Anyone subscribed has been notified for each edit. This is the opposite failure from the silent ones fixed elsewhere in this sweep: not too quiet, far too loud. Each is rewritten as a condition. Where the lifecycle admits exactly one predecessor for the target state, `from` is included as well, so the rule fires on the transition itself rather than on any update while the state holds — 10 of the 15 qualify. mvIngepland and geheimhoudingOpgeheven have two possible predecessors and the grammar takes a single `from`, so they use equals alone and say so in a _note. consultationBesluitAfwijkend and the two Transcript rules have no lifecycle on that field. Three further rules in the same register were dead for an adjacent reason — an operator fieldChangeConditionMatches does not implement (it has only `changed` and `equals`, and an unknown operator falls through to false): - decisionSuperseded, decisionRepealed: isNotEmpty -> changed, which is a faithful reading of "a link was set". - outcomeEmitted: `in` has no equivalent. Left declared with a _note rather than split into three near-identical rules, because the scheduled path gained `in` in ConductionNL/openregister#2794 and the updated path should follow; splitting now would only have to be undone. Refs #849 --------- Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
…ender (#852) * 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): move the app id, namespace and bootstrap from decidesk to decidiq Phase-3 of the decidesk -> decidiq rename: appinfo/info.xml <id>, <namespace>, navigation id and route name; the composer PSR-4 autoload prefix; the npm package name; Application::APP_ID; and the OCA\Decidesk -> OCA\Decidiq namespace across AppInfo. Registers MigrateAppConfigKeys and MigrateUserPreferences under BOTH <install> and <post-migration>, ahead of InitializeSettings. The ordering is load-bearing: InitializeSettings mints a fresh voter_token_secret when it finds none, and that value is the HMAC key signing every voting token and mail-reply link, so it has to run after the copy or it silently invalidates every outstanding vote link. The MCP provider container alias moves to IMcpToolProvider::decidiq. OpenRegister builds that lookup key as '...IMcpToolProvider::' . $appId over the installed apps, so a stale suffix is not cosmetic -- the provider is never discovered and all five tools disappear without an error. Deliberately frozen here: the 'decidesk' OpenRegister register slug, and the decidesk-decisions integration leaf id (LeafRegistry validates only the id shape, never an app-id prefix, and the id is named in the REQ-DCDH-008 requirement heading that six @SPEC anchors dereference). * feat(rename): carry appconfig and per-user preferences across the app-id rename A rename IS a data migration. oc_appconfig and oc_preferences are namespaced by app id, so renaming <id> does not rename the rows -- it makes the app ask for its data under a name nothing answers to. MigrateAppConfigKeys enumerates IAppConfig::getKeys() (exhaustive by construction), skips the Nextcloud-reserved keys, and carries the SENSITIVE flag across the copy. Dropping that flag would print voter_token_secret -- the HMAC key signing every vote token -- in cleartext in occ config:list and in every support dump. MigrateUserPreferences walks IUserManager::callForSeenUsers() and asks IConfig::getUserKeys() per user. It deliberately never enumerates by value: PreferencesController stores under an open-ended 'pref_' . $safeKey namespace sanitised only to [a-z0-9-]{1,64}, so neither the keys NOR the values are knowable up front and a getUsersForUserValue() implementation would migrate nothing while reporting success. A hardcoded key list would be equally incomplete. A test pins the choice by asserting the value-enumerating call is never made. Every read sits INSIDE the try alongside the write. Both steps are registered under <install>, where a throwing repair step does not merely fail an upgrade -- the app never enables and every route goes with it. The tests were verified to be capable of failing: emptying RESERVED_KEYS, moving a read outside the try, dropping the sensitive flag, and switching the preference walk to value-enumeration each turned the relevant test red, and all four were then restored. * feat(rename): move lib/Repair to the decidiq namespace, freezing the register slug Renames the namespace, class names and files (RenameDutchDecideskValues -> RenameDutchDecidiqValues and its Decisions twin), and fixes each renamed file's CONTENTS -- namespace, imports, class name -- not just its path. Fixes a real trap in InitializeSettings: it read and wrote voter_token_secret against the bare literal 'decidesk', which is the APP-CONFIG NAMESPACE and not the OpenRegister register slug. Two different 'decidesk' literals that grep cannot tell apart. Left alone it would have kept reading the secret under the pre-rename namespace while VotingService looked under the new one. It now uses Application::APP_ID, so it cannot drift again. Frozen with an explanatory comment at each definition site, because a future reader would otherwise 'finish the job' and orphan the data: - RenameDutchVocabularyColumns::REGISTER_SLUG - RepointConflictOfInterestBoardMember::REGISTER - MigrateBoardProxyToProxyAuthorization::REGISTER All three are the OpenRegister register slug. OpenRegister matches registers by slug, so a renamed slug resolves no register and the step reports 'nothing to do' over data it was meant to migrate. The @SPEC anchors into openspec/changes/archive/ stay byte-identical: the archive is history and the anchors still resolve against it. * wip(rename): sweep decidesk -> decidiq across lib, src, tests and docs Checkpoint commit of in-flight rename work left uncommitted by a previous session. Includes the Activity provider rename (DecideskProvider -> DecidiqProvider). Not yet verified end to end. * refactor(rename): move the PHP namespace and class names to Decidiq Converts OCA\Decidesk -> OCA\Decidiq across lib/ and tests/ (171 files still declared the old namespace, which composer's PSR-4 map no longer resolves), plus class names, log prefixes and prose. FROZEN: the Nextcloud Files folder root stays 'Decidesk'. MeetingFolderService and BoardEvaluationReportService keep $segments = ['Decidesk'], and every Decidesk/-rooted path literal in seeds, schema examples and tests stays with it — renaming it would create a new empty folder and strand every existing meeting document, silently. * refactor(rename): move the app id, l10n domain, URLs and bundle names to decidiq Covers src/, templates/, webpack, docs/, CI workflows and all 38 l10n files. - webpack emitted decidesk-*.js while Util::addScript already asked for decidiq-* — the whole bundle 404'd. appId is now decidiq. - ~490 t('decidesk', ...) l10n call sites moved, and every l10n msgid KEY moved with them (a stale key silently renders untranslated English). - VoterTokenSecret read the HMAC secret from the OLD appconfig namespace; it would have minted a replacement over the migrated key and invalidated every outstanding ballot link. FROZEN: docs host decidesk.conduction.nl (measured: the old host answers 200, decidiq.conduction.nl does not resolve), the OpenRegister register slug, MCP tool ids, RBAC group ids, the dashboard widget id, X-DECIDESK-* iCal properties and the Files folder root. * fix(rename): move app-id-derived identifiers that had gone stale The app id already read decidiq, but a set of identifiers derived from it did not move with it. Each fails quietly rather than loudly: - 11 appconfig call sites still read/wrote under the 'decidesk' namespace, so every one of them returned its DEFAULT after the migration copied the rows — chair_group, motion_min_cosigners, the participation catalog and two HMAC secrets among them. - AdminSettings::getSection() named a settings section that no longer exists. - linkToRoute('decidesk.dashboard.page') would throw; imagePath() and the /apps/decidesk/ deep links would 404. - 4 notification setApp() ids and 2 eIDAS return paths. FROZEN with comments: the dashboard widget id (NC stores per-user widget layout under it in the dashboard app's namespace, which our repair steps cannot reach) and X-Decidesk-Export-Sha256 (a response wire header regulator clients read by name; an unrecognised header reads as absent, not as an error). * wip(decidiq): checkpoint in-flight rename work before session limit * fix(register): point x-openregister.app at the new app id The register descriptor attributes the register to an owning app through x-openregister.app. It was left on the old app id when the id moved, so the descriptor claimed ownership by an app that no longer answers to that name. Safe to move now: these instances are development-only, so there is no live register whose attribution could be split. The register SLUG is deliberately NOT touched here — that is the key objects are stored against, and it is a separate decision from attribution. Other apps' ids appearing in the same file (e.g. opencatalogi) are cross-app references and stay as they are. * fix(tests): assert register attribution against APP_ID; reflow after rename Two PR-run failures. PHPUnit: RegisterJsonTest hardcoded 'decidesk' as the expected x-openregister.app. When the descriptor moved to the new id, the TEST became the stale half and reported the correct descriptor as a failure. It now asserts against Application::APP_ID, so the two cannot drift apart again. Frontend format: 'decidiq' is shorter than 'decidesk', so lines that had been wrapped now fit and prettier rejoins them. Measured 24 files failing on this branch against 2 on development, so 22 are rename reflow and 2 pre-existing; the project's own format:fix clears all of them (35 insertions, 80 deletions -- a reflow, not a reformat). * fix(l10n): restore the 38 translation artifacts the branch had deleted The most dangerous thing found in this rename. The branch had deleted ALL 38 l10n/*.js files, plus scripts/build-l10n-js.js and the l10n:build / check:l10n-js npm scripts that maintain them. Those .js files are what Nextcloud actually SERVES to the browser; the .json files are only their source. Merging this would have shipped an app with no translations in any language -- and the l10n check that reads the JSON would have stayed green throughout, because it never looks at the artifact. It surfaced only because check:l10n-js is a shared frontend check from .github@main, and the job failed with "npm script does not exist" rather than with anything about translations. Restored the generator, both scripts and all 38 artifacts, then repointed the locale data: the l10n KEY is the English source string, so renaming a user-visible string renames its key. 572 entries across 38 locales moved to the new product name, values included -- a straight substitution of the proper noun preserves each language's declension (Decidesk-gebeurtenissen -> Decidiq-gebeurtenissen). Artifacts regenerated from that JSON, and they now register the decidiq domain; on the old domain no translation would resolve at all. Note on .gitignore: l10n/nl.js matches '!**/*.js', a NEGATION. git check-ignore prints the rule and exits 0 either way, so the rule text -- not the exit code -- is what says these files are tracked. Local: l10n OK, l10n-js up to date, prettier 0, no deletions against development. * fix(spec): tag the 27 methods the reflow put back in gate-16's scope gate-16 is diff-scoped, so the prettier reflow from the previous commit -- which only rewrapped lines -- pulled 27 frontend methods back into 'changed' and surfaced their long-standing missing @SPEC. Each now cites the requirement it implements: dashboard widgets to the widget requirements, DecisionRouteTab to declarative route progress, ConsultationReactionsTab to the reaction moderation queue, deckProjection to decision list and search. Two corrections to my own first pass, both caught before pushing: - The tagger inserted a docblock INSIDE the Vue template of PendingVotesListWidget, around the countdownLabel(round) call rather than its definition. Restored, then tagged the method itself. - It cited openspec/specs/consultation-management, which does not exist. gate-46 dereferences @SPEC targets, so it failed loudly -- repointed at citizen-participation#requirement-reaction-moderation-queue, the spec that actually describes that surface. Local: gate-16 0, gate-46 0, eslint 0, prettier 0, 367/367 vitest, l10n OK, l10n-js up to date. * fix(spec): tag voteTitle at its definition, not inside the template Same tagger bug as countdownLabel: the docblock landed inside the Vue TEMPLATE around the voteTitle(round) CALL rather than above the method. gate-16 reads definitions, so it still counted the method as untagged -- and the template carried a stray comment. Both corrected. * fix(e2e): the walkthrough-seen key is app-id scoped ~40 specs cascade-failed with 'locator.click: Test timeout', every one of them reporting the same interceptor: <div class="cn-walkthrough__dim cn-walkthrough__dim--full"> from <div role="dialog" aria-label="Welcome to Decidiq"> subtree intercepts pointer events global-setup seeds localStorage to mark the first-visit tour as seen, and CnAppRoot reads that under 'cn-walkthrough-seen:<appId>'. The key still said decidesk, so after the rename the app looked up decidiq, found nothing, and auto-started the tour whose full-viewport dim overlay swallows every click. The comment directly above the line already predicted the symptom -- '~40 specs cascade-fail' -- which is exactly what the run showed. The key is app-id scoped, so it moves with the id. E2E passes on development and failed here, so this was rename damage, not a pre-existing failure. No other app-id-keyed browser storage in this suite; buildiq, thematiq and stackiq have no walkthrough key at all. * test(experiment): revert x-openregister.app to decidesk to isolate the goals failure EXPERIMENT, not a conclusion. 'Goals: index lists all five seeded goals' fails reproducibly on this branch (re-ran the job; it failed identically) and passes on development, and I could not explain it from the diff: - the goals manifest fragment src/manifest.d/organisation-goals.json is UNCHANGED and its registers correctly read decidesk - lib/Settings/register.d/66-organisation-goals.json changed only in a prose _note - the goal SCHEMA imports (ci-seed lists it among schemas present) - the page mounts: the cn-index-page assertion passes, only the rows are absent - other seeded rows DO render — 'meetings list shows multiple meeting rows' passes That leaves the app ATTRIBUTION as the only functional difference in the import path: SettingsService calls importFromApp(appId: Application::APP_ID), which is now decidiq, and the register descriptor's x-openregister.app was changed to decidiq in the same branch. If goals render with this reverted, the field is load-bearing at import time and NOT the pure metadata I described when it was authorised — which would also explain dossiq #1333, whose only change is that same field and whose E2E fails the same way. If they still fail, this is ruled out and the field goes back to decidiq. * fix: x-openregister.app is load-bearing — it stays decidesk The experiment answered it. With x-openregister.app set to 'decidiq', 'Goals: index lists all five seeded goals' failed twice in a row (58.9s, burning the full 45s row timeout). Reverting that ONE line to 'decidesk' — nothing else changed — turned E2E green. So the field is not the descriptive metadata it looks like. It participates in resolving which register the register.d fragment seedData is imported into: the goal SCHEMA still imported (ci-seed listed it), the page still mounted, and other fragment-seeded objects still rendered (facets-decision-detail reads a toezegging from fragment 45) — only the goal OBJECTS never arrived. RegisterJsonTest now pins the frozen literal instead of Application::APP_ID, with that measurement recorded next to it. I had changed it to track APP_ID earlier on the reasoning that attribution 'must follow the app id'; that reasoning was wrong, and the test would have re-broken this the moment someone trusted it. The register slug and its attribution move together, and neither moves with the app id. --------- Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
This is the app the freeze came from, so it is the one that has to change its
own record.
`tests/Unit/RegisterJsonTest.php` pinned `x-openregister.app` to `decidesk` on
the strength of a controlled experiment: with that one field on the new app id
the seeded Goal objects stopped appearing on the Goals index, twice in a row,
and came back the moment it was reverted. The observation was right. The
conclusion drawn from it — that the field, and the register slug with it, could
not move — was one step too far.
The mechanism is now known rather than inferred. For a `type: application`
configuration, ImportHandler::autoCreateRegisterIfApplication() reads
`$slug = $xOpenregister['app'] ?? $appId`: the field IS a register slug. Moving
it alone pointed the import at a register that did not exist, and OpenRegister's
not-found branch CREATES an empty one rather than failing — which is exactly the
empty Goals index that was observed. What makes it movable is renaming the
register ROW first, which MigrateRegisterSlug now does ahead of
InitializeSettings in both hooks.
The comment has been rewritten rather than deleted, and it keeps asserting the
LITERAL rather than Application::APP_ID — pinning it to the constant would
re-break this the next time an app id moves without its register.
WHY IT MOVES NO DATA. Measured: an object is bound to its register by NUMERIC id
— `_register` in every shard table, and the tables are named
`oc_openregister_table_<registerId>_<schemaId>`.
Two sweep patterns showed up here for the first time and are now part of the
tooling: Postman `path` ARRAYS (`"objects","decidesk","meeting"` — 94 of them,
and Postman builds the request from the array, not from `raw`), and
`setRegister('decidesk')` (63). A plain-URL grep sees neither.
Still frozen: the dashboard widget id `decidesk` (per-user layout), the MCP tool
provider's app id (it namespaces every tool id), `decidesk.*` event names, the
`decidesk_*` SBOM bom-refs, the `x-decidesk-*` schema extension keys, the docs
host, and `lib/Settings/decidesk_register.json`'s FILENAME — app-owned and
movable, but not by this PR.
PHPUnit 1177 passed · vitest 367 passed · PHPCS 0 errors · PHPMD clean ·
Psalm 0 errors · PHPStan 0 errors · gate-16 count=0 · gate-46 clean.
The coverage ratchet caught these on larpinq — 94.96% head vs 97.13% base, −2.17% — and it was right: the two catch blocks in migrateStoredSlugValues() had no test at all. Both matter more than an ordinary catch. This step is registered under <install>, where an escaping exception aborts the install and the app never enables, so "IAppConfig threw" must mean leave the value alone rather than take the upgrade down with it. The write branch also has to keep the summary count honest: a write that failed is not a value re-pointed. Applied to all five apps in the series so the same ratchet does not fail them one at a time.
The E2E and Newman legs both failed at "Seed test data", and the seed log shows
the import itself worked perfectly:
[ci-seed] registers present: [... 'larpinq' ...]
::error::Larpinq registers missing after import: ['larpingapp']
The register was created under its new slug and the script went looking for the
old one. Same shape in every app in the series: a hard-coded slug in the
post-import assertion, in the `registers:` list the fallback importer sends, and
in `appId=` on the OpenRegister importer call — the last of which is the schema
`application` value, so it has to match what the app's own SettingsService
passes (Application::APP_ID).
Left alone on purpose, because these are not the register slug:
- `lib/Settings/<old>_register.json` — the FILE name, app-owned but not moved
by this PR;
- `decidesk-action-items` — a VTODO calendar URI, already on users' calendars;
- `oc_openconnector_*` table names in the explanatory prose;
- the `<old>.conduction.nl` docs hosts.
…the old slug
The Newman leg failed with every request hitting
`/apps/openregister/api/objects/<old-slug>/<schema>` even though no URL in the
collection names a slug: they are all built from `{{register}}`, and the variable
was defined once as
{ "key": "register", "value": "<old-slug>" }
That is a seventh distinct syntax a register slug hides behind, and the one that
matters most, because ONE definition silently drives every request in the file.
A grep for the plain URL finds nothing, and the diff of a swept collection looks
complete.
Now a sweep rule, so the remaining apps get it without another CI round.
Eighth syntax, and the reason the E2E leg failed while the diff looked complete:
const OR_OBJECTS = `${NC_URL}/index.php/apps/openregister/api/objects/hrmq`
The sweep rule for object URLs required a trailing slash — `objects/<slug>/` —
because every occurrence found so far had the schema right after it. Here the
slug ends the constant and the schema is appended at the call site, so the rule
matched nothing and every request built from the constant kept hitting the old
register. Playwright reported it as `expect(listed.ok()).toBeTruthy()` failing,
which reads as a broken assertion rather than a missed rename.
The rule now accepts a slash, a quote, a backtick, whitespace or end-of-line
after the slug. Re-scanning the whole series on it found 11 more in integriq
(including the Postman `orBase` variable) and 7 in decidiq.
`Integration Tests (Newman)` failed with 404s on
`objects/decidesk/participatory-budget` and `objects/decidesk/budget-proposal`,
while `apps/decidiq/api/...` in the same run answered 200 — the app id had moved
and the register slug had not followed in the test data.
The slug was not sitting in the URLs. It comes from a Postman ENVIRONMENT
variable, `register`, in decidiq-environment.json, which every request
interpolates as {{register}}; a grep for `objects/decidesk` across the
collections finds nothing at all. The other eight were escaped inside raw
request bodies (`\"register\": \"decidesk\"`), which is invisible to a search
for the plain string.
DELIBERATELY LEFT ALONE — every other `decidesk` under tests/ is something else
wearing the same word: `decidesk-no-such-group` and
`nobody-matches-this@decidesk-test.invalid` are fixtures chosen precisely
BECAUSE nothing matches them, `decidesk-admset-nonadmin` and
`decidesk-proccfg-nonadmin` are test usernames, and `decidesk#443` is a
historical issue reference. Renaming any of them would be churn, and renaming
the first two would quietly weaken the negative assertions they exist for.
All collections still parse.
…o-decidiq feat(register): rename the register slug decidesk -> decidiq
…#858) * test(e2e): pin the browser-catalogue contract from the browser's side 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. * style(e2e): prettier-normalise the browser-catalogue spec 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. * fix(e2e): ask the instance where the app is served, do not assume The spec fetched /custom_apps/<app>/l10n/<locale>.js. That is right on a dev box, where apps are bind-mounted under custom_apps — and wrong in CI, which checks the app out under apps/. So it failed for a reason that had nothing to do with the catalogue. It now reads OC.appswebroots[appId] from the running instance and fetches relative to that. The assertion gets stronger rather than weaker: an app that does not resolve at all has no webroot entry, which is how the decidesk -> decidiq mount drift surfaced. --------- Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
The landing page shipped a button sending visitors to codeberg.org. GitHub is the only host we publish to, so the link opened a repository we no longer read. Now points at https://github.com/ConductionNL/decidiq.
* test(l10n): ratchet the untranslated schema strings Every string inside a form comes from the OpenRegister schema, not from the manifest: `fieldsFromSchema()` runs a property `title` and `description` through the injected `cnTranslate`, which CnAppRoot binds to THIS app's id. So a schema title is a key in THIS catalogue — and when the key is absent, `t()` hands the source string back and the field renders in English inside an otherwise translated form. Nothing errors, and no existing check looks. Measured across the fleet on 2026-08-23: 30,459 schema strings had no catalogue key. Far too much to translate in one pass, and the descriptions need rewriting for the person filling in the form before translating them is even worth doing — humaniq's own pass rewrote 592 of 739 before a word was translated. So this is a RATCHET, not a gate: it records how many strings are currently uncovered and fails only when that number GROWS. The debt is measured and cannot expand, while burning it down stays an ordinary PR. Same shape as the JSDoc baseline in @conduction/nextcloud-vue. Counted: schema titles, property titles, property descriptions, and the VALUES of `x-enum-labels`. NOT counted: enum values themselves (stored contract values, several non-English by design, never rendered once a property declares its labels) and `x-notes` (engineering rationale, never rendered). Verified must-fail: adding one untranslated title takes the count past the baseline and exits 1, naming the file and property and the command that lists what is uncovered. Lower the baseline as strings get translated: npm run check:schema-l10n -- --update * fix(l10n): the baseline file is not a locale catalogue; format for this repo Two things the fleet CI caught. `build-l10n-js.js` discovers locales by globbing `l10n/*.json`, which now also matches `l10n/.schema-l10n-baseline.json` — the ratchet's own state file, kept there so prettier ignores it. The generator read it as a locale named `.schema-l10n-baseline` and exited 1 for having no `translations`. Dotfiles are never locale catalogues, so it skips them. Also prettier-normalised both scripts to this repo's config; several apps run a format check over scripts/. --------- Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
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: Ruben van der Linde <juan.claude@conduction.nl>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Bumps [phpstan/phpstan](https://github.com/phpstan/phpstan-phar-composer-source) from 2.2.8 to 2.2.9. - [Commits](https://github.com/phpstan/phpstan-phar-composer-source/commits) --- updated-dependencies: - dependency-name: phpstan/phpstan dependency-version: 2.2.9 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.29.7 to 8.0.1. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v8.0.1/packages/babel-core) --- updated-dependencies: - dependency-name: "@babel/core" dependency-version: 8.0.1 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.2.6 to 4.1.11. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.11/packages/vitest) --- updated-dependencies: - dependency-name: vitest dependency-version: 4.1.11 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
#895) Bumps [node-polyfill-webpack-plugin](https://github.com/Richienb/node-polyfill-webpack-plugin) from 3.0.0 to 4.1.0. - [Release notes](https://github.com/Richienb/node-polyfill-webpack-plugin/releases) - [Commits](Richienb/node-polyfill-webpack-plugin@v3.0.0...v4.1.0) --- updated-dependencies: - dependency-name: node-polyfill-webpack-plugin dependency-version: 4.1.0 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [sass](https://github.com/sass/dart-sass) from 1.102.0 to 1.103.1. - [Release notes](https://github.com/sass/dart-sass/releases) - [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md) - [Commits](sass/dart-sass@1.102.0...1.103.1) --- updated-dependencies: - dependency-name: sass dependency-version: 1.103.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) from 3.2.6 to 4.1.11. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.11/packages/coverage-v8) --- updated-dependencies: - dependency-name: "@vitest/coverage-v8" dependency-version: 4.1.11 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [eslint](https://github.com/eslint/eslint) from 10.8.1 to 10.9.0. - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](eslint/eslint@v10.8.1...v10.9.0) --- updated-dependencies: - dependency-name: eslint dependency-version: 10.9.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [gridstack](https://github.com/gridstack/gridstack.js) from 12.6.0 to 13.2.0. - [Release notes](https://github.com/gridstack/gridstack.js/releases) - [Changelog](https://github.com/gridstack/gridstack.js/blob/master/doc/CHANGES.md) - [Commits](gridstack/gridstack.js@v12.6.0...v13.2.0) --- updated-dependencies: - dependency-name: gridstack dependency-version: 13.2.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [dexie](https://github.com/dexie/Dexie.js) from 4.4.4 to 4.4.5. - [Release notes](https://github.com/dexie/Dexie.js/releases) - [Commits](dexie/Dexie.js@v4.4.4...v4.4.5) --- updated-dependencies: - dependency-name: dexie dependency-version: 4.4.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [@vue/compat](https://github.com/vuejs/core) from 3.5.40 to 3.5.41. - [Release notes](https://github.com/vuejs/core/releases) - [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md) - [Commits](vuejs/core@v3.5.40...v3.5.41) --- updated-dependencies: - dependency-name: "@vue/compat" dependency-version: 3.5.41 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
… resolved (#904) #886 added a slug→UUID resolver for `urgencyPolicy.ratifyingBody` and I reported it as fixing the last two template migrations. IT DID NOT. Re-running the step on a live instance after the merge, both failures were still there: Failed to migrate process-template 8d3460b3-…: Property 'urgencyPolicy.ratifyingBody' should match format 'uuid' but 'gemeenteraad-amsterdam' does not. The resolver filtered `['slug' => $slug]`. A seeded `slug:` key is an IMPORT-TIME IDENTIFIER that OpenRegister keeps in `@self` metadata — it is not a stored object property. Measured on the live instance: filters ['slug' => 'gemeenteraad-amsterdam'] -> 0 rows scan of all 60 governance bodies for a `slug` FIELD -> none carry one filters ['@self' => ['slug' => 'gemeenteraad-amsterdam']] -> 1 row So the lookup returned null every time, and my deliberate "leave the slug as is rather than blank it" fallback then re-emitted the original error — which is why the symptom was unchanged and looked like the fix simply had not deployed.⚠️ WHY I SHIPPED IT ANYWAY. The 16 unit tests pass either way: the fake answers whatever shape the query asks for, so a filter naming a field that does not exist matches the fixture just as well as the right one. I verified `anonymousFailures=0` — the IDENTITY half — and took the format half on trust because the tests were green. A fake cannot tell you that you are querying a field the real store does not have. Live-verified after the change: the step now completes with NO failures of either kind. All 14 legacy templates migrate. 16 tests green, phpcs 0 errors. Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
Bumps [sass-loader](https://github.com/webpack/sass-loader) from 16.0.8 to 17.0.0. - [Release notes](https://github.com/webpack/sass-loader/releases) - [Changelog](https://github.com/webpack/sass-loader/blob/main/CHANGELOG.md) - [Commits](webpack/sass-loader@v16.0.8...v17.0.0) --- updated-dependencies: - dependency-name: sass-loader dependency-version: 17.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* chore: ignore agent/test scratch and untrack generated files
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 test-results/
`.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.
* chore: re-trigger CI
The checks on this PR are the 2026-08-25 21:5x runs, which failed on a GitHub
infrastructure fault, not on this change:
Failed to download action 'shivammathur/setup-php' ...
Error: Name or service not known (internal-api.service.iad.github.net:443)
`gh run rerun` refuses those runs ('cannot be rerun; its workflow file may be
broken' — the reusable-workflow case), and closing/reopening the PR did not
replace the recorded check entries because the head SHA was unchanged. An empty
commit moves the SHA, which is the only thing that gets a fresh verdict.
The diff is unchanged: this commit adds nothing.
---------
Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
* fix(manifest): drop 8 em-dashes from user-visible copy Found by gate-96 (manifest-copy-style, ConductionNL/.github#581). Two tour steps, four sidebar-integration descriptions and two empty states. All were already real user copy, so the meaning is unchanged. Four of them said "surface on the body", which is our word for it and not the reader's. Those now say "appear on the page itself", paired with "stay in the sidebar" so the sentence actually tells you where to look: "External integrations linked to this meeting. The Action items board (Deck), Discussion (Talk), files and notes appear on the page itself; linked articles, tags and the audit trail stay in the sidebar." The empty states lost "the public Participation surface" for "the public Participation page", same reason. CHECKED, NOT ASSUMED: humaniq's sibling fix broke `check:l10n` because that app asserts every manifest string has an en/nl key, so rewriting the English orphaned its Dutch. decidiq is different, and I verified rather than guessing: `test:l10n` PASSES here, and `test:l10n:parity` fails identically on development and on this branch (+553 missing on both, none of them mine). That 553 is real pre-existing translation debt, but it is not in this repo's frontend-checks list and this change neither causes nor worsens it. Verified: gate-96 0 findings over 943 strings, check:manifest PASS, test:l10n PASS, check:l10n-js PASS, check:schema-l10n PASS. * test(e2e): match the rewritten integration-surface copy The copy fix in the previous commit changed this description from "... linked Emails, files and tasks surface on the body; notes, tags and the audit trail remain in the sidebar." to "... Linked emails, files and tasks appear on the page itself; notes, tags and the audit trail stay in the sidebar." and `integration-surfaces.spec.ts:267` asserted the old wording verbatim, so it failed. 1 failed, 141 passed. My change, correctly caught. Narrowed the pattern to `/linked emails, files and tasks/i` rather than re-pinning the new sentence in full. The assertion's job, per its own comment, is to prove the AGENDA-ITEM page's copy rendered and not another surface's. The discriminator is the LIST of surfaces, which really is unique per page: meeting -> Deck + Talk + files + notes dossier -> emails + Deck + files agenda -> emails + files + tasks The verb after it ("appear on the page itself") is shared by all three, so pinning it added no discriminating power while guaranteeing this test breaks again on the next copy edit. Verified both surviving patterns still match EXACTLY ONE description each, so the test can still fail if the wrong surface renders. A pattern that matched two would have gone green while testing nothing. Line 206's pattern needed no change: it is case-insensitive and stops before the words that moved, which is why only one of the two failed.
… resolved (#913) #886 added a slug→UUID resolver for `urgencyPolicy.ratifyingBody` and I reported it as fixing the last two template migrations. IT DID NOT. Re-running the step on a live instance after the merge, both failures were still there: Failed to migrate process-template 8d3460b3-…: Property 'urgencyPolicy.ratifyingBody' should match format 'uuid' but 'gemeenteraad-amsterdam' does not. The resolver filtered `['slug' => $slug]`. A seeded `slug:` key is an IMPORT-TIME IDENTIFIER that OpenRegister keeps in `@self` metadata — it is not a stored object property. Measured on the live instance: filters ['slug' => 'gemeenteraad-amsterdam'] -> 0 rows scan of all 60 governance bodies for a `slug` FIELD -> none carry one filters ['@self' => ['slug' => 'gemeenteraad-amsterdam']] -> 1 row So the lookup returned null every time, and my deliberate "leave the slug as is rather than blank it" fallback then re-emitted the original error — which is why the symptom was unchanged and looked like the fix simply had not deployed.⚠️ WHY I SHIPPED IT ANYWAY. The 16 unit tests pass either way: the fake answers whatever shape the query asks for, so a filter naming a field that does not exist matches the fixture just as well as the right one. I verified `anonymousFailures=0` — the IDENTITY half — and took the format half on trust because the tests were green. A fake cannot tell you that you are querying a field the real store does not have. Live-verified after the change: the step now completes with NO failures of either kind. All 14 legacy templates migrate. 16 tests green, phpcs 0 errors. Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
* feat(nav): a Flows surface in this app, on the shared page types ADR-110 Decision 4. A flow is app-specific — it operates on this app's objects — so the authoring surface belongs here rather than behind a deep link to another app's list. The ENGINE stays single (ADR-065): these pages are a scoped view onto OpenRegister's one native flow store, not a per-app store. Two manifest pages and one settings entry, no component files: `type: "flows"` and `type: "flow-detail"` are shipped page types in @conduction/nextcloud-vue 2.19.0, scoped by `config.app`. Note the layout of the diff: entries are appended textually rather than by reserialising the manifest. A `json.dump` round-trip rewrote pipelinq's file as a 3,950-line diff for a 20-line addition — correct output, unreviewable change. * build(deps): @conduction/nextcloud-vue 2.19.0 for the flows page types Required by the manifest change: `type: "flows"` / `type: "flow-detail"` are rejected by the compiled validator in earlier versions, and CI installs with `npm ci` — so the LOCK is what decides, not the `^2.x` range. Several of these locks were pinned many minors back, which is why some lockfile diffs are large: npm restructures the nested tree (mostly @esbuild platform binaries under @nextcloud/vue) to satisfy 2.19.0's peers. No direct dependency other than @conduction/nextcloud-vue changes. * chore(tests): refresh the vendored manifest schema to 2.25.0 The manifest gate resolves its schema from `tests/schemas/` FIRST, ahead of node_modules — deliberately, so the gate is self-contained and does not depend on a fresh install. The cost is that the copy freezes: this one was 12 minors behind, so the gate could not see any page type, widget or field the library had added since, and reported PASS the whole time because nothing it knew about was wrong. Refreshed from the installed 2.19.0 (schema 2.25.0) so `type: "flows"` and `type: "flow-detail"` validate. Worth noting for whoever owns this gate: four apps vendor this file and all four had drifted — 2.12.0, 2.13.0, 2.13.0 and 2.22.0 against a library at 2.25.0. A copy with no refresh step is a gate with a silent expiry date. --------- Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Those were the only conflicts. The merge keeps BETA's version string (`1.0.2-beta.20260821043705`) rather than development's (`1.0.1-unstable.20260826225043`). 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.
rubenvdlinde
requested review from
Rem-Dam,
SudoThijn,
WilcoLouwerse,
bbrands02,
remko48 and
rjzondervan
as code owners
August 27, 2026 07:11
Contributor
Quality Report — ConductionNL/decidiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| check-nav-ceiling | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-l10n-js | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| composer | ✅ | ✅ 104/104 | |||
| npm | ✅ | ✅ 557/557 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-27 09:01 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.