docs(03): create phase plan for Module System & Domaincheck
4 plans across 3 waves: SDK + registry (W1), Domaincheck module + lazy loading (W2), visual verification (W3). Covers MOD-01..04, DCHK-01..03. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
---
|
||||
phase: 03-module-system-domaincheck
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- packages/module-sdk/package.json
|
||||
- packages/module-sdk/tsconfig.json
|
||||
- packages/module-sdk/src/index.ts
|
||||
- packages/module-sdk/src/types.ts
|
||||
- apps/api/prisma/schema.prisma
|
||||
- apps/api/prisma/migrations/*
|
||||
- apps/api/src/module-registry/module-registry.module.ts
|
||||
- apps/api/src/module-registry/module-registry.service.ts
|
||||
- apps/api/src/module-registry/module-registry.controller.ts
|
||||
- apps/api/src/module-registry/module.guard.ts
|
||||
- apps/api/src/module-registry/dto/activate-module.dto.ts
|
||||
- apps/api/src/app.module.ts
|
||||
- pnpm-workspace.yaml
|
||||
autonomous: true
|
||||
requirements:
|
||||
- MOD-01
|
||||
- MOD-02
|
||||
- MOD-03
|
||||
must_haves:
|
||||
truths:
|
||||
- "Module SDK package exists with versioned interface contract"
|
||||
- "Modules are stored in a database-driven registry"
|
||||
- "Admin can activate/deactivate a module for their tenant without restart"
|
||||
artifacts:
|
||||
- path: "packages/module-sdk/src/types.ts"
|
||||
provides: "TesseraModule interface, ModuleRoute interface"
|
||||
exports: ["TesseraModule", "ModuleRoute", "ModuleCategory"]
|
||||
- path: "apps/api/src/module-registry/module-registry.service.ts"
|
||||
provides: "CRUD for module registry + per-tenant activation"
|
||||
exports: ["ModuleRegistryService"]
|
||||
- path: "apps/api/src/module-registry/module.guard.ts"
|
||||
provides: "Guard checking module activation per tenant"
|
||||
exports: ["ModuleGuard"]
|
||||
key_links:
|
||||
- from: "apps/api/src/module-registry/module-registry.service.ts"
|
||||
to: "prisma.module"
|
||||
via: "Prisma client queries"
|
||||
pattern: "prisma\\.module\\.(find|create|update)"
|
||||
- from: "apps/api/src/module-registry/module.guard.ts"
|
||||
to: "ModuleRegistryService"
|
||||
via: "DI injection, checks activation"
|
||||
pattern: "moduleRegistryService\\.isModuleActive"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Deliver the Module SDK contract and a working module registry with per-tenant activation — the full vertical slice from database schema through API endpoints that an admin can call to register and activate modules.
|
||||
|
||||
Purpose: Establishes the foundation that all modules (starting with Domaincheck in Plan 02) will build on. Per D-06, D-07, D-08.
|
||||
Output: @tessera/module-sdk package, Module + TenantModuleActivation Prisma models, ModuleRegistryModule with CRUD + activation endpoints.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/vicolab/.claude/gsd-core/workflows/execute-plan.md
|
||||
@/home/vicolab/.claude/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/03-module-system-domaincheck/03-CONTEXT.md
|
||||
@.planning/phases/03-module-system-domaincheck/03-RESEARCH.md
|
||||
@apps/api/prisma/schema.prisma
|
||||
@apps/api/src/app.module.ts
|
||||
@packages/shared/src/index.ts
|
||||
@pnpm-workspace.yaml
|
||||
</context>
|
||||
|
||||
## Phase Goal
|
||||
|
||||
**As a** platform admin, **I want to** discover, register, and activate modules dynamically, **so that** tenants get access to new functionality without application restarts.
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Module SDK Package + Prisma Schema + Registry Service</name>
|
||||
<files>
|
||||
packages/module-sdk/package.json,
|
||||
packages/module-sdk/tsconfig.json,
|
||||
packages/module-sdk/src/index.ts,
|
||||
packages/module-sdk/src/types.ts,
|
||||
apps/api/prisma/schema.prisma,
|
||||
pnpm-workspace.yaml
|
||||
</files>
|
||||
<read_first>
|
||||
apps/api/prisma/schema.prisma,
|
||||
packages/shared/src/index.ts,
|
||||
pnpm-workspace.yaml
|
||||
</read_first>
|
||||
<action>
|
||||
1. Create packages/module-sdk/ with package.json (name: @tessera/module-sdk, version: 0.1.0, main: src/index.ts). Add to pnpm-workspace.yaml packages list if not already globbed.
|
||||
|
||||
2. In packages/module-sdk/src/types.ts define (per D-06, D-09):
|
||||
- ModuleCategory type: string literal union or string
|
||||
- ModuleRoute interface: method (GET|POST|PUT|DELETE), path (string), handler (string)
|
||||
- TesseraModule interface: id, name, version, slug, category (ModuleCategory), description (Record of string to string for i18n), icon (optional string), routes (optional ModuleRoute array), component (function returning Promise of React.ComponentType), cardComponent (optional function returning Promise of React.ComponentType)
|
||||
- ModuleManifest type: Omit TesseraModule of component and cardComponent (backend-only metadata)
|
||||
|
||||
3. In packages/module-sdk/src/index.ts re-export everything from types.ts.
|
||||
|
||||
4. Extend apps/api/prisma/schema.prisma with two models (per D-07):
|
||||
- model Module: id (uuid), slug (unique string), name (string), version (string), category (string), description (Json), icon (optional string), isSystem (boolean default false), createdAt, updatedAt, activations relation to TenantModuleActivation[]
|
||||
- model TenantModuleActivation: id (uuid), tenantId (string), moduleId (string), isActive (boolean default true), activatedAt (DateTime default now), module relation. Unique constraint on [tenantId, moduleId], index on [tenantId].
|
||||
|
||||
5. Run prisma migrate dev with name "add-module-registry" to create the migration.
|
||||
|
||||
6. Run pnpm install from monorepo root to link the new package.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/vicolab/projects/tessera-ctl && pnpm exec prisma migrate status --schema=apps/api/prisma/schema.prisma 2>&1 | grep -q "Database schema is up to date" && node -e "const sdk = require('./packages/module-sdk/src/index.ts'); console.log('SDK exports:', Object.keys(sdk))" 2>/dev/null || pnpm exec tsx -e "import { TesseraModule } from '@tessera/module-sdk'; console.log('SDK type-checks')"</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- packages/module-sdk/src/types.ts exports TesseraModule, ModuleRoute, ModuleManifest, ModuleCategory
|
||||
- Prisma schema contains model Module and model TenantModuleActivation with correct fields
|
||||
- Migration applied successfully to PostgreSQL
|
||||
- pnpm-workspace.yaml includes packages/module-sdk path
|
||||
</acceptance_criteria>
|
||||
<done>SDK package exists with typed interfaces, Prisma models for module registry are migrated and queryable</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: ModuleRegistry NestJS Module with CRUD + Activation Endpoints</name>
|
||||
<files>
|
||||
apps/api/src/module-registry/module-registry.module.ts,
|
||||
apps/api/src/module-registry/module-registry.service.ts,
|
||||
apps/api/src/module-registry/module-registry.controller.ts,
|
||||
apps/api/src/module-registry/module.guard.ts,
|
||||
apps/api/src/module-registry/dto/activate-module.dto.ts,
|
||||
apps/api/src/app.module.ts
|
||||
</files>
|
||||
<read_first>
|
||||
apps/api/src/app.module.ts,
|
||||
apps/api/src/tenant/tenant.middleware.ts,
|
||||
apps/api/src/auth/guards/jwt-auth.guard.ts,
|
||||
apps/api/src/auth/guards/roles.guard.ts,
|
||||
apps/api/src/prisma/prisma.service.ts
|
||||
</read_first>
|
||||
<action>
|
||||
1. Create apps/api/src/module-registry/dto/activate-module.dto.ts:
|
||||
- ActivateModuleDto: moduleId (string, required)
|
||||
- Class-validator decorators: IsUUID for moduleId
|
||||
|
||||
2. Create apps/api/src/module-registry/module-registry.service.ts:
|
||||
- Inject PrismaService
|
||||
- findAll(): returns all Module records
|
||||
- findBySlug(slug: string): finds module by slug
|
||||
- findActiveForTenant(tenantId: string): returns modules where TenantModuleActivation.isActive=true for given tenant
|
||||
- activateForTenant(tenantId: string, moduleId: string): upsert TenantModuleActivation with isActive=true (per D-08)
|
||||
- deactivateForTenant(tenantId: string, moduleId: string): update TenantModuleActivation set isActive=false (per D-08)
|
||||
- isModuleActive(tenantId: string, moduleSlug: string): boolean check for guard use
|
||||
- seedModule(manifest: object with slug, name, version, category, description, icon, isSystem): upsert Module record by slug
|
||||
|
||||
3. Create apps/api/src/module-registry/module.guard.ts:
|
||||
- Injectable CanActivate guard
|
||||
- Extract moduleSlug from request route params or a custom decorator
|
||||
- Get tenantId from request (set by TenantMiddleware)
|
||||
- Call moduleRegistryService.isModuleActive(tenantId, moduleSlug)
|
||||
- Throw ForbiddenException if module not active for tenant
|
||||
- Export UseModule decorator: SetMetadata('moduleSlug', slug) + UseGuards(ModuleGuard)
|
||||
|
||||
4. Create apps/api/src/module-registry/module-registry.controller.ts:
|
||||
- GET /modules — list all modules (any authenticated user)
|
||||
- GET /modules/active — list active modules for current tenant (tenantId from request)
|
||||
- POST /modules/:moduleId/activate — Admin only (@Roles(Role.ADMIN, Role.SUPER_ADMIN)), calls activateForTenant
|
||||
- POST /modules/:moduleId/deactivate — Admin only, calls deactivateForTenant
|
||||
|
||||
5. Create apps/api/src/module-registry/module-registry.module.ts:
|
||||
- imports: PrismaModule
|
||||
- providers: ModuleRegistryService, ModuleGuard
|
||||
- controllers: ModuleRegistryController
|
||||
- exports: ModuleRegistryService, ModuleGuard
|
||||
|
||||
6. Register ModuleRegistryModule in apps/api/src/app.module.ts imports array.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/vicolab/projects/tessera-ctl && pnpm --filter api build 2>&1 | tail -5 && curl -s http://localhost:3001/modules 2>/dev/null | head -1 || echo "Build check passed"</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- apps/api builds without errors with ModuleRegistryModule registered
|
||||
- GET /modules endpoint exists and returns array (empty initially)
|
||||
- GET /modules/active returns only tenant-activated modules
|
||||
- POST /modules/:id/activate requires ADMIN role and creates activation record
|
||||
- POST /modules/:id/deactivate sets isActive=false without deleting the record
|
||||
- ModuleGuard is exported and usable by downstream module controllers
|
||||
</acceptance_criteria>
|
||||
<done>Admin can list, activate, and deactivate modules via API. ModuleGuard available for module-specific endpoints to check activation.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| client -> API | Untrusted input for module activation/deactivation |
|
||||
| module guard | Ensures tenant cannot access modules not activated for them |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-03-01 | Spoofing | module activation endpoint | mitigate | @Roles(ADMIN) guard ensures only admins activate modules |
|
||||
| T-03-02 | Tampering | moduleId param | mitigate | IsUUID validator on DTO, Prisma parameterized queries |
|
||||
| T-03-03 | Information Disclosure | GET /modules | accept | Module catalog is non-sensitive metadata, all authenticated users may see it |
|
||||
| T-03-04 | Elevation of Privilege | module guard bypass | mitigate | ModuleGuard checks tenantId from JWT (not user-supplied), ForbiddenException on inactive |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- pnpm --filter api build succeeds
|
||||
- Prisma migration applied, Module and TenantModuleActivation tables exist
|
||||
- @tessera/module-sdk package resolves from apps/api
|
||||
- API endpoints respond correctly to authenticated requests
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Module SDK defines TesseraModule interface with all fields from RESEARCH.md
|
||||
- Database has Module + TenantModuleActivation tables
|
||||
- Admin can activate/deactivate modules per tenant via REST API without restart (D-08)
|
||||
- ModuleGuard prevents unauthorized access to inactive modules
|
||||
</success_criteria>
|
||||
|
||||
## Artifacts this phase produces
|
||||
|
||||
| Symbol | Location | Type |
|
||||
|--------|----------|------|
|
||||
| TesseraModule | packages/module-sdk/src/types.ts | interface |
|
||||
| ModuleRoute | packages/module-sdk/src/types.ts | interface |
|
||||
| ModuleManifest | packages/module-sdk/src/types.ts | type |
|
||||
| ModuleCategory | packages/module-sdk/src/types.ts | type |
|
||||
| ModuleRegistryService | apps/api/src/module-registry/module-registry.service.ts | class |
|
||||
| ModuleRegistryController | apps/api/src/module-registry/module-registry.controller.ts | class |
|
||||
| ModuleGuard | apps/api/src/module-registry/module.guard.ts | class |
|
||||
| UseModule | apps/api/src/module-registry/module.guard.ts | decorator |
|
||||
| Module (Prisma) | apps/api/prisma/schema.prisma | model |
|
||||
| TenantModuleActivation (Prisma) | apps/api/prisma/schema.prisma | model |
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/03-module-system-domaincheck/03-01-SUMMARY.md` when done
|
||||
</output>
|
||||
@@ -0,0 +1,273 @@
|
||||
---
|
||||
phase: 03-module-system-domaincheck
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["03-01"]
|
||||
files_modified:
|
||||
- apps/api/src/domaincheck/domaincheck.module.ts
|
||||
- apps/api/src/domaincheck/domaincheck.service.ts
|
||||
- apps/api/src/domaincheck/domaincheck.controller.ts
|
||||
- apps/api/src/domaincheck/dto/check-domain.dto.ts
|
||||
- apps/api/src/domaincheck/domaincheck.seed.ts
|
||||
- apps/api/src/app.module.ts
|
||||
- apps/web/src/app/(portal)/modules/domaincheck/page.tsx
|
||||
- apps/web/src/app/(portal)/modules/domaincheck/components/DomainInput.tsx
|
||||
- apps/web/src/app/(portal)/modules/domaincheck/components/ResultList.tsx
|
||||
- apps/web/src/app/(portal)/modules/domaincheck/actions.ts
|
||||
- apps/web/messages/de.json
|
||||
- apps/web/messages/en.json
|
||||
autonomous: true
|
||||
requirements:
|
||||
- DCHK-01
|
||||
- DCHK-02
|
||||
- DCHK-03
|
||||
must_haves:
|
||||
truths:
|
||||
- "User can open the Domaincheck module and see an input field"
|
||||
- "User enters a domain name and sees TLD variant results"
|
||||
- "Results show green (available) or red (registered) status per TLD"
|
||||
artifacts:
|
||||
- path: "apps/api/src/domaincheck/domaincheck.service.ts"
|
||||
provides: "DNS-based domain availability checking"
|
||||
exports: ["DomaincheckService"]
|
||||
- path: "apps/api/src/domaincheck/domaincheck.controller.ts"
|
||||
provides: "POST /modules/domaincheck/check endpoint"
|
||||
exports: ["DomaincheckController"]
|
||||
- path: "apps/web/src/app/(portal)/modules/domaincheck/page.tsx"
|
||||
provides: "Domaincheck UI page within portal"
|
||||
key_links:
|
||||
- from: "apps/web/src/app/(portal)/modules/domaincheck/actions.ts"
|
||||
to: "/modules/domaincheck/check"
|
||||
via: "fetch POST to API"
|
||||
pattern: "fetch.*modules/domaincheck/check"
|
||||
- from: "apps/api/src/domaincheck/domaincheck.service.ts"
|
||||
to: "node:dns/promises"
|
||||
via: "dns.resolve for availability check"
|
||||
pattern: "dns\\.resolve"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Deliver a working Domaincheck module end-to-end — user enters a domain name, backend checks TLD variants via DNS, frontend shows green/red results. This is the proof-of-concept module validating the entire module system.
|
||||
|
||||
Purpose: Per D-01, D-02, D-03 — validates the module system with a real, interactive module. Users get immediate value.
|
||||
Output: DomaincheckModule (NestJS), domaincheck frontend page, module seed data.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/vicolab/.claude/gsd-core/workflows/execute-plan.md
|
||||
@/home/vicolab/.claude/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/phases/03-module-system-domaincheck/03-CONTEXT.md
|
||||
@.planning/phases/03-module-system-domaincheck/03-RESEARCH.md
|
||||
@.planning/phases/03-module-system-domaincheck/03-01-SUMMARY.md
|
||||
@apps/api/src/module-registry/module-registry.service.ts
|
||||
@apps/api/src/module-registry/module.guard.ts
|
||||
@apps/web/src/app/(portal)/layout.tsx
|
||||
@apps/web/messages/de.json
|
||||
@apps/web/messages/en.json
|
||||
</context>
|
||||
|
||||
## Phase Goal
|
||||
|
||||
**As a** user, **I want to** enter a domain name and see which TLD variants are available or registered, **so that** I can quickly check domain availability without leaving the platform.
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Domaincheck Backend — DNS Service + API Endpoint + Module Seed</name>
|
||||
<files>
|
||||
apps/api/src/domaincheck/domaincheck.module.ts,
|
||||
apps/api/src/domaincheck/domaincheck.service.ts,
|
||||
apps/api/src/domaincheck/domaincheck.controller.ts,
|
||||
apps/api/src/domaincheck/dto/check-domain.dto.ts,
|
||||
apps/api/src/domaincheck/domaincheck.seed.ts,
|
||||
apps/api/src/app.module.ts
|
||||
</files>
|
||||
<read_first>
|
||||
apps/api/src/module-registry/module-registry.service.ts,
|
||||
apps/api/src/module-registry/module.guard.ts,
|
||||
apps/api/src/app.module.ts
|
||||
</read_first>
|
||||
<action>
|
||||
1. Create apps/api/src/domaincheck/dto/check-domain.dto.ts:
|
||||
- CheckDomainDto: domain (string, @IsString, @IsNotEmpty, @Matches regex for valid domain label — alphanumeric + hyphens, no dots)
|
||||
- Optional tlds (string array) — if omitted, use defaults
|
||||
|
||||
2. Create apps/api/src/domaincheck/domaincheck.service.ts:
|
||||
- DEFAULT_TLDS constant: ['de', 'com', 'net', 'org'] (per D-03 configurable, these are defaults)
|
||||
- Method checkDomain(domain: string, tlds?: string[]): Promise of array of { tld: string, fqdn: string, status: 'available' | 'registered' }
|
||||
- Implementation: use node:dns/promises — dns.resolve(fqdn) for each TLD variant
|
||||
- If resolves (any record) -> status = 'registered'
|
||||
- If throws with code ENOTFOUND or ENODATA -> status = 'available'
|
||||
- If throws with other error -> status = 'registered' (safe default)
|
||||
- Use Promise.allSettled for parallel lookups with 5-second timeout per lookup
|
||||
- Per D-02: system checks all TLD variants automatically from the base domain name
|
||||
|
||||
3. Create apps/api/src/domaincheck/domaincheck.controller.ts:
|
||||
- POST /modules/domaincheck/check
|
||||
- Protected by UseModule('domaincheck') decorator from module.guard.ts
|
||||
- Accepts CheckDomainDto body
|
||||
- Returns { domain: string, results: array of { tld, fqdn, status } }
|
||||
|
||||
4. Create apps/api/src/domaincheck/domaincheck.module.ts:
|
||||
- imports: PrismaModule, ModuleRegistryModule (for guard access)
|
||||
- providers: DomaincheckService
|
||||
- controllers: DomaincheckController
|
||||
|
||||
5. Create apps/api/src/domaincheck/domaincheck.seed.ts:
|
||||
- Export async function seedDomaincheckModule(moduleRegistryService: ModuleRegistryService)
|
||||
- Calls moduleRegistryService.seedModule with: slug='domaincheck', name='Domaincheck', version='1.0.0', category='domain-tools' (per D-09), description={ de: 'Domain-Verfuegbarkeit pruefen', en: 'Check domain availability' }, isSystem=true
|
||||
|
||||
6. Register DomaincheckModule in apps/api/src/app.module.ts imports.
|
||||
|
||||
7. Call the seed function from an OnModuleInit hook in DomaincheckModule (or add to an existing seeding mechanism) so the domaincheck module record exists in the database on startup.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/vicolab/projects/tessera-ctl && pnpm --filter api build 2>&1 | tail -3</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- API build succeeds with DomaincheckModule
|
||||
- POST /modules/domaincheck/check accepts { domain: "google" } and returns results array with tld, fqdn, status for each default TLD
|
||||
- DNS check correctly identifies google.com as registered
|
||||
- Module seed creates a "domaincheck" record in Module table on startup
|
||||
- UseModule('domaincheck') guard protects the endpoint — returns 403 if module not activated for tenant
|
||||
</acceptance_criteria>
|
||||
<done>Domaincheck API endpoint works end-to-end: accepts domain, checks DNS for TLD variants, returns structured results</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Domaincheck Frontend — Input Form + Results Display</name>
|
||||
<files>
|
||||
apps/web/src/app/(portal)/modules/domaincheck/page.tsx,
|
||||
apps/web/src/app/(portal)/modules/domaincheck/components/DomainInput.tsx,
|
||||
apps/web/src/app/(portal)/modules/domaincheck/components/ResultList.tsx,
|
||||
apps/web/src/app/(portal)/modules/domaincheck/actions.ts,
|
||||
apps/web/messages/de.json,
|
||||
apps/web/messages/en.json
|
||||
</files>
|
||||
<read_first>
|
||||
apps/web/src/app/(portal)/page.tsx,
|
||||
apps/web/src/app/(portal)/layout.tsx,
|
||||
apps/web/messages/de.json,
|
||||
apps/web/messages/en.json
|
||||
</read_first>
|
||||
<action>
|
||||
1. Add i18n keys to apps/web/messages/de.json under "domaincheck" namespace:
|
||||
- title: "Domaincheck"
|
||||
- description: "Pruefe ob eine Domain verfuegbar ist"
|
||||
- inputPlaceholder: "Domain eingeben (z.B. beispiel)"
|
||||
- checkButton: "Pruefen"
|
||||
- statusAvailable: "Verfuegbar"
|
||||
- statusRegistered: "Registriert"
|
||||
- noResults: "Gib einen Domain-Namen ein um die Verfuegbarkeit zu pruefen"
|
||||
- checking: "Pruefe..."
|
||||
- error: "Fehler bei der Pruefung"
|
||||
|
||||
2. Add same keys in English to apps/web/messages/en.json under "domaincheck" namespace:
|
||||
- title: "Domain Check"
|
||||
- description: "Check if a domain is available"
|
||||
- inputPlaceholder: "Enter domain (e.g. example)"
|
||||
- checkButton: "Check"
|
||||
- statusAvailable: "Available"
|
||||
- statusRegistered: "Registered"
|
||||
- noResults: "Enter a domain name to check availability"
|
||||
- checking: "Checking..."
|
||||
- error: "Error checking domain"
|
||||
|
||||
3. Create apps/web/src/app/(portal)/modules/domaincheck/actions.ts:
|
||||
- Server action or client-side fetch function checkDomainAction(domain: string): fetches POST to API_URL/modules/domaincheck/check with { domain }, passes auth cookie/token, returns typed results array
|
||||
|
||||
4. Create apps/web/src/app/(portal)/modules/domaincheck/components/DomainInput.tsx:
|
||||
- Client component ("use client")
|
||||
- Input field (text) with placeholder from i18n
|
||||
- Submit button
|
||||
- Calls onSubmit prop with the domain value
|
||||
- Shows loading state while checking (per i18n "checking" key)
|
||||
|
||||
5. Create apps/web/src/app/(portal)/modules/domaincheck/components/ResultList.tsx:
|
||||
- Client component
|
||||
- Receives results array prop: { tld, fqdn, status }[]
|
||||
- Renders list/table of results
|
||||
- Per D-01: green badge/pill for "available", red badge/pill for "registered"
|
||||
- Per D-02: shows all TLD variants with their status
|
||||
- Empty state shows "noResults" message
|
||||
|
||||
6. Create apps/web/src/app/(portal)/modules/domaincheck/page.tsx:
|
||||
- Page component within (portal) route group (gets AppShell layout)
|
||||
- Uses DomainInput and ResultList components
|
||||
- Manages state with useState: domain input, results array, loading, error
|
||||
- On submit: calls checkDomainAction, updates results
|
||||
- Layout: title at top, input below, results list below input (per specifics in CONTEXT.md)
|
||||
- Use Tailwind classes consistent with existing portal pages
|
||||
- Use shadcn/ui Card component for the module panel (per D-05a)
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/vicolab/projects/tessera-ctl && pnpm --filter web build 2>&1 | tail -5</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- Web app builds successfully with the new domaincheck route
|
||||
- /modules/domaincheck page renders within the portal AppShell
|
||||
- Input field accepts text, submit triggers API call
|
||||
- Results display with color-coded status badges (green=available, red=registered)
|
||||
- All visible strings use next-intl translations (no hardcoded text)
|
||||
- Works in both DE and EN locale
|
||||
</acceptance_criteria>
|
||||
<done>User can navigate to /modules/domaincheck, enter a domain name, and see color-coded availability results for .de, .com, .net, .org TLD variants</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| client -> POST /modules/domaincheck/check | User-supplied domain string |
|
||||
| API -> DNS | External DNS resolution (network) |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-03-05 | Tampering | domain input | mitigate | Regex validation in DTO — only alphanumeric + hyphens, max 63 chars, no dots allowed |
|
||||
| T-03-06 | Denial of Service | DNS lookups | mitigate | 5-second timeout per lookup, max 10 TLDs per request, Promise.allSettled prevents hanging |
|
||||
| T-03-07 | Information Disclosure | DNS results | accept | Domain availability is public information, no sensitive data exposed |
|
||||
| T-03-08 | Spoofing | module access | mitigate | UseModule('domaincheck') guard + JwtAuthGuard ensure only authenticated users with activated module can access |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- pnpm --filter api build && pnpm --filter web build both succeed
|
||||
- POST /modules/domaincheck/check returns correct results for known domains
|
||||
- Frontend page renders and submits successfully
|
||||
- Module guard blocks access when module not activated for tenant
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- User can open /modules/domaincheck in browser (DCHK-01)
|
||||
- System checks TLD variants via DNS and returns status (DCHK-02)
|
||||
- Results show clear green/red status indicators (DCHK-03, D-01)
|
||||
- TLD list uses defaults ['de','com','net','org'] per D-03
|
||||
</success_criteria>
|
||||
|
||||
## Artifacts this phase produces
|
||||
|
||||
| Symbol | Location | Type |
|
||||
|--------|----------|------|
|
||||
| DomaincheckService | apps/api/src/domaincheck/domaincheck.service.ts | class |
|
||||
| DomaincheckController | apps/api/src/domaincheck/domaincheck.controller.ts | class |
|
||||
| DomaincheckModule | apps/api/src/domaincheck/domaincheck.module.ts | NestJS module |
|
||||
| CheckDomainDto | apps/api/src/domaincheck/dto/check-domain.dto.ts | class |
|
||||
| seedDomaincheckModule | apps/api/src/domaincheck/domaincheck.seed.ts | function |
|
||||
| DomaincheckPage | apps/web/src/app/(portal)/modules/domaincheck/page.tsx | React component |
|
||||
| DomainInput | apps/web/src/app/(portal)/modules/domaincheck/components/DomainInput.tsx | React component |
|
||||
| ResultList | apps/web/src/app/(portal)/modules/domaincheck/components/ResultList.tsx | React component |
|
||||
| checkDomainAction | apps/web/src/app/(portal)/modules/domaincheck/actions.ts | function |
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/03-module-system-domaincheck/03-02-SUMMARY.md` when done
|
||||
</output>
|
||||
@@ -0,0 +1,211 @@
|
||||
---
|
||||
phase: 03-module-system-domaincheck
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["03-01"]
|
||||
files_modified:
|
||||
- apps/web/src/app/(portal)/modules/[category]/page.tsx
|
||||
- apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx
|
||||
- apps/web/src/app/(portal)/modules/[category]/components/ModuleCard.tsx
|
||||
- apps/web/src/lib/module-loader.ts
|
||||
- apps/web/src/lib/api.ts
|
||||
- apps/web/messages/de.json
|
||||
- apps/web/messages/en.json
|
||||
autonomous: true
|
||||
requirements:
|
||||
- MOD-04
|
||||
must_haves:
|
||||
truths:
|
||||
- "Module UIs load on demand via dynamic imports — inactive modules are never bundled"
|
||||
- "Category page shows module cards in a grid layout"
|
||||
- "Module can be opened in expanded view from category page"
|
||||
artifacts:
|
||||
- path: "apps/web/src/lib/module-loader.ts"
|
||||
provides: "Dynamic import registry mapping slugs to lazy components"
|
||||
exports: ["loadModuleComponent", "loadModuleCard", "MODULE_REGISTRY"]
|
||||
- path: "apps/web/src/app/(portal)/modules/[category]/page.tsx"
|
||||
provides: "Category page showing activated module cards"
|
||||
- path: "apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx"
|
||||
provides: "Expanded module view using lazy-loaded component"
|
||||
key_links:
|
||||
- from: "apps/web/src/lib/module-loader.ts"
|
||||
to: "next/dynamic"
|
||||
via: "dynamic import with ssr:false"
|
||||
pattern: "dynamic.*import"
|
||||
- from: "apps/web/src/app/(portal)/modules/[category]/page.tsx"
|
||||
to: "/modules/active"
|
||||
via: "API fetch for tenant's active modules"
|
||||
pattern: "fetch.*modules/active"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Deliver lazy-loaded module UI rendering — category pages show module cards from the registry, and modules load on demand via Next.js dynamic imports. No bundle bloat from inactive modules.
|
||||
|
||||
Purpose: Per D-04, D-05a, D-05b and MOD-04 — modules appear as cards within category pages and can expand to full view, all lazy-loaded.
|
||||
Output: Module loader utility, category page with dynamic routing, module card component.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/vicolab/.claude/gsd-core/workflows/execute-plan.md
|
||||
@/home/vicolab/.claude/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/phases/03-module-system-domaincheck/03-CONTEXT.md
|
||||
@.planning/phases/03-module-system-domaincheck/03-RESEARCH.md
|
||||
@.planning/phases/03-module-system-domaincheck/03-01-SUMMARY.md
|
||||
@apps/web/src/app/(portal)/layout.tsx
|
||||
@apps/web/src/app/(portal)/page.tsx
|
||||
</context>
|
||||
|
||||
## Phase Goal
|
||||
|
||||
**As a** user, **I want to** browse module categories and open modules without page bloat, **so that** only active modules load their UI code on demand.
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Module Loader Utility + Category Page with Lazy Cards</name>
|
||||
<files>
|
||||
apps/web/src/lib/module-loader.ts,
|
||||
apps/web/src/app/(portal)/modules/[category]/page.tsx,
|
||||
apps/web/src/app/(portal)/modules/[category]/components/ModuleCard.tsx,
|
||||
apps/web/messages/de.json,
|
||||
apps/web/messages/en.json
|
||||
</files>
|
||||
<read_first>
|
||||
apps/web/src/app/(portal)/layout.tsx,
|
||||
apps/web/src/app/(portal)/page.tsx,
|
||||
apps/web/messages/de.json
|
||||
</read_first>
|
||||
<action>
|
||||
1. Create apps/web/src/lib/module-loader.ts:
|
||||
- MODULE_REGISTRY: Record mapping module slugs to { component: () => import(...), cardComponent: () => import(...) }
|
||||
- For domaincheck: component maps to dynamic(() => import('@/app/(portal)/modules/domaincheck/page'), { ssr: false }), cardComponent maps to a DomaincheckCard component
|
||||
- Export loadModuleComponent(slug: string): returns the dynamic component or null if not in registry
|
||||
- Export loadModuleCard(slug: string): returns the card component or a generic fallback card
|
||||
- This is the lazy loading mechanism — components are only imported when rendered (per MOD-04)
|
||||
|
||||
2. Add i18n keys to both message files under "modules" namespace:
|
||||
- DE: categoryTitle: "Module: {category}", openModule: "Oeffnen", noModules: "Keine aktiven Module in dieser Kategorie"
|
||||
- EN: categoryTitle: "Modules: {category}", openModule: "Open", noModules: "No active modules in this category"
|
||||
|
||||
3. Create apps/web/src/app/(portal)/modules/[category]/components/ModuleCard.tsx:
|
||||
- Client component ("use client")
|
||||
- Props: module object (name, slug, description, icon, category)
|
||||
- Renders a shadcn/ui Card with: icon area, module name, description (from current locale), "Open" link to /modules/{category}/{slug}
|
||||
- Per D-05a: uniform card appearance, compact, looks good even with single module
|
||||
- Uses Tailwind for consistent spacing and theming
|
||||
|
||||
4. Create apps/web/src/app/(portal)/modules/[category]/page.tsx:
|
||||
- Server component that receives params.category
|
||||
- Fetches active modules for current tenant from API (GET /modules/active), filters by category matching params.category
|
||||
- Renders a responsive grid of ModuleCard components (grid-cols-1 sm:grid-cols-2 lg:grid-cols-3)
|
||||
- Empty state shows "noModules" i18n message
|
||||
- Title shows category name formatted (capitalize, replace hyphens with spaces)
|
||||
- Per D-04: category page groups all modules of that category
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/vicolab/projects/tessera-ctl && pnpm --filter web build 2>&1 | tail -5</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- Web build succeeds with dynamic route [category]
|
||||
- /modules/domain-tools page fetches and displays active modules filtered by "domain-tools" category
|
||||
- ModuleCard renders module info with link to expanded view
|
||||
- module-loader.ts maps 'domaincheck' slug to lazy-imported components
|
||||
- Inactive modules never trigger an import (verified by MODULE_REGISTRY only containing active slug mappings at build time)
|
||||
</acceptance_criteria>
|
||||
<done>Category page at /modules/domain-tools shows domaincheck as a card, lazy loading is wired via module-loader registry</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Expanded Module View with Dynamic Routing</name>
|
||||
<files>
|
||||
apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx,
|
||||
apps/web/src/lib/api.ts
|
||||
</files>
|
||||
<read_first>
|
||||
apps/web/src/lib/module-loader.ts,
|
||||
apps/web/src/app/(portal)/modules/[category]/page.tsx
|
||||
</read_first>
|
||||
<action>
|
||||
1. Create or extend apps/web/src/lib/api.ts (if not existing):
|
||||
- Export getActiveModules(cookie: string): fetches GET /modules/active with auth cookie, returns module array
|
||||
- Export getModuleBySlug(slug: string, cookie: string): fetches GET /modules, finds by slug, returns module or null
|
||||
- Base URL from NEXT_PUBLIC_API_URL env var
|
||||
|
||||
2. Create apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx:
|
||||
- Per D-05b: expanded/full view when user opens a specific module
|
||||
- Server component wrapper that validates moduleSlug exists in registry
|
||||
- Uses loadModuleComponent(params.moduleSlug) from module-loader.ts to get the lazy component
|
||||
- If module not found in registry: show 404/not-found state
|
||||
- If found: render the dynamically imported component within a container
|
||||
- The loaded component for 'domaincheck' is the DomaincheckPage from Plan 02
|
||||
- Wrapped in shadcn/ui Card with back-link to category page
|
||||
|
||||
3. Ensure the domaincheck page.tsx from Plan 02 can function both as standalone (/modules/domaincheck direct access) AND as the loaded component in expanded view. The [moduleSlug] route provides the canonical expanded path per D-05b: /modules/domain-tools/domaincheck
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/vicolab/projects/tessera-ctl && pnpm --filter web build 2>&1 | tail -5</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- /modules/domain-tools/domaincheck route renders the domaincheck module component
|
||||
- Module component loads lazily (not in initial bundle of category page)
|
||||
- Unknown module slugs show appropriate not-found state
|
||||
- Back-link navigates to category page /modules/domain-tools
|
||||
</acceptance_criteria>
|
||||
<done>User can navigate from category card grid to expanded module view. Module UI code loads on demand only when opened (MOD-04).</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| URL params -> module loader | User can craft arbitrary slugs in URL |
|
||||
| API -> frontend | Module list from API determines what renders |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-03-09 | Tampering | [moduleSlug] param | mitigate | Only load from MODULE_REGISTRY whitelist — arbitrary slugs get not-found, no arbitrary imports |
|
||||
| T-03-10 | Elevation of Privilege | category page | mitigate | API filters by tenant's active modules — inactive modules never returned, cards never rendered |
|
||||
| T-03-11 | Information Disclosure | module metadata | accept | Module names/descriptions are non-sensitive catalog data |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- pnpm --filter web build succeeds
|
||||
- /modules/domain-tools renders category page with module cards
|
||||
- /modules/domain-tools/domaincheck loads domaincheck UI lazily
|
||||
- Bundle analysis confirms domaincheck code not in category page chunk
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Module UIs load on demand via next/dynamic with ssr:false (MOD-04)
|
||||
- Category page displays modules as cards per D-04, D-05a
|
||||
- Expanded view available per D-05b
|
||||
- No bundle impact from inactive/unregistered modules
|
||||
</success_criteria>
|
||||
|
||||
## Artifacts this phase produces
|
||||
|
||||
| Symbol | Location | Type |
|
||||
|--------|----------|------|
|
||||
| MODULE_REGISTRY | apps/web/src/lib/module-loader.ts | const |
|
||||
| loadModuleComponent | apps/web/src/lib/module-loader.ts | function |
|
||||
| loadModuleCard | apps/web/src/lib/module-loader.ts | function |
|
||||
| CategoryPage | apps/web/src/app/(portal)/modules/[category]/page.tsx | React component |
|
||||
| ModuleCard | apps/web/src/app/(portal)/modules/[category]/components/ModuleCard.tsx | React component |
|
||||
| ExpandedModulePage | apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx | React component |
|
||||
| getActiveModules | apps/web/src/lib/api.ts | function |
|
||||
| getModuleBySlug | apps/web/src/lib/api.ts | function |
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/03-module-system-domaincheck/03-03-SUMMARY.md` when done
|
||||
</output>
|
||||
@@ -0,0 +1,130 @@
|
||||
---
|
||||
phase: 03-module-system-domaincheck
|
||||
plan: 04
|
||||
type: execute
|
||||
wave: 3
|
||||
depends_on: ["03-02", "03-03"]
|
||||
files_modified: []
|
||||
autonomous: false
|
||||
requirements:
|
||||
- MOD-01
|
||||
- MOD-02
|
||||
- MOD-03
|
||||
- MOD-04
|
||||
- DCHK-01
|
||||
- DCHK-02
|
||||
- DCHK-03
|
||||
must_haves:
|
||||
truths:
|
||||
- "All phase success criteria verified by human"
|
||||
- "Module system works end-to-end with domaincheck as proof"
|
||||
artifacts: []
|
||||
key_links: []
|
||||
---
|
||||
|
||||
<objective>
|
||||
Human verification that the complete module system and domaincheck module work as intended — admin activation, lazy loading, domain checking with colored results.
|
||||
|
||||
Purpose: Final validation before marking Phase 3 complete.
|
||||
Output: Human approval or issues list for gap closure.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/vicolab/.claude/gsd-core/workflows/execute-plan.md
|
||||
@/home/vicolab/.claude/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/phases/03-module-system-domaincheck/03-01-SUMMARY.md
|
||||
@.planning/phases/03-module-system-domaincheck/03-02-SUMMARY.md
|
||||
@.planning/phases/03-module-system-domaincheck/03-03-SUMMARY.md
|
||||
</context>
|
||||
|
||||
## Phase Goal
|
||||
|
||||
**As a** platform admin, **I want to** discover, register, and activate modules dynamically, **so that** tenants get access to new functionality without application restarts.
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<name>Task 1: Verify Module System End-to-End</name>
|
||||
<what-built>
|
||||
Complete module system with SDK, database registry, per-tenant activation, lazy-loaded frontend, and working Domaincheck module.
|
||||
</what-built>
|
||||
<how-to-verify>
|
||||
1. Start the Docker Compose stack: docker compose up -d
|
||||
2. Log in as Admin user
|
||||
3. Verify module registry API:
|
||||
- GET http://localhost:3001/modules — should show domaincheck module in list
|
||||
- GET http://localhost:3001/modules/active — should show activated modules for tenant
|
||||
4. If domaincheck not yet activated, activate it:
|
||||
- POST http://localhost:3001/modules/{domaincheck-id}/activate
|
||||
5. Navigate to http://localhost:3000/modules/domain-tools
|
||||
- Should see category page with Domaincheck card (icon, name, description)
|
||||
- Card should look good as a standalone module in the grid (D-05a)
|
||||
6. Click "Open" on the Domaincheck card (or navigate to /modules/domain-tools/domaincheck)
|
||||
- Should see expanded view with input field (D-05b)
|
||||
7. Enter "google" in the domain input and click "Check"
|
||||
- Should see results for google.de, google.com, google.net, google.org
|
||||
- google.com and google.de should show RED "Registered" badge (D-01)
|
||||
8. Enter a clearly available domain (e.g. "xyzabc123randomtest") and check
|
||||
- Some TLDs should show GREEN "Available" badge
|
||||
9. Verify i18n: switch language to English
|
||||
- All labels should change to English equivalents
|
||||
10. Verify theme: switch to dark mode
|
||||
- Module UI should respect dark theme
|
||||
11. Deactivate the module:
|
||||
- POST http://localhost:3001/modules/{domaincheck-id}/deactivate
|
||||
- Refresh /modules/domain-tools — domaincheck card should disappear
|
||||
- Direct access to /modules/domain-tools/domaincheck should be blocked (403 from API)
|
||||
</how-to-verify>
|
||||
<read_first>
|
||||
.planning/phases/03-module-system-domaincheck/03-01-SUMMARY.md,
|
||||
.planning/phases/03-module-system-domaincheck/03-02-SUMMARY.md,
|
||||
.planning/phases/03-module-system-domaincheck/03-03-SUMMARY.md
|
||||
</read_first>
|
||||
<acceptance_criteria>
|
||||
- Module registry shows domaincheck in database
|
||||
- Admin can activate/deactivate without restart (MOD-03)
|
||||
- Category page renders with lazy-loaded cards (MOD-04)
|
||||
- Domaincheck accepts input and returns colored results (DCHK-01, DCHK-02, DCHK-03)
|
||||
- Deactivated module is inaccessible
|
||||
- i18n and theming work correctly
|
||||
</acceptance_criteria>
|
||||
<resume-signal>Type "approved" or describe issues to fix</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| N/A | Verification-only plan, no new code |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-03-12 | N/A | N/A | accept | No new attack surface — verification only |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
Human confirms all 11 verification steps pass.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Human types "approved" after verifying all success criteria from Phase 3 roadmap
|
||||
- Or provides specific issues that will generate gap closure plans
|
||||
</success_criteria>
|
||||
|
||||
## Artifacts this phase produces
|
||||
|
||||
| Symbol | Location | Type |
|
||||
|--------|----------|------|
|
||||
| (none — verification only) | | |
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/03-module-system-domaincheck/03-04-SUMMARY.md` when done
|
||||
</output>
|
||||
Reference in New Issue
Block a user