From 35cb5e76c3fc962634a3f74879819ff301184845 Mon Sep 17 00:00:00 2001 From: Patrick Plate Date: Wed, 24 Jun 2026 14:32:48 +0200 Subject: [PATCH] plan: add Migration-InspectFlow.md (dep swap + Flyway baseline + rollback) --- Migration-InspectFlow.md | 563 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 563 insertions(+) create mode 100644 Migration-InspectFlow.md diff --git a/Migration-InspectFlow.md b/Migration-InspectFlow.md new file mode 100644 index 0000000..9f58a4b --- /dev/null +++ b/Migration-InspectFlow.md @@ -0,0 +1,563 @@ +# Migration Guide: InspectFlow → plate-auth + +**Status:** Draft v1 +**Date:** 2026-06-24 +**Owner:** Patrick +**Audience:** InspectFlow maintainer performing the dependency swap from in-tree auth code to `plate-auth-starter` +**Target plate-auth version:** `0.1.0` (after `0.0.1` validation tag) + +> This guide is **InspectFlow-specific**. For greenfield consumers, see [Integration-Guide.md](Integration-Guide.md). + +--- + +## 1. What this migration does + +Replaces InspectFlow's hand-rolled auth code with a dependency on `plate-auth-starter` + `@platesoft/auth`. After this migration: + +- ~25 backend classes under `de.platesoft.inspectflow.{security,filter,service,controller,entity,repository,dto,enums}` are **deleted** and become a single Maven dependency. +- ~5–8 frontend files under `frontend/{auth.ts, app/api/[...path]/route.ts, app/api/auth/[...nextauth]/route.ts, lib/hmac.ts}` are **deleted** and become a single npm dependency + a 5-line `auth.ts`. +- Flyway migrations `V26..V31` are **kept** but marked as baseline rows in the new `flyway_schema_history_auth` table so plate-auth's `V1..V5` are not re-applied. +- InspectFlow keeps `Company` entity + `OrgValidator` SPI implementation + onboarding logic — **T3 stays in-house**. + +**Goal:** zero behavioral change for end-users. All E2E tests stay green. ([Sprint-0-Testplan.md § 6](Sprint-0-Testplan.md)) + +--- + +## 2. Prerequisites + +Before starting: + +- [ ] InspectFlow on **Spring Boot 4.1.0** (bump from 4.0.7 if needed — Sprint 14.7 prerequisite, see [Open-Questions.md](Open-Questions.md) Q08) +- [ ] plate-auth `0.0.1` validation tag has been published and you can resolve it from Gitea +- [ ] All Sprint 14.6 work merged to `main` (clean baseline) +- [ ] Database backup taken +- [ ] Sparkboard has **already** consumed plate-auth `0.0.1` successfully (proves the library works against a real consumer) +- [ ] Feature branch: `feature/plate-auth-migration` + +If any of these is false, **stop** and resolve first. + +--- + +## 3. Migration overview + +```mermaid +flowchart LR + S0[Sprint 14.6 baseline] --> A[Step A: Add deps] + A --> B[Step B: Delete moved backend classes] + B --> C[Step C: Rename config keys] + C --> D[Step D: Add SPI implementations] + D --> E[Step E: Flyway baseline rows] + E --> F[Step F: Frontend swap] + F --> G[Step G: Run E2E suite] + G -->|green| H[Step H: Merge + delete dead code] + G -->|red| R[Rollback] +``` + +Estimated effort: **1–2 days** for an experienced maintainer assuming the library works as advertised. Most time is verification, not coding. + +--- + +## 4. Step A — Add dependencies + +### 4.1 Backend `pom.xml` + +```xml + + de.platesoft + plate-auth-starter + 0.1.0 + +``` + +Add Gitea Maven repo + credentials if not already present (see [Integration-Guide.md § 3.1](Integration-Guide.md)). + +### 4.2 Frontend `package.json` + +```bash +cd frontend +pnpm add @platesoft/auth@0.1.0 +``` + +`.npmrc` must contain the `@platesoft:` scope mapping. + +### 4.3 Verify build still passes + +```bash +cd backend && mvn clean compile +cd frontend && pnpm install +``` + +No code changes yet — both halves should still build cleanly with the new deps idle alongside the existing in-tree code. + +--- + +## 5. Step B — Delete moved backend classes + +These classes are now provided by `plate-auth-starter`. Delete the InspectFlow-local copy after verifying the package and class name match exactly. + +### 5.1 Classes to delete + +| InspectFlow path | Replaced by | +|------------------|-------------| +| [`backend/src/main/java/de/platesoft/inspectflow/service/JwtService.java`](backend/src/main/java/de/platesoft/inspectflow/service/JwtService.java:1) | `de.platesoft.auth.service.JwtService` | +| [`backend/src/main/java/de/platesoft/inspectflow/filter/JwtAuthenticationFilter.java`](backend/src/main/java/de/platesoft/inspectflow/filter/JwtAuthenticationFilter.java:1) | `de.platesoft.auth.filter.JwtAuthenticationFilter` | +| [`backend/src/main/java/de/platesoft/inspectflow/config/SecurityConfig.java`](backend/src/main/java/de/platesoft/inspectflow/config/SecurityConfig.java:1) | Auto-config in `PlateAuthAutoConfiguration` (see § 5.4 below) | +| `…/inspectflow/service/ExchangeService.java` | `de.platesoft.auth.service.ExchangeService` | +| `…/inspectflow/service/HmacEnvelope.java` | `de.platesoft.auth.crypto.HmacEnvelope` | +| `…/inspectflow/service/MembershipService.java` | `de.platesoft.auth.service.MembershipService` | +| `…/inspectflow/service/InvitationService.java` | `de.platesoft.auth.service.InvitationService` | +| `…/inspectflow/service/AccessRequestService.java` | `de.platesoft.auth.service.AccessRequestService` | +| `…/inspectflow/service/AuthService.java` (login/signup/reset) | `de.platesoft.auth.service.AuthService` | +| `…/inspectflow/controller/AuthController.java` | `de.platesoft.auth.controller.AuthController` | +| `…/inspectflow/controller/MembershipController.java` | `de.platesoft.auth.controller.MembershipController` | +| `…/inspectflow/controller/InvitationController.java` | `de.platesoft.auth.controller.InvitationController` | +| `…/inspectflow/controller/AccessRequestController.java` | `de.platesoft.auth.controller.AccessRequestController` | +| `…/inspectflow/controller/AdminAuditController.java` | `de.platesoft.auth.controller.AdminAuditController` | +| `…/inspectflow/entity/User.java` | `de.platesoft.auth.entity.User` | +| `…/inspectflow/entity/UserIdentity.java` | `de.platesoft.auth.entity.UserIdentity` | +| `…/inspectflow/entity/Membership.java` | `de.platesoft.auth.entity.Membership` (with polymorphic FK — see § 6.3) | +| `…/inspectflow/entity/Invitation.java` | `de.platesoft.auth.entity.Invitation` | +| `…/inspectflow/entity/AccessRequest.java` | `de.platesoft.auth.entity.AccessRequest` | +| `…/inspectflow/entity/LoginEvent.java` | `de.platesoft.auth.entity.LoginEvent` | +| `…/inspectflow/repository/UserRepository.java` | `de.platesoft.auth.repository.UserRepository` | +| `…/inspectflow/repository/MembershipRepository.java` | `de.platesoft.auth.repository.MembershipRepository` | +| `…/inspectflow/repository/InvitationRepository.java` | `de.platesoft.auth.repository.InvitationRepository` | +| `…/inspectflow/repository/AccessRequestRepository.java` | `de.platesoft.auth.repository.AccessRequestRepository` | +| `…/inspectflow/repository/LoginEventRepository.java` | `de.platesoft.auth.repository.LoginEventRepository` | +| `…/inspectflow/dto/...` (auth-related) | `de.platesoft.auth.dto.*` | + +### 5.2 Classes to KEEP (T3 — InspectFlow-specific) + +These stay in InspectFlow because they encode the Company domain or onboarding logic: + +| Path | Reason | +|------|--------| +| `de.platesoft.inspectflow.entity.Company` | Concrete org table — InspectFlow's domain | +| `de.platesoft.inspectflow.repository.CompanyRepository` | Same | +| `de.platesoft.inspectflow.service.CompanyService` | Same | +| `de.platesoft.inspectflow.service.OrgContextResolver` | Adapter — refactor to implement plate-auth's `OrgValidator` SPI | +| `de.platesoft.inspectflow.service.OnboardingService` | App-specific onboarding (create default machines, etc.) | + +### 5.3 Find-and-fix imports + +After deleting the classes in § 5.1, the rest of InspectFlow that references them (controllers, services that aren't auth-related) will fail to compile. Rewrite imports: + +```bash +# Find all consumers of the deleted packages +cd backend +grep -rln "de.platesoft.inspectflow.service.JwtService" src/main/java/ +grep -rln "de.platesoft.inspectflow.entity.User" src/main/java/ +# … repeat per deleted class + +# Replace package prefix +find src/main/java -name "*.java" -exec sed -i '' \ + -e 's|de.platesoft.inspectflow.service.JwtService|de.platesoft.auth.service.JwtService|g' \ + -e 's|de.platesoft.inspectflow.entity.User|de.platesoft.auth.entity.User|g' \ + -e 's|de.platesoft.inspectflow.entity.Membership|de.platesoft.auth.entity.Membership|g' \ + {} + +``` + +> **Take small steps.** Replace one package at a time and recompile. Big-bang sed will leave you with a sea of red. + +### 5.4 SecurityConfig + +The starter ships its own `SecurityFilterChain` bean named `plateAuthSecurityChain`. InspectFlow's current [`SecurityConfig.java`](backend/src/main/java/de/platesoft/inspectflow/config/SecurityConfig.java:21) declares its own beans for `passwordEncoder`, `authenticationManager`, and a `filterChain`. Three of these are auto-provided by the starter: + +- `passwordEncoder` → `PlateAuthAutoConfiguration` provides `BCryptPasswordEncoder` with cost 10. Delete InspectFlow's. +- `authenticationManager` → same. Delete InspectFlow's. +- `filterChain` → starter's `plateAuthSecurityChain` covers `/auth/**` + JWT for the rest. **Keep** an InspectFlow-specific chain for paths that need custom rules (e.g. `/api/admin/**` requires `hasRole('ADMIN')`). + +```java +// InspectFlow keeps a focused chain: +@Bean +@Order(0) // before plateAuthSecurityChain +public SecurityFilterChain inspectflowAdminChain(HttpSecurity http) throws Exception { + return http + .securityMatcher("/api/admin/**") + .authorizeHttpRequests(a -> a.anyRequest().hasRole("ADMIN")) + .csrf(c -> c.disable()) + .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .build(); +} +``` + +--- + +## 6. Step C — Rename config keys + +InspectFlow has historically scattered auth config across `inspectflow.*`, `jwt.*`, and `nextauth.*`. plate-auth consolidates everything under `plate.auth.*`. + +### 6.1 Backend: `application.yml` + +Before: + +```yaml +jwt: + secret: ${JWT_SECRET} + access-expiration-minutes: 15 + refresh-expiration-days: 30 +nextauth: + exchange-secret: ${EXCHANGE_SECRET} + exchange-max-age-seconds: 60 +inspectflow: + registration-enabled: false + cors: + allowed-origins: [...] +``` + +After: + +```yaml +plate: + auth: + jwt: + secret: ${JWT_SECRET} + access-expiration: PT15M + refresh-expiration: P30D + issuer: inspectflow + exchange: + secret: ${EXCHANGE_SECRET} + max-age: PT60S + nonce-ttl: PT5M + registration: + enabled: false + cors: + allowed-origins: [...] + providers: + google: + enabled: true + client-id: ${GOOGLE_CLIENT_ID} + client-secret: ${GOOGLE_CLIENT_SECRET} +``` + +**Format changes:** + +- Numeric `*-minutes` / `*-seconds` / `*-days` → ISO-8601 `Duration` format (`PT15M`, `PT60S`, `P30D`). Spring Boot parses both `15m` and `PT15M`, but the starter validates the ISO form. + +### 6.2 Frontend: `.env.local` / `.env.production` + +| Old | New | +|-----|-----| +| `JWT_SECRET` | (unused — backend only now) | +| `EXCHANGE_SECRET` | `PLATE_AUTH_EXCHANGE_SECRET` | +| `BACKEND_URL` | `PLATE_AUTH_BACKEND_URL` | +| `NEXTAUTH_SECRET` | `NEXTAUTH_SECRET` (unchanged) | +| `NEXTAUTH_URL` | `NEXTAUTH_URL` (unchanged) | +| `GOOGLE_CLIENT_ID` | `GOOGLE_CLIENT_ID` (unchanged) | +| `GOOGLE_CLIENT_SECRET` | `GOOGLE_CLIENT_SECRET` (unchanged) | + +Update `docker-compose.yml`, TrueNAS config, `.env.example`, and `.env.prod.example`. + +### 6.3 Membership entity column changes + +Old InspectFlow `Membership`: + +```java +@Entity +public class Membership { + @ManyToOne private User user; + @ManyToOne private Company company; // ← direct FK + private Role role; +} +``` + +New plate-auth `Membership`: + +```java +@Entity +public class Membership { + @ManyToOne private User user; + private String orgType; // "COMPANY" + private Long orgId; // company.id + private Role role; +} +``` + +**Migration:** the existing `memberships.company_id` column must be rewritten to `(org_type='COMPANY', org_id=company_id)`. Handled by the Flyway data-migration step in § 7.3. + +--- + +## 7. Step D — Add SPI implementations + +InspectFlow plugs back into plate-auth via SPI beans. Add a new config class: + +```java +// backend/src/main/java/de/platesoft/inspectflow/auth/PlateAuthBindings.java +@Configuration +public class PlateAuthBindings { + + @Bean + public OrgValidator orgValidator(CompanyRepository companies) { + return (orgType, orgId) -> + "COMPANY".equals(orgType) && companies.existsById(orgId); + } + + @Bean + public OrgDisplayNameResolver orgDisplayNameResolver(CompanyRepository companies) { + return (orgType, orgId) -> companies.findById(orgId) + .map(Company::getName) + .orElse("Unbekannt"); + } + + @Bean + public InvitationMailer invitationMailer(InspectFlowMailService mail) { + return mail::sendInvitation; // delegate to existing mailer + } + + @Bean + public AccessRequestMailer accessRequestMailer(InspectFlowMailService mail) { + return mail::sendAccessRequestNotification; + } + + @Bean + public OnboardingHook onboardingHook(OnboardingService onboarding) { + return onboarding::onUserCreated; // delegate to existing onboarding + } +} +``` + +This replaces all five default SPI beans. The starter logs "User-provided OrgValidator detected, default disabled" at INFO on boot — verify this in your first run. + +--- + +## 8. Step E — Flyway baseline rows + +InspectFlow's existing `flyway_schema_history` contains rows for `V26..V31` (your existing auth migrations). plate-auth ships its own migrations `V1..V5` and runs them against `flyway_schema_history_auth` (a separate table). The schema already has the tables — running `V1..V5` would fail. + +### 8.1 Migration approach + +Add a new InspectFlow-local migration `V32__C_seed_plate_auth_history.sql`: + +```sql +-- Pre-populate flyway_schema_history_auth so plate-auth Flyway sees the tables as already migrated. +-- The schema was created by InspectFlow's V26..V31; plate-auth's V1..V5 would otherwise re-apply them. + +CREATE TABLE IF NOT EXISTS flyway_schema_history_auth ( + installed_rank int NOT NULL, + version varchar(50), + description varchar(200) NOT NULL, + type varchar(20) NOT NULL, + script varchar(1000) NOT NULL, + checksum int, + installed_by varchar(100) NOT NULL, + installed_on timestamp NOT NULL DEFAULT now(), + execution_time int NOT NULL, + success boolean NOT NULL, + PRIMARY KEY (installed_rank) +); + +INSERT INTO flyway_schema_history_auth + (installed_rank, version, description, type, script, checksum, installed_by, execution_time, success) +VALUES + (1, '1', 'Create users', 'SQL', 'V1__C_create_users.sql', NULL, 'inspectflow-migration', 0, true), + (2, '2', 'Create user_identities','SQL', 'V2__C_create_user_identities.sql', NULL, 'inspectflow-migration', 0, true), + (3, '3', 'Create memberships', 'SQL', 'V3__C_create_memberships.sql', NULL, 'inspectflow-migration', 0, true), + (4, '4', 'Create invitations', 'SQL', 'V4__C_create_invitations.sql', NULL, 'inspectflow-migration', 0, true), + (5, '5', 'Create login_events', 'SQL', 'V5__C_create_login_events.sql', NULL, 'inspectflow-migration', 0, true) +ON CONFLICT DO NOTHING; +``` + +> **Checksum caveat.** Setting `checksum = NULL` makes Flyway skip checksum validation. If you want strict mode, run plate-auth on a scratch DB once, copy the checksums from its `flyway_schema_history_auth`, and paste them here. + +### 8.2 Data migration: memberships.company_id → (org_type, org_id) + +Same migration file or separate `V33__C_rewrite_memberships_to_polymorphic.sql`: + +```sql +-- Add new columns +ALTER TABLE memberships ADD COLUMN IF NOT EXISTS org_type varchar(64); +ALTER TABLE memberships ADD COLUMN IF NOT EXISTS org_id bigint; + +-- Backfill from existing company_id +UPDATE memberships SET org_type = 'COMPANY', org_id = company_id WHERE company_id IS NOT NULL; + +-- Index + non-null constraint +CREATE INDEX IF NOT EXISTS idx_memberships_org ON memberships(org_type, org_id); +ALTER TABLE memberships ALTER COLUMN org_type SET NOT NULL; +ALTER TABLE memberships ALTER COLUMN org_id SET NOT NULL; + +-- Drop the old FK column (after verification!) +-- ALTER TABLE memberships DROP CONSTRAINT memberships_company_id_fkey; +-- ALTER TABLE memberships DROP COLUMN company_id; +``` + +> **Caution.** Do **not** drop `company_id` in the same migration. Keep it as a shadow column until the next minor release and verify no code still reads it. + +--- + +## 9. Step F — Frontend swap + +### 9.1 Delete files + +- [`frontend/auth.ts`](frontend/auth.ts:1) → rewrite (5 lines, see § 9.3) +- `frontend/app/api/auth/[...nextauth]/route.ts` → keep, no change needed +- `frontend/app/api/[...path]/route.ts` → rewrite (3 lines, see § 9.4) +- `frontend/lib/hmac.ts` (if exists) → delete; replaced by `@platesoft/auth/edge` +- `frontend/lib/auth-config.ts` (if exists) → delete; replaced by `createAuthConfig` factory + +### 9.2 Keep files + +- `frontend/middleware.ts` — InspectFlow keeps its own (custom redirect logic for unauth users) +- `frontend/components/login-form.tsx` etc. if you have custom UI — leave them, ignore `@platesoft/auth/react` + +### 9.3 New `frontend/auth.ts` + +```typescript +import { createAuthConfig } from "@platesoft/auth/server"; +import NextAuth from "next-auth"; + +export const { handlers, auth, signIn, signOut } = NextAuth( + createAuthConfig({ + backendUrl: process.env.PLATE_AUTH_BACKEND_URL!, + exchangeSecret: process.env.PLATE_AUTH_EXCHANGE_SECRET!, + nextAuthSecret: process.env.NEXTAUTH_SECRET!, + providers: { + google: { + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + }, + }, + pages: { + signIn: "/login", + }, + }) +); +``` + +### 9.4 New `frontend/app/api/[...path]/route.ts` + +```typescript +import { createProxyHandlers } from "@platesoft/auth/edge"; + +export const runtime = "edge"; + +export const { GET, POST, PUT, PATCH, DELETE } = createProxyHandlers({ + backendUrl: process.env.PLATE_AUTH_BACKEND_URL!, + exchangeSecret: process.env.PLATE_AUTH_EXCHANGE_SECRET!, +}); +``` + +That's it. Anything else InspectFlow needs (custom error pages, custom callbacks) is wired into NextAuth options that `createAuthConfig` exposes as overridable. + +--- + +## 10. Step G — Run the E2E suite + +```bash +cd frontend +pnpm exec playwright install --with-deps # if not already +pnpm exec playwright test +``` + +All 30+ scenarios in [`frontend/e2e/`](frontend/e2e/) must pass green. The critical ones for this migration: + +- `auth-flow.unauth.spec.ts` — anonymous redirect, login form +- `companies.spec.ts` — membership reads work +- `admin-archive.spec.ts`, `admin-timeline.spec.ts`, `admin-migration.spec.ts` — admin role gate +- `command-palette.spec.ts` — JWT-protected ops +- Any "login as admin / login as user" setup script in `auth.setup.ts` + +If any test fails, **stop**. Diagnose via: + +1. Check backend logs for `OrgValidator` / SPI warnings. +2. Check `login_events` table for the failing user. +3. Diff the failing request between old + new code paths (proxy headers, exchange envelope shape). + +--- + +## 11. Step H — Merge and clean up + +After all tests pass: + +1. Squash-merge `feature/plate-auth-migration` to `main`. +2. Tag InspectFlow as `vX.Y+1.0` (minor bump — internal refactor with config breaking change for ops). +3. Update `docs/` — add a section in [`docs/SPRINT-14-OVERVIEW.md`](docs/SPRINT-14-OVERVIEW.md) noting the carve-out + library version. +4. **One sprint later**, drop `memberships.company_id` column in a follow-up migration (give yourself time to revert if production reveals issues). +5. Subscribe to plate-auth `0.2.0` for the v0.2 feature wave (refresh rotation, magic-link, multi-key secrets). + +--- + +## 12. Rollback procedure + +If the migration goes sideways: + +### Code rollback + +```bash +git checkout main +git revert feature/plate-auth-migration # or just deploy the previous tag +``` + +### Database rollback + +The data migration in § 8.2 is **additive** (`memberships.org_type`, `org_id` are new columns). The old `company_id` column is preserved. Rolling back the code means the old `Membership` entity reads `company_id` again and ignores the new columns — safe. + +The Flyway baseline rows in `flyway_schema_history_auth` are harmless if the code is reverted — the table simply exists with 5 rows and no consumer. + +### Secret rollback + +If you have already rotated `PLATE_AUTH_EXCHANGE_SECRET` and `EXCHANGE_SECRET` was different in the old code, restore the old env vars when reverting the code. + +### Total downtime + +The migration is designed for **zero-downtime deploy**: + +- New columns added before code switch (additive). +- New code reads new columns; old code reads old. +- Stop the old replica, start the new — one request is in flight, at worst. + +If you cannot do zero-downtime, plan a 5-minute maintenance window. + +--- + +## 13. What changes for end-users + +**Nothing.** Logins, sessions, sign-ups, invitations, access requests, admin views — all behave identically. The change is internal: same APIs, same UX, different code path. + +If a user reports a regression, treat it as a bug — file an InspectFlow ticket **and** a plate-auth ticket (since the regression is likely in the library, not in InspectFlow's residual code). + +--- + +## 14. Common pitfalls (migration-specific) + +| Symptom | Likely cause | Fix | +|---------|--------------|-----| +| Boot fails with `Field 'orgType' of Membership is null` | Forgot the data migration in § 8.2 | Run V33 manually | +| Flyway: "checksum mismatch on V1" | Baseline rows in § 8.1 have wrong checksums | Set `flyway.validate-on-migrate=false` temporarily, or recompute checksums | +| `OrgValidator` always returns false | Wrong `orgType` literal — case sensitive (`"COMPANY"` not `"company"`) | Check § 7 SPI binding | +| Frontend cannot reach backend → 502 | `BACKEND_URL` rename to `PLATE_AUTH_BACKEND_URL` not propagated to all envs | Grep `BACKEND_URL` across repo + deploy configs | +| E2E `companies.spec.ts` fails | `OrgDisplayNameResolver` SPI not registered → display name shows `Unbekannt` | Add bean in § 7 | +| Admin chain order wrong → users get 403 on `/api/admin/...` | `@Order(0)` missing on `inspectflowAdminChain` | Add explicit order; default starter chain is `@Order(100)` | + +--- + +## 15. Sign-off checklist + +Before declaring migration complete: + +- [ ] All Playwright E2E tests pass +- [ ] Manual smoke test: log in via Google, password, accept invite, request access, approve as admin +- [ ] `/admin/audit` shows `login_events` rows from both old and new code paths (timestamps span the migration) +- [ ] No `WARN`/`ERROR` log entries from `de.platesoft.auth.*` in first 24h post-deploy +- [ ] Database row counts before/after migration match (users, memberships, invitations, access_requests, login_events) +- [ ] `memberships.org_type` is non-null on all rows (use SQL audit) +- [ ] Deleted backend classes (§ 5.1) are actually deleted (no zombies) +- [ ] Deleted frontend files (§ 9.1) are actually deleted + +--- + +## 16. Cross-references + +- [Home.md](Home.md) +- [Vision.md](Vision.md) +- [Architecture.md](Architecture.md) +- [Roadmap.md](Roadmap.md) +- [Sprint-0-Assessment.md](Sprint-0-Assessment.md) +- [Sprint-0-Plan.md](Sprint-0-Plan.md) +- [Sprint-0-Testplan.md](Sprint-0-Testplan.md) +- [Open-Questions.md](Open-Questions.md) +- [Integration-Guide.md](Integration-Guide.md) + +--- + +**End of Migration-InspectFlow.md (v1).**