Skip to content

Sphere targetApp enum cleanup: drop DINING_APP, add SERVICES_APP

ADR: Sphere targetApp enum cleanup — drop DINING_APP, add SERVICES_APP

Section titled “ADR: Sphere targetApp enum cleanup — drop DINING_APP, add SERVICES_APP”

The DINING vertical was provisionally seeded during migration 0037_activity_spheres.sql as a triple layer:

  • enum value DINING_APP on the activities.sphere_target_app PG enum (and Drizzle SphereTargetAppEnum),
  • enum value DINING on the activities.activity_type PG enum (and Drizzle ActivityTypeEnum),
  • a seeded sphere row code='DINING' in activities.spheres, target_app='DINING_APP'.

The vertical has been frozen since planning — no customer, no Flutter app surface to render it (there is no dining_app under apps/), and the gym_app/tickets_app routers never branch on it. The triple-layer presence clutters every targetApp/activityType decision and blocks the next two cleanups in the chain:

  • 869dpxbj6 — collapse activity types to [SLOT_BASED, SERVICE] + add hasSeatSelection: boolean; merge CINEMA + SHOWS sphere rows into EVENTS.
  • 869dq1q27 — extract libs/shared/target-app/ as a shared library (helper relocation).

Both downstream tickets assume DINING is already gone. This ADR formalises the triple-drop and seeds the additive SERVICES_APP target-app enum value (reserved for a future dedicated salons/barbershops mobile app — salons currently live in gym_app via the existing SERVICES sphere targeted at GYM_APP, and that targeting is intentionally NOT changed here).

This is the first ticket in a 3-vertical cleanup chain.

Drop all three DINING layers and add a single additive SERVICES_APP enum value, in one MR across backend + two Angular consumers, gated by a single auto-generated Drizzle migration.

The Drizzle migration MUST execute in this strict order:

  1. DELETE FROM activities.spheres WHERE code='DINING' (clear the only dependent row before the enum rebuild — otherwise the column re-cast aborts).
  2. PG enum rebuild for sphere_target_app — rename old → create new ('GYM_APP','TICKETS_APP','SERVICES_APP')ALTER TABLE activities.spheres ALTER COLUMN target_app TYPE … USING target_app::text::activities.sphere_target_app → drop old.
  3. PG enum rebuild for activity_type — rename old → create new ('SHOW','MOVIE','SLOT_BASED','SERVICE') → re-cast activities.activities.type, activities.spheres.allowed_activity_types (array column), and activities.spheres.default_activity_type → drop old.

The migration is auto-generated via the backend-drizzle-migration skill from the schema edits, NOT hand-written. The generated SQL is reviewed via the migration-safety-check skill before apply. All three OpenAPI contracts are refreshed via the backend-export-openapi skill (= pnpm run sync:contracts) after the backend boots cleanly with the new schema.

Both Angular consumers (tktspace-web, tktspace-business) carry one hand-written line each that references 'DINING' in code typed against the generated union (Record<string, string> icon map in web; ActivityResponseDto['type'][] picker list in business). Both are folded into this MR so neither consumer’s dev branch breaks ng build after the contract refresh.

Mobile (tktspace-mobile-app) is out of scope — its generated Dart enums regenerate on the next mobile cycle and the two Dart references are doc comments only (no compile break).

A. Defer SERVICES_APP and ship DINING drop alone. Trade-off: smaller MR, simpler safety analysis. Rejected — adding an unused enum value is a 1-line schema edit and the PG enum rebuild is the same shape whether we drop-only or drop-and-add. Bundling avoids a second enum-rebuild migration two weeks from now when product green-lights the salons app.

B. Mark DINING_APP / DINING as @deprecated and leave them in the schema. Trade-off: zero migration risk, zero consumer churn. Rejected — both downstream cleanups (869dpxbj6, 869dq1q27) need to grep cleanly for the live target-app domain; leaving deprecated values makes their diffs much noisier and forces every new contributor to learn which values are “real”. The current sphere row referencing DINING_APP (code='DINING') has zero downstream consumers, so the safety case for deprecation is weak.

C. Soft-delete the DINING sphere row but keep the enum values. Trade-off: smaller migration, no PG enum rebuild. Rejected for the same reason as B — the value still appears in every generated client union (SphereTargetApp, ActivityType) and continues to confuse super-admins picking targetApp for new spheres.

