Yuki Takei 2 tygodni temu
rodzic
commit
0c056c6968

+ 106 - 593
.kiro/specs/suggest-path/design.md

@@ -2,55 +2,30 @@
 
 ## 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.
+**Purpose**: AI-powered path suggestion API that helps AI clients (e.g., Claude via MCP) determine optimal save locations for page content in GROWI. The system analyzes content, searches for related pages, evaluates candidates, and returns directory path suggestions 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.
+**Users**: AI clients (Claude via MCP) call this endpoint on behalf of GROWI users during the "save to GROWI" workflow.
 
 ### 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. **Implemented.**
-- Phase 2: Add AI-powered search-based suggestions with flow/stock information classification, multi-candidate evaluation, and intelligent path proposals including new paths
-- Enable independent access control via separate namespace from `/page`
+- Single POST endpoint returning path suggestions with metadata (type, path, label, description, grant)
+- Memo path: guaranteed fallback with fixed metadata
+- Search-based suggestions: AI-powered with flow/stock classification, multi-candidate evaluation, and intelligent path proposals (including new paths)
+- Independent access control via separate `ai-tools` namespace from `/page`
 
 ### Design Principles
 
-- **Client LLM independence**: Heavy reasoning (content analysis, candidate evaluation, path proposal, description generation) is centralized in GROWI AI on the server side. The API response includes structured data fields (`informationType`, `type`, `grant`) alongside natural language (`description`) so that even less capable LLM clients can make correct decisions through simple field access, without requiring advanced reasoning to interpret the response.
+- **Client LLM independence**: Heavy reasoning (content analysis, candidate evaluation, path proposal, description generation) is centralized in GROWI AI on the server side. The API response includes structured data fields (`informationType`, `type`, `grant`) alongside natural language (`description`) so that even less capable LLM clients can make correct decisions.
 
 ### Non-Goals
 
-- Page creation or saving (existing `POST /_api/v3/page` handles this)
+- Page creation/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)
 
