Skip to content

Surface-area reduction plan (October 2026)

This plan covers epic #3988: deleting dead code and packages, the v0/v1 schema branches, AutoMapper, the overlapping observability vendors, and the infrastructure carried by SharedKernel. It also coordinates the retirement of Auth0 and of the legacy authorization mode, which other sessions own. It is planning only; nothing starts until Chris approves and answers the decisions in §9. Evidence was re-checked against main@85e6facf7 (3 Oct 2026). Each row says whether the item is VERIFIED (code read), CORRECTED (the review appendix was wrong; the row says how) or NOT VERIFIED (with the reason).

1. Scope

ID Problem Impact Evidence (file:line) Status Issue
A1 API keeps the Auth0 Management API path Two identity-admin implementations, plus 2 Auth0 NuGet packages Program.cs:343-353; Services/Auth0Service.cs (234 lines), AuthManagementApiClientProvider.cs (67); SyRF.API.Endpoint.csproj:16-17 (Auth0.* 7.34.0) VERIFIED #3988 / #2466 (M005 S16)
A2 Auth0 JwtBearer scheme and a required Auth0Config Startup fails without an Auth0: section even under OpenIddict; the section also supplies the JWT authority Program.cs:90-92 (EnsureConfigured), :532-563; Auth0Config.cs:77-84 (the "named Auth0 only for backwards compatibility" message) VERIFIED #2466 (S16)
A3 IdentityProviderSelection: a 3-mode switch (OpenIddict/Auth0/NoOp) Extra runtime branch and inference rules Services/IdentityProviderSelection.cs (306 lines); Program.cs:326-357 VERIFIED. Gap: S16's plan did not name it; accepted 2026-10-05 (auth session): S16 names it #2466
A4 Swagger defaults to the Auth0 provider; the client secret is exposed Security (V4) api/.chart/values.yaml:219 (provider: auth0); Program.cs:94-99 VERIFIED #3992, draft #3966 (migration session)
A5 Web ships @auth0/auth0-angular ^2.2.3, @auth0/auth0-spa-js ^2.1.3 and a 3-mode authProvider (auth0/oidc/bff) Bundle weight; an unset or empty authProvider falls back to Auth0 package.json:45-46; core/auth/auth-provider.token.ts; main.ts:493-520,553; appConfig.default.json:29 = "auth0", generated default "" VERIFIED. Accepted 2026-10-05 (auth session): S16's acceptance criteria flip or remove the unset-authProvider→Auth0 fallback #2466 (S15, S16, S23)
A6 Auth0-era auth effects Dead code in bff/oidc modes auth.effects.ts: connectAccount$ :386, disconnectAccount$ :434, managementAccessToken$ :467, silentSignOut$ :629 (each filtered to auth0); signupUser$ :758 is dead in every mode (startSignUp is never dispatched) VERIFIED #2466 (S23A)
A7 SyRF.Identity.Migration plus its campaign Job chart 11.5K lines of code, 18K lines of tests, a 740-line chart template src/services/identity/SyRF.Identity.Migration/; identity/.chart/templates/campaign-job.yaml CORRECTED (the review said 10.2K lines). Gap: S16 says "retain migration projects", and no M005 slice retired them. Resolved 2026-10-05 (auth session): S16 keeps them deliberately through the production rollback window; new slice S17 retires them once the window closes #2466 (M005 S17)
L1 Application authority runs in three modes: Off, Shadow, Enforced Duplicate decision paths in HTTP, SignalR and reports ApplicationAuthority/ApplicationAuthorityMode.cs:10-20; mode or verdict branches: AuthorizationHandler.cs 11, ProjectAuthorityGate.cs 9, ApplicationAuthorityGate.cs 9, SignalRAuthorizationHandler.cs 6, others 9 CORRECTED: about 46 branch sites in 10 non-test files by my grep; the reviewer's "32" used a narrower definition. Sibling flags: applicationSuspension (206 refs), impersonationReadOnlyEnforcement, DelegatedWorkAdmissionEnforced #3335
S1 v0/v1 dual schema Every save branches; latent data-loss bug B9 Class maps with ShouldSerialize on SchemaVersion: ProjectRepository.cs:1366-1391 (security settings), :1400-1418 (Project), :1525-1596 (Stage), :1683-1705 (Target), plus AnnotationQuestion and ProjectMembership; InvestigatorRepository.cs:123,126; SystematicSearchRepository.cs:60-73; StudyRepository.cs:3147,3161 (Study, OutcomeData). Domain branches: Project.cs:1070,1124,1155,1182, ProjectMembership.cs:288, AnnotationQuestion.cs:172, Target.cs:312 CORRECTED: at least 10 types, not 7 #3988
S2 B9: a new aggregate's schema version is never stamped With DefaultSchemaVersion=1, a new project saves v0 Registrations and loses its v1 memberships, including the creator's Audit.cs:28-34 (OnSaving ignores schemaVersion); AggregateRoot.cs:17-24 (new Audit(userId) leaves 0; SchemaVersion reads Audit first); Project.cs:62 VERIFIED (mechanism). Config drift: API appsettings.e2etest.json:100 = 1, PM e2etest = 0. Whether E2E project creation hits the bug: NOT VERIFIED new
D1 API dead code Noise; misleading code 9 fully commented-out files (451 lines) plus 3 namespace-only files (22 lines); AccountBindingModels.cs/AccountViewModels.cs (10 classes, 0 refs); FileStreamingHelper, MultipartRequestHelper, BadRequestUnhandledExceptionFilterAttribute, CustomTokenRetriever, MappingKeys, NactemApiRoutes (+ config at Program.cs:188-191), AnnotationSummaryResolver (fake numbers), AddResponseCaching (Program.cs:186), csproj DataExportDtos excludes (:65,66,81,84) VERIFIED; CORRECTED count (9+3 files, not 10) #3988
D2 Generator endpoint — ApplicationController.cs:246-257 CORRECTED: load-bearing. It makes NSwag emit the SignalR notification DTOs that signal-r.service.ts:46-50,326-337 imports. Kept (see D6) —
D3 SyRF.API.Messages: 6 of 7 contracts unused Dead contracts; living-search events published to exchanges nothing consumes 4 unreferenced (IReviewerAddedEvent, IStudyGivenSyRFIDEvent, IAnnotationsReconsiledEvent, IAnnotationAddedEvent); 2 published without a consumer (LivingSearchHandler.cs:20,34); live: ISearchUploadStartedEvent VERIFIED #3988
D4 Unused API packages Restore and build weight; advisories SendGrid 9.29.1 (csproj:50), Serilog.Sinks.Elasticsearch (:55), Serilog.Sinks.Http (:56), Elastic.Apm.SerilogEnricher (:21), Elastic.CommonSchema.Serilog (:22); redundant Elastic.Apm.NetCoreAll (:20); Humanizer.Core.uk (:24, also Mongo.Data :14) VERIFIED. CORRECTED: Humanizer is used (StudyDto.cs:106), so swap .uk for Humanizer.Core rather than delete it #3988
D5 SharedKernel dead types Grab-bag kernel About 32 of 189 public types have no reference outside their file (Maybe, ListSetWithAction, Enumeration, PartitionAsync.cs, HttpHelpers, the 7 DataTables types, and others) VERIFIED #3988
D6 IMessageSender, whose MessageSender throws in all 3 methods 48 dead parameters in persistence signatures Infrastructure/MessageSender.cs; SyrfRegistry.cs:11; IUnitOfWork.cs (21), MongoUnitOfWorkBase.cs (21, passed on at :739), EventManager.cs:21 (ignored) VERIFIED #3988
D7 Legacy "Soles"/"C3Rs" scan prefixes Scans assemblies that no longer exist MongoLamarRegistry.cs:74-75, TaskRegistry.cs:13-14, SyrfRegistry.cs:17-18,26-27 VERIFIED; no such assemblies exist #3988
D8 PM dead code Noise; latent CSV formula injection Potential, ReferenceLibrary and ProjectDailyStat (0 refs); StudyRepository.Cursor/FilterSortCursor :1176-1298 (0 callers; quotes are escaped but formulas are not neutralised); 9 interface methods with no production caller (IStudyRepository.cs:74,122,127,150,168,195,207,230,249); NewEndnoteReader.cs VERIFIED. CORRECTED: RiskOfBiasAiJob is a deliberately mothballed storage contract (docs/architecture/mothballed-ai-risk-of-bias.md), so it is kept #3988
D9 Unused PM packages Restore weight PM Endpoint: MassTransit.EntityFrameworkCore, EF Core SqlServer and Design, HealthChecks.EF (csproj:18-20,26; EF is real only in Quartz). PM Core: Bogus, Humanizer.Core, Mvc.NewtonsoftJson, System.Linq.Async (Core.csproj:15,17,18,23) VERIFIED #3988
D10 Web dead code Noise; build time 22 unreferenced components, directives and pipes (list in §5 R0.5); info/contact-us/dynamic-locale.ts is byte-identical to core/dynamic-locale.ts; src/app/state-example.ts (0 importers) CORRECTED (22, not about 17); VERIFIED #3988
D11 Repository clutter Leaks local paths; misleads agents src/services/api/SyRF.API.Endpoint/msbuild.binlog (637 KB, contains C:\Users\chris paths), no *.binlog in .gitignore; root INCIDENT-REPORT.md, PR-DESCRIPTION.md, PROJECT_GUARD_IMPROVEMENTS.md; legacy/ (3 Tekton tasks, unreferenced); Jenkins X OWNERS/OWNERS_ALIASES (18 files), .chart/Kptfile and .chart/Makefile (4 each); docs/README.md:669 wrongly says no legacy folders remain VERIFIED #3988
D12 docs/planning staleness Agents read outdated plans 330 .md files, 238 status: Completed; quoted and unquoted status values mixed CORRECTED (238, not 231) #3988
M1 AutoMapper 16.1.1 with no licence key; duplicated scanners; I/O in resolvers Licence exposure; tests validate a configuration production doesn't use; hidden N+1 queries SharedKernel.csproj:23, API.Endpoint.csproj:18; no LicenseKey anywhere; App_Start/AutoMapperConfig.cs:28 (live) vs Infrastructure/AutoMapperConfig.cs:31 (what AutoMapperConfiguratorTests use); 54 static SyrfMapper sites in 15 files; 13 resolvers/converters, 6 touching repositories; about 116 map definitions VERIFIED #3988
O1 Backend observability overlap Four tracing paths; PII Elastic APM (HostExtensions.cs:62,193,218; chart default enabled: false, staging false, no production override, so off); Sentry (chart default enabled: true, production sentry secret synced; SendDefaultPii = true at HostExtensions.cs:134); OpenTelemetry (SyrfConfigureServices.cs:92-104; no OTLP endpoint in cluster-gitops per migration memory); Prometheus exporter 1.15.3-beta (off by default); dead ISyrfSpanAccessor VERIFIED. Whether Sentry receives production backend events: NOT VERIFIED (needs Sentry console access) #3988
O2 Browser observability overlap GDPR Elastic RUM on in production (environments/production/web/values.yaml:62 apm: true; sends user id, username and email; host hard-coded in appConfig.default.json:9); Sentry (whole user object in setUser, DSN hard-coded); LogRocket on in staging and production (records the full NgRx state and actions, name and email; no sanitisers; app id hard-coded at main.ts:~627) VERIFIED #3988
K1 SharedKernel carries infrastructure Every host, the Lambda included, pulls it all in Used: Lamar (5 files), MailKit (SmtpEmailSender), AutoMapper (4 files), AspNetCore.Http.Abstractions 2.2.0 (MyUtils.cs:19, IHubInvocationContext.cs:6). Unused: MassTransit 8.4.0, Serilog 4.2.0 CORRECTED: 6 of 13 packages are used; only 2 are dead #3988
K2 Contracts depend on the domain Lambda and pdf-agent build against PM.Core and WebHostConfig PM.Messages.csproj references PM.Core, and 8 of its 27 files import ProjectAggregate; s3-notifier and pdf-agent reference WebHostConfig (6 usings, for the RabbitMQ TLS helper) and ProjectAggregate (14 usings); pdf-agent also references PM.Core directly VERIFIED #3988 (Phase 3)

