Просмотр исходного кода

feat(suggest-path): add cc-sdd spec (requirements + design)

Add suggest-path specification for design review:
- requirements.md: 9 EARS-format requirements covering Phase 1 (MVP) and Phase 2
- design.md: Technical design with architecture, component interfaces, and data contracts
- research.md: Discovery findings (route patterns, grant system, search service)
- spec.json: Metadata tracking (requirements approved, design approved)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
VANELLOPE\tomoyuki-t 1 месяц назад
Родитель
Сommit
12ede5bfd6

+ 625 - 0
.kiro/specs/suggest-path/design.md

@@ -0,0 +1,625 @@
+# Design Document
+
+## Overview
+
+**Purpose**: The suggest-path feature delivers an AI-powered path suggestion API that helps AI clients (e.g., Claude via MCP) determine optimal save locations for page content in GROWI. Users no longer need to manually decide where to save — the system analyzes content and returns directory path candidates with metadata.
+
+**Users**: AI clients (Claude via MCP) call this endpoint on behalf of GROWI users during the "save to GROWI" workflow. The endpoint is part of the broader Smart Save architecture.
+
+**Impact**: Adds a new API namespace (`ai-tools`) and a new endpoint (`suggest-path`) to the GROWI backend. No changes to existing endpoints or data models.
+
+### Goals
+
+- Provide a single POST endpoint that returns path suggestions with metadata (type, path, label, description, grant)
+- Phase 1 (MVP): Return personal memo path with fixed metadata — zero external dependencies
+- Phase 2: Add search-based and category-based suggestions using GROWI AI keyword extraction and search service
+- Enable GROWI.cloud paid-plan gating via separate namespace from `/page`
+
+### Non-Goals
+
+- Page creation or saving (existing `POST /_api/v3/page` handles this)
+- Page title suggestion (Claude handles this via user dialogue)
+- Client-side "enter manually" option (Agent Skill responsibility)
+- GROWI AI keyword extraction implementation details (separate design)
+
+## Architecture
+
+### Existing Architecture Analysis
+
+The suggest-path endpoint integrates with GROWI's existing API infrastructure:
+
+- **Route layer**: Express Router with handler factory pattern (`(crowi: Crowi) => RequestHandler[]`)
+- **Middleware chain**: `accessTokenParser` → `loginRequiredStrictly` → validators → `apiV3FormValidator` → handler
+- **Response format**: `res.apiv3(data)` for success, `res.apiv3Err(error, status)` for errors
+- **AI features**: Existing `features/openai/` module with `certifyAiService` middleware for AI-enabled gating
+- **Search**: `searchService.searchKeyword()` for full-text search with permission scoping
+
+No existing architecture needs modification. The endpoint adds a new route namespace alongside existing ones.
+
+### Architecture Pattern & Boundary Map
+
+```mermaid
+graph TB
+    subgraph Client
+        MCP[MCP Server]
+    end
+
+    subgraph GROWI API
+        Router[ai-tools Router]
+        Handler[suggest-path Handler]
+        MemoGen[Memo Suggestion Logic]
+        SearchGen[Search Suggestion Logic - Phase 2]
+        CategoryGen[Category Suggestion Logic - Phase 2]
+    end
+
+    subgraph Existing Services
+        SearchSvc[Search Service]
+        GrantSvc[Page Grant Service]
+        AIFeature[GROWI AI - OpenAI Feature]
+    end
+
+    subgraph Data
+        ES[Elasticsearch]
+        Mongo[MongoDB - Pages]
+    end
+
+    MCP -->|POST suggest-path| Router
+    Router --> Handler
+    Handler --> MemoGen
+    Handler --> SearchGen
+    Handler --> CategoryGen
+    SearchGen --> AIFeature
+    SearchGen --> SearchSvc
+    CategoryGen --> SearchSvc
+    SearchGen --> GrantSvc
+    CategoryGen --> GrantSvc
+    SearchSvc --> ES
+    GrantSvc --> Mongo
+```
+
+**Architecture Integration**:
+
+- **Selected pattern**: Layered handler following existing GROWI route conventions. Phase 1 uses inline logic in handler; Phase 2 extracts suggestion generation into a service
+- **Domain boundaries**: Route layer (`ai-tools/`) owns the endpoint. Suggestion logic delegates to existing services (search, grant, AI) without modifying them
+- **Existing patterns preserved**: Handler factory pattern, middleware chain, `res.apiv3()` response format
+- **New components**: `ai-tools/` route directory (new namespace), `suggest-path.ts` handler
+- **Steering compliance**: Feature-based separation, named exports, TypeScript strict typing
+
+### Technology Stack
+
+| Layer | Choice / Version | Role in Feature | Notes |
+|-------|------------------|-----------------|-------|
+| Backend | Express.js (existing) | Route handling, middleware | No new dependencies |
+| Validation | express-validator (existing) | Request body validation | Existing pattern |
+| Search | Elasticsearch via searchService (existing) | Phase 2 keyword search | Used for `search` and `category` suggestions |
+| AI | OpenAI feature module (existing) | Phase 2 keyword extraction | Existing `features/openai/` infrastructure |
+| Data | MongoDB via Mongoose (existing) | Page grant lookup | For parent page grant resolution |
+
+No new dependencies introduced. All technology is already in the GROWI stack.
+
+## System Flows
+
+### Phase 1 (MVP) Flow
+
+```mermaid
+sequenceDiagram
+    participant Client as MCP Client
+    participant MW as Middleware Chain
+    participant Handler as suggest-path Handler
+
+    Client->>MW: POST /_api/v3/ai-tools/suggest-path
+    Note over MW: accessTokenParser
+    Note over MW: loginRequiredStrictly
+    Note over MW: validator + apiV3FormValidator
+    MW->>Handler: Validated request with req.user
+    Handler->>Handler: Generate memo path from req.user.username
+    Handler-->>Client: 200 suggestions array - memo only
+```
+
+### Phase 2 Flow
+
+```mermaid
+sequenceDiagram
+    participant Client as MCP Client
+    participant Handler as suggest-path Handler
+    participant AI as GROWI AI
+    participant Search as Search Service
+    participant Grant as Page Grant Service
+
+    Client->>Handler: POST with body content
+    Handler->>Handler: Generate memo suggestion
+    Handler->>AI: Extract keywords from body
+    AI-->>Handler: Keywords array
+
+    par Search-based suggestion
+        Handler->>Search: searchKeyword with keywords
+        Search-->>Handler: Related pages
+        Handler->>Grant: Resolve parent grant
+        Grant-->>Handler: Grant value
+    and Category-based suggestion
+        Handler->>Search: searchKeyword with prefix scope
+        Search-->>Handler: Top-level pages
+        Handler->>Grant: Resolve parent grant
+        Grant-->>Handler: Grant value
+    end
+
+    Handler-->>Client: 200 suggestions array - memo + search + category
+```
+
+**Key decisions**:
+
+- Search-based and category-based suggestions are generated in parallel where possible
+- If keyword extraction fails, handler falls back to memo-only response (Phase 1 behavior)
+- If search returns no results for a suggestion type, that type is omitted from the response
+
+## Requirements Traceability
+
+| Requirement | Summary | Components | Interfaces | Flows |
+|-------------|---------|------------|------------|-------|
+| 1.1 | POST endpoint returns suggestions array | SuggestPathRouter, SuggestPathHandler | API Contract | Phase 1, Phase 2 |
+| 1.2 | Suggestion fields: type, path, label, description, grant | SuggestPathHandler | PathSuggestion type | — |
+| 1.3 | Path values as directory paths with trailing slash | SuggestPathHandler | PathSuggestion type | — |
+| 1.4 | Separate namespace from /page | SuggestPathRouter | Route registration | — |
+| 2.1 | Include memo type suggestion | MemoSuggestionGenerator | PathSuggestion type | Phase 1 |
+| 2.2 | Memo path from authenticated user | MemoSuggestionGenerator | — | Phase 1 |
+| 2.3 | Memo grant = 4 (owner only) | MemoSuggestionGenerator | — | — |
+| 2.4 | Fixed description for memo | MemoSuggestionGenerator, DescriptionGenerator | — | — |
+| 3.1 | Search related pages by keywords | SearchSuggestionGenerator | SearchService | Phase 2 |
+| 3.2 | Return parent directory of most relevant page | SearchSuggestionGenerator | — | Phase 2 |
+| 3.3 | Include related page titles in description | SearchSuggestionGenerator, DescriptionGenerator | — | — |
+| 3.4 | Include parent page grant for search type | SearchSuggestionGenerator, GrantResolver | — | — |
+| 3.5 | Omit search type if no results | SearchSuggestionGenerator | — | — |
+| 4.1 | Search top-level directories by keywords | CategorySuggestionGenerator | SearchService | Phase 2 |
+| 4.2 | Extract top-level path segment | CategorySuggestionGenerator | — | Phase 2 |
+| 4.3 | Include parent page grant for category type | CategorySuggestionGenerator, GrantResolver | — | — |
+| 4.4 | Omit category type if no results | CategorySuggestionGenerator | — | — |
+| 5.1 | Delegate keyword extraction to GROWI AI | KeywordExtractor | GROWI AI interface | Phase 2 |
+| 5.2 | Use extracted keywords for search, not raw body | SearchSuggestionGenerator, CategorySuggestionGenerator | — | Phase 2 |
+| 5.3 | Fallback to memo if extraction fails | SuggestPathHandler | — | Phase 2 |
+| 6.1 | Description provides selection rationale | DescriptionGenerator | — | — |
+| 6.2 | Fixed text for memo in Phase 1 | DescriptionGenerator | — | — |
+| 6.3 | List page titles for search type in Phase 2 | DescriptionGenerator | — | — |
+| 6.4 | Path segment name for category type in Phase 2 | DescriptionGenerator | — | — |
+| 6.5 | Phase 2 descriptions mechanical, no AI | DescriptionGenerator | — | — |
+| 7.1 | Grant field = parent page grant value | GrantResolver | PageGrant type | — |
+| 7.2 | Grant = upper bound constraint, not recommendation | GrantResolver | — | — |
+| 8.1 | Require valid API token or login session | SuggestPathRouter | Middleware chain | — |
+| 8.2 | Return auth error if unauthenticated | SuggestPathRouter | — | — |
+| 8.3 | Use authenticated user for user-specific suggestions | SuggestPathHandler | — | — |
+| 9.1 | Validation error if body missing/empty | SuggestPathRouter | Validator | — |
+| 9.2 | No internal details in error responses | SuggestPathHandler | ErrorV3 | — |
+
+## Components and Interfaces
+
+| Component | Domain/Layer | Intent | Req Coverage | Key Dependencies | Contracts |
+|-----------|-------------|--------|--------------|------------------|-----------|
+| SuggestPathRouter | Route | Route registration and middleware composition | 1.4, 8.1, 8.2, 9.1 | Express Router (P0) | API |
+| SuggestPathHandler | Route | Orchestrate suggestion generation and response | 1.1, 1.2, 1.3, 5.3, 8.3, 9.2 | SuggestionGenerators (P0) | API, Service |
+| MemoSuggestionGenerator | Service | Generate memo path suggestion from user identity | 2.1, 2.2, 2.3, 2.4 | req.user (P0) | Service |
+| SearchSuggestionGenerator | Service | Generate search-based suggestion from keywords (Phase 2) | 3.1-3.5, 5.2 | SearchService (P0), GrantResolver (P1) | Service |
+| CategorySuggestionGenerator | Service | Generate category-based suggestion from keywords (Phase 2) | 4.1-4.4, 5.2 | SearchService (P0), GrantResolver (P1) | Service |
+| KeywordExtractor | Service | Extract keywords from content via GROWI AI (Phase 2) | 5.1, 5.2 | OpenAI Feature (P0) | Service |
+| DescriptionGenerator | Service | Generate description text per suggestion type | 6.1-6.5 | None | Service |
+| GrantResolver | Service | Resolve parent page grant for a given path | 7.1, 7.2, 3.4, 4.3 | Page Model (P0) | Service |
+
+### Route Layer
+
+#### SuggestPathRouter
+
+| Field | Detail |
+|-------|--------|
+| Intent | Register `POST /suggest-path` under `ai-tools` namespace with authentication and validation middleware |
+| Requirements | 1.4, 8.1, 8.2, 9.1 |
+
+**Responsibilities & Constraints**
+
+- Register route at `/_api/v3/ai-tools/suggest-path`
+- Apply standard authentication middleware chain
+- Validate request body before handler execution
+- Gate on AI-enabled configuration (reuse or replicate `certifyAiService` pattern)
+
+**Dependencies**
+
+- Inbound: MCP Client — HTTP POST requests (P0)
+- Outbound: SuggestPathHandler — request processing (P0)
+- External: Express Router, express-validator — routing and validation (P0)
+
+**Contracts**: API [x]
+
+##### API Contract
+
+| Method | Endpoint | Request | Response | Errors |
+|--------|----------|---------|----------|--------|
+| POST | `/_api/v3/ai-tools/suggest-path` | `SuggestPathRequest` | `SuggestPathResponse` | 400, 401, 403, 500 |
+
+**Implementation Notes**
+
+- Route registered in `apps/app/src/server/routes/apiv3/index.js` as `router.use('/ai-tools', ...)`
+- Middleware chain follows existing pattern: `accessTokenParser` → `loginRequiredStrictly` → `certifyAiService` → validators → `apiV3FormValidator` → handler
+- Namespace `ai-tools` is tentative pending yuki confirmation; change requires single line edit in `index.js`
+
+#### SuggestPathHandler
+
+| Field | Detail |
+|-------|--------|
+| Intent | Orchestrate suggestion generation, collect results, return unified response |
+| Requirements | 1.1, 1.2, 1.3, 5.3, 8.3, 9.2 |
+
+**Responsibilities & Constraints**
+
+- Invoke suggestion generators (memo always; search and category in Phase 2)
+- Collect non-null results into suggestions array
+- Handle errors gracefully: if Phase 2 logic fails, fall back to memo-only
+- Format response using `res.apiv3()`
+
+**Dependencies**
+
+- Inbound: SuggestPathRouter — validated request (P0)
+- Outbound: MemoSuggestionGenerator, SearchSuggestionGenerator, CategorySuggestionGenerator, KeywordExtractor — suggestion generation (P0)
+
+**Contracts**: Service [x]
+
+##### Service Interface
+
+```typescript
+// Phase 1: Handler contains inline logic
+// Phase 2: Extracted to SuggestPathService
+
+interface SuggestPathService {
+  generateSuggestions(
+    user: IUserHasId,
+    body: string,
+  ): Promise<PathSuggestion[]>;
+}
+```
+
+- Preconditions: `user` is authenticated, `body` is non-empty string
+- Postconditions: Returns array with at least one suggestion (memo type)
+- Invariants: Memo suggestion is always present regardless of Phase 2 failures
+
+**Implementation Notes**
+
+- Phase 1: Logic is inline in handler (memo generation is ~10 lines). The `body` field is required but unused in Phase 1 — this maintains API contract stability so the transition to Phase 2 introduces no breaking changes. The MCP client always has content body available in the save workflow
+- Phase 2: Extract to `SuggestPathService` class when adding search/category generators
+- Error handling: Catch Phase 2 failures, log, return memo-only response
+
+### Service Layer
+
+#### MemoSuggestionGenerator
+
+| Field | Detail |
+|-------|--------|
+| Intent | Generate personal memo area path suggestion |
+| Requirements | 2.1, 2.2, 2.3, 2.4 |
+
+**Responsibilities & Constraints**
+
+- Generate path: `/user/{username}/memo/` using `userHomepagePath(user)` utility
+- Set fixed grant value: `PageGrant.GRANT_OWNER` (4)
+- Set fixed description and label text
+- Always succeeds (no external dependencies)
+
+**Contracts**: Service [x]
+
+##### Service Interface
+
+```typescript
+function generateMemoSuggestion(user: IUserHasId): PathSuggestion {
+  // Returns memo suggestion with type 'memo'
+}
+```
+
+- Preconditions: `user` has valid `username` field
+- Postconditions: Returns a `PathSuggestion` with `type: 'memo'`, `grant: 4`
+
+#### SearchSuggestionGenerator (Phase 2)
+
+| Field | Detail |
+|-------|--------|
+| Intent | Find related pages via keyword search and suggest their parent directory |
+| Requirements | 3.1, 3.2, 3.3, 3.4, 3.5, 5.2 |
+
+**Responsibilities & Constraints**
+
+- Call `searchService.searchKeyword()` with extracted keywords
+- Select the top-1 result by Elasticsearch score; extract parent directory from its path
+- Generate description listing up to 3 related page titles (top results by score)
+- Resolve parent page grant via GrantResolver
+- Return `null` if no search results found
+- Note: Selection heuristic (top-1 by score) is the initial approach; may be refined with real-world data during Phase 2 implementation
+
+**Dependencies**
+
+- Outbound: SearchService — keyword search (P0)
+- Outbound: GrantResolver — parent page grant lookup (P1)
+
+**Contracts**: Service [x]
+
+##### Service Interface
+
+```typescript
+function generateSearchSuggestion(
+  keywords: string[],
+  user: IUserHasId,
+  userGroups: PopulatedGrantedGroup[],
+): Promise<PathSuggestion | null>;
+```
+
+- Preconditions: `keywords` is non-empty array
+- Postconditions: Returns `PathSuggestion` with `type: 'search'` or `null` if no results
+
+#### CategorySuggestionGenerator (Phase 2)
+
+| Field | Detail |
+|-------|--------|
+| Intent | Find matching top-level category directory for content |
+| Requirements | 4.1, 4.2, 4.3, 4.4, 5.2 |
+
+**Responsibilities & Constraints**
+
+- Call `searchService.searchKeyword()` with keywords scoped to top-level (`prefix:/`)
+- Select the top-1 result by Elasticsearch score; extract top-level path segment (e.g., `/tech-notes/React/hooks` → `/tech-notes/`)
+- Generate description from top-level segment name
+- Resolve parent page grant via GrantResolver
+- Return `null` if no matching top-level pages found
+- Note: Selection heuristic (top-1 by score) is the initial approach; may be refined with real-world data during Phase 2 implementation
+
+**Dependencies**
+
+- Outbound: SearchService — scoped keyword search (P0)
+- Outbound: GrantResolver — parent page grant lookup (P1)
+
+**Contracts**: Service [x]
+
+##### Service Interface
+
+```typescript
+function generateCategorySuggestion(
+  keywords: string[],
+  user: IUserHasId,
+  userGroups: PopulatedGrantedGroup[],
+): Promise<PathSuggestion | null>;
+```
+
+- Preconditions: `keywords` is non-empty array
+- Postconditions: Returns `PathSuggestion` with `type: 'category'` or `null` if no results
+
+#### KeywordExtractor (Phase 2)
+
+| Field | Detail |
+|-------|--------|
+| Intent | Extract search-relevant keywords from content body via GROWI AI |
+| Requirements | 5.1, 5.2 |
+
+**Responsibilities & Constraints**
+
+- Accept content body string
+- Delegate to GROWI AI (existing OpenAI feature) for keyword extraction
+- Return 3-5 keywords prioritizing proper nouns and technical terms
+- Avoid generic/common words
+- Implementation details are out of scope for this spec (handled in separate GROWI AI design)
+
+**Dependencies**
+
+- External: OpenAI Feature module — AI inference (P0)
+
+**Contracts**: Service [x]
+
+##### Service Interface
+
+```typescript
+interface KeywordExtractor {
+  extract(body: string): Promise<string[]>;
+}
+```
+
+- Preconditions: `body` is non-empty string
+- Postconditions: Returns array of 0-5 keyword strings
+- Error behavior: Throws on failure; caller handles fallback
+
+#### DescriptionGenerator
+
+| Field | Detail |
+|-------|--------|
+| Intent | Generate human-readable description for each suggestion type |
+| Requirements | 6.1, 6.2, 6.3, 6.4, 6.5 |
+
+**Responsibilities & Constraints**
+
+- `memo` type: Return fixed descriptive text (e.g., "Save to your personal memo area")
+- `search` type (Phase 2): List up to 3 related page titles from top search results by score. No AI usage — purely mechanical
+- `category` type (Phase 2): Generate from top-level path segment name. No AI usage — purely mechanical
+
+**Contracts**: Service [x]
+
+##### Service Interface
+
+```typescript
+function generateMemoDescription(): string;
+
+// Phase 2
+function generateSearchDescription(relatedPageTitles: string[]): string; // accepts up to 3 titles
+function generateCategoryDescription(topLevelSegment: string): string;
+```
+
+#### GrantResolver
+
+| Field | Detail |
+|-------|--------|
+| Intent | Look up the effective grant value for a parent directory path |
+| Requirements | 7.1, 7.2, 3.4, 4.3 |
+
+**Responsibilities & Constraints**
+
+- Given a directory path, find the corresponding page in MongoDB
+- Return its `grant` value as the upper bound for child pages
+- For memo path: always returns `PageGrant.GRANT_OWNER` (4) — can be hardcoded in Phase 1
+- For search/category paths (Phase 2): query Page model for the parent page's grant
+
+**Dependencies**
+
+- External: Page Model (Mongoose) — page grant lookup (P0)
+
+**Contracts**: Service [x]
+
+##### Service Interface
+
+```typescript
+function resolveParentGrant(path: string): Promise<number>;
+```
+
+- Preconditions: `path` is a valid directory path (trailing `/`)
+- Postconditions: Returns PageGrant numeric value (1, 2, 4, or 5)
+- Error behavior: Returns `PageGrant.GRANT_OWNER` (4) as safe default if page not found
+
+## Data Models
+
+### Domain Model
+
+No new database entities. The endpoint reads from existing models only.
+
+**Existing entities used**:
+
+- **Page**: Queried for parent page grant resolution (Phase 2). Fields: `path`, `grant`, `grantedGroups`
+- **User**: Available via `req.user`. Fields: `username`, `_id`
+
+### Data Contracts & Integration
+
+#### Request Schema
+
+```typescript
+interface SuggestPathRequest {
+  body: string; // Page content for keyword extraction
+}
+```
+
+**Validation rules**:
+
+- `body`: Required, non-empty string
+- No endpoint-specific maximum length. Body size is governed by GROWI's global Express body-parser configuration. The KeywordExtractor (Phase 2) handles truncation internally if content exceeds its processing capacity
+
+#### Response Schema
+
+```typescript
+type SuggestionType = 'memo' | 'search' | 'category';
+
+interface PathSuggestion {
+  type: SuggestionType;
+  path: string;        // Directory path with trailing '/'
+  label: string;       // Display label for the suggestion
+  description: string; // Selection rationale
+  grant: number;       // Parent page grant (PageGrant value)
+}
+
+interface SuggestPathResponse {
+  suggestions: PathSuggestion[];
+}
+```
+
+**Invariants**:
+
+- `suggestions` array always contains at least one element (memo type)
+- `path` always ends with `/`
+- `grant` is a valid PageGrant value (1, 2, 4, or 5)
+- `type` is one of the defined SuggestionType values
+
+#### Phase 1 Response Example
+
+```json
+{
+  "suggestions": [
+    {
+      "type": "memo",
+      "path": "/user/alice/memo/",
+      "label": "Save as memo",
+      "description": "Save to your personal memo area",
+      "grant": 4
+    }
+  ]
+}
+```
+
+#### Phase 2 Response Example
+
+```json
+{
+  "suggestions": [
+    {
+      "type": "memo",
+      "path": "/user/alice/memo/",
+      "label": "Save as memo",
+      "description": "Save to your personal memo area",
+      "grant": 4
+    },
+    {
+      "type": "search",
+      "path": "/tech-notes/React/",
+      "label": "Save near related pages",
+      "description": "Related pages under this directory: React Hooks Guide, Jotai State Management",
+      "grant": 1
+    },
+    {
+      "type": "category",
+      "path": "/tech-notes/",
+      "label": "Save under category",
+      "description": "Top-level category: tech-notes",
+      "grant": 1
+    }
+  ]
+}
+```
+
+## Error Handling
+
+### Error Categories and Responses
+
+**User Errors (4xx)**:
+
+| Error | Status | Response | Requirement |
+|-------|--------|----------|-------------|
+| Missing or empty `body` field | 400 | Validation error with field details | 9.1 |
+| No authentication token/session | 401 | Authentication required | 8.2 |
+| AI service not enabled | 403 | GROWI AI is not enabled | 1.4 |
+
+**System Errors (5xx)**:
+
+| Error | Status | Response | Behavior |
+|-------|--------|----------|----------|
+| Search service failure (Phase 2) | 200 | Memo suggestion only | Graceful degradation, log error |
+| GROWI AI failure (Phase 2) | 200 | Memo suggestion only | Graceful degradation, log error |
+| Unexpected error | 500 | Generic error, no internal details | Requirement 9.2 |
+
+**Key decision**: Phase 2 failures degrade to Phase 1 behavior (memo-only) rather than returning errors. The memo suggestion is always generated first and acts as guaranteed fallback.
+
+## Testing Strategy
+
+### Unit Tests
+
+- `MemoSuggestionGenerator`: Generates correct path from username, correct grant value, correct description
+- `DescriptionGenerator`: Fixed text for memo, page title listing for search, segment name for category
+- `GrantResolver`: Returns correct grant from page, default grant when page not found
+- `PathSuggestion` type validation: Trailing slash enforcement, required fields present
+- Request validation: Missing body, empty body, valid body
+
+### Integration Tests
+
+- `POST /suggest-path` with valid auth: Returns 200 with memo suggestion (Phase 1)
+- `POST /suggest-path` without auth: Returns 401
+- `POST /suggest-path` with empty body: Returns 400
+- `POST /suggest-path` with AI disabled: Returns 403
+- Phase 2: Search returns results → includes search/category suggestions
+- Phase 2: Search returns nothing → memo-only response
+- Phase 2: AI extraction fails → memo-only fallback
+
+### Performance (Phase 2)
+
+- Keyword extraction latency under typical content sizes
+- Search query performance with extracted keywords
+- Parallel generation of search + category suggestions
+
+## Security Considerations
+
+- **Authentication**: All requests require valid API token or login session (standard middleware)
+- **Authorization**: User can only see suggestions based on their own identity and permissions. Search results are permission-scoped via `searchKeyword()` user/group parameters
+- **Input safety**: Content body is passed to GROWI AI, not directly to Elasticsearch. No NoSQL injection risk from body content
+- **Information leakage**: Error responses use generic messages per requirement 9.2. No stack traces or internal paths exposed

