feat: rename the app from doriath to keepiq - #414
Conversation
Completes the rename through the app id: id, display names, PHP namespace, frontend URLs, l10n domain, CLI, CI and docs. 935 files. Two repair steps carry stored data across, under both <install> and <post-migration>. MigrateUserPreferences does not use the pilot's getUsersForUserValue pattern: three of the eleven per-user keys (session_timeout, expiry_max_age_days, default_secret_type) are open-valued, so no finite value list is exhaustive. It walks callForSeenUsers() + getUserKeys() instead. Every reader supplies a default, so losing these is silent and unsafe: an opted-out user gets re-subscribed to notifications, offline vault caching re-enables for someone who turned it off, and session_timeout reverts - potentially LENGTHENING the unlocked-vault window. This is a secrets vault, so several identifiers are load-bearing in a way a rename must not touch. Frozen, each commented at its definition: - doriath-passkey-kek-v1, an HKDF info string. Changing one byte derives a different KEK and locks every registered passkey out of the vault. - doriath-cxp-v1, equality-checked AND fed into the HPKE binding info. - EXPECTED_AUDIENCE = 'doriath': registered applications sign aud=doriath with keys the server does not hold, so rotating it is a coordinated apiVersion bump. - the attachment appdata namespace. IAppDataFactory->get(APP_ID) would have pointed at an empty folder: uploads keep working while every existing attachment 404s, and nothing logs it. - the published wire contracts: .doriath-backup (users' existing encrypted exports), doriath://, .well-known/doriath, Doriath-Lease-*, and the doriath_* tables, indexes and cache namespaces. Browser-side state needed the opposite treatment. The offline snapshot database and the service-worker shell cache DID move to the new names, which would have stranded a returning user's cached vault data under the old names - surviving every lock, logout and rotation, unreachable by the code meant to evict it. purge() now also deletes the legacy database and the activate handler also matches the legacy cache prefix, so the eviction guarantee this cache is built around still holds. References to other apps' ids are untouched: those apps still declare their old <id>, and the references are duck-typed lookups whose failure path is a silent no-op. composer check:strict passes; 1086 PHP tests, 652 frontend tests.
Quality Report — ConductionNL/keepiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ❌ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ❌ | ||||
| composer | ✅ | ✅ 111/111 | |||
| npm | ✅ | ✅ 533/533 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-22 09:13 UTC
Download the full PDF report from the workflow artifacts.
Two CI failures, both genuinely introduced here - development's own eslint and format checks are green, so these are not inherited. - src/offline/cache.js: the catch block I added for the legacy-database eviction bound an unused `e`. Bare `catch` instead. - 32 files reformatted with the project's own format:fix. Renaming doriath to keepiq changes string lengths, so prettier wants different line wrapping. Checked afterwards that it only rewrapped the legacy cache-prefix condition rather than altering it, and the 652 frontend tests still pass.
Quality Report — ConductionNL/keepiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| composer | ✅ | ✅ 111/111 | |||
| npm | ✅ | ✅ 533/533 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-22 10:02 UTC
Download the full PDF report from the workflow artifacts.
gate-16 is diff-scoped per ADR-020, so a method changed in a PR comes into scope. The rename edited an identifier inside 192 methods, which inherited their pre-existing annotation debt. Gate now reports count=0. 186 methods got real requirement anchors and 6 got reasoned exclusions. Every one of the 130 distinct anchors cited was machine-verified against a slug table built from the 298 live requirement headings across the 45 specs, so none of them merely looks plausible. The exclusions each name where the behaviour IS specified rather than saying "no spec": the two app-id migration steps move oc_appconfig and oc_preferences rows and add no behaviour of their own, so they cite the specs for the settings they preserve; the two development seeders are gated on the debug system value and cite the specs for the real thing they fake; the two Vue prop default() factories declare a translated fallback label and cite where the labelled behaviour lives. Noted for a follow-up, not fixed: 31 change names referenced by EXISTING @SPEC tags no longer resolve. They point at openspec/changes/<name>/, but those changes were archived to openspec/changes/archive/<date>-<name>/, so the date prefix breaks every path - roughly 250 tags, add-emergency-access alone carrying 42. PHPCS corroborates it from the other side: its sniff already says to link back to openspec/specs/{capability}, not a change dir. Every NEW tag here points at a live promoted spec path.
The eslint job failed on "There are suppressions left that do not occur anymore" - eslint exits non-zero for unpruned suppressions, even at zero errors. The rename changed the code those entries pointed at, so three of them no longer describe a real problem. Pruned with eslint's own --prune-suppressions rather than hand-edited, so the file still describes exactly the problems that remain. 0 errors either way; the 170 warnings are pre-existing.
Quality Report — ConductionNL/keepiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ❌ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| composer | ✅ | ✅ 111/111 | |||
| npm | ✅ | ✅ 533/533 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-22 10:48 UTC
Download the full PDF report from the workflow artifacts.
Coverage on the two repair steps was ~32% of 273 added statements, and the coverage guard was right to block: these carry every stored setting across the doriath -> keepiq app-id rename, and their failure mode is silent. Readers all supply a default, so a lost preference does not error, it reverts - an opted-out user gets re-subscribed, offline vault caching re-enables for someone who disabled it, and session_timeout reverts, potentially LENGTHENING the unlocked-vault window. 16 tests / 23 assertions -> 33 / 80. The later tests run against an in-memory two-namespace store, so they assert the values that land rather than that a mock was called. All eleven USER_PREF_KEYS are pinned verbatim, including the falsy-but-real offline_cache_optin='0' and the open-valued session_timeout. The enumeration STRATEGY is pinned too: callForSeenUsers is used and getUsersForUserValue is asserted never to be called, because three keys are open-valued and the value-enumerating form would migrate nothing while reporting success. Every test was mutation-checked: the never-overwrite guard, the reserved key list, the callForSeenUsers strategy and each catch clause were broken in turn and the suite confirmed RED, then restored - both lib files are byte-identical to HEAD. That exercise found a test that could not fail. testSkipsReservedKeys stubbed getValueString to return the same value for BOTH namespaces, so the never-overwrite guard suppressed the write and the test stayed green with RESERVED_KEYS emptied - it never tested the reserved skip at all. Fixture corrected; it now fails under that mutation. One behaviour worth knowing, not a bug: an old value of '' is treated as nothing-to-migrate in both classes, so a stored empty string is never copied. Harmless while readers default to '', but it is a decision.
Quality Report — ConductionNL/keepiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| composer | ✅ | ✅ 111/111 | |||
| npm | ✅ | ✅ 533/533 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-22 10:59 UTC
Download the full PDF report from the workflow artifacts.
Quality Report — ConductionNL/keepiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| composer | ✅ | ✅ 111/111 | |||
| npm | ✅ | ✅ 533/533 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-22 11:16 UTC
Download the full PDF report from the workflow artifacts.
Same defect filinq had, found by writing the tests: both getValueString
reads in MigrateAppConfigKeys sat OUTSIDE the try that was meant to
contain them. Only the write was guarded, so an unreadable value
propagated out of run().
That matters more here than it looks. This step is registered under
<install> as well as <post-migration>, so a repair step that throws does
not merely fail an upgrade - the app never enables, and every route goes
with it. The class docblock promises the opposite ("every failure is
logged and the loop continues").
Both reads moved inside the try. MigrateUserPreferences was already
correct - its per-user reads, its key enumeration and callForSeenUsers
are each guarded - so only this class changed.
The new test makes the read throw at the IAppConfig seam and asserts
run() RETURNS and the key after the unreadable one still migrates.
Mutation-checked: moving the read back outside the try turns it into an
error, restoring it makes it pass again.
composer check:strict passes; 1104 tests.
Quality Report — ConductionNL/keepiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| composer | ✅ | ✅ 111/111 | |||
| npm | ✅ | ✅ 533/533 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-22 11:33 UTC
Download the full PDF report from the workflow artifacts.
gate-46 (spec-anchor-existence) rejected it: I copied '@SPEC openspec/specs/app-identity/spec.md' from a sibling app that HAS that spec, into two that do not. Precisely the failure the gate exists for, and precisely the rule I had been enforcing elsewhere - verify the target resolves rather than that the tag looks plausible. Both now use a reason-bearing @SPEC exclude matching what the class under test already declares: the step moves config rows between namespaces and adds no behaviour, so there is no capability spec to point at; what the test pins is the step's own safety contract.
KeepiqNotifier was DoriathNotifier and had never been tested. The rename moved the file, and the coverage guard reads a rename as delete+add, so its 180 uncovered statements became "new untested code" - the whole of the reported coverage drop, since the two migration classes are at 100%. The guard documents this as a deliberate incentive: a renamed class is new code as far as the suite is concerned. So it is now covered. 34 tests, 148 assertions, roughly 98% of the file: both prepare() rejection paths, all 16 subjects across the four renderers pinned on exact subject/message/link, every placeholder fallback, and share_request_result failing closed to "denied" on garbage input. Left uncovered on purpose: two terminal defensive branches unreachable while every subject has a renderer. Mutation-checked by renaming a subject case - 6 errors, assertions 148 -> 88 - then restored. Fixes a real hazard the tests exposed: withSecretLink() built its route from the literal 'keepiq.dashboard.page' instead of from APP_ID. Route names are namespaced by the app id, so that is correct only while APP_ID happens to equal 'keepiq' - the next rename moves the route and leaves the literal behind. The catch beside it swallows the failure, so every secret deep-link would have quietly stopped appearing with no error and no log. Derived from Application::APP_ID now, with the catch documenting why it was dangerous and what to do if another failure mode appears. Also notes, without changing: the class sets only parsed subject/message, never rich parameters, so notifications render as flat text with no clickable chips. Behaviour, not a bug. composer check:strict passes; 1138 tests.
Quality Report — ConductionNL/keepiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| composer | ✅ | ✅ 111/111 | |||
| npm | ✅ | ✅ 533/533 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-22 11:45 UTC
Download the full PDF report from the workflow artifacts.
Quality Report — ConductionNL/keepiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| composer | ✅ | ✅ 111/111 | |||
| npm | ✅ | ✅ 533/533 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-22 12:00 UTC
Download the full PDF report from the workflow artifacts.
Newman failed on every run with the discovery document entirely undefined. The endpoint is frozen at /.well-known/doriath - a published wire contract that OpenConnector's resolver is pinned against - but the test was requesting /.well-known/keepiq. What made this hard to see is where the rename landed. A Postman URL carries the address TWICE: a human-readable `raw` string and a `path` array. The rename updated the array and left the raw string alone, so the diff read correctly - raw still said `doriath`, which is the value a reviewer checks - while Postman built the request from the array and went somewhere that does not exist. Swept every collection for the same divergence by rebuilding each request URL from its path array and comparing it to its own raw string: zero remaining. The earlier symptom pointed elsewhere and was worth chasing anyway: the first runs 404'd because the app was not enabling at all, which the repair-step fix resolved. This was the second, quieter cause underneath.
Quality Report — ConductionNL/keepiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| composer | ✅ | ✅ 111/111 | |||
| npm | ✅ | ✅ 533/533 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-22 12:13 UTC
Download the full PDF report from the workflow artifacts.
Quality Report — ConductionNL/keepiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| composer | ✅ | ✅ 111/111 | |||
| npm | ✅ | ✅ 533/533 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-22 12:29 UTC
Download the full PDF report from the workflow artifacts.
`.secret-list-item__name` wraps `{{ secret.name }}` AND a <StrengthBadge>,
so `textContent` yields "AWS Console Very strong" once the badge resolves.
Both call sites compared that string for EXACT equality against a name from
the API, so the match silently stopped working — folder-sharing failed on
"moved secret must exist" while the move itself had persisted correctly, and
the trace confirms the new folderId was stored. The badge loads
asynchronously, so this was always a race the read happened to win; it is
latent on development too, not something the rename introduced.
Reading only the element's own text nodes makes the extraction independent
of badge timing at both call sites.
Also move keepiq's two `@spec exclude` docblocks onto the `default()`
factories themselves, which is where the spec-coverage gate looks.
Quality Report — ConductionNL/keepiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| composer | ✅ | ✅ 111/111 | |||
| npm | ✅ | ✅ 533/533 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-22 13:00 UTC
Download the full PDF report from the workflow artifacts.
Third app through the full app-id rename, following the merged pilot (planninq#336) and the procedure at renaming-an-app. 935 files, 14 files + 3 directories renamed.
This is the secrets vault, so the interesting part is what does not move.
Frozen — renaming these locks users out
doriath-passkey-kek-v1infostring — one byte derives a different KEK and every registered passkey is locked out of the vaultdoriath-cxp-v1EXPECTED_AUDIENCE = 'doriath'aud=doriathwith keys the server does not holdIAppDataFactory->get(APP_ID)would point at an empty folder: uploads keep working, every existing attachment 404s, nothing logs.doriath-backup,doriath://,.well-known/doriath,Doriath-Lease-*,doriath_*tablesEach is commented at its definition so nobody later "finishes the job".
Browser state needed the opposite treatment
The offline snapshot database and service-worker shell cache did move to the new names — which would have stranded a returning user's cached vault data under the old ones, surviving every lock, logout and rotation, unreachable by the code meant to evict it. That breaks the eviction guarantee the cache is built around, and for a secrets vault it means encrypted vault data left in browsers that the app can no longer purge.
purge()now also deletes the legacy database, and theactivatehandler also matches the legacy cache prefix.Migration
MigrateUserPreferencesdoes not use the pilot'sgetUsersForUserValuepattern: three of the eleven per-user keys are open-valued, so no finite value list is exhaustive. It walkscallForSeenUsers(). Every reader supplies a default, so loss is silent — an opted-out user gets re-subscribed, offline caching re-enables for someone who disabled it, andsession_timeoutreverts, potentially lengthening the unlocked-vault window.Verification
composer check:strict— ALL CHECKS PASSED; Psalm and PHPStan clean; 1086 PHP tests, 652 frontend tests, manifest and l10n-parity pass.Known gap: the Go CLI's module path and imports were updated but
go buildwas not run (no Go toolchain available here) — worth a check before release. DNS/CNAME move separately.🤖 Generated with Claude Code