Codebase Audit — 2026-05-01
Sweep for three repeating defect patterns surfaced during the platform-integration-2026-04 mega-PR review.
- Branch base:
platform-integration-2026-04(HEAD22c5625ef) - Already fixed today:
file-actions(10 routes),workflow-operations(16 routes via PR #1390),FileLockHandlerdistributed-cache persistence (22c5625ef).
Pattern A — Unrouted controller methods
Public controller methods returning a JSONResponse / StreamResponse / DataDownloadResponse / TemplateResponse whose route is missing from appinfo/routes.php.
Method: scanned 85 controllers, 524 response-returning public methods. Cross-checked each against appinfo/routes.php (468 route entries). Resource auto-routes (registers, schemas, sources, configurations, applications, agents, endpoints, mappings, consumers) excluded — those auto-generate index/show/create/update/destroy.
Result: 67 unrouted methods across 17 controllers.
Tier 1 — Real route gaps (live spec features, no route)
These map 1:1 to spec items marked [x] and are functional handlers, not dead code. Recommend fixing.
| Controller / method | Expected route | Spec |
|---|---|---|
UserController::changePassword | user#changePassword | profile-actions (archived 2026-05-01) |
UserController::uploadAvatar | user#uploadAvatar | profile-actions |
UserController::deleteAvatar | user#deleteAvatar | profile-actions |
UserController::exportData | user#exportData | profile-actions |
UserController::getNotificationPreferences | user#getNotificationPreferences | profile-actions |
UserController::updateNotificationPreferences | user#updateNotificationPreferences | profile-actions |
UserController::getActivity | user#getActivity | profile-actions |
UserController::listTokens | user#listTokens | profile-actions |
UserController::createToken | user#createToken | profile-actions |
UserController::revokeToken | user#revokeToken | profile-actions |
UserController::requestDeactivation | user#requestDeactivation | profile-actions |
UserController::getDeactivationStatus | user#getDeactivationStatus | profile-actions |
UserController::cancelDeactivation | user#cancelDeactivation | profile-actions |
CalendarEventsController::index | calendarEvents#index | nextcloud-entity-relations |
CalendarEventsController::create | calendarEvents#create | nextcloud-entity-relations |
CalendarEventsController::link | calendarEvents#link | nextcloud-entity-relations |
CalendarEventsController::destroy | calendarEvents#destroy | nextcloud-entity-relations |
DeckController::index | deck#index | nextcloud-entity-relations |
DeckController::create | deck#create | nextcloud-entity-relations |
DeckController::destroy | deck#destroy | nextcloud-entity-relations |
DeckController::objects | deck#objects | nextcloud-entity-relations |
ContactsController::index | contacts#index | nextcloud-entity-relations |
ContactsController::create | contacts#create | nextcloud-entity-relations |
ContactsController::update | contacts#update | nextcloud-entity-relations |
ContactsController::destroy | contacts#destroy | nextcloud-entity-relations |
ContactsController::objects | contacts#objects | nextcloud-entity-relations |
RelationsController::index | relations#index | nextcloud-entity-relations |
LinkedEntityController::addObjectLink | linkedEntity#addObjectLink | linked-entity-types |
LinkedEntityController::removeObjectLink | linkedEntity#removeObjectLink | linked-entity-types |
LinkedEntityController::addRegisterLink | linkedEntity#addRegisterLink | linked-entity-types |
LinkedEntityController::addSchemaLink | linkedEntity#addSchemaLink | linked-entity-types |
LinkedEntityController::reverseLookup | linkedEntity#reverseLookup | linked-entity-types |
TmloController::summary | tmlo#summary | tmlo-metadata |
TmloController::exportSingle | tmlo#exportSingle (Response) | tmlo-metadata |
TmloController::exportBatch | tmlo#exportBatch (Response) | tmlo-metadata |
TagsController::index | tags#index | (no spec — pre-existing) |
TagsController::add | tags#add | (no spec — pre-existing) |
TagsController::remove | tags#remove | (no spec — pre-existing) |
NotesController::update | notes#update | (no spec — pre-existing) |
ConfigurationsController::export | configurations#export | (configuration#export exists; this is an alternate handler on the plural controller) |
FileSidebarController::getObjectsForFile | fileSidebar#getObjectsForFile | (no spec found) |
FileSidebarController::getExtractionStatus | fileSidebar#getExtractionStatus | (no spec found) |
Settings\CacheSettings::clearSpecificCollection | Settings\CacheSettings#clearSpecificCollection | settings refactor |
Settings\ConfigurationSettings::getObjectCollectionFields | Settings\ConfigurationSettings#getObjectCollectionFields | settings refactor |
Settings\ConfigurationSettings::createMissingObjectFields | Settings\ConfigurationSettings#createMissingObjectFields | settings refactor |
Settings\FileSettings::getFileCollectionFields | Settings\FileSettings#getFileCollectionFields | settings refactor |
Settings\FileSettings::createMissingFileFields | Settings\FileSettings#createMissingFileFields | settings refactor |
Settings\LlmSettings::getVectorStats | Settings\LlmSettings#getVectorStats | settings refactor |
Settings\SolrManagement::listSolrCollections | Settings\SolrManagement#listSolrCollections | settings refactor |
Settings\SolrManagement::listSolrConfigSets | Settings\SolrManagement#listSolrConfigSets | settings refactor |
Settings\SolrManagement::createSolrConfigSet | Settings\SolrManagement#createSolrConfigSet | settings refactor |
Settings\SolrManagement::deleteSolrConfigSet | Settings\SolrManagement#deleteSolrConfigSet | settings refactor |
Settings\SolrManagement::createSolrCollection | Settings\SolrManagement#createSolrCollection | settings refactor |
Settings\SolrManagement::copySolrCollection | Settings\SolrManagement#copySolrCollection | settings refactor |
Tier 2 — Dead duplicate methods (superseded, safe to delete)
These exist on legacy controllers but the work moved to namespaced sub-controllers. Routes already point to the new homes. The legacy methods are dead code.
| Legacy method (no route) | Live replacement |
|---|---|
SettingsController::testSchemaMapping | Settings\SolrOperations#testSchemaMapping |
SettingsController::testSetupHandler | Settings\SolrOperations#testSetupHandler |
SettingsController::reindexSpecificCollection | Settings\SolrManagement#reindexSpecificCollection |
SettingsController::semanticSearch | solr#semanticSearch |
SettingsController::hybridSearch | solr#hybridSearch |
SettingsController::load | (no replacement — legacy load endpoint, dead since refactor) |
SettingsController::updatePublishingOptions | (no replacement — legacy publishing config endpoint) |
ConfigurationController::index / show / create / update / destroy | ConfigurationsController (plural, registered as resources entry) |
ChatController::page | (no replacement — chat#page template render legacy) |
ObjectsController::logs | auditTrail#objects (same URL) |
RegistersController::objects | objects#index (registered) |
Pattern A verdict
Needs design call. 67 instances — far above the 3-instance inline-fix threshold. The nextcloud-entity-relations cluster (calendar/deck/contacts/relations) and the profile-actions cluster (13 user endpoints) are the largest gaps and ship in a single PR per spec, not in this audit. Tier 2 dead duplicates should be removed in a separate cleanup pass.
Pattern B — In-memory state that should be persistent
Scanned 68 private array $... declarations across lib/Service/ and lib/Db/.
Result: Zero unfixed SUSPECT instances.
| Class::$property | Verdict | Note |
|---|---|---|
FileLockHandler::$localFallback | OK | Already fixed in 22c5625ef; ICacheFactory backed, in-memory is documented test-only fallback. |
RateLimiter (uses ?ICache $cache, no private array) | OK | ICacheFactory-backed token buckets. |
LockHandler (object locks) | OK | Persists via magicMapper → DB. |
LifecycleGuardRegistry::$cache | OK | Per-request DI resolution cache; documented. |
RequestScopedCache::$cache | OK | Class name says it. |
SchemaMapper::$findCache, RegisterMapper::$findCache | OK | Per-request memoisation; mappers are request-scoped. |
SetupHandler::$infrastructureCreated, $setupProgress | OK | Per-setup-call run state, returned in response envelope. |
RenderObject::$registersCache, $schemasCache, $objectsCache, $ultraPreloadCache, $inverseRelationCache | OK | Per-request render aggregation cache; documented. |
SaveObject::$createdSubObjects, $schemaCache, $registerCache, $schemaReferenceCache, $referenceValidationCache | OK | Per-save-tx state. |
PermissionHandler::$cachedRegisterAuth, $cachedRegisterConfig, $permissionCache | OK | Per-request permission cache. |
CacheHandler::$objectCache, $nameCache, $inMemoryQueryCache, $stats | OK | Documented per-request fast-lookup. |
MagicFacetHandler::$facetCache, $uuidLabelCache, $fieldLabelCache, $warmedFields, $cacheStats, $columnCache | OK | Per-request facet aggregation. |
MappingService::$templateCache | OK | Twig template instance cache. |
OasService::$oas, LanguageService::$acceptedLanguages, ToolRegistry::$tools, VectorizationService::$strategies | OK | Boot-time DI / strategy registry. |
ImportHandler::$registersMap, $schemasMap, ExportHandler::$registersMap, $schemasMap | OK | Per-import/export run state. |
ImportService::$schemaPropertiesCache | OK | Per-import memoisation. |
Configuration*.bak2 files | Skipped | Backup files, not loaded by autoloader. |
All GraphQL/ private arrays | OK | Per-query schema generation cache. |
Pattern B verdict
Clean. No new persistence-layer gaps beyond the already-fixed FileLockHandler. Every private array reviewed is either explicitly per-request (cache, render, save tx, query) or already backed by a persistent store at the class level (?ICache, MagicMapper, etc.).
Pattern C — Phantom ticks in tasks.md
Sampled 3 open changes from openspec/changes/: data-import-export, nextcloud-entity-relations, notificatie-engine. Sampled 5 ticked items per spec.
Spec: data-import-export (15 sampled)
| Claim | Reality |
|---|---|
[x] importFromExcel at lib/Service/ImportService.php:265 | Exists at line 267 (off-by-2, fine) |
[x] importFromCsv at line 349 | Exists at line 353 (off-by-4) |
[x] resolveUuidNameMap at line 397+ | Exists at line 479 (line drift) |
[x] RegistersController::importTemplate route present | Route at appinfo/routes.php:411 |
[x] ImportServiceIntegrationTest 30 tests | 30 public function test* methods present |
Verdict: clean.
Spec: nextcloud-entity-relations (5 sampled)
| Claim | Reality |
|---|---|
[x] Add email routes to routes.php | 5 route entries present |
[x] Add calendar event routes to routes.php | phantom — zero calendarEvents# routes in routes.php |
[x] Add contact routes to routes.php | phantom — only contacts#match present; index/create/update/destroy/objects unrouted |
[x] Add deck routes to routes.php | phantom — zero deck# routes in routes.php |
[x] Implement RelationsController with unified endpoint | Class exists, but relations#index is unrouted — endpoint unreachable |
Verdict: 4 phantom ticks. The line "44/53 backend tasks shipped" in the status block overstates progress.
Spec: notificatie-engine (5 sampled)
| Claim | Reality |
|---|---|
[x] AnnotationNotificationDispatcher::emitNotification | Method at line 757 |
[x] resolveLocalizedSubject method | Method at line 639 |
[x] AnnotationNotificationDispatcherTest 14 tests | 14 test* methods present |
[x] NotificationAnnotationValidatorTest 26 tests | 27 present (1 extra is fine) |
[x] RateLimiterTest 8 tests covering token-bucket | 8 test* methods present |
Verdict: clean.
Pattern C verdict
Mixed. 2 of 3 sampled specs clean; nextcloud-entity-relations has 4 phantom route ticks that map directly to the Tier 1 route gaps in Pattern A. Recommend retroactively unticking those items and folding them into a per-spec route-completion PR (calendar/contacts/deck/relations). Coincidentally the same gap surfaces in tmlo-metadata (archived but with phantom route tick on tasks 4.3 + 5.2) — those should be re-opened.
Summary
| Pattern | Instances | Verdict |
|---|---|---|
| A — unrouted controller methods | 67 (Tier 1: 55 real gaps; Tier 2: 12 dead duplicates) | needs design call |
| B — in-memory state | 0 unfixed | clean |
| C — phantom ticks (sampled) | 4 in nextcloud-entity-relations, 2 in archived tmlo-metadata | needs design call |
Patterns A and C overlap heavily — each phantom route tick corresponds to a Pattern A gap. A coordinated PR per affected spec (profile-actions, nextcloud-entity-relations cluster, tmlo-metadata, linked-entity-types) is the natural unit of work; this audit is the catalogue.