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.OnSavingstampSchemaVersionon first save only (never downgrade). Or, as the minimum, have the PM and API startup refuseDefaultSchemaVersion != 0until R5. Recommended: the startup guard, which leaves data behaviour unchanged. - Align API
appsettings.e2etest.json:100to 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.
Generatorendpoint. Keep it, or replace it with an NSwagDocumentProcessorthat registers the notification DTOs explicitly. Recommended: keep it for now; record a follow-up issue. - D7. Auth0 gaps. Send the R7 table (add
IdentityProviderSelectionto S16, an Auth0-free default forauthProvider, a new slice retiring the Migration project, early deletion ofsignupUser$) 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 ofsignupUser$, and a management token from Chris so the session can compare the deployed Auth0 Action with the repo copy before touchingemail-lookup. - D8. Planning archive. Archive
Completedplans underdocs/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).