+ 116 - 0
.kiro/specs/suggest-path/requirements.md

@@ -0,0 +1,116 @@
+# Requirements Document
+
+## Introduction
+
+The suggest-path feature provides an AI-powered API endpoint for GROWI that suggests optimal page save locations. When an AI client (e.g., Claude via MCP) sends page content, the endpoint analyzes it and returns directory path suggestions with metadata including descriptions and grant (permission) constraints. This enables users to save content to well-organized locations without manually determining paths.
+
+The feature is delivered incrementally in two phases:
+
+- **Phase 1 (MVP)**: Personal memo path suggestion — establishes the endpoint, authentication, and response structure. Implemented first to provide immediate value.
+- **Phase 2 (Full)**: Search-based and category-based path suggestions powered by GROWI AI keyword extraction. Builds on the Phase 1 foundation.
+
+Both phases are covered by this specification. Implementation proceeds Phase 1 first, then Phase 2.
+
+## Out of Scope
+
+The following are explicitly **not** part of this feature:
+
+- **Page creation/saving**: The actual save operation uses the existing `POST /_api/v3/page` endpoint. This feature only suggests *where* to save.
+- **Page title determination**: Page naming is handled through dialogue between the AI client (e.g., Claude) and the user. GROWI does not suggest titles.
+
+## Requirements
+
+### Requirement 1: Path Suggestion API Endpoint
+
+**Objective:** As an AI client (e.g., Claude via MCP), I want to request page path suggestions by sending content body, so that users can save content to appropriate locations without manually determining paths.
+
+#### Acceptance Criteria
+
+1. When the client sends a POST request with a `body` field containing page content, the Suggest Path Service shall return a response containing an array of path suggestions.
+2. The Suggest Path Service shall include `type`, `path`, `label`, `description`, and `grant` fields in each suggestion.
+3. The Suggest Path Service shall return `path` values as directory paths with a trailing slash (`/`).
+4. The Suggest Path Service shall expose the endpoint under a namespace separate from `/_api/v3/page/` to support independent access control (e.g., GROWI.cloud paid-plan gating).
+
+### Requirement 2: Memo Path Suggestion (Phase 1 MVP)
+
+**Objective:** As a user, I want my personal memo area suggested as a save destination, so that I always have a guaranteed fallback location for saving content.
+
+#### Acceptance Criteria
+
+1. When the client sends a valid request, the Suggest Path Service shall include a suggestion with type `memo`.
+2. The Suggest Path Service shall generate the memo path based on the authenticated user's identity (pattern: `/user/{username}/memo/`).
+3. The Suggest Path Service shall set `grant` to `4` (owner only) for memo type suggestions.
+4. The Suggest Path Service shall provide a fixed descriptive text in the `description` field for memo type suggestions.
+
+### Requirement 3: Search-Based Path Suggestion (Phase 2)
+
+**Objective:** As a user, I want save locations suggested near related existing pages, so that my content is organized alongside relevant material.
+
+#### Acceptance Criteria
+
+1. When keywords have been extracted from the content, the Suggest Path Service shall search for related existing pages using those keywords.
+2. When related pages are found, the Suggest Path Service shall return the parent directory of the most relevant page as a suggestion with type `search`.
+3. When related pages are found, the Suggest Path Service shall include related page titles in the `description` field as selection rationale.
+4. The Suggest Path Service shall include the parent page's `grant` value for `search` type suggestions.
+5. If no related pages are found, the Suggest Path Service shall omit the `search` type suggestion from the response.
+
+### Requirement 4: Category-Based Path Suggestion (Phase 2)
+
+**Objective:** As a user, I want a top-level category directory suggested, so that content can be organized under broad topic areas.
+
+#### Acceptance Criteria
+
+1. When keywords have been extracted from the content, the Suggest Path Service shall search for matching pages scoped to top-level directories.
+2. When matching pages are found, the Suggest Path Service shall extract the top-level path segment and return it as a suggestion with type `category`.
+3. The Suggest Path Service shall include the parent page's `grant` value for `category` type suggestions.
+4. If no matching top-level pages are found, the Suggest Path Service shall omit the `category` type suggestion from the response.
+
+### Requirement 5: Content Keyword Extraction (Phase 2)
+
+**Objective:** As a system operator, I want keyword extraction centralized in GROWI AI, so that suggestion quality is consistent regardless of the calling client's capabilities.
+
+#### Acceptance Criteria
+
+1. When the client sends content body, the Suggest Path Service shall delegate keyword extraction to GROWI AI rather than requiring the client to pre-extract keywords.
+2. The Suggest Path Service shall use extracted keywords (not raw content body) for search operations.
+3. If keyword extraction fails or produces no usable keywords, the Suggest Path Service shall still return the memo suggestion (Phase 1 fallback).
+
+### Requirement 6: Suggestion Description Generation
+
+**Objective:** As a user, I want each suggestion to include a meaningful description, so that I can make an informed choice about where to save my content.
+
+#### Acceptance Criteria
+
+1. The Suggest Path Service shall include a `description` field in each suggestion that provides rationale for selecting that save location.
+2. While in Phase 1, the Suggest Path Service shall use fixed descriptive text for `memo` type suggestions.
+3. While in Phase 2, when returning `search` type suggestions, the Suggest Path Service shall generate the `description` by listing titles of related pages found under the suggested directory.
+4. While in Phase 2, when returning `category` type suggestions, the Suggest Path Service shall generate the `description` from the top-level path segment name.
+5. The Suggest Path Service shall generate Phase 2 descriptions mechanically from search results without using GROWI AI.
+
+### Requirement 7: Grant Constraint Information
+
+**Objective:** As an AI client, I want permission constraints for each suggested path, so that the appropriate grant level can be set when saving the page.
+
+#### Acceptance Criteria
+
+1. The Suggest Path Service shall include a `grant` field in each suggestion representing the parent page's grant value.
+2. The `grant` field shall represent the upper bound of settable permissions for child pages created under the suggested path (not a recommendation, but a constraint).
+
+### Requirement 8: Authentication and Authorization
+
+**Objective:** As a system operator, I want the endpoint protected by authentication, so that only authorized users can request path suggestions.
+
+#### Acceptance Criteria
+
+1. The Suggest Path Service shall require a valid API token or active login session for all requests.
+2. If the request lacks valid authentication, the Suggest Path Service shall return an authentication error.
+3. The Suggest Path Service shall use the authenticated user's identity to generate user-specific suggestions.
+
+### Requirement 9: Input Validation and Error Handling
+
+**Objective:** As a system, I want invalid requests rejected with clear feedback, so that clients can correct their requests.
+
+#### Acceptance Criteria
+
+1. If the `body` field is missing or empty in the request, the Suggest Path Service shall return a validation error.
+2. If an internal error occurs during path suggestion generation, the Suggest Path Service shall return an appropriate error response without exposing internal system details.

