22 KiB
Namespace Governance Implementation Plan
For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Build the namespace governance lifecycle end-to-end: team namespace freeze/archive/restore, immutable @global, split management read models, state-aware backend policies, and dashboard interactions.
Architecture: Extend the existing namespace domain with a dedicated governance service and a shared access-policy helper instead of overloading NamespaceService. Keep public and management reads separate by adding /me/namespaces, then thread namespace status rules through publish/review/promotion/query/search and surface them in the React dashboard with role-aware controls.
Tech Stack: Spring Boot 3.x, Spring Data JPA, Spring Security, JUnit 5, Mockito, React 19, TypeScript, TanStack Query, TanStack Router, pnpm
Spec: docs/superpowers/specs/2026-03-16-namespace-governance-design.md
File Structure Mapping
Backend domain and portal
- Create:
server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceGovernanceService.java - Create:
server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceAccessPolicy.java - Create:
server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/namespace/NamespaceGovernanceServiceTest.java - Create:
server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/NamespaceLifecycleRequest.java - Create:
server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/MyNamespaceResponse.java - Create:
server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/NamespacePortalControllerTest.java - Modify:
server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/Namespace.java - Modify:
server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceService.java - Modify:
server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceMemberService.java - Modify:
server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceRepository.java - Modify:
server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/NamespaceJpaRepository.java - Modify:
server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/NamespaceController.java - Modify:
server/skillhub-app/src/main/resources/messages.properties - Modify:
server/skillhub-app/src/main/resources/messages_zh.properties
Cross-module state enforcement
- Modify:
server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillPublishService.java - Modify:
server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewService.java - Modify:
server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/PromotionService.java - Modify:
server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillQueryService.java - Modify:
server/skillhub-app/src/main/java/com/iflytek/skillhub/service/SkillSearchAppService.java - Modify:
server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillPublishServiceTest.java - Modify:
server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/ReviewPortalControllerTest.java - Modify:
server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/PromotionPortalControllerTest.java - Modify:
server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/SkillControllerTest.java - Modify:
server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/SkillSearchControllerTest.java
Frontend dashboard
- Modify:
web/src/api/types.ts - Modify:
web/src/api/client.ts - Modify:
web/src/shared/hooks/use-skill-queries.ts - Modify:
web/src/pages/dashboard/my-namespaces.tsx - Modify:
web/src/pages/dashboard/namespace-members.tsx - Modify:
web/src/pages/dashboard/namespace-reviews.tsx - Modify:
web/src/features/namespace/namespace-header.tsx - Modify:
web/src/i18n/locales/zh.json - Modify:
web/src/i18n/locales/en.json
Chunk 1: Namespace Governance Backend
Task 1: Add lifecycle policy and immutable-global guard
Files:
-
Create:
server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceGovernanceService.java -
Create:
server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceAccessPolicy.java -
Create:
server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/namespace/NamespaceGovernanceServiceTest.java -
Modify:
server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/Namespace.java -
Modify:
server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceService.java -
Modify:
server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceMemberService.java -
Modify:
server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/namespace/NamespaceServiceTest.java -
Modify:
server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/namespace/NamespaceMemberServiceTest.java -
Step 1: Write the failing domain tests
@Test
void freezeNamespace_allowsAdminOnActiveTeamNamespace() {
Namespace namespace = namespace("team-a", NamespaceType.TEAM, NamespaceStatus.ACTIVE);
when(namespaceRepository.findBySlug("team-a")).thenReturn(Optional.of(namespace));
when(namespaceMemberRepository.findByNamespaceIdAndUserId(1L, "admin-1"))
.thenReturn(Optional.of(new NamespaceMember(1L, "admin-1", NamespaceRole.ADMIN)));
Namespace updated = governanceService.freezeNamespace("team-a", "admin-1", null, null, null);
assertEquals(NamespaceStatus.FROZEN, updated.getStatus());
}
@Test
void archiveNamespace_rejectsAdminAndAllowsOnlyOwner() { ... }
@Test
void updateNamespace_rejectsFrozenNamespace() { ... }
@Test
void addMember_rejectsArchivedNamespace() { ... }
- Step 2: Run the domain tests to verify they fail
Run: cd server && ./mvnw -pl skillhub-domain -Dtest=NamespaceGovernanceServiceTest,NamespaceServiceTest,NamespaceMemberServiceTest test
Expected: FAIL because NamespaceGovernanceService, namespace status setters, and read-only guards do not exist yet.
- Step 3: Implement the lifecycle policy
public final class NamespaceAccessPolicy {
public boolean isSystemImmutable(Namespace namespace) {
return namespace.getType() == NamespaceType.GLOBAL;
}
public boolean canMutateSettings(Namespace namespace) {
return namespace.getType() == NamespaceType.TEAM
&& namespace.getStatus() == NamespaceStatus.ACTIVE;
}
public boolean canArchive(Namespace namespace, NamespaceRole role) {
return namespace.getType() == NamespaceType.TEAM
&& role == NamespaceRole.OWNER
&& namespace.getStatus() != NamespaceStatus.ARCHIVED;
}
}
public Namespace freezeNamespace(String slug, String actorUserId, String requestId, String clientIp, String userAgent) {
Namespace namespace = loadMutableNamespace(slug);
NamespaceRole role = requireRole(namespace.getId(), actorUserId);
if (role != NamespaceRole.OWNER && role != NamespaceRole.ADMIN) {
throw new DomainForbiddenException("error.namespace.lifecycle.freeze.forbidden");
}
if (namespace.getStatus() != NamespaceStatus.ACTIVE) {
throw new DomainBadRequestException("error.namespace.state.transition.invalid");
}
namespace.setStatus(NamespaceStatus.FROZEN);
return namespaceRepository.save(namespace);
}
- Step 4: Re-run the domain tests and keep them green
Run: cd server && ./mvnw -pl skillhub-domain -Dtest=NamespaceGovernanceServiceTest,NamespaceServiceTest,NamespaceMemberServiceTest test
Expected: PASS for lifecycle transitions, immutable @global, and read-only enforcement on settings/member operations.
- Step 5: Commit the domain governance changes
git add server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace \
server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/namespace
git commit -m "feat: add namespace lifecycle governance"
git push origin feature/project-namespace
Task 2: Expose management read model and lifecycle APIs
Files:
-
Create:
server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/NamespaceLifecycleRequest.java -
Create:
server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/MyNamespaceResponse.java -
Create:
server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/NamespacePortalControllerTest.java -
Modify:
server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceRepository.java -
Modify:
server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/NamespaceJpaRepository.java -
Modify:
server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/NamespaceController.java -
Modify:
server/skillhub-app/src/main/resources/messages.properties -
Modify:
server/skillhub-app/src/main/resources/messages_zh.properties -
Step 1: Write the failing portal tests
@Test
void listMyNamespaces_returnsFrozenAndArchivedNamespacesWithCurrentRole() throws Exception {
mockMvc.perform(get("/api/v1/me/namespaces").with(auth("owner-1")))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data[0].status").value("ARCHIVED"))
.andExpect(jsonPath("$.data[0].currentUserRole").value("OWNER"));
}
@Test
void archiveNamespace_returnsUpdatedNamespace() throws Exception {
mockMvc.perform(post("/api/v1/namespaces/team-a/archive")
.with(csrf())
.with(auth("owner-1"))
.contentType(MediaType.APPLICATION_JSON)
.content("{\"reason\":\"cleanup\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("ARCHIVED"));
}
- Step 2: Run the portal tests to verify they fail
Run: cd server && ./mvnw -pl skillhub-app -Dtest=NamespacePortalControllerTest test
Expected: FAIL because /me/namespaces, lifecycle endpoints, and management DTOs do not exist.
- Step 3: Implement controller and DTO support
public record MyNamespaceResponse(
Long id,
String slug,
String displayName,
NamespaceStatus status,
NamespaceType type,
NamespaceRole currentUserRole,
boolean immutable,
boolean canFreeze,
boolean canArchive,
boolean canRestore
) {}
@GetMapping("/me/namespaces")
public ApiResponse<List<MyNamespaceResponse>> listMyNamespaces(
@RequestAttribute("userId") String userId,
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles) {
return ok("response.success.read",
namespaceService.listMyNamespaces(userId, userNsRoles != null ? userNsRoles : Map.of()));
}
- Step 4: Re-run the portal tests
Run: cd server && ./mvnw -pl skillhub-app -Dtest=NamespacePortalControllerTest test
Expected: PASS with currentUserRole, lifecycle booleans, and updated namespace payloads serialized correctly.
- Step 5: Commit the portal API changes
git add server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/NamespaceController.java \
server/skillhub-app/src/main/java/com/iflytek/skillhub/dto \
server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/NamespacePortalControllerTest.java \
server/skillhub-app/src/main/resources/messages.properties \
server/skillhub-app/src/main/resources/messages_zh.properties \
server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/NamespaceJpaRepository.java \
server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceRepository.java
git commit -m "feat: add namespace management endpoints"
git push origin feature/project-namespace
Chunk 2: State Enforcement Across Publish, Review, Promotion, and Public Reads
Task 3: Block write workflows when namespace is not ACTIVE
Files:
-
Modify:
server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillPublishService.java -
Modify:
server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewService.java -
Modify:
server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/PromotionService.java -
Modify:
server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillPublishServiceTest.java -
Modify:
server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/ReviewPortalControllerTest.java -
Modify:
server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/PromotionPortalControllerTest.java -
Step 1: Add failing tests for frozen and archived namespaces
@Test
void publishFromEntries_rejectsFrozenNamespace() { ... }
@Test
void submitReview_rejectsArchivedNamespace() throws Exception { ... }
@Test
void submitPromotion_rejectsFrozenNamespace() throws Exception { ... }
- Step 2: Run the workflow tests to confirm the gap
Run: cd server && ./mvnw -pl skillhub-domain -Dtest=SkillPublishServiceTest test && ./mvnw -pl skillhub-app -Dtest=ReviewPortalControllerTest,PromotionPortalControllerTest test
Expected: FAIL because the publish/review/promotion flows currently ignore namespace lifecycle state.
- Step 3: Implement the shared ACTIVE-state guard
private void assertNamespaceActive(Namespace namespace, String messageKey) {
if (namespace.getStatus() == NamespaceStatus.FROZEN) {
throw new DomainBadRequestException("error.namespace.frozen", namespace.getSlug());
}
if (namespace.getStatus() == NamespaceStatus.ARCHIVED) {
throw new DomainBadRequestException("error.namespace.archived", namespace.getSlug());
}
}
Apply it before:
-
publish package acceptance
-
review submit/approve/reject/withdraw writes
-
promotion submit writes
-
Step 4: Re-run the workflow tests
Run: cd server && ./mvnw -pl skillhub-domain -Dtest=SkillPublishServiceTest test && ./mvnw -pl skillhub-app -Dtest=ReviewPortalControllerTest,PromotionPortalControllerTest test
Expected: PASS with stable error envelopes for frozen and archived namespaces.
- Step 5: Commit the write-path enforcement
git add server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillPublishService.java \
server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewService.java \
server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/PromotionService.java \
server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillPublishServiceTest.java \
server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/ReviewPortalControllerTest.java \
server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/PromotionPortalControllerTest.java
git commit -m "feat: enforce namespace lifecycle on workflows"
git push origin feature/project-namespace
Task 4: Hide archived namespaces from public skill reads and search
Files:
-
Modify:
server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillQueryService.java -
Modify:
server/skillhub-app/src/main/java/com/iflytek/skillhub/service/SkillSearchAppService.java -
Modify:
server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/SkillControllerTest.java -
Modify:
server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/SkillSearchControllerTest.java -
Step 1: Write the failing public-read tests
@Test
void getSkillDetail_returnsForbiddenOrNotFoundForArchivedNamespaceToAnonymousUser() throws Exception { ... }
@Test
void search_excludesSkillsFromArchivedNamespaces() throws Exception { ... }
- Step 2: Run the public-read tests to verify failure
Run: cd server && ./mvnw -pl skillhub-app -Dtest=SkillControllerTest,SkillSearchControllerTest test
Expected: FAIL because archived namespace state is not filtered in skill detail or search response assembly.
- Step 3: Implement archived visibility filtering
private void assertNamespaceReadable(Namespace namespace, String currentUserId, Map<Long, NamespaceRole> userNsRoles) {
boolean isMember = currentUserId != null && userNsRoles.containsKey(namespace.getId());
if (namespace.getStatus() == NamespaceStatus.ARCHIVED && !isMember) {
throw new DomainForbiddenException("error.namespace.archived");
}
}
In search assembly, drop matched skills whose namespace is archived unless the current user is a member:
.filter(skill -> namespaceVisible(skill.getNamespaceId(), userId, userNsRoles))
- Step 4: Re-run the public-read tests
Run: cd server && ./mvnw -pl skillhub-app -Dtest=SkillControllerTest,SkillSearchControllerTest test
Expected: PASS with archived namespaces hidden from public detail/search while frozen namespaces remain visible.
- Step 5: Commit the public-read visibility changes
git add server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillQueryService.java \
server/skillhub-app/src/main/java/com/iflytek/skillhub/service/SkillSearchAppService.java \
server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/SkillControllerTest.java \
server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/SkillSearchControllerTest.java
git commit -m "feat: hide archived namespaces from public reads"
git push origin feature/project-namespace
Chunk 3: Dashboard Integration
Task 5: Add management DTOs and mutations to the web client
Files:
-
Modify:
web/src/api/types.ts -
Modify:
web/src/api/client.ts -
Modify:
web/src/shared/hooks/use-skill-queries.ts -
Step 1: Add the failing frontend type and query integration
Implement the client shape first so TypeScript fails until all consumers are updated:
export interface ManagedNamespace extends Namespace {
currentUserRole: 'OWNER' | 'ADMIN' | 'MEMBER'
immutable: boolean
canFreeze: boolean
canArchive: boolean
canRestore: boolean
}
- Step 2: Run frontend typecheck
Run: cd web && pnpm typecheck
Expected: FAIL because useMyNamespaces() still returns the old Namespace[] shape and lifecycle mutations are missing.
- Step 3: Implement API helpers and hooks
async function getMyNamespaces(): Promise<ManagedNamespace[]> {
return fetchJson<ManagedNamespace[]>(`${WEB_API_PREFIX}/me/namespaces`)
}
async function mutateNamespaceLifecycle(slug: string, action: 'freeze' | 'unfreeze' | 'archive' | 'restore', reason?: string) {
return fetchJson<ManagedNamespace>(`${WEB_API_PREFIX}/namespaces/${slug}/${action}`, {
method: 'POST',
headers: getCsrfHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify(reason ? { reason } : {}),
})
}
- Step 4: Re-run frontend typecheck
Run: cd web && pnpm typecheck
Expected: PASS for the API layer, even though UI pages still need updates in the next task.
- Step 5: Commit the client-layer changes
git add web/src/api/types.ts web/src/api/client.ts web/src/shared/hooks/use-skill-queries.ts
git commit -m "feat: add namespace governance client hooks"
git push origin feature/project-namespace
Task 6: Enable namespace governance in dashboard pages
Files:
-
Modify:
web/src/pages/dashboard/my-namespaces.tsx -
Modify:
web/src/pages/dashboard/namespace-members.tsx -
Modify:
web/src/pages/dashboard/namespace-reviews.tsx -
Modify:
web/src/features/namespace/namespace-header.tsx -
Modify:
web/src/i18n/locales/zh.json -
Modify:
web/src/i18n/locales/en.json -
Step 1: Update the pages to fail fast on missing state fields
Render status pills and lifecycle buttons from the new managed response shape so lint/typecheck catch any missing branch:
{namespace.status === 'FROZEN' ? <Badge>{t('namespace.statusFrozen')}</Badge> : null}
{namespace.canArchive ? <Button onClick={() => archiveMutation.mutate({ slug: namespace.slug })}>...</Button> : null}
- Step 2: Run frontend validation to capture incomplete UI wiring
Run: cd web && pnpm lint && pnpm typecheck
Expected: FAIL until the pages, translations, and mutation invalidation logic are updated consistently.
- Step 3: Implement the dashboard behavior
Apply these rules:
my-namespaces: show status badge, immutable@globalhint, role-aware lifecycle buttonsnamespace-members: disable add/remove/role actions when status isFROZENorARCHIVEDnamespace-reviews: keep lists visible but disable review actions when namespace is notACTIVEnamespace-header: show namespace status and short governance hint
Suggested UI snippet:
const readOnly = namespace.status !== 'ACTIVE' || namespace.immutable
<Button disabled={readOnly}>{t('members.addMember')}</Button>
{namespace.canFreeze ? <Button onClick={() => freezeMutation.mutate({ slug: namespace.slug })}>{t('namespace.freeze')}</Button> : null}
- Step 4: Re-run frontend validation
Run: cd web && pnpm lint && pnpm typecheck
Expected: PASS with no TypeScript or ESLint regressions.
- Step 5: Commit the dashboard integration
git add web/src/pages/dashboard/my-namespaces.tsx \
web/src/pages/dashboard/namespace-members.tsx \
web/src/pages/dashboard/namespace-reviews.tsx \
web/src/features/namespace/namespace-header.tsx \
web/src/i18n/locales/zh.json \
web/src/i18n/locales/en.json
git commit -m "feat: add namespace governance dashboard"
git push origin feature/project-namespace
Final Verification
- Run backend targeted verification:
cd server
./mvnw -pl skillhub-domain -Dtest=NamespaceGovernanceServiceTest,NamespaceServiceTest,NamespaceMemberServiceTest,SkillPublishServiceTest test
./mvnw -pl skillhub-app -Dtest=NamespacePortalControllerTest,ReviewPortalControllerTest,PromotionPortalControllerTest,SkillControllerTest,SkillSearchControllerTest test
- Run frontend verification:
cd web
pnpm lint
pnpm typecheck
- Run workspace status check:
git status --short
git log --oneline -n 5
- Push final branch state:
git push origin feature/project-namespace
Plan complete and saved to docs/superpowers/plans/2026-03-16-namespace-governance.md. Ready to execute?