audit(backend): score backend/ against legibility rubric C1–C10 #389
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
Part of Epic #387 — Codebase Legibility Audit. This is AUDIT-2: read-only assessment of
backend/against the full Legibility Rubric. The output is a Markdown report underdocs/audits/audit-backend-report.mdfollowing the per-subsystem orient template. No code changes in this issue.Inputs (read first)
See the first comment of #387 for the complete reference bundle (Brief, Personas, Canonical Domain Set + boundary decisions, Rubric, Orient Template).
Scope (in)
backend/src/main/java/— controllers, services, repositories, entities, DTOs, exceptions, security, configbackend/src/main/resources/—application*.yml, static resources (Flyway migrations are AUDIT-4's scope, but note schema clarity in §3 if relevant)backend/src/test/java/— test layout, naming, mutation-resiliencebackend/pom.xml, Maven wrapper,.mvn/backend/Dockerfile,backend/api_tests/backend/CLAUDE.md(evaluate as documentation surface — much of it documents the current layered structure that the refactor will replace)Scope (out)
backend/target/ocr-service/and Python tooling (AUDIT-3)Rubric checks particularly relevant to this subsystem
All of C1.5, C2, C3.1, C3.2, C3.3, C3.4, C3.6, C4 (excluding C4.4's frontend half), C5, C6.1, C6.2, C6.4, C7, C8.1, C8.3, C8.4, C9, C10.
Specific things to verify (subsystem-specific)
grep.@RequirePermissioncoverage. Note any controller method that mutates state but lacks@RequirePermission.DomainExceptionvsResponseStatusExceptionvs raw — count usages, flag inconsistencies.Acceptance criteria
docs/audits/audit-backend-report.mdexists on a feature branchshared/with admission-criteria justificationDefinition of Done
Report committed under
docs/audits/audit-backend-report.mdonmain. Top-5 recommendations summarized as a closing comment on this issue.Dispatch
Hand to a parallel Explore agent with the prompt:
AUDIT-2 —
backend/Legibility AuditRead-only assessment of
backend/againstRUBRIC-LEGIBILITY-001perTEMPLATE-ORIENT-001. Reference bundle: issue #387 first comment.§1 Subsystem profile
backend/is the Spring Boot 4 monolith REST API for Familienarchiv. It owns the canonical write side of all business state (Documents, Persons, Tags, Users, Transcription, Annotations, Comments, Notifications, OCR jobs, Geschichten, Stammbaum), persistence (PostgreSQL + Flyway), object storage adapters (MinIO/S3), security (Spring Security + custom@RequirePermissionAOP), and async batch orchestration (mass import, OCR, thumbnails). 212 Java source files (excluding tests), 92 test files, 57 Flyway migrations.The package style is mixed: the original layered packaging coexists with three feature packages (
audit/,dashboard/,relationship/) — a partial migration toward domain layout that the planned refactor will complete.§2 Inventory
Controllers (19) → canonical-domain mapping
DocumentControllercontroller/DocumentController.javadocumentPersonControllercontroller/PersonController.javapersonTagControllercontroller/TagController.javatagGeschichteControllercontroller/GeschichteController.javageschichteNotificationControllercontroller/NotificationController.javanotificationOcrControllercontroller/OcrController.javaocrTranscriptionBlockControllercontroller/TranscriptionBlockController.javadocument.transcriptionAnnotationControllercontroller/AnnotationController.javadocument.annotationCommentControllercontroller/CommentController.javadocument.commentTranscriptionQueueControllercontroller/TranscriptionQueueController.javashared/transcription-queueUserControllercontroller/UserController.javauserGroupControllercontroller/GroupController.javauserUserSearchControllercontroller/UserSearchController.javauser(mention search)AuthControllercontroller/AuthController.javauser(auth/invite/reset)AuthE2EControllercontroller/AuthE2EController.javauser(test-only,@Profile("e2e"))InviteControllercontroller/InviteController.javauser(invite admin)AdminControllercontroller/AdminController.javashared/import+shared/dashboard-ishStatsControllercontroller/StatsController.javashared/dashboard(orshared/)RelationshipControllerrelationship/RelationshipController.javaperson.relationship(D-OQ-7)DashboardControllerdashboard/DashboardController.javashared/dashboard(D-OQ-8)GlobalExceptionHandlercontroller/GlobalExceptionHandler.javashared/error-handlingServices (37) → canonical-domain mapping
DocumentService,DocumentVersionServicedocumentTranscriptionService,TranscriptionBlockQueryServicedocument.transcriptionAnnotationServicedocument.annotationCommentServicedocument.commentPersonService,PersonNameParser,PersonTypeClassifierpersonRelationshipService,RelationshipInferenceServiceperson.relationshipTagServicetagGeschichteServicegeschichteNotificationService,SseEmitterRegistrynotificationOcrService,OcrBatchService,OcrAsyncRunner,OcrClient,OcrHealthClient,RestClientOcrClient,OcrProgressService,OcrTrainingServiceocrSenderModelService,TrainingDataExportService,SegmentationTrainingExportServiceocr(training sub-area)UserService,UserSearchService,CustomUserDetailsService,InviteService,PasswordResetServiceuserFileService,ThumbnailService,ThumbnailBackfillService,ThumbnailAsyncRunnershared/file-storageMassImportServiceshared/importTranscriptionQueueServiceshared/transcription-queueAuditService,AuditLogQueryServiceshared/auditDashboardServiceshared/dashboardRepositories (24)
DocumentRepository,DocumentVersionRepository,DocumentSpecificationsdocumentTranscriptionBlockRepository,TranscriptionBlockVersionRepository,TranscriptionQueueProjection,TranscriptionWeeklyStatsProjectiondocument.transcriptionAnnotationRepositorydocument.annotationCommentRepositorydocument.commentPersonRepository,PersonNameAliasRepositorypersonPersonRelationshipRepositoryperson.relationshipTagRepositorytagGeschichteRepository,GeschichteSpecificationsgeschichteNotificationRepositorynotificationOcrJobRepository,OcrJobDocumentRepository,OcrTrainingRunRepository,SenderModelRepositoryocrAppUserRepository,UserGroupRepository,InviteTokenRepository,PasswordResetTokenRepositoryuserAuditLogRepository,AuditLogQueryRepository(inaudit/)shared/auditCompletionStatsRowrepository/)Entities (35) — sample mapping
Document, DocumentStatus, DocumentVersion, DocumentSort (model/), TrainingLabel, ScriptType, ThumbnailAspect→document;TranscriptionBlock, TranscriptionBlockVersion, BlockSource, PolygonConverter→document.transcription;DocumentAnnotation→document.annotation;DocumentComment, PersonMention→document.comment;Person, PersonNameAlias, PersonNameAliasType, PersonType, DisplayNameFormatter→person;Tag→tag;AppUser, UserGroup, InviteToken, PasswordResetToken→user;Notification, NotificationType→notification;OcrJob, OcrJobDocument, OcrJobStatus, OcrDocumentStatus, OcrTrainingRun, SenderModel, TrainingStatus→ocr;Geschichte, GeschichteStatus→geschichte;PersonRelationship, RelationToken, RelationType(inrelationship/) →person.relationship;AuditLog, AuditKind(inaudit/) →shared/audit.DTOs (~55)
Mostly request DTOs co-located with the service that consumes them (e.g.
DocumentUpdateDTO,CreateUserRequest,TagUpdateDTO). A small number of response DTOs exist (PersonSummaryDTO,IncompleteDocumentDTO,DocumentSearchResult,NotificationDTO,MentionDTO,StatsDTO,TagTreeNodeDTO,TranscriptionQueueItemDTO,BulkEditResult,BackfillResult). Validators (UniquePoints,UniquePointsValidator) live indto/.§3 Rubric scorecard
.agent/,frontend/.svelte-kit.old/,frontend/test-results.locked/,proofshot-artifacts/,node_modules/per gitStatusbackend/CLAUDE.mddocuments./mvnw spring-boot:run, rootdocker-compose up -dCLAUDE.mdlists Java 21, Maven via wrapper, PostgreSQL 16, MinIO;Dockerfileuseseclipse-temurin:21.0.10_7application.yaml:65-67defaultsadmin/admin123; documented in user MEMORY.md but not in any human-readable doc the stranger would finddocs/DEPLOYMENT.mdor backend READMEbackend/CLAUDE.mdor HELP.mdbackend/HELP.mdis the Spring Initializr default;backend/CLAUDE.mdexists but is AI-targeted not human-targetedbackend/README.mdwith run/build/test/regen-API stepsdocs/ARCHITECTURE.md; onlydocs/architecture/c4-diagrams.mdexists with no domain listing in prosedocs/ARCHITECTURE.mdenumerating canonical domain setMEMORY.md(project_person_appuser_separation.md)docs/GLOSSARY.mdcovering Person, AppUser, Geschichte, TranscriptionBlock, Annotation, Comment, OcrJobcontroller/,service/,repository/,model/,dto/,exception/are layer-packaged. Onlyaudit/,dashboard/,relationship/are feature-packaged.controller/DocumentController.java,service/DocumentService.java,repository/DocumentRepository.java,model/Document.java,dto/DocumentUpdateDTO.javaall live in different packagesdocument/,person/,tag/,user/,geschichte/,notification/,ocr/,shared/per REFACTOR-1shared/only genuinely cross-cutting)shared/package exists yetDocumentlives inmodel/,controller/,service/,repository/, plusdto/Document*.java(×11)Helper/Utils/Managerwithout modifier)PersonNameParser,PersonTypeClassifier,DisplayNameFormatter); no anonymousUtil.java/Helper.javafound/api/documents/conversation(DocumentController.java:427) is mounted under documents but the conversation concept is its own canonical view domain./api/stats(StatsController) is unobvious.UserSearchControllermounts/api/users/searchbut the class name doesn't match the path. Dashboard endpoints are clean.docs/API.mdbackend/CLAUDE.mddescribes some conventions; nothing tells a stranger how to add a new endpoint or domainDomainException(137 sites),ResponseStatusException(3 controllers +PersonService, 11 sites), rawRuntimeException/IllegalArgumentException/IllegalStateException(5 files, 9 sites — see C5.2 evidence below)ResponseStatusExceptionvalidation in controllers to bean validation orDomainException; routeRuntimeExceptioninMassImportService.java:159,FileService.java:91,RestClientOcrClient.java:218,250,288throughDomainException.internal(...)backend/CLAUDE.mdis the only place layering, error-handling, entity-style, OCR-integration, and run instructions are documented. Tobias would not look there.backend/README.md+docs/ARCHITECTURE.md*Controller, services*Service, repos*Repository, DTOs*DTO/*Request. ≥95% conformancecontroller/StatsController.java:18-19injectsPersonRepository personRepository; DocumentRepository documentRepository;.controller/AuthE2EController.java:27injectsPasswordResetTokenRepository tokenRepository;(test-only profile, but still a violation by the letter of the rule)StatsControllerbody into aStatsService(orDashboardService). Add agetResetTokenForTest()toPasswordResetService(gated by@Profile("e2e")) and call fromAuthE2EController.MassImportService.java:58→DocumentRepository;ThumbnailService.java:65,ThumbnailBackfillService.java:40,ThumbnailAsyncRunner.java:32→DocumentRepository;TranscriptionQueueService.java:23→DocumentRepository;SegmentationTrainingExportService.java:30-32→TranscriptionBlockRepository, AnnotationRepository, DocumentRepository;TrainingDataExportService.java:31-33→ same triple;SenderModelService.java:35→TranscriptionBlockRepository;OcrTrainingService.java:40→TranscriptionBlockRepository;TranscriptionService.java:40→AnnotationRepository;AnnotationService.java:28→TranscriptionBlockRepository;PasswordResetService.java:33→AppUserRepositoryDocumentService.findIdsForThumbnailBackfill(),TranscriptionService.findBlocksForTraining())shared/, not duplicated)AuditService,FileService,MinioConfig,RateLimitInterceptor,GlobalExceptionHandler,SecurityUtils) exist as singletons — not duplicated. But they live inaudit/,service/,config/,controller/,security/rather than a unifiedshared/.shared/during REFACTOR-1SecurityConfig.java:40-46(CSRF rationale),NotificationController.java:39-41,RelationshipController.java:23-29,37-38,AuthE2EController.java:29-30,application.yaml(open-in-view: falserationale, multipart cap). A few opaque blocks remain (e.g.SenderModelService@Lazy @Autowired selfself-reference has only a one-line comment that requires Spring AOP knowledge)@Lazy selfself-injection and atOcrAsyncRunnerTransactionSynchronizationManagerblockgrep -rn "TODO|FIXME|XXX|HACK|ask Marcel|Claude generated"overorg/raddatz/familienarchiv/returned only one German clarification comment inrepository/DocumentRepository.java:32devprofile (application-dev.yaml);backend/CLAUDE.mddescribes the regen flow. No human-readable backend README points to itbackend/README.mdbackend/src/main/resources/db/SCHEMA.mdsummarising tables grouped by domain (this is also AUDIT-4 territory)DocumentServiceTest,PersonServiceTest,TranscriptionServiceTest) but several services are large (DocumentService=971 LOC, MassImportService=385 LOC) and integration-test-heavyDocumentService,PersonService,TranscriptionService,MassImportService,NotificationService,OcrAsyncRunnerbefore REFACTOR-1DocumentServiceTestusesmethod_behavior_whenConditionform (deleteDocument_throwsNotFound_whenMissing,updateDocument_setsArchiveBoxAndFolder); other test files follow same pattern@TestMethodOrderor@Orderannotations found; integration tests use Testcontainers with isolated DBdocs/DEPLOYMENT.md;Dockerfileexists;docker-compose.ymlorchestrates locally; nothing about prodapplication.yaml(SPRING_DATASOURCE_URL,S3_*,APP_*,MAIL_*,APP_ADMIN_*); no consolidated referencebackend/README.mdlisting every${...}knob with default and meaningV{n}__{snake_description}.sql. No comment headers spotted in spot-check; this is AUDIT-4's call@Slf4jused widely;actuator/healthis exposed (SecurityConfig.java:50); no doc points to log location or/actuatorendpoints_old/.backupfiles)model/DocumentSort.javais duplicated bydto/DocumentSort.java; only the DTO version is referenced (grep "model.DocumentSort"returns zero hits anywhere in backend, frontend, or tests)model/DocumentSort.javaOcrClient/OcrHealthClientinterfaces with one impl (RestClientOcrClient) are justified by mockability for tests (test files referenceOcrClient).CompletionStatsRow,TranscriptionQueueProjectionetc. are legitimate Spring Data projectionsTranscriptionService.MAX_TEXT_LENGTH,TRANSCRIPTION_COLOR,ocr.sender-model.activation-thresholdconfig). A couple of inline literals inDocumentController(Math.min(limit, 40)inDashboardController.java:49) are tolerableif (false)/ commented blocks)if (false)matches; no large commented-out blocks found in spot-checkOcrController.resolveUserId()swallows exceptions returning null — fragile pattern;RestClientOcrClient.java:218,250,288wrap-and-rethrow asRuntimeException(also a C5.2 hit)OcrController.resolveUserId(); route OCR exceptions throughDomainException.internal(ErrorCode.OCR_PROCESSING_FAILED, ...)§4 Subsystem health summary
Aggregate: 15 PASS / 6 FAIL-Critical / 12 FAIL-Major / 5 FAIL-Minor / 0 Cosmetic + 1 Unverified-Critical (C8.1).
Overall verdict: 🔴 — six Critical fails, all driven by (a) absent human-readable architecture/glossary documentation and (b) layer-based packaging that the planned REFACTOR-1 will fix.
§5 Domain mapping gaps
Each item below is unclear under the current package layout. Canonical homes are derived from D-OQ-1..8.
TranscriptionBlock,TranscriptionBlockVersion,BlockSource,PolygonConverter,TranscriptionService,TranscriptionBlockQueryService,TranscriptionBlockController,TranscriptionBlockRepository,TranscriptionBlockVersionRepositorydocument.transcriptionvs owntranscription/domaindocument.transcription/sub-packageDocumentComment,PersonMention,CommentService,CommentController,CommentRepository, related DTOs (CreateCommentDTO,MentionDTO)document.commentvs owncomment/domaindocument.comment/sub-packageDocumentAnnotation,AnnotationService,AnnotationController,AnnotationRepository,CreateAnnotationDTO,UpdateAnnotationDTO,UniquePoints/Validatordocument.annotationvs owndocument.annotation/sub-packageOcrJob,OcrJobDocument,OcrJobStatus,OcrDocumentStatus,OcrTrainingRun,SenderModel,TrainingStatus,TrainingLabel,OcrService,OcrBatchService,OcrAsyncRunner,OcrClient,OcrHealthClient,RestClientOcrClient,OcrProgressService,OcrTrainingService,OcrController,OcrJobRepository,OcrJobDocumentRepository,OcrTrainingRunRepository,SenderModelRepository, OCR DTOsocr/domain vs sub-package ofdocumentocr/domainTrainingLabelis currently adocumentfield (Document.trainingLabel); does it stay indocument(the field is on Document) or move toocr(semantics)?Notification,NotificationType,NotificationService,NotificationController,NotificationDTO,NotificationPreferenceDTO,NotificationRepository,SseEmitterRegistrynotification/domainnotification/domainNotificationPreferenceis currently fields onAppUser(isNotifyOnReply,isNotifyOnMention); does it stay on User or moveTag,TagService,TagController,TagRepository,MergeTagDTO,TagUpdateDTO,TagOperator,TagTreeNodeDTOtag/domaintag/(Tier-1)Geschichte,GeschichteStatus,GeschichteService,GeschichteController,GeschichteRepository,GeschichteSpecifications,GeschichteUpdateDTOgeschichte/domaingeschichte/(Tier-1)MassImportService,BulkEditError/Result,DocumentBulkEditDTO, ODS parser codeshared/importvsdocumentshared/importapp.import.col.*is import-specific; safe to live inshared/importAuditService,AuditLog,AuditKind,AuditLogRepository,AuditLogQueryService,AuditLogQueryRepository,ActivityActorDTO,ActivityFeedRow,ContributorRow,PulseStatsRowshared/auditshared/auditFileService,MinioConfig, S3 clientshared/file-storageshared/file-storageThumbnailService,ThumbnailBackfillService,ThumbnailAsyncRunnershared/file-storage(thumbnails are file derivatives) vsdocument(thumbnails belong to documents)shared/file-storagesince thumbnails are a file-storage concern that consumers any document type.document.thumbnailsub-package?TranscriptionQueueService,TranscriptionQueueController,TranscriptionQueueProjection,TranscriptionWeeklyStatsProjection,TranscriptionQueueItemDTO,TranscriptionWeeklyStatsDTOshared/transcription-queue(per canonical set's "transcription-queue" cross-cutting entry)shared/transcription-queue/shared/rather thandocument.transcription/queue/DashboardService,DashboardController,DashboardResumeDTO,DashboardPulseDTO,ActivityFeedItemDTOshared/dashboardshared/dashboardStatsController,StatsDTOshared/dashboardshared/dashboard(it's a 2-row count endpoint)AdminController(mass-import + backfill triggers + thumbnail status)shared/import+shared/file-storageshared/import/, backfill+thumbnail status endpoints with their respective ownersRelationshipController,RelationshipService,RelationshipInferenceService,PersonRelationship,PersonRelationshipRepository,RelationToken,RelationType, relationship DTOsperson.relationshipperson.relationship/sub-package — already feature-packaged at top-level, just nest itDisplayNameFormatter(currently inmodel/, used byPersonandPersonSummaryDTO)person/person/PolygonConverter(inmodel/, JPAAttributeConverterfor transcription block polygons)document.transcription/document.transcription/CustomUserDetailsService,SecurityUtils,Permission,RequirePermission,PermissionAspectshared/securityshared/securityPermissionenum has been silently extended (ANNOTATE_ALL,BLOG_WRITE) —CLAUDE.mddoesn't mention theseRateLimitInterceptorshared/securityorshared/webconfig/and applied to login; lives in security territory§6 Cross-cutting candidates (
shared/admission)Admission criteria: (a) no entity, (b) no user-facing CRUD, (c) ≥2 consumers OR genuine framework infrastructure.
FileService+MinioConfigshared/file-storageAuditService+AuditLogRepository+AuditLog(entity!)AuditLogentityshared/audit(entity is internal to the cross-cutting concern; admission-criterion (a) interpretation: the entity is not a domain entity — it's audit infrastructure)DashboardService+DashboardController+ dashboard DTOsshared/dashboardMassImportServiceshared/importTranscriptionQueueServiceshared/transcription-queueThumbnailService+ThumbnailBackfillService+ThumbnailAsyncRunnershared/file-storage(likely; see §5 blocker)RateLimitInterceptorshared/security(orshared/web)GlobalExceptionHandler+DomainException+ErrorCodeshared/error-handlingPermission+RequirePermission+PermissionAspect+SecurityUtils+CustomUserDetailsService+SecurityConfigshared/securityAsyncConfig+WebConfig+FlywayConfig+DataInitializershared/configSseEmitterRegistrynotification/; not admitted toshared/(only one consumer outsidenotification/)PersonNameParser+PersonTypeClassifier+DisplayNameFormatterperson/person/(rejectshared/— single-domain helpers)OcrClient+OcrHealthClient+RestClientOcrClientocr/ocr/(rejectshared/— all consumers within one domain)Net: ten
shared/sub-packages (shared/file-storage,shared/audit,shared/dashboard,shared/import,shared/transcription-queue,shared/security,shared/error-handling,shared/config, plus optionalshared/web).§7 Dead/suspicious code register
model/DocumentSort.java:1-6dto/DocumentSort.java; zero references anywherecontroller/StatsController.java:18-19StatsServicecontroller/AuthE2EController.java:27@Profile("e2e"))getResetTokenForTest()toPasswordResetServicegated on profile, call from controllerservice/MassImportService.java:159throw new RuntimeException("Ungültige ODS-Datei: content.xml fehlt")— bypasses DomainExceptionDomainException.badRequest(ErrorCode.VALIDATION_ERROR, ...)service/FileService.java:91throw new RuntimeException("Storage Error: " + e.getMessage())DomainException.internal(ErrorCode.FILE_UPLOAD_FAILED, ...)service/FileService.java:167throw new IllegalStateException("SHA-256 not available", e)— same MD pattern inDocumentService.java:967service/RestClientOcrClient.java:218,250,288RuntimeExceptionrethrows from OCR streamingDomainException.internal(ErrorCode.OCR_PROCESSING_FAILED, ...)model/PolygonConverter.java:23,33throw new IllegalArgumentException("Failed to (de)serialize polygon", e)— JPA AttributeConverter; OK to keep but note it doesn't surface a frontend ErrorCodeDomainException.internal(...)if these can ever be hit at runtime; otherwise leave with why-commentservice/OcrController.javaresolveUserId(around line 161)DomainException.unauthorized(...)if auth missingservice/SenderModelService@Lazy @Autowired self@Asyncself-call — common Spring pattern but only one commentcontroller/UserController.java:47-62(updateProfile,changePassword)@RequirePermission— currently rely on Spring SecurityanyRequest().authenticated()@RequirePermission(Permission.READ_ALL)for documentation, or document why personal endpoints don't need it (matchesNotificationController.java:39-41style)controller/StatsController.java@RequirePermission— visible to any authenticated user@RequirePermission(Permission.READ_ALL)for consistencyrepository/DocumentRepository.java:32// z.B. um alle offenen "PLACEHOLDER" zu finden— minor, language inconsistency vs rest of codebase (English)service/MassImportService.java:343// --- Helpers ---section divider is fine; theHelperword is a section label, not a class namebackend/HELP.md§8 Documentation gaps
Missing files (none of these exist):
backend/README.md(human-readable;HELP.mdis Initializr boilerplate,CLAUDE.mdis AI-targeted)document/,person/,tag/,user/,geschichte/,notification/,ocr/)shared/sub-package READMEsdocs/ARCHITECTURE.md(onlyc4-diagrams.mdexists; needs prose enumerating canonical domain set + boundary decisions)docs/GLOSSARY.md(Person ≠ AppUser, TranscriptionBlock vs Annotation vs Comment, DocumentStatus lifecycle, Permission semantics, Geschichte vs Document)docs/DEPLOYMENT.md(env vars, profiles, prod vs dev, default creds)backend/src/main/resources/db/SCHEMA.md(per-table comments grouped by domain — also AUDIT-4 territory)backend/CLAUDE.mdsections that need migration to human-readable docs (C5.3):backend/README.mddocs/ARCHITECTURE.md(after refactor) and a "Conventions" section inbackend/README.mddocs/GLOSSARY.mddocs/CONVENTIONS.md(new) or backend README "How to add an entity" walkthrough@Transactional, cross-domain via service) → backend README "How to add a service"docs/SECURITY.md(alsodocs/security-guide.mdalready exists at root — consolidate); notePermissionenum drift: CLAUDE.md lists 6 permissions butPermission.javahas 8 (ANNOTATE_ALL,BLOG_WRITEundocumented)docs/integrations/ocr.mdorocr/README.mdafter refactorMissing inline why-comments (C7.2 caveat):
SenderModelService@Lazy @Autowired self(proxy/self-call rationale)OcrAsyncRunnerTransactionSynchronizationManager.registerSynchronization(...)block (why post-commit dispatch matters)application.yaml:38max-request-size: 500MBreferences#317— link to issue is good but the reasoning ("supports 10-file chunk at max per-file size") could be a short header in the file rather than only inlineAPI contract gaps (C7.4):
npm run generate:api) is documented infrontend/package.jsonandCLAUDE.mdbut not in any backend doc that a contributor would discover first§9 Prerequisites for big-bang refactor (REFACTOR-1 #407)
Before moving classes between packages, the following must be true. Any service whose tests fall short of mutation-validation risks silent regressions during the move.
Mutation-validate test suites for these services first (large, central, business-logic-heavy):
DocumentServiceMassImportServiceTranscriptionServicePersonServiceNotificationServiceOcrAsyncRunnerLayering violations that MUST be fixed before the package move (otherwise the move surfaces them as cross-package imports, masking the real fix):
MassImportService→DocumentRepositoryDocumentService(introduce write helpers)ThumbnailService/ThumbnailBackfillService/ThumbnailAsyncRunner→DocumentRepositoryDocumentService.findIdsForThumbnailBackfill()andDocumentService.updateThumbnailMetadata()TranscriptionQueueService→DocumentRepositoryDocumentService.queueProjection(...)or admit as documented exceptionSegmentationTrainingExportService/TrainingDataExportService→Document/Block/AnnotationreposSenderModelService/OcrTrainingService→TranscriptionBlockRepositoryTranscriptionServiceTranscriptionService→AnnotationRepositoryAnnotationService(sub-package, easier)AnnotationService→TranscriptionBlockRepositoryTranscriptionService(sub-package)PasswordResetService→AppUserRepositoryUserService.findByEmail/saveStatsController→Person/DocumentRepositoryStatsService(or fold intoDashboardService)AuthE2EController→PasswordResetTokenRepositoryPasswordResetService.findE2EToken(email)gated on@Profile("e2e")Prerequisite to delete (avoid carrying dead code into new packages):
model/DocumentSort.java(duplicate ofdto/DocumentSort.java, unused)backend/HELP.md(replace with real README)Decisions still needed (one-line ratifications):
shared/file-storageordocument.thumbnail?Document.trainingLabelfield move toocr/or stay indocument/?AppUsernotification-preference fields stay on User or move tonotification/?§10 Top-5 prioritized recommendations
docs/ARCHITECTURE.md+docs/GLOSSARY.md— closes 4 of 6 Critical fails (C1.1, C3.1, C3.2, C3.3) and unblocks Anja and Tobias before any code moves; the package refactor without these docs is a worse legibility outcome, not a better one.throw new RuntimeException(...)/IllegalArgumentException(...)inMassImportService,FileService,RestClientOcrClient,PolygonConverter, andDocumentServicetoDomainException, and convert controllerResponseStatusExceptionvalidation to@Valid+DomainException; this satisfies C5.2 and gives the frontend a uniformcodeto translate.DocumentService,PersonService,TranscriptionService,MassImportService,NotificationService,OcrAsyncRunner— without this, REFACTOR-1's "tests still green" signal is unreliable for the largest blast-radius services (C8.1 currently Unverified-Critical).model/DocumentSort.java, replacebackend/HELP.mdwith a realbackend/README.md(run/build/test/regen-API/env-vars/troubleshooting), and document the silently-addedPermission.ANNOTATE_ALLandPermission.BLOG_WRITE— small, fast cleanups that close C2.5, C5.3, C7.4, C9.2, C9.4, and C10.1 in one PR each.