+ 133 - 0
.kiro/specs/suggest-path/research.md

@@ -0,0 +1,133 @@
+# Research & Design Decisions
+
+## Summary
+
+- **Feature**: `suggest-path`
+- **Discovery Scope**: Extension (new endpoint added to existing API infrastructure)
+- **Key Findings**:
+  - GROWI uses a handler factory pattern (`(crowi: Crowi) => RequestHandler[]`) for API routes
+  - The `ai-tools` namespace does not exist yet; closest is `/openai` under `features/openai/`
+  - Grant parent-child constraints are enforced by `page-grant.ts` — GRANT_OWNER children must share the same owner
+  - `searchService.searchKeyword()` accepts keyword string and returns scored results with page metadata
+  - User home path utilities exist in `@growi/core` (`userHomepagePath`, `isUsersHomepage`)
+
+## Research Log
+
+### GROWI API Route Patterns
+
+- **Context**: Need to understand how to add a new route namespace
+- **Sources Consulted**: `apps/app/src/server/routes/apiv3/index.js`, `page/create-page.ts`, `features/openai/server/routes/index.ts`
+- **Findings**:
+  - Three router types: standard, admin, auth. New endpoints go on standard router
+  - Route registration: `router.use('/namespace', require('./namespace')(crowi))` or factory import
+  - Handler factory pattern: exports `(crowi: Crowi) => RequestHandler[]` returning middleware chain
+  - Middleware ordering: `accessTokenParser` → `loginRequiredStrictly` → validators → `apiV3FormValidator` → handler
+  - Response helpers: `res.apiv3(data)` for success, `res.apiv3Err(error, status)` for errors
+  - Feature-based routes use dynamic import pattern (see openai routes)
+- **Implications**: suggest-path follows the handler factory pattern. New `ai-tools` directory under `routes/apiv3/`
+
+### OpenAI Feature Structure
+
+- **Context**: Understanding existing AI feature patterns for alignment
+- **Sources Consulted**: `features/openai/server/routes/index.ts`, `middlewares/certify-ai-service.ts`
+- **Findings**:
+  - AI routes gate on `aiEnabled` config via `certifyAiService` middleware
+  - Dynamic imports used for route handlers
+  - Dedicated middleware directory for AI-specific checks
+  - Routes organized under `features/openai/` not `routes/apiv3/`
+- **Implications**: suggest-path should gate on AI-enabled config. However, since `ai-tools` is a separate namespace from `openai`, it lives under `routes/apiv3/ai-tools/` rather than `features/openai/`. The AI gating middleware can be reused or replicated.
+
+### Grant System Constraints
+
+- **Context**: Need to return accurate grant constraints for suggested paths
+- **Sources Consulted**: `@growi/core` PageGrant enum, `apps/app/src/server/service/page-grant.ts`
+- **Findings**:
+  - PageGrant values: PUBLIC(1), RESTRICTED(2), SPECIFIED(3-deprecated), OWNER(4), USER_GROUP(5)
+  - Parent constrains child: OWNER parent → child must be OWNER by same user; USER_GROUP parent → child cannot be PUBLIC
+  - `calcApplicableGrantData(page, user)` returns allowed grant types for a page
+  - For memo path (`/user/{username}/memo/`), the user homepage `/user/{username}` is GRANT_OWNER(4) by default → memo path grant is fixed at 4
+- **Implications**: Phase 1 memo grant is trivially 4. Phase 2 needs to look up actual parent page grant via Page model
+
+### Search Service Integration
+
+- **Context**: Phase 2 requires keyword-based search for related pages
+- **Sources Consulted**: `apps/app/src/server/service/search.ts`
+- **Findings**:
+  - `searchKeyword(keyword, nqName, user, userGroups, searchOpts)` → `[ISearchResult, delegatorName]`
+  - Results include `_id`, `_score`, `_source`, `_highlight`
+  - Supports `prefix:` queries for path-scoped search
+  - User groups needed for permission-scoped search results
+- **Implications**: Phase 2 uses `searchKeyword` with extracted keywords. Category search uses `prefix:/` to scope to top-level. Need `getUserRelatedGroups()` for permission-correct results.
+
+### User Home Path Utilities
+
+- **Context**: Memo path generation needs user home path
+- **Sources Consulted**: `@growi/core` `page-path-utils/index.ts`
+- **Findings**:
+  - `userHomepagePath(user)` → `/user/{username}`
+  - `isUsersHomepage(path)` → boolean check
+  - `getUsernameByPath(path)` → extract username from path
+- **Implications**: Use `userHomepagePath(req.user)` + `/memo/` for memo suggestion path
+
+## Architecture Pattern Evaluation
+
+| Option | Description | Strengths | Risks / Limitations | Notes |
+|--------|-------------|-----------|---------------------|-------|
+| Route under `routes/apiv3/ai-tools/` | New namespace in standard routes | Clean separation, follows `ai-tools` naming decision from review | New directory, needs registration in index.js | Aligns with GROWI.cloud access control needs |
+| Route under `features/openai/` | Extend existing AI feature module | Reuses AI infrastructure, minimal setup | Provider-specific name, harder to separate for GC billing | Rejected in review — namespace should be provider-agnostic |
+| Route under `routes/apiv3/page/` | Add to existing page routes | Close to page creation | Cannot gate independently for GC paid plans | Rejected in review — yuki requested separation |
+
+## Design Decisions
+
+### Decision: Route Namespace Placement
+
+- **Context**: Endpoint needs independent access control for GROWI.cloud paid plans
+- **Alternatives Considered**:
+  1. `/openai/suggest-path` — groups with AI features but provider-specific
+  2. `/page/suggest-path` — close to page creation but cannot gate independently
+  3. `/ai-tools/suggest-path` — new provider-agnostic namespace
+- **Selected Approach**: `/_api/v3/ai-tools/suggest-path` under `routes/apiv3/ai-tools/`
+- **Rationale**: Matches existing unmerged PR naming, provider-agnostic, enables independent GC access control
+- **Trade-offs**: Requires new directory and route registration. Namespace is tentative (pending yuki confirmation)
+- **Follow-up**: Confirm `ai-tools` namespace with yuki
+
+### Decision: Phase 1 Handler Simplicity
+
+- **Context**: Phase 1 (MVP) only returns memo path — very simple logic
+- **Alternatives Considered**:
+  1. Full service layer from the start (SuggestionService class)
+  2. Inline logic in handler, extract to service when Phase 2 arrives
+- **Selected Approach**: Inline logic in handler for Phase 1, extract to service for Phase 2
+- **Rationale**: Avoid over-engineering. Phase 1 is ~10 lines of logic. Service abstraction added when needed
+- **Trade-offs**: Phase 2 will require refactoring handler → service extraction
+- **Follow-up**: Define service interface in design for Phase 2 readiness
+
+### Decision: GROWI AI Keyword Extraction Approach
+
+- **Context**: Phase 2 needs keyword extraction from content body
+- **Alternatives Considered**:
+  1. New dedicated keyword extraction service
+  2. Extend existing OpenAI feature module
+  3. Client-side keyword extraction (fallback option)
+- **Selected Approach**: Leverage existing `features/openai/` infrastructure for keyword extraction
+- **Rationale**: GROWI already has OpenAI integration. Keyword extraction is a new capability within the existing AI feature
+- **Trade-offs**: Couples suggest-path to OpenAI feature availability. Mitigated by fallback to memo-only response
+- **Follow-up**: Detailed keyword extraction implementation is out of scope for this spec (separate design)
+
+## Risks & Mitigations
+
+- **Namespace not finalized**: `ai-tools` is tentative. Mitigation: design for easy namespace change (single line in route registration)
+- **Large content body performance**: Sending full content for AI keyword extraction may be slow. Mitigation: Phase 1 does not require AI; Phase 2 has fallback to memo-only if extraction fails
+- **Search service dependency**: Phase 2 depends on Elasticsearch being available. Mitigation: graceful degradation — return memo suggestion if search fails
+- **GROWI AI implementation details unknown**: Keyword extraction specifics are out of scope. Mitigation: define clean interface boundary; implementation details handled separately
+
+## References
+
+- [GROWI Search Internals](https://dev.growi.org/69842ea0cb3a20a69b0a1985) — Search feature internal architecture
+- `apps/app/src/server/routes/apiv3/index.js` — Route registration entry point
+- `apps/app/src/server/routes/apiv3/page/create-page.ts` — Reference handler pattern
+- `apps/app/src/features/openai/server/routes/index.ts` — AI feature route pattern
+- `packages/core/src/interfaces/page.ts` — PageGrant enum definition
+- `apps/app/src/server/service/page-grant.ts` — Grant validation logic
+- `apps/app/src/server/service/search.ts` — Search service interface
+- `packages/core/src/utils/page-path-utils/index.ts` — User path utilities

+ 22 - 0
.kiro/specs/suggest-path/spec.json

@@ -0,0 +1,22 @@
+{
+  "feature_name": "suggest-path",
+  "created_at": "2026-02-10T12:00:00Z",
+  "updated_at": "2026-02-10T13:00:00Z",
+  "language": "en",
+  "phase": "design-generated",
+  "approvals": {
+    "requirements": {
+      "generated": true,
+      "approved": true
+    },
+    "design": {
+      "generated": true,
+      "approved": true
+    },
+    "tasks": {
+      "generated": false,
+      "approved": false
+    }
+  },
+  "ready_for_implementation": false
+}