mirror of
https://github.com/iflytek/skillhub.git
synced 2026-10-09 03:17:52 +00:00
520 lines
22 KiB
Markdown
520 lines
22 KiB
Markdown
# 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**
|
|
|
|
```java
|
|
@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**
|
|
|
|
```java
|
|
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;
|
|
}
|
|
}
|
|
```
|
|
|
|
```java
|
|
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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```java
|
|
@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**
|
|
|
|
```java
|
|
public record MyNamespaceResponse(
|
|
Long id,
|
|
String slug,
|
|
String displayName,
|
|
NamespaceStatus status,
|
|
NamespaceType type,
|
|
NamespaceRole currentUserRole,
|
|
boolean immutable,
|
|
boolean canFreeze,
|
|
boolean canArchive,
|
|
boolean canRestore
|
|
) {}
|
|
```
|
|
|
|
```java
|
|
@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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```java
|
|
@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**
|
|
|
|
```java
|
|
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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```java
|
|
@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**
|
|
|
|
```java
|
|
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:
|
|
|
|
```java
|
|
.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**
|
|
|
|
```bash
|
|
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:
|
|
|
|
```ts
|
|
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**
|
|
|
|
```ts
|
|
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**
|
|
|
|
```bash
|
|
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:
|
|
|
|
```tsx
|
|
{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 `@global` hint, role-aware lifecycle buttons
|
|
- `namespace-members`: disable add/remove/role actions when status is `FROZEN` or `ARCHIVED`
|
|
- `namespace-reviews`: keep lists visible but disable review actions when namespace is not `ACTIVE`
|
|
- `namespace-header`: show namespace status and short governance hint
|
|
|
|
Suggested UI snippet:
|
|
|
|
```tsx
|
|
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**
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
cd web
|
|
pnpm lint
|
|
pnpm typecheck
|
|
```
|
|
|
|
- [ ] Run workspace status check:
|
|
|
|
```bash
|
|
git status --short
|
|
git log --oneline -n 5
|
|
```
|
|
|
|
- [ ] Push final branch state:
|
|
|
|
```bash
|
|
git push origin feature/project-namespace
|
|
```
|
|
|
|
Plan complete and saved to `docs/superpowers/plans/2026-03-16-namespace-governance.md`. Ready to execute?
|