name: noir-feature-add
description: Scaffold a new NOIR feature/module with correct registration across all 5 registries (Sidebar, Permissions, OpenAPI, Modules, MCP Tools). Use when the user asks to add a new feature, module, domain area, or a new top-level sidebar entry. Prevents the common "added sidebar item but forgot OpenAPI tag / permission / MCP tool" class of bug covered in .claude/rules/feature-registry-sync.md.
noir-feature-add — 5-Registry Feature Scaffold
NOIR requires every new feature to be registered in 5 places. Skipping any one of them produces a silent bug (API docs miss the group, role UI can't assign permission, AI agents can't call the feature, etc.). This skill walks the registration with a mandatory checklist.
Inputs to collect upfront
Before editing any file, confirm with the user:
- Feature name (PascalCase, singular): e.g.
Promotion,Invoice,Warehouse - Domain category (determines sidebar section + module category):
Dashboard|Marketing|Orders & Fulfillment|Customers|Catalog|Human Resources|Project Management|CRM|Content|Users & Access|Settings|System
- Is this a toggleable module? (27 toggleable vs 8 core — default: yes, toggleable)
- Permissions needed (default:
Read,Create,Update,Delete,Manage) - Initial MCP tools (default:
list,get—create/update/deleteare optional) - Frontend path (default:
/portal/{category-kebab}/{feature-kebab}s)
If any of the above is ambiguous, ASK. Do not guess.
The 5 registries — must ALL be updated
| # | File | What to add |
|---|---|---|
| 1 | src/NOIR.Application/Modules/{Feature}ModuleDefinition.cs + ModuleNames.cs + ModuleCatalog.cs |
Module definition, ModuleNames.{Category}.{Name} constant, register in catalog |
| 2 | src/NOIR.Domain/Common/Permissions.cs |
{Feature}Read/Create/Update/Delete/Manage constants, group entry, add all to All array in the correct section |
| 3 | src/NOIR.Web/OpenApi/SecuritySchemeDocumentTransformer.cs |
OpenApiTag in document.Tags AND x-tagGroups JSON entry (both must match the domain category) |
| 4 | src/NOIR.Web/frontend/src/components/portal/Sidebar.tsx |
Item in correct navSection with titleKey, icon, path, permission, feature |
| 5 | src/NOIR.Web/Mcp/Tools/{Feature}Tools.cs |
[McpServerToolType] class with [RequiresModule(ModuleNames.{Cat}.{Name})]. Tool names follow noir_{domain}_{action}. At minimum: list + get. |
Plus:
- i18n keys in both
public/locales/en/common.jsonANDpublic/locales/vi/common.jsonfornav.*,modules.*,permissions.*— pure Vietnamese (no mixed-language labels persidebar-naming-convention.md) - Module test count in
tests/NOIR.Application.UnitTests/Modules/ModuleCatalogTests.cs— bump the expected count - Integration tests in
tests/NOIR.IntegrationTests/Endpoints/{Feature}EndpointsTests.cs— at minimum: happy path, 401 unauthenticated, 400/404 invalid input (Rule 22)
Workflow (strict order)
Phase 1 — Read patterns first (Rule 1)
Before writing anything, read 2–3 recent similar features to match the current pattern. Good examples:
- Simple CRUD:
Brands(Features/Brands/,Web/Mcp/Tools/BrandTools.cs,ProductCategoryModuleDefinition.cs) - With workflow:
Promotions - CRM/HR/PM: pick the matching category's latest feature
Commands:
# Latest Module definitions
ls -t src/NOIR.Application/Modules/*.cs | head -5
# Latest MCP tool classes
ls -t src/NOIR.Web/Mcp/Tools/*.cs | head -5
Phase 2 — Backend scaffolding (Domain → Application → Infrastructure → Web)
- Entity in
src/NOIR.Domain/Entities/{Feature}.cs(implementIAuditableEntity+ITenantEntityif scoped) {Feature}Configurationinsrc/NOIR.Infrastructure/Persistence/Configurations/(Rule 18: unique indexes includeTenantId)- Repository in
src/NOIR.Infrastructure/Persistence/Repositories/{Feature}Repository.cs+ DI verification test (Rule 21) - Commands + Queries co-located in
src/NOIR.Application/Features/{Feature}/{Commands|Queries}/{Action}/(Rule 10) - Mutation commands that go through frontend MUST implement
IAuditableCommand<TResult>(Rule 11) Updatecommands MUST register a before-state resolver inDependencyInjection.cs(Rule 12)- Endpoints under
src/NOIR.Web/Endpoints/{Feature}Endpoints.cs, tag with.WithTags("...")and gate with.RequireFeature(ModuleNames...) - Migration:
dotnet ef migrations add Add{Feature} --context ApplicationDbContext --output-dir Migrations/App(Rule 23)
Phase 3 — The 5 registries
Now update all 5. Refer to the table above. Use a TodoWrite list to track — one task per registry — and don't mark a registry done until you've grep'd the file to confirm the edit stuck.
Phase 4 — Frontend
- Run
cd src/NOIR.Web/frontend && pnpm run generate:apito sync types + Zod schemas - Page components under
src/portal-app/{category}/{feature}/— followtable-list-standard.md+datatable-standard.md+ audit columns standard - Forms use
useValidatedForm(Rule: form-validation-standard) usePageContext('FeatureName')in the page component (audit logging — Rule 11)- URL-synced state:
useUrlTab()/useUrlDialog()/useUrlEditDialog()perurl-tab-state.md
Phase 5 — Tests
- Unit:
tests/NOIR.Application.UnitTests/Features/{Feature}/— handler tests with mockedIEntityUpdateHubContextfor any handler that publishes signals - Domain:
tests/NOIR.Domain.UnitTests/if new value objects / invariants - Integration:
tests/NOIR.IntegrationTests/Endpoints/{Feature}EndpointsTests.cs— 100% endpoint coverage (Rule 22) - Repository DI:
tests/NOIR.Infrastructure.UnitTests/Persistence/RepositoryRegistrationTests.cs— add assertion for new repo - Module catalog: bump expected count in
ModuleCatalogTests.cs
Phase 6 — Quality gates (MUST pass before reporting done)
dotnet build src/NOIR.sln # 0 errors
dotnet test src/NOIR.sln # ALL pass, zero skipped
cd src/NOIR.Web/frontend && pnpm run build # 0 errors, 0 warnings (strict)
cd src/NOIR.Web/frontend && pnpm build-storybook # 0 errors (if UIKit touched)
Plus manual verification via browser (Rule: UI changes need browser test):
- New feature appears in sidebar (check both EN and VI)
- Permission shows up in Role editor
- API docs show tag in correct
x-tagGroupssection - Feature toggle visible in Settings > Features
- MCP tool callable (check
curl http://localhost:4000/api/mcp/tools/listor via MCP Inspector)
Final checklist (read aloud before reporting complete)
Group ordering everywhere — MUST match:
Dashboard → Marketing → Orders & Fulfillment → Customers → Catalog → Human Resources → Project Management → CRM → Content → Users & Access → Settings → System
-
ModuleNames.{Category}.{Name}constant added -
{Name}ModuleDefinition.cscreated with correctSortOrderwithin its category - Registered in
ModuleCatalog.cs -
ModuleCatalogTests.csexpected count updated — tests pass - Permission constants in
Permissions.cs(constants + group + appended toAllarray in correct section) - Sidebar item in correct
navSectionwithpermission+featureprops - OpenAPI tag in
document.Tagswith description - OpenAPI
x-tagGroupsJSON updated — tag in correct group - Endpoint group tagged
.WithTags("Tag Name")AND gated.RequireFeature(ModuleNames.X.Y) - MCP tool class at
Web/Mcp/Tools/{Feature}Tools.cswith[RequiresModule] - MCP tool names follow
noir_{domain}_{action}— explicit[McpServerTool(Name = "...")] - i18n keys in BOTH
en/common.jsonANDvi/common.json(nav.*,modules.*,permissions.*) - VI labels follow sentence case + pure Vietnamese (no "Bài viết Blog" style mixing)
- Integration tests cover happy path + 401 + 400/404
- Repository DI registration test added
- Migration added with
--context ApplicationDbContext(or Tenant) -
dotnet build,dotnet test,pnpm run buildall green
Common mistakes this skill prevents
- Adding sidebar item but forgetting the OpenAPI tag (API docs won't show the group)
- Adding permissions but not updating the
Allarray (role assignment UI won't show them) - Adding endpoint but forgetting
.RequireFeature()gate (module toggle does nothing) - Adding MCP tool without
[RequiresModule](bypasses feature gate) - MCP tool naming like
GetProductsinstead ofnoir_products_list(breaks AI discoverability) - Accepting
Guidin MCP tool signature instead ofstring(AI clients send strings — Rule 27) - Using
.With("Name")single tag when a feature needs grouping in OpenAPI sidebar - Module test count in
ModuleCatalogTests.csout of sync after adding a module - i18n labels mixing EN + VI (e.g. "Bài viết Blog" — violates
sidebar-naming-convention.md)