suggest-path(crowi: Crowi) => RequestHandler[]) for API routesai-tools namespace does not exist yet; closest is /openai under features/openai/page-grant.ts — GRANT_OWNER children must share the same ownersearchService.searchKeyword() accepts keyword string and returns scored results with page metadata@growi/core (userHomepagePath, isUsersHomepage)apps/app/src/server/routes/apiv3/index.js, page/create-page.ts, features/openai/server/routes/index.tsrouter.use('/namespace', require('./namespace')(crowi)) or factory import(crowi: Crowi) => RequestHandler[] returning middleware chainaccessTokenParser → loginRequiredStrictly → validators → apiV3FormValidator → handlerres.apiv3(data) for success, res.apiv3Err(error, status) for errorsfeatures/ai-tools/suggest-path/server/routes/apiv3/, aggregation router in features/ai-tools/server/routes/apiv3/features/openai/server/routes/index.ts, middlewares/certify-ai-service.tsaiEnabled config via certifyAiService middlewarefeatures/openai/ not routes/apiv3/certifyAiService. Code lives under features/ai-tools/suggest-path/ with an aggregation router at features/ai-tools/server/routes/apiv3/.@growi/core PageGrant enum, apps/app/src/server/service/page-grant.tscalcApplicableGrantData(page, user) returns allowed grant types for a page/user/{username}/memo/), the user homepage /user/{username} is GRANT_OWNER(4) by default → memo path grant is fixed at 4apps/app/src/server/service/search.tssearchKeyword(keyword, nqName, user, userGroups, searchOpts) → [ISearchResult, delegatorName]_id, _score, _source, _highlightprefix: queries for path-scoped searchsearchKeyword with extracted keywords. Category search uses prefix:/ to scope to top-level. Need getUserRelatedGroups() for permission-correct results.@growi/core page-path-utils/index.tsuserHomepagePath(user) → /user/{username}isUsersHomepage(path) → boolean checkgetUsernameByPath(path) → extract username from pathuserHomepagePath(req.user) + /memo/ for memo suggestion path| Option | Description | Strengths | Risks / Limitations | Notes |
|---|---|---|---|---|
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 |
/openai/suggest-path — groups with AI features but provider-specific/page/suggest-path — close to page creation but cannot gate independently/ai-tools/suggest-path — new provider-agnostic namespace/_api/v3/ai-tools/suggest-path under features/ai-tools/suggest-path/features/ai-tools/server/routes/apiv3/ allows future ai-tools features under the same namespacefeatures/openai/ infrastructure for keyword extractionGenerateSuggestionsDeps pattern — a deps parameter containing 5 callback functions injected into the orchestrator for testabilityvi.mock() for testing), added route handler boilerplate (10 lines wiring callbacks), and forced unnecessary abstractions like RetrieveSearchCandidatesOptionsdeps 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 modulesvi.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)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-allfindAllUserGroupIdsRelatedToUser() which returns ObjectIdLike[] (from @growi/core), and propagated it through the SearchService interface and all service functionsunknownapps/app/src/server/routes/apiv3/index.js — Route registration entry pointapps/app/src/server/routes/apiv3/page/create-page.ts — Reference handler patternapps/app/src/features/openai/server/routes/index.ts — AI feature route patternpackages/core/src/interfaces/page.ts — PageGrant enum definitionapps/app/src/server/service/page-grant.ts — Grant validation logicapps/app/src/server/service/search.ts — Search service interfacepackages/core/src/utils/page-path-utils/index.ts — User path utilities