Yuki Takei hace 2 semanas
padre
commit
40a51af60d

+ 25 - 29
.kiro/specs/suggest-path/design.md

@@ -171,43 +171,25 @@ sequenceDiagram
 ### Orchestrator
 
 ```typescript
-interface SuggestPathDependencies {
-  analyzeContent: (body: string) => Promise<ContentAnalysis>;
-  retrieveSearchCandidates: (
-    keywords: string[],
-    user: IUserHasId,
-    userGroups: PopulatedGrantedGroup[],
-  ) => Promise<SearchCandidate[]>;
-  evaluateCandidates: (
-    body: string,
-    analysis: ContentAnalysis,
-    candidates: SearchCandidate[],
-  ) => Promise<EvaluatedSuggestion[]>;
-  generateCategorySuggestion: (
-    keywords: string[],
-    user: IUserHasId,
-    userGroups: PopulatedGrantedGroup[],
-  ) => Promise<PathSuggestion | null>;
-  resolveParentGrant: (path: string) => Promise<number>;
-}
-
 function generateSuggestions(
   user: IUserHasId,
   body: string,
-  deps: SuggestPathDependencies,
+  userGroups: ObjectIdLike[],
+  searchService: SearchService,
 ): Promise<PathSuggestion[]>;
 ```
 
+- **No DI pattern**: Imports service functions directly; only `searchService` is passed as a parameter (the sole external dependency that cannot be statically imported)
 - **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)
 
 ### Content Analyzer (1st AI Call)
 
 ```typescript
-interface ContentAnalysis {
+type ContentAnalysis = {
   keywords: string[];            // 1-5 keywords, proper nouns prioritized
   informationType: 'flow' | 'stock';
-}
+};
 
 function analyzeContent(body: string): Promise<ContentAnalysis>;
 ```
@@ -215,29 +197,32 @@ function analyzeContent(body: string): Promise<ContentAnalysis>;
 ### Search Candidate Retriever
 
 ```typescript
-interface SearchCandidate {
+type SearchCandidate = {
   pagePath: string;
   snippet: string;
   score: number;
-}
+};
 
 function retrieveSearchCandidates(
   keywords: string[],
   user: IUserHasId,
-  userGroups: PopulatedGrantedGroup[],
+  userGroups: ObjectIdLike[],
+  searchService: SearchService,
 ): Promise<SearchCandidate[]>;
 ```
 
+- `searchService` is a direct positional argument (not wrapped in an options object)
+- Score threshold is a module-level constant (`SCORE_THRESHOLD = 5.0`)
 - Filters by ES score threshold; returns empty array if no results pass
 
 ### Candidate Evaluator (2nd AI Call)
 
 ```typescript
-interface EvaluatedSuggestion {
+type EvaluatedSuggestion = {
   path: string;        // Proposed directory path with trailing /
   label: string;
   description: string; // AI-generated rationale
-}
+};
 
 function evaluateCandidates(
   body: string,
@@ -250,10 +235,21 @@ function evaluateCandidates(
 - Flow/stock alignment is a ranking factor, not a hard filter
 - Grant resolution performed by orchestrator after this returns
 
+### Category Suggestion Generator
+
+```typescript
+function generateCategorySuggestion(
+  candidates: SearchCandidate[],
+): Promise<PathSuggestion | null>;
+```
+
+- Under review — may be merged into AI evaluation approach post-discussion
+- Returns `null` when no matching top-level pages are found
+
 ### Grant Resolver
 
 ```typescript
