Scholiq becomes Learniq: declared RBAC, reachable Compliance, and the rename - #452
Conversation
Specs only; no product code. - rbac-declare-groups: 118 schemas declare no authorization OpenRegister reads, while 60 rules sit under x-openregister-authorization and x-property-rbac, keys no OR code path consumes. Declares the real blocks plus the eight unprefixed groups GroupProvisioner creates. - fix-dead-role-gates: 6 of 10 role literals in manifest visibleIf gates can never be produced; 17 items are silently admin-only and Compliance has no door for an administrator at all. - rename-to-learniq: scholiq -> learniq across 479 files, plus the register slug and two silent cross-app couplings (hermiq, pipelinq). - manifest-fragment-split / menu-six-main-items: ADR-044 prerequisite and the 23-to-6 navigation collapse.
… scope map lib/Settings/scholiq_register.json gains: - Register-level authorization cascade (staff-only read-write role, no learners grant) governing the 76 Tier-2 learner-attributed schemas by default. - Schema-level authorization on 21 Tier-1 sensitive-data schemas across four narrow profiles (A/B/C/D) — no learners in any profile. - Schema-level authorization on 21 Tier-3 catalogue schemas across two write profiles (3a excludes instructors/hr, 3b includes them) — read is authenticated (wide, learners included), deliberately wider than the Tier-2 cascade. - components.securitySchemes.oauth2 scope map declaring all eight canonical group ids so OpenRegister's GroupProvisioner creates them. - Removal of the 20 dead x-openregister-authorization decoy keys (never read by any OpenRegister code path) — 9 replaced by real blocks, 11 deleted outright to fall back to the register cascade. Closes instance-wide open read/write access on both the catalogue and learner-attributed populations. Does not yet close per-learner scoping of learner-attributed records (tracked separately per design.md).
Import re-run on localhost:8080 provisioned all eight canonical groups (28 -> 36 total groups, zero canonical matches before, all eight present after, each zero members). Paired read test (TC-7) on a single learners-only user: Course read succeeds (200), a different learner's GradeEntry read and create are both refused (404/403). Tier-1 refusal/admission pair (TC-3/TC-4) confirmed on DossierNote with a no-group user and an instructors-group user. Note: RBAC-denied single-object reads surface as 404, not 403 - OpenRegister's ObjectsController::show() deliberately remaps the internal NotAuthorizedException (403-shaped) to 404 to avoid leaking object existence to an unauthorized caller. This is documented, intended behaviour in OpenRegister, not a defect in this change.
Observed live: a learners-only user reading another learner's GradeEntry gets 404, not the 403 the scenarios named. OpenRegister's ObjectsController remaps a read denial deliberately - 'Mirror show(): 404, not 403/500, so denial reveals nothing'. Writes still surface as a true 403. The positive control is what makes a masked 404 readable as a refusal: admin reads the same object (d4caa278) with 200 and a body, so the learner's 404 is a denial rather than an absence. Without that pair the two are indistinguishable, and the test would pass against an empty register.
… vocabulary DashboardRoleService::resolvePrimaryRole() could only ever emit six values (admin-first) while src/manifest.json named ten role literals across 24 visibleIf.user.primaryRole gates — six of the manifest's literals (coordinator, finance, mentor, parent, principal, teacher) could never be produced, silently locking 17 role-delegated menu items to admin-only and hiding Compliance and BookConferenceSlotsMenu from admin entirely. - DashboardRoleService: GROUP_BACKED_ROLES becomes an explicit role => unprefixed-group-id map (compliance-officer, hr, administration-manager, team-lead, coordinator, instructor, guardian), pointed at the groups rbac-declare-groups provisions instead of the never-existing scholiq-* prefix. resolveViews() updated in lockstep; learner stays the unconditional, non-group-gated fallback. - manifest.json: 13 of 24 visibleIf.user.primaryRole gates corrected onto the canonical vocabulary (teacher->instructor, principal/manager-> administration-manager, mentor->team-lead, parent->guardian), admin added to the two admin-invisible gates (Compliance, BookConferenceSlotsMenu), finance dropped from the four Payments gates per ADR-081. - DashboardRoleServiceTest: vocabulary updated, new coverage for administration-manager/team-lead/coordinator/guardian plus the no-privileged-group refusal case. openspec/changes/fix-dead-role-gates, Tasks 1-4.
…oles Verified live on localhost:8080 with a named user per group-backed role (instructors, coordinators, guardians, team-leads, hr, compliance-officers, administration-managers) plus a no-privileged-group learner and admin, reading server-resolved runtime.user.primaryRole and the rendered nav DOM for the exact 24-item role-gated checklist per test-plan.md TC-1..TC-6: - instructor: exactly 2/24 (GroupTrendHeatmapMenu, CourseEvaluationResponsesMenu) - coordinator: exactly 7/24 (the coordinator-gated engagement/progress/timetable set) - guardian: exactly 1/24 (BookConferenceSlotsMenu) — positive control pairs with the no-group learner also seeing it via the shared `learner` literal - team-lead: exactly 1/24 (ConferenceScheduleBoardMenu) - hr: exactly 2/24 (Compliance, ExternalTraining) - compliance-officer: exactly 5/24 - administration-manager: exactly 5/24 - no-privileged-group learner (rbactest): exactly 1/24 (BookConferenceSlotsMenu), the other 23 individually confirmed absent, with My learning present as the positive control that the nav rendered at all - admin: 24/24 — every role-gated gate is reachable by admin Compliance and Book Conference Slots confirmed absent from the admin nav on the pre-fix build (temporarily rebuilt from the prior commit) and present on the post-fix build, both via the rendered DOM, not the manifest JSON.
Task 6/7: hydra-gates check_manifest_crossref.js (gate 30) gained
role-resolvable and group-declared checks, with fixtures and self-tests
(conduction/hydra-gates package, .github repo, branch
fix/gate-30-role-resolvable-group-declared — not pushed/PR'd from here).
Verified against scholiq's real post-fix tree (zero findings) and against
a deliberately broken manifest+resolver pair (both checks FAIL
independently, exit 1), then reverted and re-confirmed clean.
Task 8: hydra's gate-30 skill doc updated with the two new check classes,
fix recipes, and diff-scope paths (hydra repo, branch
docs/gate-30-role-resolvable-group-declared — not pushed/PR'd from here).
Scholiq's docs/Technical/architecture.md was checked for the retired
scholiq-{role} convention or school-only role words as identifiers — it
names none (its primaryRole mentions are RoleSelector's own, deliberately
unchanged vocabulary per design.md Non-Goals), so no doc edit was needed.
openspec validate fix-dead-role-gates: valid. All 16 tasks complete.
…Learniq Boundary 1 of the rename-to-learniq change: renames every namespace and use-statement declaration, plus FQCN references in docblocks, across all 374 lib/ and tests/ PHP files that declared or referenced OCA\Scholiq. Lowercase 'scholiq' string literals (app id, register slug, config-key prefixes) are untouched here — those are separate boundaries. openspec/changes/rename-to-learniq/tasks.md Task 1
…ation steps Boundary 2: app id scholiq -> learniq in appinfo/info.xml (<id>, <namespace>, nav route id learniq.page.index, repair-step class paths), composer.json (package name), package.json, and the psalm/phpstan/phpcs/ phpmd tool-config names. Boundary 3+4 (data-migration infrastructure): two new idempotent, non-destructive IRepairStep classes. - RenameRegisterSlug: single-row UPDATE renaming the OpenRegister register's slug scholiq -> learniq. Verified against the live DB that the register's 118 shard tables are keyed on the numeric register id, not the slug, so no shard table is touched. - MigrateAppConfigKeys: copies every stored IAppConfig value from the scholiq app-config namespace to learniq via IAppConfig::getKeys(), superseding the design doc's hardcoded 9-key list, which missed several real keys (register, lti_ags_subscription_id, lti_ags_pull_cursor, openconnector_api_token/user/callback_token, docudesk_api_token, keygen.last_at.*) and treated credential.signing.* as 4 fixed keys when they are actually per-tenant-UUID prefixes. Both new steps are registered in BOTH <install> and <post-migration>, ordered BEFORE InitializeSettings in each block. Renaming the app's <id> means Nextcloud discovers learniq as a different app id than scholiq — there is no in-place app-id upgrade in Nextcloud, so <install> is the realistic path a real deployment goes through. OpenRegister's ImportHandler::importRegister() matches an existing register by slug; if InitializeSettings's register-JSON import ran before the slug rename, it would find no register at 'learniq' yet, create a new empty one, and the rename step's collision guard would then refuse to touch the real, data-carrying register — orphaning every existing object. Running the slug rename first avoids that. Also fixes RenameDutchColumns.php's REGISTER_SLUG_PREFIX, which a mechanical batch pass renamed to 'learniq' — it must stay 'scholiq' because that step runs before RenameRegisterSlug in the same pass, so the database still has the old slug when it executes. openspec/changes/rename-to-learniq/tasks.md Task 2, Task 3 (partial — repair step + code call-sites; DB verification tracked separately), Task 4 (partial — repair step; DB verification tracked separately)
…ister-slug/app-id strings Boundary 5: renames lib/Settings/scholiq_register.json -> learniq_register.json, updating x-openregister.app, the components. registers key + nested slug, the 13 fixture registers[].register references, and 71 OCA\Scholiq FQCN strings in lifecycle 'requires' tags — all verified via a structural JSON diff showing only intended values changed. src/manifest.json: all 345 register/registerSlug 'scholiq' values, the 5 /apps/scholiq/ route-prefix urlTemplates, 3 metric names, 7 Scholiq*.vue component-name references (kept in sync with the not-yet-landed filename rename), and the scholiq.conduction.nl documentation links, verified the same way. Boundary 6: the register-slug/app-id literal 'scholiq' string constant across lib/ and tests/ (178 files, ~250 occurrences) -> 'learniq', including the credential.signing.* IAppConfig key-prefix family, credentialVerify.verify, two IThrottler bucket names, three tempnam() prefixes, and the DID/URN identifier formats CredentialSigningService and LearningRecordExportSigningService embed into issued Open Badges/DID documents (urn:scholiq:* -> urn:learniq:*, did:web:scholiq: -> did:web:learniq:) — these are forward-only: already-issued credentials keep their historical urn:scholiq: identity, only future issuance moves to the new scheme, same posture as the register-slug migration. Deliberately NOT renamed (out of scope, same category as domain-model vocabulary): the scholiq-json / scholiq-learning-record / scholiqNative course-package and portable-learning-record wire-format identifiers embedded in exported/imported files — renaming those is a file-format compatibility question, not an identity boundary, and needs its own migration design if ever done. openspec/changes/rename-to-learniq/tasks.md Task 3 (call sites), Task 4 (call sites), Task 5
… provider, course importer, and postman collections Boundary 6/7 (part 1 of 2, the 6 non-Vue files): - lib/Mcp/ScholiqToolProvider.php -> LearniqToolProvider.php (class + getAppId() return value + every reference in Application.php, CourseToolPresenter.php, tests/bootstrap*.php, .github/workflows/code-quality.yml) - tests/Unit/Mcp/ScholiqToolProviderTest.php -> LearniqToolProviderTest.php (class + test method name) - lib/Service/CoursePackage/ScholiqJsonCourseImporter.php -> LearniqJsonCourseImporter.php (class + every reference) - tests/Unit/ScholiqTest.php -> LearniqTest.php (class) - tests/integration/scholiq.postman_collection.json -> learniq.postman_collection.json, tests/wedge-scaffolds/ scholiq-wedge.postman_collection.json -> learniq-wedge.postman_collection.json — every register-slug URL segment and register value inside both renamed (structural JSON walk, word-boundary substitution, verified valid JSON after); referencing READMEs updated. Deliberately left alone: the exportScholiqJson()/scholiq-json/ scholiqNative course-package and portable-learning-record wire-format names — same out-of-scope call as the previous commit. openspec/changes/rename-to-learniq/tasks.md Task 6, Task 7
…and l10n
Completes boundary 6 (literal 'scholiq' register-slug/app-id string
constants across lib/ and tests/, 178 files - the actual content
change; the prior commit's message named this but the change was not
fully staged there).
New boundaries found beyond design.md's stated scope, all required for
the renamed app to actually run:
- 66 Vue files: t('scholiq', msgid) -> t('learniq', msgid) - the first
argument selects which app's l10n bundle @nextcloud/l10n searches;
left as 'scholiq' every translated string in the UI silently falls
back to its untranslated English source.
- 37 Vue files: hardcoded OpenRegister object-API URLs
(/apps/openregister/api/objects/scholiq/...) and 14 files using this
app's own /apps/scholiq/api/... route prefix -> .../learniq/... -
left as 'scholiq' these calls 404 once the register-slug migration
runs.
- src/main.js: loadState('scholiq', ...) (initial-state key), the
vue-router history base generateUrl('/apps/scholiq'), and the
#scholiq-app mount selector (+ matching templates/index.php host
div). src/settings.js: the #scholiq-settings mount selector (+
matching templates/settings/admin.php). src/App.vue: appId="scholiq"
passed to CnAppRoot - the frontend's own root identity prop.
- templates/index.php and templates/settings/admin.php also still
referenced the PHP class OCA\Scholiq\AppInfo\Application, which no
longer exists after the namespace rename - a fatal "class not found"
on every page load. Neither file is under lib/ or tests/, so no
earlier namespace-rename batch touched them.
- webpack.config.js hardcoded const appId = 'scholiq', which names
the built entry bundles (scholiq-main.js / scholiq-settings.js).
appinfo/info.xml's Util::addScript(APP_ID, APP_ID . '-main') now
requests learniq-main.js - left unfixed, the built filename would
never match what the app requests and the main script 404s.
Confirmed via two full npm run build runs: the first (before this
fix) still emitted scholiq-main.js/scholiq-settings.js despite every
other chunk already reading learniq-* (those derive their name from
package.json's already-renamed "name" field); the second, after this
fix, emits learniq-main.js / learniq-settings.js - verified present
in js/ with 0 remaining 'scholiq' references in the built bundle.
- lib/Service/VerwerkingsregisterCsvBuilder.php: a hardcoded
'?register=scholiq&from=...' query string on the AVG export URL -
same class of bug as the frontend API paths, just server-side.
- The 7 src/views/Scholiq*.vue files renamed to Learniq*.vue (class
Vue name: property + every reference in src/registry.js,
src/App.vue, DashboardAdmin/Student/Teacher.vue,
src/views/settings/AdminRoot.vue,
src/views/settings/DataExchangeSettingsSection.vue).
l10n (Task 8): 13 English source keys in l10n/en.json that name the
product ('Scholiq settings', 'Scholiq processing activities', etc.)
renamed (key + value) across all 37 locale files; every other key's
already-translated VALUE gets the literal word Scholiq/scholiq
mechanically substituted to Learniq/learniq (case-preserving), keys
and surrounding translated text untouched - verified via a structural
JSON diff per file showing only the product-name token changed
anywhere. Deliberately NOT touched: 86 dead 'scholiq.*'-namespaced
l10n keys (scholiq.page.dashboard.title, scholiq.col.course, etc.) -
grepped across src/ and manifest.json, referenced nowhere; legacy
leftovers from a prior (pre-ADR-036 v2 registry) architecture, out of
this rename's scope.
Deliberately left as-is (cosmetic, same category as prior commits):
CSS class names (.scholiq-admin__*, .scholiq-settings__*), local JS
variable names (scholiqResults, SCHOLIQ_SCHEMAS), DOM id attributes
paired within one component (#scholiq-default-register), the
scholiq-json/scholiq-learning-record/scholiqNative wire-format
identifiers and their AVG processing-activity 'code' values, and
stale prose/docblock references to the old
lib/Settings/scholiq_register.json filename.
npm run build verified green both before and after the webpack.config.js
fix (no compile errors either time - only pre-existing bundle-size
warnings).
openspec/changes/rename-to-learniq/tasks.md Task 6 (completing), Task 8
44 files, a 1:1 substitution of the app id in route prefixes and the [data-app] selector. No assertion logic changes.
1. MigrateAppConfigKeys copied Nextcloud's reserved keys. AppManager writes 'enabled' via the deprecated setValue() (type MIXED); copying scholiq's with setValueString() stores STRING, and every subsequent app:enable then dies with AppConfigTypeConflictException. The failure happens before the app can run anything that would repair it, so the app was permanently un-enableable. Reserved keys are now skipped. 2. AiProcessingDisclosureController still read scholiq_register.json, which no longer exists. It threw on boot. 3. RoleSelector still built 'scholiq-' . $role group names. The RBAC change retired that prefix - OpenRegister's rbac-scopes requires group ids to be free-form and unprefixed - so the branch could never be true. It now reads the single canonical role->group map, promoted to a public const on DashboardRoleService. Verified on the dev instance: app enables, register slug migrated 9 scholiq -> 9 learniq, and the canary GradeEntry d4caa278 has an identical field fingerprint before and after (6ca294bb6174...).
…pp id app-name drives where CI installs and mounts the app, so leaving it as scholiq would fail the build rather than merely mislabel it. Covers code-quality (plus the Playwright seed path), release (x3), issue-triage and openspec-sync. documentation.yml's cname is deliberately NOT changed: scholiq.conduction.nl is a live domain, and repointing it needs a DNS decision plus a redirect, not a one-line edit.
…constants and messages Comment and docblock prose, the admin settings sectionName, stale config-key names in operator-facing log messages, and the SCHOLIQ_REGISTER constant NAME where its value was already learniq. Partial by design: seven sweep agents were killed mid-run by a session limit. Every file is internally consistent - checked that no file uses a constant it does not declare, and that all 75 changed PHP files parse - so this is a safe stopping point, not a half-applied rename.
Two groups. 1. Four tests asserted x-openregister-authorization. No code path in OpenRegister reads that key - RbacGroupCollector and PermissionHandler both key on 'authorization' - so those tests were GREEN while asserting a write posture that was never enforced. They now assert the key that is actually consulted, with the canonical unprefixed group ids, and additionally assert the decoy key is gone. 2. did:web:scholiq -> did:web:learniq. Checked first that this is safe: the DID is a synthetic label derived from the tenant public key's fingerprint, emitted in two places and string-matched by no verification path - verify() is openssl_verify against the key. So previously issued credentials still verify. Also repoints 10 test files at learniq_register.json, which was failing 25 tests in setUp before they ran a single assertion. Parity with origin/development: 21 errors / 1 failure on both. The one remaining failure (CoursePackageImportServiceTest:166) fails identically on development and is pre-existing.
…nce defect Two faults, both of which made e2e results untrustworthy rather than red. 1. ensureLoggedIn probed the session by loading Nextcloud's DASHBOARD app and waiting for domcontentloaded within 20s, commented as 'a lightweight dashboard check'. It is not lightweight where many apps are installed - the dashboard pulls a widget bundle from each. Measured on the shared instance (44 custom apps): HTML in 2.7s, domContentLoadedEventEnd at 28.5s. Since loggedInPage is the shared fixture, that one number failed EVERY spec, and it failed in the fixture rather than an assertion, so it read as 'the app is broken'. Now probes /ocs/v2.php/cloud/user - a few hundred bytes, real session required, no app bundles. 2. shell.spec asserted Compliance must be ABSENT from the nav. That encoded the F2 defect as intended behaviour: Compliance is the headline feature and was invisible to every administrator. fix-dead-role-gates added admin to the gate, so the assertion now checks both directions against the real entitled set. Its role probe also could not work: it read initial state via .textContent, but Nextcloud renders initial state as <input type=hidden value=base64>, so textContent is always '' whether set or not. primaryRole was permanently null and the branch always ran. Now reads the value attribute. Result: 2/2 pass, and the run is ~40% faster.
…eframe # Conflicts: # lib/Mcp/LearniqToolProvider.php
…ir steps Three CI regressions this branch introduced, all caught by the quality gate. - appinfo/info.xml: the rewritten <summary> was 171/158 chars against the App Store schema's 128 cap. Shortened both locales. - src/registry.js: renaming Scholiq* -> Learniq* moved the imports' alphabetical position, so perfectionist/sort-imports failed. Moved the block rather than running eslint --fix, which also rewrites SUPPRESSED violations. eslint is now 0 errors (25 warnings, unchanged). - Coverage guard: base 10814/15098 -> head 11031/15547, +449 statements. MigrateAppConfigKeys (218 lines) and RenameRegisterSlug (166 lines) were brand new with no tests at all - there was no tests/Unit/Repair directory. The new tests are regression guards, not coverage filler. The reserved-key test was PROVEN to fail: removing the guard makes it fail with exactly the un-enableable-app message, restoring it makes it pass. RenameRegisterSlug's cover the case that actually matters - the collision guard failing CLOSED, including when the collision check itself errors, because merging two registers is the unrecoverable outcome. Also adds a self-guarding Doctrine\DBAL\ParameterType stub. IQueryBuilder references it in its constant declarations, so doubling OCP\IDBConnection loads it, and on a bare host it is absent - an environment gap that reads as a code failure. The guard means it is a no-op where real Doctrine exists rather than a shadowing second definition. Verified on both runners: 11/11.
|
Checked one thing a rename can silently drop, so a reviewer does not have to: the MCP tool taxonomy survives it.
'id' => 'learniq.listCourses', 'subject' => 'course', 'action' => 'list',
'id' => 'learniq.getCourseDetails', 'subject' => 'course', 'action' => 'get',Verified live on an instance carrying this branch alongside hermiq: Context for why it was worth checking: an undeclared I filed scholiq#451 when an earlier state of this branch reported 2 undeclared; your merge of
No action needed on this PR. |
Quality Report — ConductionNL/scholiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ❌ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| format | ✅ | ||||
| composer | ✅ | ✅ 104/104 | |||
| npm | ✅ | ✅ 634/634 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ❌ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ❌ | ||||
| Newman | ⏭️ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-19 18:01 UTC
Download the full PDF report from the workflow artifacts.
gate-26 flagged five 'new page components' — they are the renamed Scholiq*.vue files. Chasing that surfaced the real bug: pages.visual.spec.ts still asserted manifestComponentFor(...) === 'ScholiqCompliance', 'ScholiqLearnerHome' and 'ScholiqAiProcessingDisclosure'. The manifest now declares the Learniq* names, so those three assertions would have failed in CI's e2e job. The earlier rename sweep updated route prefixes and the [data-app] selector but not component-name assertions.
Annotates the 19 changed methods hydra gate-16 (spec-coverage) reported as missing @SPEC: 12 get a real @SPEC pointing at the openspec requirement/scenario that already governs their behaviour (repair-step config/register-slug migration, credential fingerprint resolution, dashboard widget composition, drag-and-drop reordering, notification override persistence, etc.), and 7 pure presentational label/formatting helpers get a reason-bearing @SPEC exclude, matching the precedent in PageController.php and RenameDutchColumns.php.
Closes gate-26. LearniqDashboards (route /) and LearniqAccessibilityStatement (route /accessibility) were the last two 'new page components missing a visual baseline' — new only because the rename changed their filenames. Both routes were already reachable in other specs, but neither was named in the visual spec, which is what gate-26 reads. Added real assertions rather than @visual exclude: the root test asserts the manifest mounts LearniqDashboards and the shell renders, which is the thing the rename could plausibly have broken. Gates now: RESULT 2 FAILED, down from 4. gate-16 PASS, gate-26 PASS. COVERAGE held at 63 of 63 applicable, so this is not gates going quiet. Remaining 19 (93 scenarios missing @e2e) and 61 (7 listeners, ADR-078) are pre-existing and fail identically on development.
Quality Report — ConductionNL/scholiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ⏭️ | ||||
| phpcs | ⏭️ | ||||
| phpmd | ⏭️ | ||||
| psalm | ⏭️ | ||||
| phpstan | ⏭️ | ||||
| phpmetrics | ⏭️ | ||||
| eslint | ⏭️ | ||||
| stylelint | ⏭️ | ||||
| build | ⏭️ | ||||
| composer | ⏭️ | ⏭️ | |||
| npm | ⏭️ | ⏭️ | |||
| app:check-code | ⏭️ | ||||
| info.xml | ⏭️ | ||||
| REUSE | ⏭️ | ||||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-19 20:00 UTC
Download the full PDF report from the workflow artifacts.
…eframe # Conflicts: # appinfo/info.xml
Quality Report — ConductionNL/scholiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ⏭️ | ||||
| phpcs | ⏭️ | ||||
| phpmd | ⏭️ | ||||
| psalm | ⏭️ | ||||
| phpstan | ⏭️ | ||||
| phpmetrics | ⏭️ | ||||
| eslint | ⏭️ | ||||
| stylelint | ⏭️ | ||||
| build | ⏭️ | ||||
| composer | ⏭️ | ⏭️ | |||
| npm | ⏭️ | ⏭️ | |||
| app:check-code | ⏭️ | ||||
| info.xml | ⏭️ | ||||
| REUSE | ⏭️ | ||||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-19 22:10 UTC
Download the full PDF report from the workflow artifacts.
Quality Report — ConductionNL/scholiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ❌ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| format | ❌ | ||||
| composer | ✅ | ✅ 104/104 | |||
| npm | ✅ | ✅ 634/634 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ❌ | ||||
| Newman | ⏭️ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-19 23:05 UTC
Download the full PDF report from the workflow artifacts.
Three OpenSpec changes, applied and live-verified on the dev instance. The two navigation changes are specced but held — see Not in this PR.
1.
rbac-declare-groups— the register declared access control that did nothingThe register carried 60 authorization declarations under key names OpenRegister never reads:
x-openregister-authorization(20 schemas) andx-property-rbac(40). Neither is consulted by any code path —RbacGroupCollectorandPermissionHandlerboth key onauthorization, which appeared zero times. Positive control:x-openregister-lifecycle, a key OR really does read, is consumed in 3+ files.So this was never "nobody thought about access control". It was sixty declarations that read as working RBAC in review and enforced nothing.
PermissionHandler's undeclared branch is commented "Default-OPEN behaviour preserved", and theenforce_default_closedmitigation only coverscreate/update/delete—readis never closable by configuration.42 schemas now declare real blocks across three tiers (21 sensitive · 21 catalogue · 76 cascade), plus the OAuth2 scope map so
GroupProvisionercreates the groups.Verified live: the instance went from 28 groups to 36. A
learners-only user reads aCourse(200) and is refused another learner'sGradeEntry(404 — OR masks read denials deliberately) and its create (403).2.
fix-dead-role-gates— Compliance had no door24 menu items gate on
user.primaryRole. They named ten role values; the resolver could produce six, and not the same six. Result: 17 items were silently admin-only while claiming to delegate, and two were invisible to admins — including Compliance, the app's headline feature, whose page was healthy all along.Worse, none of the
scholiq-*groups the resolver tested had ever existed (28 groups on the instance, zero matching), so in practice onlyadminandlearnerwere reachable at all.Role vocabulary is now canonical and sector-neutral —
learner · instructor · team-lead · coordinator · hr · compliance-officer · guardian · administration-manager— because the product is being reframed for companies as well as schools.teacher,principal,mentor,parentbecome profile labels, not identifiers.CI gate 30 gained
role-resolvableandgroup-declaredchecks, and was watched FAILING on a deliberately broken manifest before being trusted. A gate that has only ever passed is the exact defect this change exists to fix.3.
rename-to-learniq479 files, the register slug, and two silent cross-app couplings (hermiq's
CourseRecommendationEngine, pipelinq'sDOWNSTREAM_SYSTEMS).Three defects found while applying it, one severe:
MigrateAppConfigKeyscopied Nextcloud's reserved keys.AppManagerwritesenabledvia the deprecatedsetValue()(MIXED); copying withsetValueString()stores STRING, and every laterapp:enabledies withAppConfigTypeConflictException— permanently, because the failure precedes anything that could repair it. The app was un-enableable.AiProcessingDisclosureControllerstill read the old register filename; it threw on boot.RoleSelectorstill built'scholiq-' . $rolegroup names, contradicting change 1.Migration safety, measured not argued: shards key on the numeric register id (
oc_openregister_table_9_*, 118 of them);table_name ~ 'oc_openregister_table_[a-z]'returns 0. The canaryGradeEntry d4caa278has an identical field fingerprint before and after (6ca294bb6174…).Tests
development(host)Exact parity; the one failure is the same on both. The 21 "errors" are environment, not code — the host lacks
ext-zipand openregister. In the container (which has both) it is 2 errors / 2 failures, in classes this branch does not touch in production code.Four unit tests were asserting
x-openregister-authorization— green while asserting a fiction. They now assert the key that is actually read.e2e
Two harness faults fixed, both of which made results untrustworthy rather than red:
loggedInPagefixture probed the session by loading Nextcloud's dashboard with a 20s budget. Measured here: HTML in 2.7s,domContentLoadedEventEndat 28.5s. It failed every spec, in the fixture rather than an assertion, so it read as "the app is broken".shell.specasserted Compliance must be absent — encoding the F2 defect as intended behaviour. Its role probe also read initial state via.textContent, which is always''because Nextcloud renders it as<input type="hidden" value="base64">.Not in this PR
manifest-fragment-splitandmenu-six-main-items(23 → 6 top-level, ADR-097-compliant at 4 counted) are specced but blocked on a release of@conduction/nextcloud-vuecarryingCnNavCardGrid(ConductionNL/nextcloud-vue#701). ADR-044 §4 instructs apps to build card-grid landing pages; no component in the library could render one. Apps install from npm, so a merge alone reaches nobody.documentation.yml'scname: scholiq.conduction.nlis deliberately unchanged — repointing a live domain needs a DNS decision and a redirect, not a one-line edit.🤖 Generated with Claude Code