Postgres has no native ALTER TYPE ... DROP VALUE. The drizzle-kit-emitted pattern for an enum-value drop + add (mixed) is the rebuild:

ALTER TYPE activities.sphere_target_app RENAME TO sphere_target_app_old;
CREATE TYPE activities.sphere_target_app AS ENUM ('GYM_APP','TICKETS_APP','SERVICES_APP');
ALTER TABLE activities.spheres
ALTER COLUMN target_app TYPE activities.sphere_target_app
USING target_app::text::activities.sphere_target_app;
DROP TYPE activities.sphere_target_app_old;

This is destructive at the type level: any row whose target_app::text = 'DINING_APP' (or type::text = 'DINING', allowed_activity_types containing 'DINING', default_activity_type = 'DINING') would abort the ALTER COLUMN ... USING step because the cast finds no matching value in the new enum. That is the desired safety — production must not silently mangle data.

The pre-migration data-integrity check (AC-13) MUST therefore pass before apply on dev or prod:

SELECT count(*) FROM activities.activities WHERE type::text = 'DINING';
SELECT count(*) FROM activities.spheres
WHERE 'DINING' = ANY(allowed_activity_types::text[])
OR default_activity_type::text = 'DINING';
SELECT count(*) FROM activities.spheres WHERE target_app::text = 'DINING_APP';

Acceptance: queries (1) and (2) MUST return 0. Query (3) is allowed to return exactly 1 if and only if that single row is the seeded code='DINING' row (step 1 of the migration deletes it). To make this check unambiguous, run the verifier below instead — it returns 0 for the healthy case and otherwise lists offending rows:

SELECT id, code, target_app::text FROM activities.spheres
WHERE target_app::text = 'DINING_APP' AND code <> 'DINING';
-- expected: 0 rows

If the verifier returns ≥1 row, STOP and surface to the user — additional DINING_APP usage exists beyond the seeded row.

The migration-safety-check skill reads the generated SQL and surfaces any unguarded CASCADE or table drops outside the enum rebuild — none expected here.

5. Backend file layout (verified against current code 2026-06-15)

Section titled “5. Backend file layout (verified against current code 2026-06-15)”

All line numbers below confirmed by Read before drafting this ADR.

  • libs/shared/data-access-db/src/lib/schema/activities.schema.ts:26-30SphereTargetAppEnum array currently ['GYM_APP','TICKETS_APP','DINING_APP'] → must become ['GYM_APP','TICKETS_APP','SERVICES_APP'].
  • libs/shared/data-access-db/src/lib/schema/activities.schema.ts:39-45ActivityTypeEnum array currently ['SHOW','MOVIE','SLOT_BASED','DINING','SERVICE'] → must become ['SHOW','MOVIE','SLOT_BASED','SERVICE'].

5.2 Sphere DTOs (7 hardcoded literal spots)

Section titled “5.2 Sphere DTOs (7 hardcoded literal spots)”
  • libs/features/spheres/src/lib/dto/sphere.dto.ts:31example: ['DINING', 'SERVICE']example: ['SERVICE'].
  • libs/features/spheres/src/lib/dto/sphere.dto.ts:70@ApiProperty({ enum: ['GYM_APP','TICKETS_APP','DINING_APP'], ... }) → triple updated to ['GYM_APP','TICKETS_APP','SERVICES_APP'].
  • libs/features/spheres/src/lib/dto/sphere.dto.ts:72@IsIn(['GYM_APP','TICKETS_APP','DINING_APP']) → same triple.
  • libs/features/spheres/src/lib/dto/sphere.dto.ts:80example: ['DINING', 'SERVICE']['SERVICE'].
  • libs/features/spheres/src/lib/dto/sphere.dto.ts:118@ApiPropertyOptional({ enum: ['GYM_APP','TICKETS_APP','DINING_APP'] }) → new triple.
  • libs/features/spheres/src/lib/dto/sphere.dto.ts:121@IsIn(['GYM_APP','TICKETS_APP','DINING_APP']) → new triple.
  • libs/features/spheres/src/lib/dto/sphere.dto.ts:129example: ['DINING', 'SERVICE']['SERVICE'].