-### Phase 2 Revision Summary
-
-Phase 2 design was revised based on reviewer feedback. Key architectural changes from the prior revision:
-
-1. **AI calls: 1 → 2**: Content analysis (keyword extraction + flow/stock classification) followed by candidate evaluation. Elasticsearch sits between the two calls, making consolidation structurally impossible.
-2. **Candidate selection: mechanical → AI-evaluated**: Instead of top-1 by ES score, multiple candidates are passed to GROWI AI for content-destination fit evaluation.
-3. **Path proposals: 3 patterns**: Parent directory, subdirectory, sibling page (including new paths that don't yet exist).
-4. **Descriptions: mechanical → AI-generated**: Phase 2 descriptions are generated by the candidate evaluator as part of the evaluation.
-5. **Category type: under review**: The existing `category` implementation is retained, but may be merged into the AI evaluation approach after implementation and reviewer discussion.
-
 ## 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
-- **Flow/stock classification**: Existing `instructionsForInformationTypes` in `features/openai/server/services/assistant/instructions/commons.ts`
-
-No existing architecture needs modification. The endpoint adds a new route namespace alongside existing ones.
-
-### Architecture Pattern & Boundary Map
+### Boundary Map
 
 ```mermaid
 graph TB
@@ -96,109 +71,52 @@ graph TB
     GrantSvc --> Mongo
 ```
 
-**Architecture Integration**:
-
-- **Selected pattern**: Layered handler following existing GROWI route conventions. Phase 1 uses inline logic in handler; Phase 2 adds function components called by the handler (see [Implementation Paradigm](#implementation-paradigm))
-- **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**: ContentAnalyzer and CandidateEvaluator wrap GROWI AI calls with suggest-path-specific prompting
-- **Steering compliance**: Feature-based separation, named exports, TypeScript strict typing
+**Integration notes**:
 
-### Code Organization (Directory Structure)
+- Layered handler following existing GROWI route conventions
+- Domain boundaries: Route layer owns the endpoint, delegates to existing services (search, grant, AI) without modifying them
+- Existing patterns preserved: Handler factory pattern, middleware chain, `res.apiv3()` response format
 
-All suggest-path code resides in `features/suggest-path/` following the project's feature-based architecture pattern (reference: `features/openai/`).
+### Code Organization
 
-**Target structure**:
+All suggest-path code resides in `features/ai-tools/suggest-path/` following the project's feature-based architecture pattern.
 
 ```text
-apps/app/src/features/suggest-path/
-├── interfaces/
-│   └── suggest-path-types.ts           # Shared types (PathSuggestion, ContentAnalysis, etc.)
-├── server/
-│   ├── routes/
-│   │   └── apiv3/
-│   │       └── index.ts                # Router factory, handler + middleware chain
-│   ├── services/
-│   │   ├── generate-suggestions.ts     # Orchestrator
-│   │   ├── generate-suggestions.spec.ts
-│   │   ├── generate-memo-suggestion.ts
-│   │   ├── generate-memo-suggestion.spec.ts
-│   │   ├── analyze-content.ts          # AI call #1
-│   │   ├── analyze-content.spec.ts
-│   │   ├── evaluate-candidates.ts      # AI call #2
-│   │   ├── evaluate-candidates.spec.ts
-│   │   ├── retrieve-search-candidates.ts
-│   │   ├── retrieve-search-candidates.spec.ts
-│   │   ├── generate-category-suggestion.ts
-│   │   ├── generate-category-suggestion.spec.ts
-│   │   ├── resolve-parent-grant.ts
-│   │   └── resolve-parent-grant.spec.ts
-│   └── integration-tests/
-│       └── suggest-path-integration.spec.ts
+apps/app/src/features/ai-tools/
+├── server/routes/apiv3/
+│   └── index.ts                              # Aggregation router for ai-tools namespace
+└── suggest-path/
+    ├── interfaces/
+    │   └── suggest-path-types.ts              # Shared types (PathSuggestion, ContentAnalysis, etc.)
+    └── server/
+        ├── routes/apiv3/
+        │   ├── index.ts                       # Route factory, handler + middleware chain
+        │   └── index.spec.ts
+        ├── services/
+        │   ├── generate-suggestions.ts        # Orchestrator
+        │   ├── generate-memo-suggestion.ts
+        │   ├── analyze-content.ts             # AI call #1: keyword extraction + flow/stock
+        │   ├── retrieve-search-candidates.ts  # ES search with score filtering
+        │   ├── evaluate-candidates.ts         # AI call #2: candidate evaluation + path proposal
+        │   ├── call-llm-for-json.ts           # Shared LLM call utility
+        │   ├── generate-category-suggestion.ts # Under review
+        │   ├── resolve-parent-grant.ts
+        │   └── *.spec.ts                      # Co-located tests
+        └── integration-tests/
+            └── suggest-path-integration.spec.ts
 ```
 
 **Key decisions**:
 
 - **No barrel export**: Consumers import directly from subpaths (following `features/openai/` convention)
-- **Aggregation router retained**: The `ai-tools` router remains in `server/routes/apiv3/` as an aggregation point, importing the suggest-path route factory from `~/features/suggest-path/server/routes/apiv3`. This allows future ai-tools features to register under the same namespace
-- **R4 (CategorySuggestionGenerator)**: Migrated as-is. The "under review" status is a requirements-level decision deferred to post-implementation reviewer discussion; the migration does not change its behavior
-- **`generate-search-suggestion.ts`**: Superseded by the `retrieveSearchCandidates` → `evaluateCandidates` pipeline and already removed from the codebase. Not part of the migration
+- **Aggregation router retained**: The `ai-tools` router at `features/ai-tools/server/routes/apiv3/` imports the suggest-path route factory. This allows future ai-tools features to register under the same namespace
+- **R4 (CategorySuggestionGenerator)**: Under review — may be merged into AI evaluation approach post-discussion
 
 ### Implementation Paradigm
 
-**Default**: All components are implemented as pure functions with immutable data. No classes unless explicitly justified.
-
-**Class adoption criteria** — a class is permitted only when at least one of the following applies AND a function-based alternative would be clearly inferior:
-
-1. **Shared dependency management**: Multiple exported functions within a module depend on the same external services, making argument passing verbose.
-2. **Singleton state/cache management**: The module must maintain mutable state or cached data.
+All components are pure functions with immutable data. No classes — no component currently meets class adoption criteria (shared dependency management or singleton state).
 
-**Component assessment**:
-
-| Component | Paradigm | Rationale |
-| --- | --- | --- |
-| MemoSuggestionGenerator | Function | No external service dependencies beyond `user`. Single function. |
-| ContentAnalyzer (Phase 2) | Function | Single function delegating to OpenAI Feature. |
-| SearchCandidateRetriever (Phase 2) | Function | Single function. SearchService passed as argument. |
-| CandidateEvaluator (Phase 2) | Function | Single function delegating to OpenAI Feature. GrantResolver passed as argument. |
-| CategorySuggestionGenerator (Phase 2) | Function | Single function. SearchService and GrantResolver passed as arguments. Under review. |
-| GrantResolver | Function | Single function. Page Model accessed via argument. |
-| SuggestPathOrchestrator (Phase 2) | Function | Single public function. Dependencies as arguments. |
-
-No component currently meets the class adoption criteria.
-
-### 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 candidate retrieval | Score threshold filtering added |
-| AI | OpenAI feature module (existing) | Phase 2: 1st call (content analysis), 2nd call (candidate evaluation) | 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 (Revised)
+### Request Flow
 
 ```mermaid
 sequenceDiagram
@@ -242,141 +160,15 @@ sequenceDiagram
 
 **Key decisions**:
 
-- Content analysis (1st AI call) and candidate evaluation (2nd AI call) are structurally sequential — Elasticsearch sits between them
-- Search-evaluate flow and category generation run in parallel where possible
-- If content analysis fails, handler falls back to memo-only response (Phase 1 behavior)
-- If candidate evaluation fails, handler falls back to memo-only response
-- If search returns no results above the score threshold, search-based suggestions are omitted
-- Category generator runs independently as existing implementation (under review)
-
-## Requirements Traceability
-
-| Requirement | Summary | Components | Interfaces | Flows |
-|-------------|---------|------------|------------|-------|
-| 1.1 | POST endpoint returns suggestions array | SuggestPathRouter, Orchestrator | API Contract | Phase 1, Phase 2 |
-| 1.2 | Suggestion fields: type, path, label, description, grant | Orchestrator | PathSuggestion type | — |
-| 1.3 | Path values as directory paths with trailing slash | Orchestrator | 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 under user home directory | MemoSuggestionGenerator | — | Phase 1 |
-| 2.3 | Memo path under alternative namespace (user pages disabled) | MemoSuggestionGenerator | — | Phase 1 |
-| 2.4 | Memo grant = 4 when user pages enabled | MemoSuggestionGenerator, GrantResolver | — | — |
-| 2.5 | Fixed description for memo | MemoSuggestionGenerator | — | — |
-| 3.1 | Search related pages by keywords | SearchCandidateRetriever | SearchService | Phase 2 |
-| 3.2 | Filter candidates by ES score threshold | SearchCandidateRetriever | — | Phase 2 |
-| 3.3 | Pass candidates to AI evaluation | Orchestrator, CandidateEvaluator | — | Phase 2 |
-| 3.4 | Include parent page grant for search-based suggestions | GrantResolver | — | — |
-| 3.5 | Omit search-based suggestions if no results | SearchCandidateRetriever, Orchestrator | — | — |
-| 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 content analysis to GROWI AI (single call: keywords + flow/stock) | ContentAnalyzer | GROWI AI interface | Phase 2 |
-| 5.2 | Extract 3-5 keywords prioritizing proper nouns | ContentAnalyzer | — | Phase 2 |
-| 5.3 | Use extracted keywords for search, not raw body | SearchCandidateRetriever, CategorySuggestionGenerator | — | Phase 2 |
-| 5.4 | Classify content as flow or stock | ContentAnalyzer | ContentAnalysis type | Phase 2 |
-| 5.5 | Fallback to memo if analysis fails | Orchestrator | — | Phase 2 |
-| 6.1 | Description provides selection rationale | MemoSuggestionGenerator, CandidateEvaluator | — | — |
-| 6.2 | Fixed text for memo in Phase 1 | MemoSuggestionGenerator | — | — |
-| 6.3 | AI-generated description for search-based suggestions | CandidateEvaluator | — | Phase 2 |
-| 7.1 | Grant field = parent page grant value | GrantResolver | PageGrant type | — |
-| 7.2 | Grant = upper bound constraint | 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 | Orchestrator | — | — |
-| 9.1 | Validation error if body missing/empty | SuggestPathRouter | Validator | — |
-| 9.2 | No internal details in error responses | Orchestrator | ErrorV3 | — |
-| 10.1 | Consider flow/stock alignment in candidate evaluation | CandidateEvaluator | ContentAnalysis type | Phase 2 |
-| 10.2 | Identify flow characteristics in candidate locations | CandidateEvaluator | — | Phase 2 |
-| 10.3 | Identify stock characteristics in candidate locations | CandidateEvaluator | — | Phase 2 |
-| 10.4 | Flow/stock as ranking factor, not hard filter | CandidateEvaluator | — | Phase 2 |
-| 11.1 | Evaluate candidates by passing body + path + snippet to AI | CandidateEvaluator | GROWI AI interface | Phase 2 |
-| 11.2 | Rank by content-destination fit and flow/stock alignment | CandidateEvaluator | — | Phase 2 |
-| 11.3 | Generate description per suggestion as part of evaluation | CandidateEvaluator | EvaluatedSuggestion type | Phase 2 |
-| 11.4 | Fallback to memo-only if evaluation fails | Orchestrator | — | Phase 2 |
-| 12.1 | Consider 3 structural patterns: parent, subdirectory, sibling | CandidateEvaluator | — | Phase 2 |
-| 12.2 | Generate new directory names for sibling pattern | CandidateEvaluator | — | Phase 2 |
-| 12.3 | Determine appropriate pattern based on content-destination fit | CandidateEvaluator | — | Phase 2 |
-| 12.4 | Sibling pattern paths at same hierarchy level as candidate | CandidateEvaluator | — | Phase 2 |
-| 13.1 | Include informationType field in search-based suggestions | CandidateEvaluator, Orchestrator | PathSuggestion type | Phase 2 |
-| 13.2 | Provide both structured metadata and natural language context | Orchestrator | PathSuggestion type | Phase 2 |
-| 13.3 | All reasoning-intensive operations server-side | ContentAnalyzer, CandidateEvaluator | — | Phase 2 |
-
-## Components and Interfaces
-
-| Component | Domain/Layer | Intent | Req Coverage | Key Dependencies | Contracts |
-|-----------|-------------|--------|--------------|------------------|-----------|
-| SuggestPathRouter | Route | Route registration and middleware | 1.4, 8.1, 8.2, 9.1 | Express Router (P0) | API |
-| SuggestPathOrchestrator | Service | Orchestrate all suggestion generators | 1.1-1.3, 3.3, 3.5, 5.5, 8.3, 9.2, 11.4, 13.2 | All generators (P0) | Service |
-| MemoSuggestionGenerator | Service | Generate memo path suggestion | 2.1-2.5, 6.2 | req.user (P0) | Service |
-| ContentAnalyzer | Service | Extract keywords + classify flow/stock (1st AI call) | 5.1-5.4 | OpenAI Feature (P0) | Service |
-| SearchCandidateRetriever | Service | Search ES and filter by score threshold | 3.1, 3.2, 3.5 | SearchService (P0) | Service |
-| CandidateEvaluator | Service | AI-evaluate candidates, propose paths, generate descriptions (2nd AI call) | 3.3, 6.3, 10.1-10.4, 11.1-11.3, 12.1-12.4, 13.1 | OpenAI Feature (P0), GrantResolver (P1) | Service |
-| CategorySuggestionGenerator | Service | Generate category suggestion (under review) | 4.1-4.4 | SearchService (P0), GrantResolver (P1) | Service |
-| GrantResolver | Service | Resolve parent page grant for a 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: SuggestPathOrchestrator — 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 handler lives in `features/suggest-path/server/routes/apiv3/index.ts`
-- Aggregation router in `apps/app/src/server/routes/apiv3/ai-tools/index.ts` imports the feature's route factory and mounts it: `router.use('/suggest-path', suggestPathRouteFactory(crowi))`
-- Aggregation router registered in `apps/app/src/server/routes/apiv3/index.js` as `router.use('/ai-tools', ...)`
-- Middleware chain: `accessTokenParser` → `loginRequiredStrictly` → `certifyAiService` → validators → `apiV3FormValidator` → handler
-
-### Service Layer
+- Content analysis and candidate evaluation are structurally sequential — Elasticsearch sits between them
+- Search-evaluate flow and category generation run in parallel
+- If content analysis fails → memo-only response
+- If candidate evaluation fails → memo + category (if available)
+- Category generator runs independently (under review)
 
-#### SuggestPathOrchestrator
+## Component Interfaces
 
-| Field | Detail |
-|-------|--------|
-| Intent | Orchestrate all suggestion generators, handle failures with graceful degradation |
-| Requirements | 1.1, 1.2, 1.3, 3.3, 3.5, 5.5, 8.3, 9.2, 11.4 |
-
-**Responsibilities & Constraints**
-
-- Always generate memo suggestion first (guaranteed fallback)
-- Invoke ContentAnalyzer, then pass results to SearchCandidateRetriever and CandidateEvaluator
-- Run category generation in parallel with the search-evaluate pipeline
-- Collect non-null results into suggestions array
-- On any Phase 2 failure, fall back to memo-only response
-
-**Dependencies**
-
-- Inbound: SuggestPathRouter — validated request (P0)
-- Outbound: All service layer components — suggestion generation (P0)
-
-**Contracts**: Service [x]
-
-##### Service Interface
+### Orchestrator
 
 ```typescript
 interface SuggestPathDependencies {
@@ -406,102 +198,21 @@ function generateSuggestions(
 ): 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
-- Phase 2: Orchestration function calls content analyzer, then fans out to search-evaluate pipeline and category generator in parallel. Dependencies injected for testability
-- Error handling: Catch Phase 2 failures at orchestration level, log, return memo-only
-- informationType mapping: When building `PathSuggestion` from `EvaluatedSuggestion`, the orchestrator attaches `ContentAnalysis.informationType` to each search-type suggestion (Req 13.1)
-
-#### MemoSuggestionGenerator
-
-| Field | Detail |
-|-------|--------|
-| Intent | Generate personal memo area path suggestion |
-| Requirements | 2.1, 2.2, 2.3, 2.4, 2.5, 6.2 |
-
-**Responsibilities & Constraints**
-
-- Check `disableUserPages` configuration via `crowi.configManager`
-- When user pages are enabled (default): Generate path `/user/{username}/memo/` using `userHomepagePath(user)` utility, set grant to `PageGrant.GRANT_OWNER` (4)
-- When user pages are disabled: Generate path under alternative namespace (e.g., `/memo/{username}/`), resolve grant from parent page
-- Set fixed description and label text
-- Always succeeds
-
-**Contracts**: Service [x]
-
-##### Service Interface
-
-```typescript
-function generateMemoSuggestion(user: IUserHasId): PathSuggestion;
-```
-
-- Preconditions: `user` has valid `username` field
-- Postconditions: Returns `PathSuggestion` with `type: 'memo'`, `grant: 4` when user pages enabled
-
-#### ContentAnalyzer (Phase 2)
-
-| Field | Detail |
-|-------|--------|
-| Intent | Extract keywords and classify content information type via GROWI AI (1st AI call) |
-| Requirements | 5.1, 5.2, 5.3, 5.4 |
-
-**Responsibilities & Constraints**
+- **Invariant**: Returns array with at least one suggestion (memo type), regardless of failures
+- **informationType mapping**: Attaches `ContentAnalysis.informationType` to each search-type suggestion (Req 13.1)
 
-- Accept content body string
-- Delegate to GROWI AI (existing OpenAI feature) for a single AI call that performs:
-  - Keyword extraction: 3-5 keywords, prioritizing proper nouns and technical terms
-  - Flow/stock classification: Determine if content is flow information (time-bound: meeting notes, diaries, reports) or stock information (reference: documentation, knowledge base)
-- Return structured `ContentAnalysis` result
-- Existing `instructionsForInformationTypes` in commons.ts serves as reference for AI prompting but is not the sole classification criterion (per reviewer feedback)
-
-**Dependencies**
-
-- External: OpenAI Feature module — AI inference (P0)
-
-**Contracts**: Service [x]
-
-##### Service Interface
+### Content Analyzer (1st AI Call)
 
 ```typescript
 interface ContentAnalysis {
-  keywords: string[];
+  keywords: string[];            // 1-5 keywords, proper nouns prioritized
   informationType: 'flow' | 'stock';
 }
 
 function analyzeContent(body: string): Promise<ContentAnalysis>;
 ```
 
-- Preconditions: `body` is non-empty string
-- Postconditions: Returns `ContentAnalysis` with 1-5 keywords and informationType
-- Error behavior: Throws on failure; caller handles fallback
-
-#### SearchCandidateRetriever (Phase 2)
-
-| Field | Detail |
-|-------|--------|
-| Intent | Search for related pages using keywords and filter by score threshold |
-| Requirements | 3.1, 3.2, 3.5 |
-
-**Responsibilities & Constraints**
-
-- Call `searchService.searchKeyword()` with extracted keywords
-- Filter results by Elasticsearch score threshold to retain only sufficiently relevant candidates
-- Return array of `SearchCandidate` objects with page path, snippet, and score
-- Return empty array if no results pass the threshold
-- Score threshold value is tunable (determined during implementation with real data)
-
-**Dependencies**
-
-- Outbound: SearchService — keyword search (P0)
-
-**Contracts**: Service [x]
-
-##### Service Interface
+### Search Candidate Retriever
 
 ```typescript
 interface SearchCandidate {
@@ -517,46 +228,14 @@ function retrieveSearchCandidates(
 ): Promise<SearchCandidate[]>;
 ```
 
-- Preconditions: `keywords` is non-empty array
-- Postconditions: Returns array of `SearchCandidate` (may be empty). All candidates have scores above the configured threshold
-- Note: Replaces the prior `SearchSuggestionGenerator` which performed top-1 selection and description generation. Those responsibilities moved to `CandidateEvaluator`
-
-#### CandidateEvaluator (Phase 2)
-
-| Field | Detail |
-|-------|--------|
-| Intent | Evaluate search candidates via GROWI AI for content-destination fit, propose paths, generate descriptions (2nd AI call) |
-| Requirements | 3.3, 6.3, 10.1, 10.2, 10.3, 10.4, 11.1, 11.2, 11.3, 12.1, 12.2, 12.3, 12.4, 13.1 |
-
-**Responsibilities & Constraints**
+- Filters by ES score threshold; returns empty array if no results pass
 
-- Accept content body, content analysis (from 1st AI call), and search candidates
-- Delegate to GROWI AI for a single AI call that performs:
-  - **Candidate evaluation**: Assess each candidate's suitability considering content relevance and flow/stock alignment
-  - **Path proposal**: For each suitable candidate, propose a save location using one of 3 structural patterns:
-    - (a) Parent directory of the matching page
-    - (b) Subdirectory under the matching page
-    - (c) Sibling directory alongside the matching page (may generate new path)
-  - **Description generation**: Generate a description for each suggestion explaining why the location is suitable
-  - **Ranking**: Order suggestions by content-destination fit
-- Flow/stock alignment is a ranking factor, not a hard filter (10.4)
-- Sibling pattern (c) may generate paths that don't yet exist in GROWI (12.2). Generated paths must be at the same hierarchy level as the matching search candidate page (12.4)
-- AI context budget: Pass candidate paths + ES snippets, NOT full page bodies (see architecture doc)
-- Resolve grant for each proposed path via GrantResolver after AI evaluation returns
-
-**Dependencies**
-
-- External: OpenAI Feature module — AI inference (P0)
-- Outbound: GrantResolver — grant resolution for proposed paths (P1)
-
-**Contracts**: Service [x]
-
-##### Service Interface
+### Candidate Evaluator (2nd AI Call)
 
 ```typescript
 interface EvaluatedSuggestion {
   path: string;        // Proposed directory path with trailing /
-  label: string;       // Display label
+  label: string;
   description: string; // AI-generated rationale
 }
 
@@ -567,179 +246,56 @@ function evaluateCandidates(
 ): Promise<EvaluatedSuggestion[]>;
 ```
 
-- Preconditions: `candidates` is non-empty array, `analysis` contains valid informationType
-- Postconditions: Returns array of `EvaluatedSuggestion` ordered by fit score (best first). May return empty array if no candidates are suitable. All paths end with `/`
-- Error behavior: Throws on failure; caller handles fallback
-- Note: Grant resolution is performed by the orchestrator after this function returns, not inside this function
-
-**Implementation Notes**
-
-- The 2nd AI call receives: POST body, informationType (from 1st call), and for each candidate: pagePath + ES snippet
-- The AI is instructed to consider the 3 path proposal patterns and select the most appropriate for each candidate
-- Existing `instructionsForInformationTypes` from commons.ts is referenced in the AI prompt as guidance for flow/stock assessment of candidate locations
-- AI prompt design details are deferred to implementation phase
-
-#### CategorySuggestionGenerator (Phase 2 — Under Review)
-
-| Field | Detail |
-|-------|--------|
-| Intent | Find matching top-level category directory for content |
-| Requirements | 4.1, 4.2, 4.3, 4.4 |
-
-> **Note**: This component has an existing implementation from the prior Phase 2 design. With the introduction of AI-based candidate evaluation, this component may overlap with CandidateEvaluator's path proposal capabilities. Whether to retain, merge, or remove is deferred to post-implementation reviewer discussion. The existing implementation is maintained as-is.
-
-**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
-- Generate description from top-level segment name (mechanical, no AI)
-- Resolve parent page grant via GrantResolver
-- Return `null` if no matching top-level pages found
-
-**Dependencies**
-
-- Outbound: SearchService — scoped keyword search (P0)
-- Outbound: GrantResolver — parent page grant lookup (P1)
-
-**Contracts**: Service [x]
+- Proposes paths using 3 structural patterns: (a) parent directory, (b) subdirectory, (c) sibling (may generate new paths at same hierarchy level)
+- Flow/stock alignment is a ranking factor, not a hard filter
+- Grant resolution performed by orchestrator after this returns
 
-##### 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
-
-#### 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)
-- For search/category/evaluated paths (Phase 2): query Page model for the parent page's grant
-- For new paths (sibling pattern): traverse upward to find the nearest existing ancestor page's grant
-
-**Dependencies**
-
-- External: Page Model (Mongoose) — page grant lookup (P0)
-
-**Contracts**: Service [x]
-
-##### Service Interface
+### Grant Resolver
 
 ```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
+- Traverses upward through ancestors for new paths (sibling pattern)
+- Returns `GRANT_OWNER` (4) as safe default if no ancestor found
 
-### Domain Model
+## Data Contracts
 
-No new database entities. The endpoint reads from existing models only.
+### API Contract
 
-**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
+| Method | Endpoint | Request | Response | Errors |
+|--------|----------|---------|----------|--------|
+| POST | `/_api/v3/ai-tools/suggest-path` | `SuggestPathRequest` | `SuggestPathResponse` | 400, 401, 403, 500 |
 
-#### Request Schema
+### Request / Response Types
 
 ```typescript
+// Request
 interface SuggestPathRequest {
-  body: string; // Page content for analysis
+  body: string; // Page content for analysis (required, non-empty)
 }
-```
-
-**Validation rules**:
-
-- `body`: Required, non-empty string
-- No endpoint-specific maximum length. Body size is governed by GROWI's global Express body-parser configuration
 
-#### Response Schema
-
-```typescript
+// Response
 type SuggestionType = 'memo' | 'search' | 'category';
 type InformationType = 'flow' | 'stock';
 
 interface PathSuggestion {
   type: SuggestionType;
-  path: string;              // Directory path with trailing '/'
-  label: string;             // Display label for the suggestion
-  description: string;       // Selection rationale (fixed for memo, AI-generated for search)
-  grant: number;             // Parent page grant (PageGrant value)
-  informationType?: InformationType; // Content's information type as classified by GROWI AI (Phase 2, search-based only)
+  path: string;                        // Directory path with trailing '/'
+  label: string;
+  description: string;                 // Fixed for memo, AI-generated for search
+  grant: number;                       // Parent page grant (PageGrant value)
+  informationType?: InformationType;   // Search-based only
 }
 
 interface SuggestPathResponse {
-  suggestions: PathSuggestion[];
+  suggestions: PathSuggestion[];       // Always ≥1 element (memo)
 }
 ```
 
-**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
-
-> **Resolved**: The `informationType` field has been added to `PathSuggestion` as an optional field for search-based suggestions. This supports the Client LLM Independence design principle (Requirement 13) by providing structured metadata that any client can use regardless of reasoning capability.
-
-#### Internal Types (Phase 2)
-
-```typescript
-interface ContentAnalysis {
-  keywords: string[];
-  informationType: 'flow' | 'stock';
-}
+**Invariants**: `path` ends with `/`, `grant` is a valid PageGrant value (1, 2, 4, or 5)
 
-interface SearchCandidate {
-  pagePath: string;
-  snippet: string;
-  score: number;
-}
-
-interface EvaluatedSuggestion {
-  path: string;        // Proposed directory path with trailing /
-  label: string;
-  description: string; // AI-generated rationale
-}
-```
-
-#### 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
+### Response Example
 
 ```json
 {
@@ -755,15 +311,7 @@ interface EvaluatedSuggestion {
       "type": "search",
       "path": "/tech-notes/React/state-management/",
       "label": "Save near related pages",
-      "description": "This area contains pages about React state management including Jotai and Redux. Your stock content fits well alongside this existing reference material.",
-      "grant": 1,
-      "informationType": "stock"
-    },
-    {
-      "type": "search",
-      "path": "/tech-notes/React/backend/",
-      "label": "New section for backend topics",
-      "description": "Related frontend pages exist nearby. This new section organizes your backend content as a sibling to the existing frontend knowledge.",
+      "description": "This area contains pages about React state management. Your stock content fits well alongside this existing reference material.",
       "grant": 1,
       "informationType": "stock"
     },
@@ -778,73 +326,38 @@ interface EvaluatedSuggestion {
 }
 ```
 
-Note: Phase 2 may return multiple `search`-type suggestions (one per evaluated candidate). The `category` suggestion appears if the CategorySuggestionGenerator finds a match (component under review).
-
-## Error Handling
-
-### Error Categories and Responses
-
-**User Errors (4xx)**:
+## Error Handling & Graceful Degradation
 
-| 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 |
+### User Errors (4xx)
 
-**System Errors — Graceful Degradation (returns 200)**:
+| Error | Status | Requirement |
+|-------|--------|-------------|
+| Missing or empty `body` | 400 | 9.1 |
+| No authentication | 401 | 8.2 |
+| AI service not enabled | 403 | 1.4 |
 
-| Error | Behavior | Fallback | Requirement |
-|-------|----------|----------|-------------|
-| Content analysis failure (1st AI call) | Log error, skip search pipeline | Memo suggestion only | 5.5 |
-| Search service failure | Log error, skip search-based suggestions | Memo + category (if available) | 3.5 |
-| Candidate evaluation failure (2nd AI call) | Log error, skip search-based suggestions | Memo + category (if available) | 11.4 |
-| Category generation failure | Log error, skip category suggestion | Memo + search-based (if available) | 4.4 |
+### Graceful Degradation (returns 200)
 
-**System Errors (5xx)**:
+| Failure | Fallback |
+|---------|----------|
+| Content analysis (1st AI call) | Memo only (skips entire search pipeline) |
+| Search service | Memo + category (if available) |
+| Candidate evaluation (2nd AI call) | Memo + category (if available) |
+| Category generation | Memo + search-based (if available) |
 
-| Error | Status | Response | Requirement |
-|-------|--------|----------|-------------|
-| Unexpected error | 500 | Generic error, no internal details | 9.2 |
-
-**Key decision**: Phase 2 failures degrade gracefully rather than returning errors. The memo suggestion is generated first and acts as guaranteed fallback. Each Phase 2 component fails independently — content analysis failure skips the entire search pipeline, but category generation can still proceed if it runs independently.
-
-## Testing Strategy
-
-### Unit Tests
-
-- `MemoSuggestionGenerator`: Correct path, grant, description for both user-pages-enabled and disabled cases
-- `ContentAnalyzer`: Correct keyword extraction, flow/stock classification, error propagation
-- `SearchCandidateRetriever`: Score threshold filtering, empty result handling, candidate structure
-- `CandidateEvaluator`: Path proposal patterns (parent/subdirectory/sibling), description generation, ranking, flow/stock consideration, error propagation
-- `CategorySuggestionGenerator`: Top-level extraction, description, grant, empty result handling
-- `GrantResolver`: Returns correct grant from page, default grant when page not found, ancestor traversal for new paths
-- `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: Full pipeline — content analysis → search → candidate evaluation → response with multiple suggestion types
-- Phase 2: Content analysis fails → memo-only fallback
-- Phase 2: Search returns nothing → search-based suggestions omitted, category may still appear
-- Phase 2: Candidate evaluation fails → memo + category fallback
-- Phase 2: All Phase 2 components fail → memo-only response
-
-### Performance (Phase 2)
-
-- Content analysis (1st AI call) latency under typical content sizes
-- Candidate evaluation (2nd AI call) latency with varying numbers of candidates
-- Total end-to-end latency for the 2-AI-call flow
-- Parallel execution: search-evaluate pipeline vs category generation
+Each component fails independently. Memo is always generated first as guaranteed fallback.
 
 ## 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
-- **AI prompt injection**: Content body is user-provided and passed to AI. AI prompts should be structured to minimize prompt injection risk (system prompt + user content separation)
-- **Information leakage**: Error responses use generic messages per requirement 9.2. No stack traces or internal paths exposed
+- **Authorization**: 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
+- **AI prompt injection**: System prompt and user content are separated to minimize prompt injection risk
+- **Information leakage**: Error responses use generic messages (Req 9.2)
+
+## Performance Considerations
+
+- Content analysis and candidate evaluation are sequential (ES sits between) — 2 AI roundtrips minimum
+- Search-evaluate pipeline and category generation run in parallel to minimize total latency
+- ES snippets (not full page bodies) are passed to AI to manage context budget
+- Score threshold filtering reduces the number of candidates passed to the 2nd AI call

+ 0 - 231
.kiro/specs/suggest-path/gap-analysis.md

@@ -1,231 +0,0 @@
-# Gap Analysis: suggest-path Feature
-
-## 1. Analysis Summary
-
-- **Scope**: The suggest-path feature's Phase 1 (memo) and Phase 2 (AI-powered search-based suggestions) are **both fully implemented** in terms of business logic. All 13 requirements have corresponding code.
-- **Key Gap**: The implementation resides in `server/routes/apiv3/ai-tools/` (a legacy flat route layout), **not** in the `features/` directory pattern that is the project standard for new features. The user explicitly requests migration to the features directory pattern.
-- **Architecture Pattern Mismatch**: Current code is organized as flat files in a route directory, mixing service logic, AI calls, search integration, type definitions, and tests in one folder (~15 files). The `features/` pattern separates these into `server/services/`, `server/routes/`, `interfaces/`, and co-located tests.
-- **No Functional Gaps**: All requirements (R1–R13) have working implementations with comprehensive test coverage (8 unit test files + 1 integration test). The AI pipeline (analyze → search → evaluate → suggest) is complete and includes graceful degradation.
-- **Recommendation**: The primary work is a **structural refactoring** — reorganize existing code into `features/suggest-path/` with proper separation of concerns, without changing business logic. This provides a clean foundation if further iteration on the feature is needed.
-
----
-
-## 2. Requirement-to-Asset Map
-
-| Req | Description | Status | Existing Assets | Gap |
-|-----|-------------|--------|-----------------|-----|
-| R1 | Path Suggestion API Endpoint | Implemented | `suggest-path.ts`, `ai-tools/index.ts` | Structure only: route lives in `routes/apiv3/ai-tools/` not `features/` |
-| R2 | Memo Path Suggestion (Phase 1) | Implemented | `generate-memo-suggestion.ts` | None |
-| R3 | Search-Based Path Suggestion | Implemented | `retrieve-search-candidates.ts`, `generate-suggestions.ts` | None |
-| R4 | Category-Based Path Suggestion (Under Review) | Implemented | `generate-category-suggestion.ts` | Retained as-is per requirements |
-| R5 | Content Analysis via GROWI AI | Implemented | `analyze-content.ts` | None |
-| R6 | Suggestion Description Generation | Implemented | AI-generated in `evaluate-candidates.ts`, fixed in `generate-memo-suggestion.ts` | None |
-| R7 | Grant Constraint Information | Implemented | `resolve-parent-grant.ts` | None |
-| R8 | Authentication and Authorization | Implemented | Middleware chain in `suggest-path.ts` | None |
-| R9 | Input Validation and Error Handling | Implemented | express-validator in `suggest-path.ts` | None |
-| R10 | Flow/Stock Information Type Awareness | Implemented | `analyze-content.ts` + `evaluate-candidates.ts` SYSTEM_PROMPT | None |
-| R11 | AI-Based Candidate Evaluation | Implemented | `evaluate-candidates.ts` with 3-pattern proposal | None |
-| R12 | Path Proposal Patterns | Implemented | Parent/subdirectory/sibling in SYSTEM_PROMPT | None |
-| R13 | Client LLM Independence | Implemented | `informationType` field in `PathSuggestion` type | None |
-
-**Legend**: All requirements are functionally complete. The only gap is structural — code organization does not follow the `features/` directory pattern.
-
----
-
-## 3. Current Code Structure (As-Is)
-
-```
-server/routes/apiv3/ai-tools/          # Legacy flat route directory
-├── index.ts                            # Router factory (7 lines)
-├── suggest-path.ts                     # Handler + middleware chain
-├── suggest-path-types.ts               # All type definitions
-├── generate-suggestions.ts             # Orchestrator (pipeline coordinator)
-├── generate-memo-suggestion.ts         # Phase 1: memo suggestion
-├── analyze-content.ts                  # AI call #1: keyword + flow/stock
-├── retrieve-search-candidates.ts       # ES search with score filtering
-├── evaluate-candidates.ts              # AI call #2: candidate evaluation
-├── generate-category-suggestion.ts     # Category suggestion (under review)
-├── generate-search-suggestion.ts       # Legacy/utility search helper
-├── resolve-parent-grant.ts             # Grant resolution by ancestor lookup
-├── suggest-path.spec.ts                # Handler unit tests
-├── suggest-path-integration.spec.ts    # Full integration tests
-├── generate-suggestions.spec.ts        # Orchestrator tests
-├── analyze-content.spec.ts             # AI analysis tests
-├── evaluate-candidates.spec.ts         # AI evaluation tests
-├── retrieve-search-candidates.spec.ts  # Search retrieval tests
-├── generate-memo-suggestion.spec.ts    # Memo tests
-├── generate-category-suggestion.spec.ts # Category tests
-└── resolve-parent-grant.spec.ts        # Grant resolution tests
-```
-
-**Observations**:
-- 20+ files in a single flat directory
-- Service logic (AI calls, search, grant resolution) mixed with route handler code
-- Type definitions are local to the route directory
-- No `server/` vs `interfaces/` separation
-- Tests are co-located (good), but the directory is bloated
-
----
-
-## 4. Target Code Structure (To-Be: Features Pattern)
-
-Based on the `features/openai/` reference pattern and user preference:
-
-```
-features/suggest-path/
-├── interfaces/                         # Shared types (server + client reusable)
-│   └── suggest-path-types.ts           # PathSuggestion, ContentAnalysis, etc.
-├── server/
-│   ├── routes/
-│   │   └── apiv3/
-│   │       └── index.ts                # Router factory, handler + middleware
-│   ├── services/
-│   │   ├── generate-suggestions.ts     # Orchestrator
-│   │   ├── generate-suggestions.spec.ts
-│   │   ├── generate-memo-suggestion.ts
-│   │   ├── generate-memo-suggestion.spec.ts
-│   │   ├── analyze-content.ts          # AI call #1
-│   │   ├── analyze-content.spec.ts
-│   │   ├── evaluate-candidates.ts      # AI call #2
-│   │   ├── evaluate-candidates.spec.ts
-│   │   ├── retrieve-search-candidates.ts
-│   │   ├── retrieve-search-candidates.spec.ts
-│   │   ├── generate-category-suggestion.ts
-│   │   ├── generate-category-suggestion.spec.ts
-│   │   ├── resolve-parent-grant.ts
-│   │   └── resolve-parent-grant.spec.ts
-│   └── integration-tests/
-│       └── suggest-path-integration.spec.ts
-└── index.ts                            # Feature barrel export (optional)
-```
-
----
-
-## 5. Implementation Approach Options
-
-### Option A: Refactor to Features Directory (Recommended)
-
-**What**: Move all suggest-path code from `server/routes/apiv3/ai-tools/` to `features/suggest-path/` with proper separation.
-
-**Steps**:
-1. Create `features/suggest-path/` directory structure
-2. Move types to `interfaces/`
-3. Move service modules (AI calls, search, grant, orchestrator) to `server/services/`
-4. Move/rewrite route handler to `server/routes/apiv3/`
-5. Update `server/routes/apiv3/index.js` mount point from `./ai-tools` to `~/features/suggest-path/server/routes/apiv3`
-6. Update all internal import paths
-7. Run tests, lint, typecheck to verify
-
-**Trade-offs**:
-- ✅ Aligns with project architecture standard (`features/openai/` pattern)
-- ✅ Clear separation: services vs. routes vs. interfaces
-- ✅ Easier to extend with client-side components later
-- ✅ Consistent with team convention
-- ❌ Significant import path changes across ~20 files
-- ❌ Risk of regressions in test setup (mock paths change)
-- ❌ Git history split on file moves
-
-### Option B: Keep Current Location, Reorganize Internally
-
-**What**: Keep code in `server/routes/apiv3/ai-tools/` but create subdirectories for services/types.
-
-**Trade-offs**:
-- ✅ Minimal import changes
-- ✅ Lower regression risk
-- ❌ Does **not** align with project architecture standard
-- ❌ Continues the pattern of mixing service logic in route directories
-
-### Option C: Hybrid — Feature Directory for New Code, Legacy for Existing
-
-**What**: Create `features/suggest-path/` for new additions, keep existing working code in place.
-
-**Trade-offs**:
-- ✅ Non-breaking for existing code
-- ❌ Split feature across two locations — confusing for developers
-- ❌ Deferred tech debt
-
----
-
-## 6. Detailed Gap Analysis: Features Pattern Migration
-
-### 6.1 Route Registration
-
-**Current** (`server/routes/apiv3/index.js`):
-```javascript
-import { factory as aiToolsRouteFactory } from './ai-tools';
-router.use('/ai-tools', aiToolsRouteFactory(crowi));
-```
-
-**Target**:
-```javascript
-import { factory as suggestPathRouteFactory } from '~/features/suggest-path/server/routes/apiv3';
-router.use('/ai-tools', suggestPathRouteFactory(crowi));
-```
-
-**Gap**: The `ai-tools/index.ts` currently only mounts `suggest-path`. If `ai-tools` is solely for suggest-path, the entire namespace can be owned by the feature. If other ai-tools routes are planned, an aggregation router may be needed.
-
-### 6.2 Cross-Feature Dependencies
-
-The suggest-path code depends on:
-- **`features/openai/`**: `certifyAiService` middleware, `getClient` / `isStreamResponse`, `instructionsForInformationTypes`
-- **`features/external-user-group/`**: `ExternalUserGroupRelation` model
-- **`server/service/search.ts`**: `searchService.searchKeyword()`
-- **`server/models/`**: `Page` model (grant resolution), `UserGroupRelation`
-- **`@growi/core`**: `PageGrant`, `userHomepagePath`, `SCOPE`, `IUserHasId`
-
-These dependencies are already cross-feature imports and will work the same from `features/suggest-path/`. No dependency issues.
-
-### 6.3 Test Migration
-
-All 9 test files use:
-- `vi.mock()` with module path strings — **all mock paths must be updated**
-- `vi.hoisted()` for mock setup
-- Integration test uses `supertest` — mount path and factory import will change
-
-**Risk**: Medium — mock path updates are mechanical but error-prone. Must verify all tests pass after migration.
-
-### 6.4 Import Path Updates
-
-Approximately **50+ import statements** across production and test files will need path updates:
-- `./suggest-path-types` → `~/features/suggest-path/interfaces/suggest-path-types`
-- `./analyze-content` → `../services/analyze-content` (from route handler)
-- `~/features/openai/...` → remains the same (already feature-relative)
-
----
-
-## 7. Implementation Complexity & Risk
-
-| Aspect | Assessment |
-|--------|------------|
-| **Effort** | **S (1–3 days)** — Pure structural refactoring with no business logic changes. All code already written and tested. |
-| **Risk** | **Low** — Familiar tech (file moves + import rewrites), no architectural shifts, comprehensive existing test suite acts as safety net. |
-
-**Justification**:
-- No new functionality to implement (all R1–R13 done)
-- The refactoring is mechanical: create directories, move files, update imports
-- Existing test suite (unit + integration) provides confidence
-- Pattern is well-established in codebase (`features/openai/` as reference)
-
----
-
-## 8. Recommendations for Design Phase
-
-### Preferred Approach
-
-**Option A: Full migration to `features/suggest-path/`** is recommended because:
-1. It fulfills the user's explicit request for features directory pattern
-2. It aligns with the established project convention
-3. The effort is low (S) with low risk due to comprehensive test coverage
-4. It creates a clean foundation for any future suggest-path enhancements
-
-### Key Decisions for Design
-
-1. **API Path Preservation**: Keep `POST /_api/v3/ai-tools/suggest-path` unchanged — only internal file organization changes, not the public API.
-2. **`ai-tools` Router Ownership**: Determine if `features/suggest-path/` owns the `/ai-tools` route namespace, or if an aggregation router should remain in `routes/apiv3/`.
-3. **Barrel Exports**: Decide whether `features/suggest-path/index.ts` exports types/services for potential reuse by other features.
-4. **`generate-search-suggestion.ts`**: This file appears to be a legacy/alternate helper. Determine if it should be migrated or removed.
-
-### Research Items
-
-- **R4 (Category-Based Suggestion)**: Marked "under review" in requirements. Design phase should confirm whether to retain, merge, or remove the category type during migration.
-- **`ai-tools` Namespace**: Verify no other features plan to use `/_api/v3/ai-tools/` before assigning it to the suggest-path feature module.

+ 8 - 11
.kiro/specs/suggest-path/research.md

@@ -24,7 +24,7 @@
   - 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/`
+- **Implications**: suggest-path follows the handler factory pattern. Route factory in `features/ai-tools/suggest-path/server/routes/apiv3/`, aggregation router in `features/ai-tools/server/routes/apiv3/`
 
 ### OpenAI Feature Structure
 
@@ -35,7 +35,7 @@
   - 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.
+- **Implications**: suggest-path gates on AI-enabled config via `certifyAiService`. Code lives under `features/ai-tools/suggest-path/` with an aggregation router at `features/ai-tools/server/routes/apiv3/`.
 
 ### Grant System Constraints
 
@@ -73,7 +73,7 @@
 
 | 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 independent access control needs |
+| Route under `features/ai-tools/` | Feature-based directory with aggregation router | Clean separation, follows features pattern and `ai-tools` naming | — | **Selected** — aligns with project architecture and independent access control |
 | Route under `features/openai/` | Extend existing AI feature module | Reuses AI infrastructure, minimal setup | Provider-specific name, harder to separate for independent access control | 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 access control | Rejected in review — yuki requested separation |
 
@@ -86,10 +86,9 @@
   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 access control
-- **Trade-offs**: Requires new directory and route registration. Namespace is tentative (pending yuki confirmation)
-- **Follow-up**: Confirm `ai-tools` namespace with yuki
+- **Selected Approach**: `/_api/v3/ai-tools/suggest-path` under `features/ai-tools/suggest-path/`
+- **Rationale**: Provider-agnostic, enables independent access control, follows features directory pattern
+- **Trade-offs**: Aggregation router at `features/ai-tools/server/routes/apiv3/` allows future ai-tools features under the same namespace
 
 ### Decision: Phase 1 Handler Simplicity
 
@@ -116,10 +115,8 @@
 
 ## 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
+- **Large content body performance**: Sending full content for AI keyword extraction may be slow. Mitigation: fallback to memo-only if extraction fails
+- **Search service dependency**: Depends on Elasticsearch being available. Mitigation: graceful degradation — return memo suggestion if search fails
 
 ## References
 

+ 4 - 3
.kiro/specs/suggest-path/spec.json

@@ -1,9 +1,9 @@
 {
   "feature_name": "suggest-path",
   "created_at": "2026-02-10T12:00:00Z",
-  "updated_at": "2026-02-20T08:15:00Z",
+  "updated_at": "2026-03-23T00:00:00Z",
   "language": "en",
-  "phase": "tasks-generated",
+  "phase": "implementation-complete",
   "approvals": {
     "requirements": {
       "generated": true,
@@ -18,5 +18,6 @@
       "approved": true
     }
   },
-  "ready_for_implementation": true
+  "ready_for_implementation": true,
+  "cleanup_completed": true
 }