-function resolveParentGrant(path: string): Promise<number>;
+function resolveParentGrant(dirPath: string): Promise<number>;
 ```
 
 - Traverses upward through ancestors for new paths (sibling pattern)

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

@@ -1,60 +0,0 @@
-# Post-Implementation Gap Analysis: suggest-path Services
-
-## Summary
-
-Code review of `src/features/ai-tools/suggest-path/server/services/` identified three improvement areas in the implemented Phase 2 code:
-
-- **Over-abstraction**: `GenerateSuggestionsDeps` DI pattern and `RetrieveSearchCandidatesOptions` add unnecessary indirection for a feature-specific service layer
-- **Missing code comments**: `callLlmForJson` is a justified shared utility, but lacks documentation explaining why the abstraction exists
-- **Weak typing**: `userGroups: unknown` propagated through multiple layers can be narrowed to `ObjectIdLike[]`
-
-## Detailed Analysis
-
-### Gap 1: `GenerateSuggestionsDeps` Dependency Injection (Over-Abstraction)
-
-**Current state**: `generateSuggestions()` accepts a `deps` parameter containing 5 callback functions (`analyzeContent`, `retrieveSearchCandidates`, `evaluateCandidates`, `generateCategorySuggestion`, `resolveParentGrant`). The route handler wires these dependencies manually (10 lines of boilerplate).
-
-**Problem**: This is a testability-motivated DI pattern, but Vitest's `vi.mock()` achieves the same goal. Other modules in the same directory (e.g., `generate-memo-suggestion`) already use `vi.mock()` for testing. The `deps` pattern is inconsistent with the rest of the codebase and adds maintenance overhead.
-
-**Impact**: Route handler verbosity, extra type definition (`GenerateSuggestionsDeps`), and forces `retrieveSearchCandidates` to use a lambda wrapper for partial application of `searchService`.
-
-**Recommendation**: Remove `deps` parameter. Import service functions directly. Pass `searchService` as a direct argument (the only true external dependency). Test with `vi.mock()`.
-
-### Gap 2: `RetrieveSearchCandidatesOptions` (Over-Abstraction)
-
-**Current state**: `retrieveSearchCandidates()` takes an `options` object containing `searchService` (required) and `scoreThreshold` (optional, never overridden).
-
-**Problem**: `searchService` is effectively a required dependency, not an optional configuration. Wrapping it in an options object obscures this. `scoreThreshold` has a sensible default and no caller overrides it.
-
-**Impact**: The options pattern exists primarily to support the lambda wrapper in the route handler (which itself exists only because of `GenerateSuggestionsDeps`). Removing Gap 1 simplifies this naturally.
-
-**Recommendation**: Make `searchService` a direct positional argument. Keep `scoreThreshold` as a module-level constant (no options object needed unless a caller actually needs to override it).
-
-### Gap 3: `callLlmForJson` Missing Documentation
-
-**Current state**: `callLlmForJson` is a shared utility used by both `analyzeContent` and `evaluateCandidates`. This is a justified abstraction — it eliminates duplication of LLM client initialization, JSON parsing, and validation logic.
-
-**Problem**: No code comment explains why this utility exists or what it encapsulates. A future reader might question whether it's another instance of over-abstraction.
-
-**Recommendation**: Add a brief JSDoc comment explaining its purpose and the two consumers.
-
-### Gap 4: `userGroups: unknown` Type Weakness
-
-**Current state**: `userGroups` is typed as `unknown` in `SearchService` interface, `retrieveSearchCandidates`, `generateSuggestions`, and `GenerateSuggestionsDeps`.
-
-**Root cause**: The upstream `searchKeyword` method in `src/server/service/search.ts` has untyped parameters (legacy JS-to-TS migration). The suggest-path code used `unknown` as a safe catch-all.
-
-**Actual type**: `findAllUserGroupIdsRelatedToUser()` returns `ObjectIdLike[]` (from `@growi/core`). This type can be used in the `SearchService` interface and propagated through the suggest-path service layer.
-
-**Recommendation**: Update `SearchService.searchKeyword` parameter type from `unknown` to `ObjectIdLike[]`, and propagate through `retrieveSearchCandidates` and `generateSuggestions`.
-
-## Effort & Risk
-
-| Gap | Effort | Risk | Notes |
-|-----|--------|------|-------|
-| Gap 1 (remove deps DI) | S | Low | Straightforward refactor, tests need rewrite to use `vi.mock()` |
-| Gap 2 (remove options) | S | Low | Naturally follows from Gap 1 |
-| Gap 3 (add JSDoc) | S | Low | Comment-only change |
-| Gap 4 (type narrowing) | S | Low | Type change only, no runtime impact |
-
-**Overall**: S effort (1-2 days), Low risk. All changes are internal refactoring with no API surface or behavioral changes.

+ 26 - 129
.kiro/specs/suggest-path/requirements.md

@@ -2,179 +2,76 @@
 
 ## 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 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.
 
-The feature is delivered incrementally in two phases:
+The feature was delivered in two phases:
 
-- **Phase 1 (MVP)**: Personal memo path suggestion — establishes the endpoint, authentication, and response structure. Implemented first to provide immediate value. **Implemented.**
-- **Phase 2 (Full)**: AI-powered search-based path suggestions with flow/stock information classification, multi-candidate evaluation, and intelligent path proposal. GROWI AI extracts keywords and classifies content type, searches for related pages, then evaluates candidates and proposes optimal save locations including newly generated paths.
-
-Both phases are covered by this specification. Phase 1 is implemented. Phase 2 builds on the Phase 1 foundation.
+- **Phase 1 (MVP)**: Personal memo path suggestion — endpoint, authentication, and response structure.
+- **Phase 2 (Full)**: AI-powered search-based path suggestions with flow/stock information classification, multi-candidate evaluation, and intelligent path proposal (including new paths).
 
 ### Phase 2 Revision History
 
-Phase 2 requirements were revised based on reviewer feedback to incorporate:
-
-1. **Flow/stock information classification**: Content is classified as flow (time-bound) or stock (reference) information, and this classification informs save location evaluation.
-2. **Multi-candidate AI evaluation**: Instead of mechanically selecting the top-1 search result, multiple candidates are retrieved and evaluated by GROWI AI for content-destination fit.
-3. **Three-pattern path proposals**: The AI proposes paths using three structural patterns (parent directory, subdirectory, sibling page including new paths), enabling more precise save location suggestions.
-4. **AI-generated descriptions**: Phase 2 suggestion descriptions are generated by GROWI AI as part of candidate evaluation, providing richer context for user decision-making.
-
-Requirements 3–6 have been updated and new requirements 10–12 added to reflect these changes. Requirement 4 (Category-Based Path Suggestion) is under review — the existing implementation is retained, but its relationship to the new AI-based evaluation approach will be determined after implementation and reviewer discussion.
+Phase 2 was revised based on reviewer feedback: (1) flow/stock information classification, (2) multi-candidate AI evaluation instead of top-1 selection, (3) three-pattern path proposals (parent/subdirectory/sibling), (4) AI-generated descriptions.
 
 ## 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.
+- **Page creation/saving**: Uses existing `POST /_api/v3/page`. This feature only suggests *where* to save.
+- **Page title determination**: Handled via AI client-user dialogue.
 
 ## 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.
-
-### 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.
+**Summary**: POST endpoint at `/_api/v3/ai-tools/suggest-path` accepts a `body` field and returns an array of path suggestions. Each suggestion includes `type`, `path` (directory with trailing `/`), `label`, `description`, and `grant`. Endpoint is under a separate namespace from `/_api/v3/page/` for independent access control.
 
-#### Acceptance Criteria
+### Requirement 2: Memo Path Suggestion (Phase 1)
 
-1. When the client sends a valid request, the Suggest Path Service shall include a suggestion with type `memo`.
-2. When user pages are enabled (default), the Suggest Path Service shall generate the memo path under the user's home directory (pattern: `/user/{username}/memo/`).
-3. When user pages are disabled (`disableUserPages` is true), the Suggest Path Service shall generate the memo path under an alternative namespace (e.g., `/memo/{username}/`). The exact alternative path and grant handling are subject to confirmation.
-4. The Suggest Path Service shall set `grant` to `4` (owner only) for memo type suggestions when using the user home directory path.
-5. The Suggest Path Service shall provide a fixed descriptive text in the `description` field for memo type suggestions.
+**Summary**: Always includes a `memo` type suggestion as guaranteed fallback. Path is `/user/{username}/memo/` when user pages are enabled, or `/memo/{username}/` when disabled. Grant is `4` (owner only). Description is fixed text.
 
-### Requirement 3: Search-Based Path Suggestion (Phase 2) — Revised
+### Requirement 3: Search-Based Path Suggestion (Phase 2)
 
-**Objective:** As a user, I want save locations suggested based on search results that have been evaluated for relevance and content-destination fit, so that my content is organized alongside the most appropriate related 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 search results are returned, the Suggest Path Service shall filter candidates using an Elasticsearch score threshold to retain only sufficiently relevant results.
-3. When multiple candidates pass the score threshold, the Suggest Path Service shall pass all candidates to AI-based evaluation (see Requirement 11) rather than mechanically selecting a single top result.
-4. The Suggest Path Service shall include the parent page's `grant` value for each search-based suggestion.
-5. If no related pages are found or no candidates pass the score threshold, the Suggest Path Service shall omit search-based suggestions from the response.
+**Summary**: Searches for related pages using extracted keywords, filters by Elasticsearch score threshold, then passes all passing candidates to AI-based evaluation (Req 11). Includes parent page's grant. Omitted if no candidates pass the threshold.
 
 ### Requirement 4: Category-Based Path Suggestion (Phase 2) — Under Review
 
-**Objective:** As a user, I want a top-level category directory suggested, so that content can be organized under broad topic areas.
-
-> **Note**: This requirement has an existing implementation (Phase 2, prior revision). With the introduction of AI-based candidate evaluation (Requirements 11, 12), the `category` suggestion type may overlap with the search-based approach that now evaluates candidates holistically across all tree levels. Whether to retain `category` as a distinct type, merge it into `search`, or remove it will be determined after implementation and reviewer discussion. The existing implementation is maintained as-is until that decision is made.
-
-#### Acceptance Criteria (prior revision — retained)
-
-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 Analysis via GROWI AI (Phase 2) — Revised
-
-**Objective:** As a system operator, I want content analysis (keyword extraction and information type classification) centralized in GROWI AI, so that suggestion quality is consistent regardless of the calling client's capabilities.
-
-#### Acceptance Criteria
+**Summary**: Extracts top-level path segment from keyword-matched pages as a `category` type suggestion. Includes parent grant. Omitted if no match found.
 
-1. When the client sends content body, the Suggest Path Service shall delegate content analysis to GROWI AI in a single AI call that performs both keyword extraction and flow/stock information type classification.
-2. The Suggest Path Service shall extract 3–5 keywords from the content, prioritizing proper nouns and technical terms.
-3. The Suggest Path Service shall use extracted keywords (not raw content body) for search operations.
-4. The Suggest Path Service shall classify the content as either flow information (time-bound: meeting notes, diaries, reports) or stock information (reference: documentation, knowledge base articles).
-5. If content analysis fails or produces no usable keywords, the Suggest Path Service shall still return the memo suggestion (Phase 1 fallback).
+> **Note**: May overlap with the AI-based evaluation approach (Reqs 11, 12). Whether to retain, merge, or remove will be determined after reviewer discussion.
 
-### Requirement 6: Suggestion Description Generation — Revised
+### Requirement 5: Content Analysis via GROWI AI (Phase 2)
 
-**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.
+**Summary**: Single AI call performs keyword extraction (1-5 keywords, proper nouns prioritized) and flow/stock information type classification. Keywords (not raw content) are used for search. On failure, falls back to memo-only response.
 
-#### Acceptance Criteria
+### Requirement 6: Suggestion Description Generation
 
-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-based suggestions, the Suggest Path Service shall generate the `description` as part of AI-based candidate evaluation (see Requirement 11), providing context about content relevance and flow/stock alignment.
+**Summary**: Each suggestion includes a `description` field. Memo uses fixed text. Search-based suggestions use AI-generated descriptions from candidate evaluation (Req 11).
 
 ### 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).
+**Summary**: Each suggestion includes a `grant` field representing the parent page's grant value — the upper bound of settable permissions for child pages (a constraint, not a recommendation).
 
 ### 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.
+**Summary**: Requires valid API token or login session. Returns authentication error if missing. Uses authenticated user's identity for 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.
+**Summary**: Returns validation error for missing/empty `body`. Internal errors return appropriate responses without exposing system details.
 
 ### Requirement 10: Flow/Stock Information Type Awareness (Phase 2)
 
-**Objective:** As a user, I want save location suggestions that consider whether my content is time-bound (flow) or reference (stock) information, so that content is placed in locations that match its information type.
-
-#### Acceptance Criteria
-
-1. When evaluating search candidates, the Suggest Path Service shall consider the flow/stock alignment between the content being saved and the candidate save locations.
-2. When a candidate's path or surrounding content suggests flow characteristics (date-based paths, meeting-related terms), the Suggest Path Service shall treat it as a flow-oriented location.
-3. When a candidate's path or surrounding content suggests stock characteristics (topic-based paths, reference material), the Suggest Path Service shall treat it as a stock-oriented location.
-4. The Suggest Path Service shall use flow/stock alignment as one factor in candidate ranking, not as a hard filter — suggestions may include both matching and mismatched information types.
+**Summary**: Candidate evaluation considers flow/stock alignment between content and candidate locations. Flow = time-bound (date-based paths, meeting terms). Stock = reference (topic-based paths). Used as a ranking factor, not a hard filter.
 
 ### Requirement 11: AI-Based Candidate Evaluation and Ranking (Phase 2)
 
-**Objective:** As a user, I want search result candidates evaluated by AI for content-destination fit, so that the most appropriate save locations are prioritized in the suggestions.
-
-#### Acceptance Criteria
-
-1. When multiple search candidates are available, the Suggest Path Service shall evaluate each candidate's suitability by passing the content body along with each candidate's path and search snippet to GROWI AI.
-2. The Suggest Path Service shall rank candidates based on content-destination fit, considering content relevance and flow/stock information type alignment.
-3. The Suggest Path Service shall generate a description for each suggestion as part of the evaluation, explaining why the location is suitable.
-4. If AI-based candidate evaluation fails, the Suggest Path Service shall fall back to memo-only response.
+**Summary**: GROWI AI evaluates each candidate's suitability using content body, candidate path, and snippet. Ranks by content-destination fit considering relevance and flow/stock alignment. Generates description per suggestion. Falls back to memo-only on failure.
 
 ### Requirement 12: Path Proposal Patterns (Phase 2)
 
-**Objective:** As a user, I want path suggestions that include not only existing directories but also newly generated paths, so that my content can be organized in the most logical location even when no perfect existing directory matches.
-
-#### Acceptance Criteria
-
-1. When proposing save locations based on search results, the Suggest Path Service shall consider three structural patterns relative to each matching page: (a) parent directory of the matching page, (b) subdirectory under the matching page, (c) sibling directory alongside the matching page.
-2. When the sibling directory pattern is selected, the Suggest Path Service shall generate an appropriate new directory name based on the content being saved. This path may not yet exist in GROWI.
-3. The Suggest Path Service shall determine which pattern(s) are most appropriate based on the content-destination fit evaluation.
-4. When the sibling directory pattern is selected, the generated path shall be at the same hierarchy level as the matching search candidate page. The AI shall not generate paths deeper or shallower than the candidate's level.
+**Summary**: Three structural patterns relative to each matching page: (a) parent directory, (b) subdirectory, (c) sibling directory. Sibling pattern generates new directory names at the same hierarchy level as the candidate. AI determines the most appropriate pattern.
 
 ### Requirement 13: Client LLM Independence (Phase 2)
 
-**Objective:** As a system operator, I want the API response to be usable by any AI client regardless of its reasoning capability, so that suggestion quality does not degrade when accessed by less capable LLM clients.
-
-#### Design Rationale
-
-The suggest-path API is consumed by MCP clients powered by various LLM models, which may differ significantly in reasoning capability. To minimize the impact of client-side model performance differences:
-
-- Heavy reasoning tasks (content analysis, candidate evaluation, path proposal) are centralized in GROWI AI on the server side.
-- The API response includes structured data fields (not just natural language descriptions) so that even less capable clients can make correct decisions through simple field access.
-
-#### Acceptance Criteria
+**Summary**: Response includes both structured metadata (`informationType`, `type`, `grant`) and natural language (`description`) so any LLM client can use it regardless of reasoning capability. All reasoning-intensive operations are server-side.
 
-1. The Suggest Path Service shall include an `informationType` field (`'flow'` or `'stock'`) in each search-based suggestion, representing the content's information type as determined by GROWI AI.
-2. The Suggest Path Service shall provide both structured metadata (`informationType`, `type`, `grant`) and natural language context (`description`) in each suggestion, enabling clients to use whichever is appropriate for their capability level.
-3. The Suggest Path Service shall ensure that all reasoning-intensive operations (keyword extraction, flow/stock classification, candidate evaluation, path proposal, description generation) are performed server-side by GROWI AI, not delegated to the client.
+**Design Rationale**: MCP clients are powered by varying LLM models. Heavy reasoning is centralized in GROWI AI to prevent quality degradation with less capable clients.

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

@@ -118,6 +118,21 @@
 - **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
 
+## Post-Implementation Discoveries
+
+### Lesson: Avoid Testability-Motivated DI in Feature Services
+
+- **Context**: Initial Phase 2 implementation used a `GenerateSuggestionsDeps` pattern — a `deps` parameter containing 5 callback functions injected into the orchestrator for testability
+- **Problem**: The pattern was inconsistent with the rest of the codebase (other modules use `vi.mock()` for testing), added route handler boilerplate (10 lines wiring callbacks), and forced unnecessary abstractions like `RetrieveSearchCandidatesOptions`
+- **Resolution**: Removed `deps` pattern; service functions are imported directly. Only `searchService` is passed as a parameter (the sole external dependency that cannot be statically imported). Tests use `vi.mock()` — consistent with `generate-memo-suggestion` and other modules
+- **Guideline**: In this codebase, prefer `vi.mock()` over DI patterns for feature-specific service layers. Reserve DI for true cross-cutting concerns or when the dependency is a runtime-varying service instance (like `searchService`)
+
+### Lesson: Type Propagation from Legacy Code
+
+- **Context**: `searchService.searchKeyword()` in `src/server/service/search.ts` has untyped parameters (legacy JS-to-TS migration), so the suggest-path code initially used `userGroups: unknown` as a safe catch-all
+- **Resolution**: Traced the actual type from `findAllUserGroupIdsRelatedToUser()` which returns `ObjectIdLike[]` (from `@growi/core`), and propagated it through the `SearchService` interface and all service functions
+- **Guideline**: When integrating with legacy untyped services, trace the actual runtime type from the call site rather than defaulting to `unknown`
+
 ## References
 
 - [GROWI Search Internals](https://dev.growi.org/69842ea0cb3a20a69b0a1985) — Search feature internal architecture

+ 1 - 1
.kiro/specs/suggest-path/spec.json

@@ -3,7 +3,7 @@
   "created_at": "2026-02-10T12:00:00Z",
   "updated_at": "2026-03-23T00:00:00Z",
   "language": "en",
-  "phase": "refactoring",
+  "phase": "implementation-complete",
   "approvals": {
     "requirements": {
       "generated": true,