Phase C dev: grep 'DINING' and 'DINING_APP' across this file to be exhaustive — do not rely on the count above alone.

  • libs/features/spheres/src/lib/services/spheres.service.ts:32 — internal type alias type ActivityTypeValue = 'SHOW' | 'MOVIE' | 'SLOT_BASED' | 'DINING' | 'SERVICE' → drop 'DINING'.
  • libs/features/spheres/src/lib/services/spheres.service.ts:92dto.targetApp as 'GYM_APP' | 'TICKETS_APP' | 'DINING_APP''GYM_APP' | 'TICKETS_APP' | 'SERVICES_APP'.
  • libs/features/spheres/src/lib/services/spheres.service.ts:184 — identical cast updated identically.
  • libs/features/activities/src/lib/dto/create-activity.dto.ts:23 — hand-written TS enum ActivityType carries DINING = 'DINING'. This is SEPARATE from the Drizzle PG enum at activities.schema.ts:39-45 — both must be kept in sync. Drop the line.
  • libs/features/activities/src/lib/dto/favorite.dto.ts:41 — docstring example '(e.g. SHOW, MOVIE, SLOT_BASED, DINING)' → drop , DINING from the prose.

5.5 Helper comments (accuracy fixes, not behaviour changes)

Section titled “5.5 Helper comments (accuracy fixes, not behaviour changes)”
  • libs/shared/common/src/helpers/resolve-target-app.ts:9 — JSDoc reads (GYM_APP / TICKETS_APP / DINING_APP today)(GYM_APP / TICKETS_APP / SERVICES_APP today).
  • libs/features/auth/src/lib/interceptors/target-app.interceptor.spec.ts:10X-Target-App: GYM_APP (or TICKETS_APP / DINING_APP)(or TICKETS_APP / SERVICES_APP).

These two changes do not alter behaviour. They keep the prose accurate so the next reader does not misroute a bug report.

  • libs/features/spheres/src/lib/__tests__/spheres-unit.spec.ts:38 — case AC-1: DINING sphere has targetApp=DINING_APP and allowedActivityTypes=[DINING] no longer holds. Rewrite to assert SERVICES sphere has targetApp=GYM_APP and allowedActivityTypes includes SERVICE.
  • libs/features/spheres/src/lib/dto/sphere.dto.spec.ts:96-99 — fixture uses 'DINING'; swap for another currently-valid ActivityType ('SERVICE').
  • apps/api-e2e/src/spheres/spheres-schema.e2e-spec.ts:41expect(spheres).toHaveLength(5)4; drop 'DINING' from the expected codes ordering.
  • apps/api-e2e/src/spheres/spheres-superadmin.e2e-spec.ts:196,202 — payload ['DINING','SERVICE'] was the shrink-conflict path. Rewrite to ['SERVICE']. Semantic preserved: still drops SLOT_BASED from allowedActivityTypes, still triggers AC-24 (errors.sphere.activity_type_in_use). Do NOT change the activities seeded in the test setup — only the request payload.
  • apps/api-e2e/src/client/wallet-target-app.e2e-spec.ts:38 — docstring mention of DINING_APP → swap for a currently-valid value (illustrative only).

No new modules. All edits stay in existing libs/features/spheres/, libs/features/activities/, libs/shared/data-access-db/, libs/shared/common/, and libs/features/auth/ libraries. No new shared utilities in libs/shared/ — helper relocation is explicitly deferred to 869dq1q27.

  • Auto-regenerated by npm run generate from refreshed _workflow/contracts/client.openapi.yaml:
    • src/app/core/api/models/activity-type-array.ts:13
    • src/app/core/api/models/activity-type.ts:8
    • src/app/core/api/models/favorite-activity-dto.ts:23
  • Hand-written (manual fix in this MR):
    • src/app/pages/explore/explore.page.ts:118 — drop the DINING: '@tui.utensils' entry from SPHERE_FALLBACK_ICONS (lines 113-119). Index type is Record<string, string> (verified), so the removal is a clean one-line delete with no keyof typeof cast needed.

After both: run npm run generate then npm run build (= ng build). Build MUST exit 0. No new tests.

6.2 tktspace-business — known regen gotcha

