diff --git a/docs/work/conformance-system-v1/06-feature-migrations/_story.md b/docs/work/conformance-system-v1/06-feature-migrations/_story.md new file mode 100644 index 0000000..5f99c99 --- /dev/null +++ b/docs/work/conformance-system-v1/06-feature-migrations/_story.md @@ -0,0 +1,39 @@ +--- +id: 06-feature-migrations +epic: conformance-system-v1 +title: Migrate blog/media/navigation/marketing-pages to conformance pattern +type: technical-story +status: done +feature: +depends-on: [05-generator-updates] +blocks: [] +--- + +## Goal +Every feature in the repo has a manifest + a self-asserting bindProduction. +After this milestone, `feature-must-have-manifest` flips from WARN to ERROR. + +## Why +Three of the four enforcement layers already exist; the only thing keeping +them from being fully effective is that 4 of 5 features still don't have +manifests. This story closes that gap. + +## In scope +- `feature.manifest.ts` for blog / media / navigation / marketing-pages +- Manifest re-export from each feature's `src/index.ts` +- `bind-production.ts` update for each: imports + tail `assertFeatureConformance` call +- Flip `feature-must-have-manifest` from `warn` to `error` in base.js + +## Out of scope +- Adding publishes/audits to existing use cases (declared empty for now) +- Cross-feature event wiring (blog→marketing welcome flows, etc.) +- Migrating realtime channels or jobs into manifests + +## Tasks +- [x] Story scaffold +- [x] blog manifest + binding + re-export +- [x] media manifest + binding + re-export +- [x] navigation manifest + binding + re-export +- [x] marketing-pages manifest + binding + re-export +- [x] Flip feature-must-have-manifest to error +- [x] Final verification + closeout diff --git a/docs/work/conformance-system-v1/_epic.md b/docs/work/conformance-system-v1/_epic.md index 15e46d6..523763a 100644 --- a/docs/work/conformance-system-v1/_epic.md +++ b/docs/work/conformance-system-v1/_epic.md @@ -3,7 +3,7 @@ id: conformance-system-v1 prd: null title: Conformance system v1 type: epic -status: in-progress +status: done features: [cross-cutting] created: 2026-05-12 --- @@ -37,5 +37,5 @@ See `docs/architecture/feature-conformance-explainer.html` and - [x] [03.b — Manifest-aware AST rules](03-b-ast-eslint-rules/_story.md) - [x] [04 — CI drift gate](04-ci-drift-gate/_story.md) - [x] [05 — Generator updates](05-generator-updates/_story.md) -- [ ] 06 — Documentation rewrite (later plan) -- [ ] 07 — Migrate auth feature reference (later plan) +- [x] [06 — Migrate blog / media / navigation / marketing-pages](06-feature-migrations/_story.md) +- [x] 07 — Migrate auth feature reference (completed inline in milestone i; signIn through ProductionUseCase slot) diff --git a/packages/blog/src/di/bind-production.ts b/packages/blog/src/di/bind-production.ts index 2bc52c3..dcaf9b3 100644 --- a/packages/blog/src/di/bind-production.ts +++ b/packages/blog/src/di/bind-production.ts @@ -6,8 +6,10 @@ import { type ILogger, } from "@repo/core-shared/instrumentation"; import type { BindProductionContext } from "@repo/core-shared/di"; +import { assertFeatureConformance } from "@repo/core-shared/conformance"; import { blogContainer } from "./container"; import { BLOG_SYMBOLS } from "./symbols"; +import { blogManifest } from "../feature.manifest"; import { ArticlesRepository } from "../infrastructure/repositories/articles.repository"; import { getArticlesUseCase } from "../application/use-cases/get-articles.use-case"; import { getArticleBySlugUseCase } from "../application/use-cases/get-article-by-slug.use-case"; @@ -138,4 +140,16 @@ export function bindProductionBlog(ctx: BindProductionContext): void { // // // + + // Boot-time conformance check. + assertFeatureConformance( + blogContainer, + blogManifest, + { + getArticles: BLOG_SYMBOLS.IGetArticlesUseCase, + getArticleBySlug: BLOG_SYMBOLS.IGetArticleBySlugUseCase, + createArticle: BLOG_SYMBOLS.ICreateArticleUseCase, + }, + ctx, + ); } diff --git a/packages/blog/src/feature.manifest.ts b/packages/blog/src/feature.manifest.ts new file mode 100644 index 0000000..4382a47 --- /dev/null +++ b/packages/blog/src/feature.manifest.ts @@ -0,0 +1,33 @@ +import { defineFeature } from "@repo/core-shared/conformance"; + +/** + * The blog feature's conformance manifest. + */ +export const blogManifest = defineFeature({ + name: "blog", + requiredCores: [], + useCases: { + getArticles: { + mutates: false, + audits: [], + publishes: [], + consumes: [], + }, + getArticleBySlug: { + mutates: false, + audits: [], + publishes: [], + consumes: [], + }, + createArticle: { + mutates: true, + audits: [], + publishes: [], + consumes: [], + }, + }, + realtimeChannels: [], + jobs: [], +} as const); + +export type BlogManifest = typeof blogManifest; diff --git a/packages/blog/src/index.ts b/packages/blog/src/index.ts index 26c4cb3..5ace2ab 100644 --- a/packages/blog/src/index.ts +++ b/packages/blog/src/index.ts @@ -33,3 +33,4 @@ export type { IGetArticleBySlugController } from "./interface-adapters/controlle // // +export { blogManifest, type BlogManifest } from "./feature.manifest"; diff --git a/packages/core-eslint/base.js b/packages/core-eslint/base.js index 130aa20..02f839e 100644 --- a/packages/core-eslint/base.js +++ b/packages/core-eslint/base.js @@ -33,10 +33,9 @@ export default [ plugins: { conformance: conformancePlugin }, rules: { // Structural conformance rules (milestone iii.a). - // `feature-must-have-manifest` is WARN today because only auth has a manifest; - // flip to ERROR after blog/media/navigation/marketing-pages migrate. + // All 5 features now have manifests; promoted to ERROR. "conformance/feature-must-have-manifest": [ - "warn", + "error", { repoRoot }, ], "conformance/usecase-must-have-test-file": "error", diff --git a/packages/marketing-pages/src/di/bind-production.ts b/packages/marketing-pages/src/di/bind-production.ts index de21601..71ae7b4 100644 --- a/packages/marketing-pages/src/di/bind-production.ts +++ b/packages/marketing-pages/src/di/bind-production.ts @@ -6,8 +6,10 @@ import { type ILogger, } from "@repo/core-shared/instrumentation"; import type { BindProductionContext } from "@repo/core-shared/di"; +import { assertFeatureConformance } from "@repo/core-shared/conformance"; import { marketingPagesContainer } from "./container"; import { MARKETING_PAGES_SYMBOLS } from "./symbols"; +import { marketingPagesManifest } from "../feature.manifest"; import { PagesRepository } from "../infrastructure/repositories/pages.repository"; import { SiteSettingsRepository } from "../infrastructure/repositories/site-settings.repository"; import { getSiteSettingsUseCase } from "../application/use-cases/get-site-settings.use-case"; @@ -172,4 +174,15 @@ export function bindProductionMarketingPages(ctx: BindProductionContext): void { void realtime; void realtimeRegistry; // + + // Boot-time conformance check. + assertFeatureConformance( + marketingPagesContainer, + marketingPagesManifest, + { + getPageBySlug: MARKETING_PAGES_SYMBOLS.IGetPageBySlugUseCase, + getSiteSettings: MARKETING_PAGES_SYMBOLS.IGetSiteSettingsUseCase, + }, + ctx, + ); } diff --git a/packages/marketing-pages/src/feature.manifest.ts b/packages/marketing-pages/src/feature.manifest.ts new file mode 100644 index 0000000..8c0c01e --- /dev/null +++ b/packages/marketing-pages/src/feature.manifest.ts @@ -0,0 +1,17 @@ +import { defineFeature } from "@repo/core-shared/conformance"; + +/** + * The marketing-pages feature's conformance manifest. + */ +export const marketingPagesManifest = defineFeature({ + name: "marketing-pages", + requiredCores: [], + useCases: { + getPageBySlug: { mutates: false, audits: [], publishes: [], consumes: [] }, + getSiteSettings: { mutates: false, audits: [], publishes: [], consumes: [] }, + }, + realtimeChannels: [], + jobs: [], +} as const); + +export type MarketingPagesManifest = typeof marketingPagesManifest; diff --git a/packages/marketing-pages/src/index.ts b/packages/marketing-pages/src/index.ts index f821700..5404b98 100644 --- a/packages/marketing-pages/src/index.ts +++ b/packages/marketing-pages/src/index.ts @@ -26,3 +26,4 @@ export type { IGetSiteSettingsController } from "./interface-adapters/controller // // +export { marketingPagesManifest, type MarketingPagesManifest } from "./feature.manifest"; diff --git a/packages/media/src/di/bind-production.ts b/packages/media/src/di/bind-production.ts index 3c7c0f5..d3b304a 100644 --- a/packages/media/src/di/bind-production.ts +++ b/packages/media/src/di/bind-production.ts @@ -6,8 +6,10 @@ import { type ILogger, } from "@repo/core-shared/instrumentation"; import type { BindProductionContext } from "@repo/core-shared/di"; +import { assertFeatureConformance } from "@repo/core-shared/conformance"; import { mediaContainer } from "./container"; import { MEDIA_SYMBOLS } from "./symbols"; +import { mediaManifest } from "../feature.manifest"; import { MediaRepository } from "../infrastructure/repositories/media.repository"; import { getMediaUseCase } from "../application/use-cases/get-media.use-case"; import { listMediaUseCase } from "../application/use-cases/list-media.use-case"; @@ -134,4 +136,16 @@ export function bindProductionMedia(ctx: BindProductionContext): void { // // // + + // Boot-time conformance check. + assertFeatureConformance( + mediaContainer, + mediaManifest, + { + getMedia: MEDIA_SYMBOLS.IGetMediaUseCase, + listMedia: MEDIA_SYMBOLS.IListMediaUseCase, + deleteMedia: MEDIA_SYMBOLS.IDeleteMediaUseCase, + }, + ctx, + ); } diff --git a/packages/media/src/feature.manifest.ts b/packages/media/src/feature.manifest.ts new file mode 100644 index 0000000..3f227a6 --- /dev/null +++ b/packages/media/src/feature.manifest.ts @@ -0,0 +1,18 @@ +import { defineFeature } from "@repo/core-shared/conformance"; + +/** + * The media feature's conformance manifest. + */ +export const mediaManifest = defineFeature({ + name: "media", + requiredCores: [], + useCases: { + getMedia: { mutates: false, audits: [], publishes: [], consumes: [] }, + listMedia: { mutates: false, audits: [], publishes: [], consumes: [] }, + deleteMedia: { mutates: true, audits: [], publishes: [], consumes: [] }, + }, + realtimeChannels: [], + jobs: [], +} as const); + +export type MediaManifest = typeof mediaManifest; diff --git a/packages/media/src/index.ts b/packages/media/src/index.ts index 4ac69a5..afd7c2c 100644 --- a/packages/media/src/index.ts +++ b/packages/media/src/index.ts @@ -31,3 +31,4 @@ export type { IDeleteMediaController } from "./interface-adapters/controllers/de // // +export { mediaManifest, type MediaManifest } from "./feature.manifest"; diff --git a/packages/navigation/src/di/bind-production.ts b/packages/navigation/src/di/bind-production.ts index f535180..3eb7bd1 100644 --- a/packages/navigation/src/di/bind-production.ts +++ b/packages/navigation/src/di/bind-production.ts @@ -6,8 +6,10 @@ import { type ILogger, } from "@repo/core-shared/instrumentation"; import type { BindProductionContext } from "@repo/core-shared/di"; +import { assertFeatureConformance } from "@repo/core-shared/conformance"; import { navigationContainer } from "./container"; import { NAVIGATION_SYMBOLS } from "./symbols"; +import { navigationManifest } from "../feature.manifest"; import { HeaderRepository } from "../infrastructure/repositories/header.repository"; import { getHeaderUseCase } from "../application/use-cases/get-header.use-case"; import { getHeaderController } from "../interface-adapters/controllers/get-header.controller"; @@ -78,4 +80,12 @@ export function bindProductionNavigation(ctx: BindProductionContext): void { // // // + + // Boot-time conformance check. + assertFeatureConformance( + navigationContainer, + navigationManifest, + { getHeader: NAVIGATION_SYMBOLS.IGetHeaderUseCase }, + ctx, + ); } diff --git a/packages/navigation/src/feature.manifest.ts b/packages/navigation/src/feature.manifest.ts new file mode 100644 index 0000000..98b9300 --- /dev/null +++ b/packages/navigation/src/feature.manifest.ts @@ -0,0 +1,16 @@ +import { defineFeature } from "@repo/core-shared/conformance"; + +/** + * The navigation feature's conformance manifest. + */ +export const navigationManifest = defineFeature({ + name: "navigation", + requiredCores: [], + useCases: { + getHeader: { mutates: false, audits: [], publishes: [], consumes: [] }, + }, + realtimeChannels: [], + jobs: [], +} as const); + +export type NavigationManifest = typeof navigationManifest; diff --git a/packages/navigation/src/index.ts b/packages/navigation/src/index.ts index 089c6c1..439183b 100644 --- a/packages/navigation/src/index.ts +++ b/packages/navigation/src/index.ts @@ -17,3 +17,4 @@ export type { IGetHeaderController } from "./interface-adapters/controllers/get- // // +export { navigationManifest, type NavigationManifest } from "./feature.manifest";