2. MVP boundary, out of scope, feature-flag decision

MVP = R0 (quick deletions, 6 parallel PRs) plus R1.0 (fix the B9 hazard and the config drift). Acceptance: - no behaviour change; - every deleted symbol has zero references, and the build plus focused tests pass; - the repo-hygiene items are gone.

The shortest critical path is R0.1 (repo hygiene), which shares no files with anything else.

After the MVP: - R2: observability, decision-led; - R3: AutoMapper to Mapperly; - R4: SharedKernel slimming and PM.Contracts; - R5: the v0/v1 migration; - R6 and R7: coordination only, for legacy authorization and Auth0.

Out of scope, and where each is tracked:

Item Tracked in
Auth0 code cleanup itself (S15–S25), rollout, the tenant Migration session, #2466 / M005
Swagger secret fix #3992, draft #3966
Authority M6–M8 #3335, application-authority-transition/implementation-plan.md
Lamar to built-in DI; await domain events, then an outbox Phase 0–3 issues #3975 and #3973; DI direction in the synthesis §5
Email consolidation (SendGrid, SES, SMTP) Synthesis V17; only the dead SendGrid package is removed here
Web state packages (@ngrx/*, logrocket-ngrx, @rx-angular/state) #3989 state plan R6
MassTransit after v8 #3986
Central package management #3982
Replacing the Generator NSwag trick D6
ProjectStatistics freeze or isolate #3987

Feature-flag decision: - R0, R1.0, R3 and R4: not flagged. These are deletions or behaviour-preserving refactors. Parity is enforced by zero-reference checks, contract snapshots and wire-compatibility tests, not by a kill switch. - R2: config-driven per vendor, using the existing sentry.enabled, elasticApm.enabled, featureFlags.apm and featureFlags.logRocket switches. Removing code comes only after the flag has been off in every environment for one release. - R5 changes persisted data, so it is flagged. The writer schema stays on AppSettingsConfig:DefaultSchemaVersion (already config), and the read path stays dual until the contract PR. - R6 and R7 follow their owners' gates.

3. Common acceptance criteria (every PR)

# Criterion Verification
C1 Every removed type, member, file or package → zero references remain in src/, e2e/, charts, workflows and docs rg output in the PR body; dotnet build of the affected projects; tsc for web
C2 No behaviour change unless the PR body declares one Focused tests of the touched projects (dotnet test <proj> --filter …, niced); web pnpm exec ng test --include=… plus the 3 repo-wide guard specs
C3 Package removals → dotnet list package / pnpm why show them gone, and no transitive break Restore log; CI build
C4 No file outside the PR's declared boundary is touched, so parallel PRs stay conflict-free Diff review
C5 Docs that describe removed things are updated (docs/, CLAUDE.md, dependency-map.yaml) Review; validate-docs --skip-indexes
C6 Reviews settled on the head (pr-review-settled.sh) and CI green Governance
C7 Production rollout = a separate Chris-approved step; no production promotion PR is merged by this plan Governance

4. Value and safety ordering

Release Value Risk Parallel?
R0 quick deletions Medium: noise, packages, GDPR-adjacent clutter Very low Yes, 6 PRs
R1.0 B9 guard High: prevents silent membership loss Low Yes, alongside R0
R2 observability High: GDPR and cost Low to medium (production telemetry changes) After D2 and D3
R3 AutoMapper → Mapperly High: licence, N+1, test fidelity Medium (many DTOs) Per area after R3.1
R4 SharedKernel / PM.Contracts Medium: Lambda isolation, build graph Medium-high (wire names) After #3986 direction
R5 v0/v1 High: removes branching in every save High (production data) After the census and D4
R6 legacy authorization High Gated on the owner's M6 Owner
R7 Auth0 High Gated on M005 G4 Owner

5. Releases and PRs

R0: quick deletions (6 parallel PRs, no shared files)

R0.1 Repository hygiene (S). - Delete msbuild.binlog and add *.binlog to .gitignore. - Delete the 3 stale root files; move anything worth keeping to docs/archive/. - Delete legacy/ (Tekton). - Delete the Jenkins X OWNERS/OWNERS_ALIASES (18 files) and the .chart/Kptfile and .chart/Makefile files (4 each). - Fix docs/README.md:669.

# Acceptance criterion Verification
0.1.1 git ls-files '*.binlog' → empty, and a new binlog is ignored git check-ignore in the PR body
0.1.2 legacy/, the Jenkins X files and the 3 root files are gone, and nothing references them rg (C1)
0.1.3 Every chart still lints and renders helm lint/helm unittest for api, pm, quartz and web (CI)

R0.2 API dead code and packages (S–M). - Delete everything in D1. - Delete the 4 unreferenced D3 contracts. - Remove the D4 packages, and replace Humanizer.Core.uk with Humanizer.Core. - Remove the living-search publishes (LivingSearchHandler.cs:20,34) and their 2 contracts only after a read-only broker check shows no queue bound to those exchanges. - Keep Generator. - Keep the Program.cs edits to 2 deleted lines (:186, :188-191) to limit conflicts with the notifications stack.

# Acceptance criterion Verification
0.2.1 The API builds; the SyRF.API.Endpoint.Tests focused filters for touched areas pass Build; tests
0.2.2 The 5 packages are gone, and Humanizer title-casing still works in StudyDto dotnet list package; a unit test asserting a humanized value
0.2.3 Staging and production broker definitions (read-only) → no binding on the living-search exchanges; otherwise this part is split out rabbitmq_broker_list_bindings output (read-only)
0.2.4 NSwag regeneration → api-client.generated.ts changes only for removed DTOs (none expected) nswag:all after a Release build; diff

R0.3 SharedKernel dead types (S). - Delete the about 32 unreferenced types. - Remove the MassTransit and Serilog package references. - Remove the Soles/C3Rs prefixes. - Leave IMessageSender out of this PR. It is removed in the PR that awaits domain events (#3973), because both edit MongoUnitOfWorkBase.cs:737-740.

# Acceptance criterion Verification
0.3.1 Every deleted type has zero references outside its own file, including tests Script output (C1)
0.3.2 Lamar AssertConfigurationIsValid passes in API, PM, Quartz and Identity startup tests Existing container-validation tests
0.3.3 MassTransit and Serilog are absent from SyRF.SharedKernel.csproj, and the solution restores Restore log

R0.4 PM dead code and packages (S). - Delete Potential, ReferenceLibrary, ProjectDailyStat, Cursor/FilterSortCursor and NewEndnoteReader. - Delete the 9 dead IStudyRepository methods with their implementations and tests. Keep SetSlotReservationSuspendedScheduleTokenAsync, which looks in progress. - Remove the D9 packages. - Keep RiskOfBiasAiJob.

# Acceptance criterion Verification
0.4.1 PM Core, Mongo.Data and the PM Endpoint build, and their focused tests pass Tests
0.4.2 No EF package remains in the PM Endpoint, and Quartz's EF use is unchanged dotnet list package
0.4.3 No CSV builder without formula neutralisation remains in StudyRepository rg 'Cursor\('; review

R0.5 Web dead code (S). - Delete the 22 unreferenced classes: - ProjectInfoDialogEntryComponent - TestDialogComponent - HybridExampleComponent - CreateProjectGroupComponent - RiskOfBiasPanelComponent - RegisterPanelComponent - StageComponent - StageStudiesComponent - StudyIndexComponent - ExpandedDetailComponent - SmoothProgressDirective - RadioGroupComponent - ProgressDemoComponent - EditableTextDisplayComponent - TypedTemplateDirective - ValidatePatternPipe - UnauthenticatedComponent - VersionCheckDialogComponent - AutofocusDirective - SignedOutComponent - CustomErrorStateMatcherDirective - PdfDisplayComponent - Delete the duplicate dynamic-locale.ts (repoint contact-us.component.ts:54 to @core/dynamic-locale) and state-example.ts, together with its eslint-suppressions.json entry. - SignedOutComponent and UnauthenticatedComponent sit under core/auth. Confirm with the migration session that S15/S23 don't plan to route to them; otherwise drop them from this PR.

# Acceptance criterion Verification
0.5.1 The production build and the full web suite pass CI Test Web (Angular)
0.5.2 Contact-us still renders the locale-specific form Existing spec
0.5.3 Bundle size does not grow ng build stats

R0.6 docs/planning archive (M). - Move the 238 Completed plans to docs/planning/_archive/<year>/, keeping redirects through the index files. - Normalise status quoting. - .planning/ (141 files) is not touched (D8).

# Acceptance criterion Verification
0.6.1 docs/planning/ root has no Completed file rg -l '^status: *"?Completed' docs/planning --max-depth 1
0.6.2 No broken internal link validate-docs --skip-indexes plus the link check

R1.0: B9 guard and config drift (S; parallel with R0)

  • Make Audit.OnSaving stamp SchemaVersion on first save only (never downgrade). Or, as the minimum, have the PM and API startup refuse DefaultSchemaVersion != 0 until R5. Recommended: the startup guard, which leaves data behaviour unchanged.
  • Align API appsettings.e2etest.json:100 to 0.
# Acceptance criterion Verification
1.0.1 Host starts with DefaultSchemaVersion=1 → startup fails with a named error Unit test of the options validator
1.0.2 A new project created through the API in the hermetic stack → its creator membership persists after reload E2E (e2e/ stack)
1.0.3 API and PM e2etest configs agree Config diff

R2: observability consolidation (needs D2 and D3)

Target (recommended): - One instrumentation API, OpenTelemetry. - One error and trace backend, Sentry, which Sentry.OpenTelemetry 4.0.2 already bridges. - Optionally, Prometheus metrics for Grafana. - Elastic APM (backend) and Elastic RUM (browser) go. - LogRocket goes unless Chris keeps it under D3.

R2.0 ADR (S). Records the target and a PII policy: no SendDefaultPii, user identified by GUID only, and no names or emails sent to any vendor.

# Acceptance criterion Verification
2.0.1 The ADR is approved by Chris and merged Governance

R2.1 Backend (M). - Remove Elastic APM: the HostExtensions calls, both package references, the chart elasticApm values and the env-mapping block (regenerate). The GitOps secret cleanup is a separate PR. - Set SendDefaultPii = false. - Delete ISyrfSpanAccessor/SyrfSpanAccessor. - Verify reviewer claim B9 of Appendix B (Mongo command text in traces, MongoContext.cs:52; NOT VERIFIED here); if it is true, disable command text capture.

# Acceptance criterion Verification
2.1.1 No Elastic.Apm* package or elasticApm chart key remains rg; helm unittest
2.1.2 Sentry events carry no IP, cookies or email Unit test over the SentryOptions configuration
2.1.3 A trace span never contains a full BSON command Unit test of the subscriber options
2.1.4 Staging: one forced error appears in Sentry with environment staging Staging rollout check

R2.2 Browser (M). - Remove @elastic/apm-rum-angular, apm-setup.service.ts and featureFlags.apm. - Production loses Elastic RUM, which is live today, so this changes production telemetry and needs D2. - Sentry setUser gets the user GUID only, and project/study names are dropped from tags.

# Acceptance criterion Verification
2.2.1 No @elastic/* import or package remains pnpm why; rg
2.2.2 The Sentry user payload is {id} only Spec of sentry.service.ts

R2.3 LogRocket (S, per D3). - Remove: delete logrocket and logrocket-ngrx, log-rocket.service.ts, the meta-reducer factory (main.ts:612-669) and featureFlags.logRocket. This unblocks state-plan R6. - Or keep: add DOM, network and console sanitisers, plus an NgRx actionSanitizer/stateSanitizer, a GUID-only identify, and a consent decision recorded in the privacy notice.

# Acceptance criterion Verification
2.3.1 Remove → no LogRocket network request on any page E2E network assertion in the hermetic stack
2.3.2 Keep → recorded payloads contain no email, name or study text Spec over the sanitiser configuration

R3: AutoMapper → Mapperly

R3.0 Licence posture now (S, D5). Register for the AutoMapper Community licence and configure LicenseKey. Or pin to the last MIT-licensed major (believed to be 14.x; NOT VERIFIED, so confirm on the NuGet licence page before deciding).

R3.1 One configuration, tested (S). - Delete Infrastructure/AutoMapperConfig.cs. - Make AutoMapperConfiguratorTests build the production configuration (App_Start) and call AssertConfigurationIsValid.

# Acceptance criterion Verification
3.1.1 One AutoMapperConfigurator exists rg
3.1.2 The test fails if any production map is invalid Throwaway red commit

R3.2 I/O out of resolvers (M). The 6 resolvers or converters that use repositories get their data pre-loaded by the caller (controller or application service), batched to remove the N+1.

# Acceptance criterion Verification
3.2.1 No resolver or converter takes IPmUnitOfWork or a repository Architecture test (reflection)
3.2.2 Affected endpoints return byte-identical JSON before and after Contract snapshot tests (Testcontainers Mongo)
3.2.3 Query count per request for list endpoints is ≤ the AutoMapper version Mongo command counter in an integration test

R3.3 Mapperly by area (M each). One PR per controller family (Project, Study, Review, Search/Export, Account/Investigator, PM Core). Each PR adds [Mapper] partial classes injected through DI, replaces the SyrfMapper static calls, and deletes those profiles.

# Acceptance criterion Verification
3.3.1 Each migrated DTO serialises identically to the AutoMapper output for fixture aggregates Snapshot tests written before the swap
3.3.2 Mapperly unmapped-member diagnostics are errors for the new mappers Build (RMG diagnostics as errors)
3.3.3 SyrfMapper call sites fall area by area to 0 rg -c in the PR body

R3.4 Removal (S). Uninstall AutoMapper from the API and SharedKernel, and delete AutoMapperExtensions.

# Acceptance criterion Verification
3.4.1 No AutoMapper package remains in the solution dotnet list package

R4: SharedKernel slimming and PM.Contracts (after the #3986 direction)

PR Change Effort
R4.1 Move the Lamar registries (SyrfRegistry, TaskRegistry, proxy convention) into SyRF.WebHostConfig.Common (or a new SyRF.Hosting); SharedKernel loses Lamar. Coordinate with the DI plan: if Lamar is leaving, they move once M
R4.2 Move SmtpEmailSender (MailKit) into an email infrastructure library; SharedKernel loses MailKit S
R4.3 Move the HttpContext helpers (MyUtils IHttpContextAccessor, IHubInvocationContext) into a web library with FrameworkReference Microsoft.AspNetCore.App; drop Http.Abstractions 2.2.0 S
R4.4 SyRF.Messaging: extract the 74-line RabbitMQ TLS helper so s3-notifier and pdf-agent drop WebHostConfig S
R4.5 PM.Contracts: primitive records for the PM messages; PM.Messages stops referencing PM.Core; s3-notifier and pdf-agent stop importing ProjectAggregate L
# Acceptance criterion Verification
4.1 SyRF.SharedKernel.csproj references none of Lamar, MassTransit, MailKit, AutoMapper or Http.Abstractions csproj diff
4.2 s3-notifier and pdf-agent have no reference (direct or transitive) to PM.Core or WebHostConfig dotnet list reference plus a transitive check script
4.3 Every moved message keeps its exchange/URN name ([MessageUrn]/[EntityName] pinned) and round-trips through STJ Contract tests (shared with #3984)
4.4 Mixed versions (old PM, new API, and the reverse) exchange every moved message Testcontainers RabbitMQ test with the two serialisers
4.5 dependency-map.yaml and the change detector match the new graph Registry validation (#3983)

R5: v0/v1 schema (needs D4; production data)

R5.0 Census (S, read-only). - An aggregate count per collection (pmProject, pmStudy, pmInvestigator, pmSystematicSearch) of Audit.SchemaVersion and of v0/v1-only element presence (Registrations vs Memberships, CustomProjectResourceSecurity vs CustomProjectPermissions, …) on syrftest. - Use counts only, through the read-only MCP, with maxTimeMS. - The 2026-09-08 census found all 2,307 projects at schema 0. The other collections were not counted.

# Acceptance criterion Verification
5.0.1 The count table, by collection × version × element, is recorded in docs/planning/ with no identifiers Docs

R5.1 Migration runner (M). This is WP-M1 from the authorization programme, accepted 2026-09-08 and not yet built: no runner exists in the repo. - A one-shot console with an ArgoCD PreSync Job that waits for db-ready. - A syrf_metadata stamp. - --dry-run as the default. - Batched, resumable and idempotent: CAS on Audit.Version, then $inc.

# Acceptance criterion Verification
5.1.1 Dry run → counts only, no writes Testcontainers test asserting zero writes
5.1.2 An interrupted run resumes without double-applying Testcontainers kill-and-resume test
5.1.3 A concurrent app save during migration → no lost write (CAS conflict retried) Testcontainers concurrency test

R5.2 Per-type transform (M). Applied per D4's choice for each type, either "rewrite to v1" or "collapse: v0 is canonical, delete v1 code, no data write". The rollback is an Atlas snapshot taken immediately before, plus the inverse transform tested on the snapshot restore.

# Acceptance criterion Verification
5.2.1 Migrated documents load to domain objects equal to the v0 load of the same document Property-based test over seeded and anonymised fixtures
5.2.2 A snapshot-preview (use-snapshot) run → dry-run counts match the census ±0, and the real run then completes Preview rehearsal
5.2.3 Inverse transform on the rehearsal → documents match the pre-run backup Rehearsal diff
5.2.4 Production run = a separate Chris-approved step with a fresh backup Governance

R5.3 Contract (M). This is WP-M3: delete every ShouldSerialize on SchemaVersion, the domain branches, DefaultSchemaVersion and the R1.0 guard. It merges only after R5.2 has been verified in production.

# Acceptance criterion Verification
5.3.1 rg 'SchemaVersion\s*(==\|>\|<)' src → 0 hits outside ProjectStatistics and Identity rg
5.3.2 The full PM and API suites pass CI

R6: legacy authorization retirement (coordination; owner = authority programme)

The authority plan stops at M6 (staged cutover), M7 (claim and storage cleanup) and M8 (topology ADR). No milestone deletes the Off/Shadow code. This plan proposes one to the owner as M9, starting once M6 is enforced in production and G-R (the rollback window) has closed: - remove ApplicationAuthorityMode.Off/Shadow, CompareShadow and the legacy claim branches; - collapse syrfOwnedApplicationRoles(+Enforced) into one always-on path; - do the same for impersonationReadOnlyEnforcement, applicationSuspension and DelegatedWorkAdmissionEnforced once each is enforced in production.

# Acceptance criterion Verification
6.1 Mode or verdict branch sites go from about 46 to 0, and the enum is deleted rg count
6.2 The authority E2E lane (bash e2e/authority/run.sh) passes with no flag set E2E (hermetic)
6.3 The flags are removed from env-mapping.yaml, and their GitOps values are cleaned in a separate PR helm unittest; GitOps PR

R7: Auth0 retirement (coordination; owner = SyRF authentication migration session)

This plan opens no Auth0 PR. It asked the migration session to absorb three gaps into M005. The session replied on 2026-10-05 (the "Owner response" column); the gaps are now its slices.

Item M005 slice Gap or request Owner response (auth session, 2026-10-05)
A1, A2 S16 Already covered; S16 moves provider-neutral authority values to narrowly named options —
A3 IdentityProviderSelection S16 Add it explicitly to S16's file boundary Accepted (a): S16 names IdentityProviderSelection
A4 Swagger #3992 / #3966 → S18, S21 Covered —
A5 packages and the provider switch S15, S16, S23 Add: after cleanup, an unset authProvider must fail loudly or default to bff, never to Auth0 Accepted (b): S16's acceptance criteria flip or remove the unset-authProvider→Auth0 fallback
A6 effects S23A Covered (auth.effects.ts is in the boundary). signupUser$ is dead in every mode and could go earlier if the owner agrees (D7) —
A7 Migration project and campaign chart S17 (new) Delete SyRF.Identity.Migration*, campaign-job.yaml, its tests and the CI routing; keep the export and evidence archives ©: S16 deliberately keeps the migration projects through the production rollback window. New slice S17 retires SyRF.Identity.Migration and the campaign Job chart once that window closes; S17 is dated from the production cutover

email-lookup (S5 in the backend plan): the session makes no change until the deployed Auth0 Action has been compared with docs/auth0-actions/transform-token.js. That comparison needs a management token from Chris.

The preconditions are the owner's: - S10–S13 production cutover, still held; - S14: four reviews spanning at least 28 days, with zero Auth0 traffic; - G4.

The application-authority M1 gate has already landed.

6. Order and critical path

flowchart LR
  START((approve)) --> R01[R0.1 hygiene] & R02[R0.2 API] & R03[R0.3 kernel] & R04[R0.4 PM] & R05[R0.5 web] & R06[R0.6 docs] & R10[R1.0 B9 guard]
  D23{{D2/D3}} --> R20[R2.0 ADR] --> R21[R2.1 backend] & R22[R2.2 browser] & R23[R2.3 LogRocket]
  R30[R3.0 licence] --> R31[R3.1] --> R32[R3.2] --> R33[R3.3 areas] --> R34[R3.4]
  R34 --> R4[R4.1–R4.5]
  MT{{#3986 MassTransit ADR}} --> R45[R4.5 PM.Contracts]
  R50[R5.0 census] --> D4{{D4}} --> R51[R5.1 runner] --> R52[R5.2] --> PROD{{Chris prod run}} --> R53[R5.3 contract]
  • Parallel:
  • all of R0 and R1.0;
  • R2.1, R2.2 and R2.3;
  • R3.3 area PRs (disjoint controllers);
  • R4.2, R4.3 and R4.4.
  • Sequential:
  • R3.1 → R3.2 → R3.3 → R3.4 → R4 (SharedKernel loses AutoMapper only after R3.4);
  • R5.0 → R5.3.
  • Critical path for the epic: R5 (production data, gated on Chris) and R7 (gated on M005 G4).

7. Risks

Risk Mitigation
A "dead" symbol is reached by reflection, Lamar scans, NSwag or BSON class maps C1 includes charts, docs and generated code; container validation tests (0.3.2); NSwag diff (0.2.4); the Generator lesson recorded
Removing Elastic RUM or LogRocket loses diagnostics Chris uses D2 and D3 first; flag off for one release before code removal
Mapperly output differs subtly (null handling, enum names, dates) Snapshot tests written before each swap (3.3.1)
Moving contracts changes exchange names and silently drops messages Pinned URNs plus a mixed-version test (4.3, 4.4); deploy order documented
The schema migration corrupts production documents Census, dry run, CAS, snapshot rehearsal, inverse transform, Chris-approved run, contract last
Conflicts with in-flight streams Boundaries in §8; R0.2 keeps its Program.cs edits to 2 lines; IMessageSender is deferred to #3973
Archived plans break links agents rely on Index redirects plus the link check (0.6.2)

8. Coordination with other active work

Stream / PR Overlap Handling
Notifications/attention stack #3932, #3942–#3945, #3947, #3965 API Program.cs, AutoMapper profiles, SharedKernel R0.2 touches 2 Program.cs lines; R3.3 area PRs wait until those merge; R0.3 rebases after them
#3939 progressive review batches StudyRepository.cs, SharedKernel R0.4 deletes only the dead methods; rebase after #3939, or ask its owner
#2934 deletion lifecycle kernel; #2572 QM v2 (dormant); #2224 custom groups ProjectRepository.cs/StudyRepository.cs, schema R5 owner checks them at R5.0; #2224 (custom groups) depends on the v1 security model, which feeds D4
Migration session (#2466, #3992, #3966, #3994) All of A1–A7; core/auth (R0.5) R7 table sent as a cross-session message; R0.5 confirms the two core/auth components
Authority programme (#3335, M6–M8) L1, AuthorizationHandler.cs R6 proposed as M9; no edits to authority files by this plan
Persistence and DI issues #3973, #3975, #3985 MongoUnitOfWorkBase, Lamar registries IMessageSender goes with #3973; R4.1 waits for the DI direction
State-management plan #3989 Web packages R2.3 (remove LogRocket) unblocks its R6 logrocket-ngrx removal; the state plan retires @ngrx/store, @rx-angular/state, normalizr and ngrx-forms, which this plan doesn't touch
MassTransit ADR #3986 Contracts R4.5 waits for it; if the outcome is a migration, PM.Contracts is designed for the new bus
Build plan, CPM #3982 Packages With CPM landed first, package removals are one-line Directory.Packages.props edits and drift can't reappear; R0 doesn't wait for it
CI registry #3983 Project graph 4.5 relies on it
FEAT-024 statistics session ProjectStatistics* Excluded from the R5.3 rg and from every R0 deletion

9. Decisions needed from Chris

  • D1. MVP scope. Ship R0 (6 parallel PRs) plus R1.0 now. Recommended.
  • D2. Observability target. OpenTelemetry plus Sentry; drop Elastic APM (already off) and Elastic RUM (live in production). Recommended. Is Elastic RUM data used anywhere today?
  • D3. LogRocket (GDPR). It is on in staging and production, records the full NgRx state and actions, names and emails, with no sanitisers. Recommended: remove it. If you keep it: sanitisers plus a consent and privacy-notice update.
  • D4. v0/v1 direction per type, decided after the R5.0 census. Recommended:
  • collapse to v0 where v1 adds nothing (Investigator, SystematicSearch, Study/OutcomeData, living searches): no data write;
  • rewrite Project memberships and security settings to v1 under WP-M1/M2, because authority gate G-D and custom groups depend on it.
  • D5. AutoMapper licence now. Community licence key, or pin to the last MIT major. Recommended: the Community licence key now, Mapperly over time.
  • D6. Generator endpoint. Keep it, or replace it with an NSwag DocumentProcessor that registers the notification DTOs explicitly. Recommended: keep it for now; record a follow-up issue.
  • D7. Auth0 gaps. Send the R7 table (add IdentityProviderSelection to S16, an Auth0-free default for authProvider, a new slice retiring the Migration project, early deletion of signupUser$) to the migration session. Sent and answered 2026-10-05: (a) and (b) accepted into S16; the Migration project is retired by a new S17 after the production rollback window. Still open: early deletion of signupUser$, and a management token from Chris so the session can compare the deployed Auth0 Action with the repo copy before touching email-lookup.
  • D8. Planning archive. Archive Completed plans under docs/planning/_archive/; leave .planning/ (GSD tooling) alone. Recommended.
  • D9. Legacy authorization. Ask the authority programme to add M9 (delete Off/Shadow after production enforcement plus the rollback window). Recommended.
  • D10. PROPOSAL values: none numeric beyond "query count ≤ AutoMapper version" (3.2.3) and "bundle does not grow" (0.5.3).