Section titled “6.2 tktspace-business — known regen gotcha”
  • Auto-regenerated (after the manual recipe below): client/src/app/core/api/models/sphere-target-app.ts:7, sphere-target-app-array.ts:12, sphere-code.ts:7, sphere-code-array.ts:14, activity-type-code.ts:11, activity-type-code-array.ts:13, create-activity-dto.ts:38, update-activity-dto.ts:38, activity-response-dto.ts:36, fn/activities/activities-admin-list.ts:41.
  • Hand-written (manual fix in this MR):
    • client/src/app/features/dashboard/activities/pages/activity-form/activity-form.page.ts:106 — drop the 'DINING', entry from public activityTypes: ActivityResponseDto['type'][] = [...] (lines 102-108). Final list: ['SHOW','MOVIE','SLOT_BASED','SERVICE'].

The regen is NOT self-contained. Verified 2026-06-15:

  • tktspace-business/tools/gen/sync-business-contract.js is referenced from patch-passes-swagger.js:16 (header comment “Run automatically via tools/gen/sync-business-contract.js before ng-openapi-gen”) but DOES NOT exist on disk. ls tools/gen/ returned: api-gateway.json, business.openapi.json, business.openapi.yaml, patch-passes-swagger.js, patch-spheres-swagger.js, swagger-api.json.
  • npm run generate:api only runs ng-openapi-gen -c tools/gen/api-gateway.json against the existing tools/gen/swagger-api.json (286 KB hand-curated snapshot augmented by patch-spheres-swagger.js + patch-passes-swagger.js with ~40 schemas absent from the canonical YAML).
  • A naive overwrite of swagger-api.json from _workflow/contracts/business.openapi.yaml (or from localhost:5005/api/business/openapi.json) would strip those ~40 patched schemas and downstream ng build would fail (the picker, customer-pass adjust page, activities-list filters all reference them).

Phase C dev MUST follow this manual recipe (per spec AC-18, repeated verbatim):

  • Step A: Leave tools/gen/swagger-api.json as the live working snapshot. Do NOT overwrite from _workflow/contracts/business.openapi.yaml or from a running backend.

  • Step B: Hand-edit tools/gen/swagger-api.json in place. Locate the three enum schemas under components.schemas:

    • SphereTargetApp-DINING_APP, +SERVICES_APP.
    • ActivityTypeCode-DINING.
    • SphereCode-DINING.

    Then update tools/gen/patch-spheres-swagger.js so the patch remains idempotent on the next dev’s machine. Verified line numbers (read 2026-06-15):

    • patch-spheres-swagger.js:43SphereCode.enum: ['SPORT','CINEMA','SHOWS','SERVICES','DINING'] → drop 'DINING'.
    • patch-spheres-swagger.js:49SphereTargetApp.enum: ['GYM_APP','TICKETS_APP','DINING_APP']['GYM_APP','TICKETS_APP','SERVICES_APP'].
    • patch-spheres-swagger.js:55ActivityTypeCode.enum: ['SHOW','MOVIE','SLOT_BASED','DINING','SERVICE'] → drop 'DINING'.
  • Step C: Run npm run generate:api. Confirm the regenerated client/src/app/core/api/models/ files reflect the new enum domains.

  • Step D: Apply the hand-written fix in activity-form.page.ts:106 (drop the 'DINING', line).

  • Step E: Run npm run build. Build MUST exit 0.

Fallback (if Step B feels too brittle): same as any stub pattern used in 869dp8b71 (refundable, shipped 2026-06-13). Keep swagger-api.json unchanged, apply only the AC-17 manual fix in activity-form.page.ts, and add a // TODO(869e-followup): regenerate when sync-business-contract.js wrapper lands comment next to the cast. The chosen path (manual recipe vs as any fallback) MUST be documented in the MR description.

Astro static site. No API references touched. No changes.

Generated Dart enums in packages/api/lib/src/generated/swagger_api.enums.swagger.dart and the bundled swagger-api.json will regenerate clean on the next melos run sync:spec && melos run generate:api. Hand-written doc comments in packages/favorites/lib/src/favorites_repository.dart:50 and favorites_page.dart:238 mention DINING but do not compile-break — cosmetic follow-up.

  • activities.spheres — one row deleted (code='DINING'). Columns re-cast: target_app, allowed_activity_types (array), default_activity_type. Other rows (SPORT, CINEMA, SHOWS, SERVICES) untouched in code, target_app, allowed_activity_types, default_activity_type.
  • activities.activities — column type re-cast through the activity-type enum rebuild. No row content changes (zero rows reference 'DINING' on dev — see AC-13).
libs/shared/data-access-db/src/lib/schema/activities.schema.ts
export const SphereTargetAppEnum = activitiesSchema.enum('sphere_target_app', [
'GYM_APP',
'TICKETS_APP',
- 'DINING_APP',
+ 'SERVICES_APP',
]);
export const ActivityTypeEnum = activitiesSchema.enum('activity_type', [
'SHOW',
'MOVIE',
'SLOT_BASED',
- 'DINING',
'SERVICE',
]);

See drafts/migration-sphere-targetapp-enum-cleanup-drop-dining-add-services.sql. Real migration is generated by drizzle-kit generate in the tktspace-backend repo via the backend-drizzle-migration skill; it lands under libs/shared/data-access-db/migrations/.

None added, none dropped. The PG enum rebuild does not touch indexes — ALTER COLUMN ... TYPE … USING … preserves them.

Pure enum-value diff. No endpoint paths added or removed, no request/response field changes, no auth changes. All three surfaces emit the same enum schemas (SphereTargetApp, ActivityType) — that is intentional duplication consistent with the project’s “never share types across surfaces” rule, but the underlying domain is by definition identical because there is one PG enum per type.

ContractEndpointSchema diff
contracts/client.openapi.yamlGET /api/client/spheres, POST /api/client/favorites/activities (response shapes)SphereTargetApp: -DINING_APP, +SERVICES_APP. ActivityType: -DINING.
contracts/business.openapi.yamlactivity-form picker, activities-list filter, sphere read shapessame diff
contracts/super-admin.openapi.yamlPOST/PUT /api/superadmin/spheres (CreateSphereDto, UpdateSphereDtotargetApp, allowedActivityTypes, defaultActivityType)same diff

Refresh via pnpm run sync:contracts (= the backend-export-openapi skill) against a freshly migrated locally-booted backend on :5005. The script canonicalises key order — commit the YAML diffs alongside the backend edits. pnpm run sync:contracts:check MUST exit 0 in CI.

9. Surface impact (field-level visibility)

Section titled “9. Surface impact (field-level visibility)”

No field-level differences across surfaces — the enum-value domain is global (one PG type per enum). The three contracts are still regenerated independently because each is the canonical snapshot for its own client codegen.

Visibility justification per surface:

  • /api/client: client-facing UI rendering a sphere picker / activity-type badge. SphereTargetApp MUST narrow to what mobile actually renders. Leaving DINING_APP advertises a vertical that has no UI surface. Adding SERVICES_APP is forward-compatible: clients reading it as an unknown enum value will fall back to the default sphere icon (verified — SPHERE_FALLBACK_ICONS is a Record<string, string> with DEFAULT_SPHERE_ICON = '@tui.sparkles' fallback at tktspace-web/src/app/pages/explore/explore.page.ts:121).
  • /api/business: business-admin activity-form picker for staff creating activities under a tenant. Same narrowing rationale; salons live under GYM_APP until SERVICES_APP boots, so the picker continues to show today’s options minus DINING.
  • /api/superadmin: super-admin sphere CRUD. @IsIn validators reject invalid targetApp / defaultActivityType / allowedActivityTypes payloads with HTTP 400. After the change, attempts to POST targetApp: 'DINING_APP' are 400. Attempts with targetApp: 'SERVICES_APP' are 201 (no sphere row uses it yet, but the type accepts it). Internal fields (audit log, raw flags) are not affected.

No super-admin-only enum value is being introduced. SERVICES_APP is exposed identically on all three surfaces because the underlying PG enum is single-source.

  • client/src/app/features/dashboard/activities/pages/activity-form/activity-form.page.ts:106 — drop 'DINING', from activityTypes picker (ActivityResponseDto['type'][]).
  • Routing unchanged. No Taiga component swap. No new pages.
  • Regeneration: follow §6.2 Steps A–E exactly. If the manual recipe is rejected by the dev, as any fallback documented in the MR description.
  • src/app/pages/explore/explore.page.ts:118 — drop the DINING: '@tui.utensils' icon-map entry.
  • Routing unchanged. No new pages.
  • Regeneration: npm run generate against refreshed _workflow/contracts/client.openapi.yaml. Then npm run build exits 0.

Not touched. Astro static content has no DINING/DINING_APP references on disk (re-grepped at draft time — landing’s i18n strings live in Google Sheets; the consumer-list verification in AC-11 explicitly scoped to backend + web + business).

NOT in scope.

  • Affected apps if shipped later: both apps/gym_app and apps/tickets_app consume packages/api, so a regen of packages/api/lib/src/generated/swagger_api.enums.swagger.dart would propagate to both. No shared packages need source changes — the enum drop is purely additive on the client side.
  • Hand-written references that don’t compile-break:
    • packages/favorites/lib/src/favorites_repository.dart:50 — doc comment.
    • packages/favorites/lib/src/favorites_page.dart:238 — doc comment.
  • No screen, state, offline, deep-link, brand, or flavor changes.
  • No new mobile app being added in this ADR. SERVICES_APP Flutter app is a future product decision NOT scoped here.
  • Migration aborts on a non-empty DINING reference set. Mitigated by AC-13 pre-migration counts (three SQL queries returning 0). The seeded DINING row from migration 0037 is deleted by step 1 of the migration itself. If dev or staging has additional DINING activities created during testing, the migration will abort and the dev must clean up before re-running. This is the desired safety.
  • business regen gotcha leaves typed-client divergence. If the dev takes the as any fallback path instead of hand-editing swagger-api.json, the typed client for tktspace-business will continue advertising 'DINING' in ActivityResponseDto['type'] until a follow-up regen lands. Mitigated by requiring the MR description to explicitly flag the fallback.
  • Stale patched schemas in swagger-api.json drift further from canonical. Already known debt (the 286 KB hand-curated snapshot has ~40 schemas absent from _workflow/contracts/business.openapi.yaml). Not made worse by this MR. Surfaced as a recommendation in §15.
  • Performance: none. Enum rebuilds rewrite the column data files for affected columns. On a fresh dev DB this is microseconds. On production with no DINING references, the ALTER COLUMN ... USING is a fast no-op data check.
  • Security: none. No new endpoints, no auth changes, no PII surface.

Single MR, no feature flag, no phased rollout.

  1. Backend MR contains: schema edit (§5.1) + DTO edits (§5.2) + service casts (§5.3) + activities DTO edits (§5.4) + helper comment fixes (§5.5) + test rewrites (§5.6) + auto-generated Drizzle migration (§7.3) + refreshed contract YAMLs (§8).
  2. Web sub-MR (or same MR if monorepo wiring allows — both repos are independent here, so two MRs in parallel are fine) contains: regenerated core/api/** + manual icon-map fix.
  3. Business sub-MR contains: hand-edited swagger-api.json + updated patch-spheres-swagger.js line numbers 43/49/55 + regenerated core/api/** + manual picker fix. OR as any fallback path documented.
  4. No backfill. No data migration beyond the single DELETE in migration step 1. No rollback automation needed; 0037-generation seed SQL preserved in git history if ever required.
  5. CI gates: pnpm run sync:contracts:check exit 0; npm test green in spheres feature; e2e green on superadmin + client spheres + wallet-target-app; npm run build green in both Angular apps.
  6. Required runtime smoke (per feedback_runtime_smoke): npx nx serve api boots; Swagger UI at http://localhost:5005/api/superadmin/docs shows the two new enum domains.

14. Cross-surface coordination — mandatory backend checklist (verbatim, do NOT skip)

Section titled “14. Cross-surface coordination — mandatory backend checklist (verbatim, do NOT skip)”
  1. Pre-migration data integrity check (AC-13) — run the three SQL queries from §4 against dev DB before applying. Each MUST return 0 (the DINING sphere row will be deleted by step 1 of the migration; the queries account for that). If any query returns non-zero on dev or any pre-prod environment, STOP and surface to the user — do not attempt to force the migration through.
  2. All 7 file edits in sphere.dto.ts — do not miss any. Grep 'DINING' and 'DINING_APP' after edits to verify zero hits in this file.
  3. Both TS enum + Drizzle PG enum updated. The hand-written ActivityType TS enum at create-activity.dto.ts:23 is SEPARATE from the Drizzle PG enum at activities.schema.ts:39. Both must drop DINING.
  4. Comment updates in resolve-target-app.ts:9 + target-app.interceptor.spec.ts:10 are accuracy fixes, not behaviour changes. Do not refactor surrounding code.
  5. E2e test rewrites (5 spots) — see §5.6. The shrink-conflict path in spheres-superadmin.e2e-spec.ts MUST keep its semantic: still drops SLOT_BASED from allowedActivityTypes, still triggers AC-24 (errors.sphere.activity_type_in_use). Verify by reading the surrounding it() block before editing.
  6. Use the backend-drizzle-migration skill to auto-generate the SQL. Do NOT hand-write the migration.
  7. Use the migration-safety-check skill on the generated SQL before applying. Surface any unguarded CASCADE or non-enum-rebuild table drop to the user.
  8. Use the backend-export-openapi skill (= pnpm run sync:contracts) against a locally booted backend with migrations applied. Commit the three YAML diffs in the same MR as the backend edits.
  9. pnpm run sync:contracts:check MUST exit 0 before pushing.
  • tktspace-web dev: drop the icon-map line at src/app/pages/explore/explore.page.ts:118. Run npm run generate, confirm regenerated files in core/api/models/. Run npm run build, confirm exit 0. No new tests.
  • tktspace-business dev: follow §6.2 Steps A–E exactly. Hand-edit tools/gen/swagger-api.json AND update tools/gen/patch-spheres-swagger.js:43,49,55 AND drop 'DINING', from activity-form.page.ts:106. Then npm run generate:api, then npm run build. If the manual recipe is rejected, take the as any fallback path AND document the choice in the MR description for the reviewer.
  • 869dpxbj6 — next in the cleanup chain. Collapses ActivityType to [SLOT_BASED, SERVICE] + adds hasSeatSelection: boolean. Merges CINEMA + SHOWS sphere rows into EVENTS. Assumes this ADR shipped first.

  • 869dq1q27 — extract libs/shared/target-app/ library. Relocates resolve-target-app.ts + target-app.interceptor.ts. Assumes this ADR shipped first.

  • NEW recommendation (NOT in scope here, flagged for user decision): open a separate ticket for tktspace-business/tools/gen/sync-business-contract.js wrapper that does fetch-from-canonical + run all patch-*.js scripts + ng-openapi-gen in one command. Verified absent on disk at draft time (ls tools/gen/) yet referenced by patch-passes-swagger.js:16. Without it, every business contract refresh repeats the brittle Step B hand-edit. Recommend prioritising before 869dpxbj6 (which has even more enum churn — activity-type collapse + sphere row merges + new boolean field). Final decision deferred to user.

    Tech-debt compounding note (per critic SUGGESTION): if the Phase C dev picks the as any fallback in AC-18 instead of the manual recipe, the divergence between tools/gen/swagger-api.json and the canonical _workflow/contracts/business.openapi.yaml widens further. Each successive ticket in the cleanup chain (869dpxbj6 adds 3+ enum changes; 869dq1q27 no codegen impact) compounds the gap. Strongly prefer the manual recipe even though it’s brittle, OR escalate the wrapper ticket priority above 869dpxbj6 so the next cleanup ships with a clean regen flow.

17. Verification notes from drafting (2026-06-15)

Section titled “17. Verification notes from drafting (2026-06-15)”
  • Spec line numbers re-verified against current code via Read. All 22 cited line numbers match exactly: schema 26-30,39-45; sphere.dto.ts 31,70,72,80,118,121,129; spheres.service.ts 32,92,184; create-activity.dto.ts:23; favorite.dto.ts:41; resolve-target-app.ts:9; target-app.interceptor.spec.ts:10; patch-spheres-swagger.js:43,49,55; explore.page.ts:118; activity-form.page.ts:106.
  • tools/gen/sync-business-contract.js confirmed ABSENT (ls tools/gen/ returned 6 files, no wrapper).
  • patch-passes-swagger.js:16 header confirmed to reference the missing wrapper.
  • SPHERE_FALLBACK_ICONS index type re-verified as Record<string, string> at explore.page.ts:113 (clean delete confirmed safe, no keyof typeof cast surface).
  • No drift discovered.

STATUS: READY_FOR_REVIEW