From 3f25dbb11bda40c68b709ec7a700261b5f46bc3c Mon Sep 17 00:00:00 2001 From: catlog22 Date: Fri, 27 Feb 2026 09:45:28 +0800 Subject: [PATCH] feat: add injection preview functionality and enhance specs management - Implemented injection preview feature in InjectionControlTab with file listing and content preview. - Added new API endpoint for fetching injection preview data. - Introduced content length caching for performance optimization. - Enhanced spec loading to support category filtering. - Updated localization files for new features and terms. - Created new personal and project specs for coding style and architecture constraints. - Improved CLI options for category selection in spec commands. --- .ccw/personal/coding-style.md | 27 +++ .ccw/personal/tool-preferences.md | 25 +++ .ccw/specs/architecture-constraints.md | 32 +++ .ccw/specs/coding-conventions.md | 38 ++++ .../components/specs/GlobalSettingsTab.tsx | 66 +++--- .../components/specs/InjectionControlTab.tsx | 212 +++++++++++++++++- ccw/frontend/src/lib/api.ts | 50 +++++ ccw/frontend/src/locales/en/specs.json | 79 ++++--- ccw/frontend/src/locales/zh/specs.json | 67 +++--- ccw/src/cli.ts | 4 + ccw/src/commands/spec.ts | 8 +- ccw/src/core/routes/spec-routes.ts | 120 ++++++++-- ccw/src/core/server.ts | 3 +- ccw/src/tools/spec-index-builder.ts | 12 +- ccw/src/tools/spec-loader.ts | 25 ++- 15 files changed, 648 insertions(+), 120 deletions(-) create mode 100644 .ccw/personal/coding-style.md create mode 100644 .ccw/personal/tool-preferences.md create mode 100644 .ccw/specs/architecture-constraints.md create mode 100644 .ccw/specs/coding-conventions.md diff --git a/.ccw/personal/coding-style.md b/.ccw/personal/coding-style.md new file mode 100644 index 00000000..4d11e746 --- /dev/null +++ b/.ccw/personal/coding-style.md @@ -0,0 +1,27 @@ +--- +title: "Personal Coding Style" +dimension: personal +category: general +keywords: + - style + - preference +readMode: optional +priority: medium +--- + +# Personal Coding Style + +## Preferences + +- Describe your preferred coding style here +- Example: verbose variable names vs terse, functional vs imperative + +## Patterns I Prefer + +- List patterns you reach for most often +- Example: builder pattern, factory functions, tagged unions + +## Things I Avoid + +- List anti-patterns or approaches you dislike +- Example: deep inheritance hierarchies, magic strings diff --git a/.ccw/personal/tool-preferences.md b/.ccw/personal/tool-preferences.md new file mode 100644 index 00000000..3eadc0d5 --- /dev/null +++ b/.ccw/personal/tool-preferences.md @@ -0,0 +1,25 @@ +--- +title: "Tool Preferences" +dimension: personal +category: general +keywords: + - tool + - cli + - editor +readMode: optional +priority: low +--- + +# Tool Preferences + +## Editor + +- Preferred editor and key extensions/plugins + +## CLI Tools + +- Preferred shell, package manager, build tools + +## Debugging + +- Preferred debugging approach and tools diff --git a/.ccw/specs/architecture-constraints.md b/.ccw/specs/architecture-constraints.md new file mode 100644 index 00000000..4c55bb32 --- /dev/null +++ b/.ccw/specs/architecture-constraints.md @@ -0,0 +1,32 @@ +--- +title: "Architecture Constraints" +dimension: specs +category: planning +keywords: + - architecture + - module + - layer + - pattern +readMode: required +priority: high +--- + +# Architecture Constraints + +## Module Boundaries + +- Each module owns its data and exposes a public API +- No circular dependencies between modules +- Shared utilities live in a dedicated shared layer + +## Layer Separation + +- Presentation layer must not import data layer directly +- Business logic must be independent of framework specifics +- Configuration must be externalized, not hardcoded + +## Dependency Rules + +- External dependencies require justification +- Prefer standard library when available +- Pin dependency versions for reproducibility diff --git a/.ccw/specs/coding-conventions.md b/.ccw/specs/coding-conventions.md new file mode 100644 index 00000000..f134f9b7 --- /dev/null +++ b/.ccw/specs/coding-conventions.md @@ -0,0 +1,38 @@ +--- +title: "Coding Conventions" +dimension: specs +category: general +keywords: + - typescript + - naming + - style + - convention +readMode: required +priority: high +--- + +# Coding Conventions + +## Naming + +- Use camelCase for variables and functions +- Use PascalCase for classes and interfaces +- Use UPPER_SNAKE_CASE for constants + +## Formatting + +- 2-space indentation +- Single quotes for strings +- Trailing commas in multi-line constructs + +## Patterns + +- Prefer composition over inheritance +- Use early returns to reduce nesting +- Keep functions under 30 lines when practical + +## Error Handling + +- Always handle errors explicitly +- Prefer typed errors over generic catch-all +- Log errors with sufficient context diff --git a/ccw/frontend/src/components/specs/GlobalSettingsTab.tsx b/ccw/frontend/src/components/specs/GlobalSettingsTab.tsx index 32966f3b..fa87f7c2 100644 --- a/ccw/frontend/src/components/specs/GlobalSettingsTab.tsx +++ b/ccw/frontend/src/components/specs/GlobalSettingsTab.tsx @@ -5,6 +5,7 @@ import { useState, useEffect } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { useIntl } from 'react-intl'; import { toast } from 'sonner'; import { Settings, RefreshCw } from 'lucide-react'; import { @@ -113,6 +114,7 @@ const settingsKeys = { // ========== Component ========== export function GlobalSettingsTab() { + const { formatMessage } = useIntl(); const queryClient = useQueryClient(); // Local state for immediate UI feedback @@ -149,10 +151,13 @@ export function GlobalSettingsTab() { mutationFn: updateSystemSettings, onSuccess: (data) => { queryClient.setQueryData(settingsKeys.settings(), data.settings); - toast.success('Settings saved successfully'); + toast.success(formatMessage({ id: 'specs.injection.saveSuccess', defaultMessage: 'Settings saved successfully' })); }, onError: (error) => { - toast.error(`Failed to save settings: ${error.message}`); + toast.error(formatMessage( + { id: 'specs.injection.saveError', defaultMessage: 'Failed to save settings: {error}' }, + { error: error.message } + )); }, }); @@ -194,12 +199,6 @@ export function GlobalSettingsTab() { const isLoading = isLoadingSettings || isLoadingStats; const hasError = settingsError || statsError; - // Dimension display config - const dimensionLabels: Record = { - specs: 'Specs', - personal: 'Personal', - }; - return (
{/* Personal Spec Defaults Card */} @@ -207,16 +206,20 @@ export function GlobalSettingsTab() {
- Personal Spec Defaults + + {formatMessage({ id: 'specs.settings.personalSpecDefaults', defaultMessage: 'Personal Spec Defaults' })} +
- These settings will be applied when creating new personal specs + {formatMessage({ id: 'specs.settings.personalSpecDefaultsDesc', defaultMessage: 'These settings will be applied when creating new personal specs' })}
{/* Default Read Mode */}
- +

- The default read mode for newly created personal specs + {formatMessage({ id: 'specs.settings.defaultReadModeHelp', defaultMessage: 'The default read mode for newly created personal specs' })}

{/* Auto Enable */}
- +

- Automatically enable newly created personal specs + {formatMessage({ id: 'specs.settings.autoEnableDescription', defaultMessage: 'Automatically enable newly created personal specs' })}

- Spec Statistics + + {formatMessage({ id: 'specs.settings.specStatistics', defaultMessage: 'Spec Statistics' })} + + +
+
+ + {previewData && ( + + {previewData.stats.count} {formatMessage({ id: 'specs.injection.files', defaultMessage: 'files' })} • {formatNumber(previewData.stats.totalLength)} {formatMessage({ id: 'specs.injection.characters', defaultMessage: 'characters' })} + + )} + + + + {previewLoading ? ( +
+ +
+ ) : ( +
+
+ {Object.entries(filesByDimension).map(([dim, files]) => ( +
+ + {expandedDimensions[dim] && ( +
+ {files.map((file) => ( +
+
+ {file.scope === 'global' ? ( + + ) : ( + + )} +
+
{file.title}
+
{file.file}
+
+
+
+ + {formatMessage({ id: `specs.priority.${file.priority}`, defaultMessage: file.priority })} + + + {formatNumber(file.contentLength)} + + +
+
+ ))} +
+ )} +
+ ))} +
+
+ )} +
+ + {/* Settings Card */} @@ -645,6 +826,23 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) { )}
+ + {/* File Preview Dialog */} + + + + + + {previewFile?.title} + + +
+
+              {previewFile?.content || formatMessage({ id: 'specs.content.noContent', defaultMessage: 'No content available' })}
+            
+
+
+
); } diff --git a/ccw/frontend/src/lib/api.ts b/ccw/frontend/src/lib/api.ts index f28289cc..b363d5c6 100644 --- a/ccw/frontend/src/lib/api.ts +++ b/ccw/frontend/src/lib/api.ts @@ -7273,6 +7273,8 @@ export interface SpecEntry { priority: 'critical' | 'high' | 'medium' | 'low'; keywords: string[]; scope: 'global' | 'project'; + /** Content length (body only, cached for performance) */ + contentLength: number; } /** @@ -7305,6 +7307,54 @@ export async function rebuildSpecIndex(projectPath?: string): Promise<{ success: }); } +/** + * Injection preview file info + */ +export interface InjectionPreviewFile { + file: string; + title: string; + dimension: string; + category: string; + scope: string; + readMode: string; + priority: string; + contentLength: number; + content?: string; +} + +/** + * Injection preview response + */ +export interface InjectionPreviewResponse { + files: InjectionPreviewFile[]; + stats: { + count: number; + totalLength: number; + maxLength: number; + percentage: number; + }; +} + +/** + * Get injection preview with file list + * @param mode - 'required' | 'all' | 'keywords' + * @param preview - Include content preview + * @param projectPath - Optional project path + */ +export async function getInjectionPreview( + mode: 'required' | 'all' | 'keywords' = 'required', + preview: boolean = false, + projectPath?: string +): Promise { + const params = new URLSearchParams(); + params.set('mode', mode); + params.set('preview', String(preview)); + if (projectPath) { + params.set('path', projectPath); + } + return fetchApi(`/api/specs/injection-preview?${params.toString()}`); +} + /** * Update spec frontmatter (toggle readMode) */ diff --git a/ccw/frontend/src/locales/en/specs.json b/ccw/frontend/src/locales/en/specs.json index 78675e09..e989341c 100644 --- a/ccw/frontend/src/locales/en/specs.json +++ b/ccw/frontend/src/locales/en/specs.json @@ -14,7 +14,9 @@ "dimension": { "specs": "Project Specs", - "personal": "Personal" + "personal": "Personal", + "roadmap": "Roadmap", + "changelog": "Changelog" }, "scope": { @@ -23,8 +25,10 @@ "project": "Project" }, "filterByScope": "Filter by scope:", + "filterByCategory": "Workflow stage:", "category": { + "all": "All", "general": "General", "exploration": "Exploration", "planning": "Planning", @@ -43,11 +47,38 @@ "install": "Install", "installed": "Installed", "installing": "Installing...", + "installedHooks": "Installed Hooks", "installedHooksDesc": "Manage your installed hooks configuration", "searchHooks": "Search hooks...", "noHooks": "No hooks installed. Install recommended hooks above.", + "actions": { + "view": "View Content", + "edit": "Edit", + "delete": "Delete", + "reset": "Reset", + "save": "Save", + "saving": "Saving..." + }, + + "status": { + "enabled": "Enabled", + "disabled": "Disabled" + }, + + "readMode": { + "required": "Required", + "optional": "Optional" + }, + + "priority": { + "critical": "Critical", + "high": "High", + "medium": "Medium", + "low": "Low" + }, + "spec": { "edit": "Edit Spec", "toggle": "Toggle Status", @@ -91,37 +122,11 @@ "failModeWarn": "Warn" }, - "actions": { - "edit": "Edit", - "delete": "Delete", - "reset": "Reset", - "save": "Save", - "saving": "Saving...", - "view": "View Content" - }, - - "status": { - "enabled": "Enabled", - "disabled": "Disabled" - }, - - "readMode": { - "required": "Required", - "optional": "Optional" - }, - - "priority": { - "critical": "Critical", - "high": "High", - "medium": "Medium", - "low": "Low" - }, - "hooks": { "dialog": { "createTitle": "Create Hook", "editTitle": "Edit Hook", - "description": "Configure the hook trigger event, command, and other settings." + "description": "Configure hook trigger event, command, and other settings." }, "fields": { "name": "Hook Name", @@ -149,9 +154,9 @@ "project": "Project" }, "failModes": { - "continue": "Continue", - "warn": "Show Warning", - "block": "Block Operation" + "continue": "Continue execution", + "warn": "Show warning", + "block": "Block operation" }, "validation": { "nameRequired": "Name is required", @@ -185,7 +190,7 @@ "metadata": "Metadata", "markdownContent": "Markdown Content", "noContent": "No content available", - "editHint": "Edit the full markdown content including frontmatter. Changes to frontmatter will be reflected in the spec metadata.", + "editHint": "Edit the full markdown content including frontmatter. Changes to frontmatter will be reflected in spec metadata.", "placeholder": "# Spec Title\n\nContent here..." }, @@ -221,10 +226,14 @@ "title": "Global Settings", "description": "Configure personal spec defaults and system settings", "personalSpecDefaults": "Personal Spec Defaults", + "personalSpecDefaultsDesc": "These settings will be applied when creating new personal specs", "defaultReadMode": "Default Read Mode", - "defaultReadModeHelp": "Default read mode for newly created personal specs", - "autoEnable": "Auto Enable", - "autoEnableDescription": "Automatically enable newly created personal specs" + "defaultReadModeHelp": "The default read mode for newly created personal specs", + "selectReadMode": "Select read mode", + "autoEnable": "Auto Enable New Specs", + "autoEnableDescription": "Automatically enable newly created personal specs", + "specStatistics": "Spec Statistics", + "totalSpecs": "Total: {count} spec files" }, "dialog": { diff --git a/ccw/frontend/src/locales/zh/specs.json b/ccw/frontend/src/locales/zh/specs.json index d7c07a7d..eb1cb05e 100644 --- a/ccw/frontend/src/locales/zh/specs.json +++ b/ccw/frontend/src/locales/zh/specs.json @@ -14,7 +14,9 @@ "dimension": { "specs": "项目规范", - "personal": "个人规范" + "personal": "个人规范", + "roadmap": "路线图", + "changelog": "变更日志" }, "scope": { @@ -23,8 +25,10 @@ "project": "项目" }, "filterByScope": "按范围筛选:", + "filterByCategory": "工作流阶段:", "category": { + "all": "全部", "general": "通用", "exploration": "探索", "planning": "规划", @@ -76,7 +80,6 @@ }, "spec": { - "view": "查看内容", "edit": "编辑规范", "toggle": "切换状态", "delete": "删除规范", @@ -89,23 +92,6 @@ "file": "文件路径" }, - "content": { - "edit": "编辑", - "view": "查看", - "metadata": "元数据", - "markdownContent": "Markdown 内容", - "noContent": "无内容", - "editHint": "编辑完整的 Markdown 内容(包括 frontmatter)。frontmatter 的更改将反映到规范元数据中。", - "placeholder": "# 规范标题\n\n内容..." - }, - - "common": { - "cancel": "取消", - "save": "保存", - "saving": "保存中...", - "close": "关闭" - }, - "hook": { "install": "安装", "uninstall": "卸载", @@ -137,9 +123,6 @@ }, "hooks": { - "installSuccess": "钩子安装成功", - "installError": "钩子安装失败", - "installAllSuccess": "所有钩子安装成功", "dialog": { "createTitle": "创建钩子", "editTitle": "编辑钩子", @@ -191,6 +174,26 @@ "hookFailMode": "命令执行失败时的处理方式" }, + "common": { + "cancel": "取消", + "save": "保存", + "delete": "删除", + "edit": "编辑", + "reset": "重置", + "confirm": "确认", + "close": "关闭" + }, + + "content": { + "edit": "编辑", + "view": "查看", + "metadata": "元数据", + "markdownContent": "Markdown 内容", + "noContent": "无可用内容", + "editHint": "编辑完整的 markdown 内容包括 frontmatter。对 frontmatter 的更改将反映在规范元数据中。", + "placeholder": "# 规范标题\n\n内容在这里..." + }, + "injection": { "title": "注入控制", "statusTitle": "当前注入状态", @@ -210,23 +213,37 @@ "warning": "接近限制", "normal": "正常", "characters": "字符", + "chars": "字符", "statsInfo": "统计信息", "requiredLength": "必读规范长度:", "matchedLength": "关键词匹配长度:", "remaining": "剩余空间:", "loadError": "加载统计数据失败", "saveSuccess": "设置已保存", - "saveError": "保存设置失败" + "saveError": "保存设置失败", + "filesList": "注入文件列表", + "files": "个文件" + }, + + "priority": { + "critical": "关键", + "high": "高", + "medium": "中", + "low": "低" }, "settings": { "title": "全局设置", "description": "配置个人规范默认值和系统设置", "personalSpecDefaults": "个人规范默认值", + "personalSpecDefaultsDesc": "创建新的个人规范时将应用这些设置", "defaultReadMode": "默认读取模式", "defaultReadModeHelp": "新创建的个人规范的默认读取模式", - "autoEnable": "自动启用", - "autoEnableDescription": "新创建的个人规范自动启用" + "selectReadMode": "选择读取模式", + "autoEnable": "自动启用新规范", + "autoEnableDescription": "自动启用新创建的个人规范", + "specStatistics": "规范统计", + "totalSpecs": "总计:{count} 个规范文件" }, "dialog": { diff --git a/ccw/src/cli.ts b/ccw/src/cli.ts index 91ee5208..f1adb019 100644 --- a/ccw/src/cli.ts +++ b/ccw/src/cli.ts @@ -303,6 +303,7 @@ export function run(argv: string[]): void { .command('spec [subcommand] [args...]') .description('Project spec management for conventions and guidelines') .option('--dimension ', 'Target dimension: specs, personal') + .option('--category ', 'Workflow stage: general, exploration, planning, execution') .option('--keywords ', 'Keywords for spec matching (CLI mode)') .option('--stdin', 'Read input from stdin (Hook mode)') .option('--json', 'Output as JSON') @@ -374,3 +375,6 @@ export function run(argv: string[]): void { program.parse(argv); } + +// Invoke CLI when run directly +run(process.argv); diff --git a/ccw/src/commands/spec.ts b/ccw/src/commands/spec.ts index 049d52e3..9826d397 100644 --- a/ccw/src/commands/spec.ts +++ b/ccw/src/commands/spec.ts @@ -11,6 +11,7 @@ import chalk from 'chalk'; interface SpecOptions { dimension?: string; + category?: string; keywords?: string; stdin?: boolean; json?: boolean; @@ -58,13 +59,13 @@ function getProjectPath(hookCwd?: string): string { // ============================================================================ /** - * Load action - load specs matching dimension/keywords. + * Load action - load specs matching dimension/category/keywords. * - * CLI mode: --dimension and --keywords options, outputs formatted markdown. + * CLI mode: --dimension, --category, --keywords options, outputs formatted markdown. * Hook mode: --stdin reads JSON {session_id, cwd, user_prompt}, outputs JSON {continue, systemMessage}. */ async function loadAction(options: SpecOptions): Promise { - const { stdin, dimension, keywords: keywordsInput } = options; + const { stdin, dimension, category, keywords: keywordsInput } = options; let projectPath: string; let stdinData: StdinData | undefined; @@ -96,6 +97,7 @@ async function loadAction(options: SpecOptions): Promise { const result = await loadSpecs({ projectPath, dimension: dimension as 'specs' | 'personal' | undefined, + category: category as 'general' | 'exploration' | 'planning' | 'execution' | undefined, keywords, outputFormat: stdin ? 'hook' : 'cli', stdinData, diff --git a/ccw/src/core/routes/spec-routes.ts b/ccw/src/core/routes/spec-routes.ts index c7e8c851..575fbfc1 100644 --- a/ccw/src/core/routes/spec-routes.ts +++ b/ccw/src/core/routes/spec-routes.ts @@ -151,7 +151,7 @@ export async function handleSpecRoutes(ctx: RouteContext): Promise { return true; } - // API: Get spec stats (dimensions count + injection length info) + // API: Get spec stats (optimized - uses cached contentLength) if (pathname === '/api/specs/stats' && req.method === 'GET') { const projectPath = url.searchParams.get('path') || initialPath; const resolvedPath = resolvePath(projectPath); @@ -186,18 +186,8 @@ export async function handleSpecRoutes(ctx: RouteContext): Promise { for (const entry of index.entries) { count++; - // Calculate content length by reading the file - const filePath = join(resolvedPath, entry.file); - let contentLength = 0; - try { - if (existsSync(filePath)) { - const rawContent = readFileSync(filePath, 'utf-8'); - // Strip frontmatter to get actual content length - const matter = (await import('gray-matter')).default; - const parsed = matter(rawContent); - contentLength = parsed.content.length; - } - } catch { /* ignore */ } + // Use cached contentLength instead of re-reading file + const contentLength = entry.contentLength || 0; if (entry.readMode === 'required') { requiredCount++; @@ -228,5 +218,109 @@ export async function handleSpecRoutes(ctx: RouteContext): Promise { return true; } + // API: Get injection preview (files list and content preview) + if (pathname === '/api/specs/injection-preview' && req.method === 'GET') { + const projectPath = url.searchParams.get('path') || initialPath; + const resolvedPath = resolvePath(projectPath); + const mode = url.searchParams.get('mode') || 'required'; // required | all | keywords + const preview = url.searchParams.get('preview') === 'true'; + + try { + const { getDimensionIndex, SPEC_DIMENSIONS } = await import( + '../../tools/spec-index-builder.js' + ); + + interface InjectionFile { + file: string; + title: string; + dimension: string; + category: string; + scope: string; + readMode: string; + priority: string; + contentLength: number; + content?: string; + } + + const files: InjectionFile[] = []; + let totalLength = 0; + + for (const dim of SPEC_DIMENSIONS) { + const index = await getDimensionIndex(resolvedPath, dim); + + for (const entry of index.entries) { + // Filter by mode + if (mode === 'required' && entry.readMode !== 'required') { + continue; + } + + const fileData: InjectionFile = { + file: entry.file, + title: entry.title, + dimension: entry.dimension, + category: entry.category || 'general', + scope: entry.scope, + readMode: entry.readMode, + priority: entry.priority, + contentLength: entry.contentLength || 0 + }; + + // Include content if preview requested + if (preview) { + const filePath = join(resolvedPath, entry.file); + if (existsSync(filePath)) { + try { + const rawContent = readFileSync(filePath, 'utf-8'); + const matter = (await import('gray-matter')).default; + const parsed = matter(rawContent); + fileData.content = parsed.content.trim(); + } catch { + fileData.content = ''; + } + } + } + + files.push(fileData); + totalLength += fileData.contentLength; + } + } + + // Sort by priority + const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 }; + files.sort((a, b) => + (priorityOrder[a.priority as keyof typeof priorityOrder] || 2) - + (priorityOrder[b.priority as keyof typeof priorityOrder] || 2) + ); + + // Get maxLength for percentage calculation + let maxLength = 8000; + const settingsPath = join(homedir(), '.claude', 'settings.json'); + if (existsSync(settingsPath)) { + try { + const rawSettings = readFileSync(settingsPath, 'utf-8'); + const settings = JSON.parse(rawSettings) as { + system?: { injectionControl?: { maxLength?: number } }; + }; + maxLength = settings?.system?.injectionControl?.maxLength || 8000; + } catch { /* ignore */ } + } + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + files, + stats: { + count: files.length, + totalLength, + maxLength, + percentage: Math.round((totalLength / maxLength) * 100) + } + })); + } catch (err) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: (err as Error).message })); + } + return true; + } + return false; } diff --git a/ccw/src/core/server.ts b/ccw/src/core/server.ts index 54caff69..b9603800 100644 --- a/ccw/src/core/server.ts +++ b/ccw/src/core/server.ts @@ -626,11 +626,12 @@ export async function startServer(options: ServerOptions = {}): Promise; + // Calculate content length (body only, without frontmatter) + const contentLength = parsed.content.length; // Extract and validate frontmatter fields const title = extractString(data, 'title'); if (!title) { // Title is required - use filename as fallback const fallbackTitle = basename(filePath, extname(filePath)); - return buildEntry(fallbackTitle, filePath, dimension, projectPath, data, scope); + return buildEntry(fallbackTitle, filePath, dimension, projectPath, data, scope, contentLength); } - return buildEntry(title, filePath, dimension, projectPath, data, scope); + return buildEntry(title, filePath, dimension, projectPath, data, scope, contentLength); } /** @@ -368,7 +372,8 @@ function buildEntry( dimension: string, projectPath: string, data: Record, - scope: 'global' | 'project' = 'project' + scope: 'global' | 'project' = 'project', + contentLength: number = 0 ): SpecIndexEntry { // Compute relative file path from project root using path.relative // Normalize to forward slashes for cross-platform consistency @@ -398,6 +403,7 @@ function buildEntry( readMode, priority, scope, + contentLength, }; } diff --git a/ccw/src/tools/spec-loader.ts b/ccw/src/tools/spec-loader.ts index 0decdfe7..364393f9 100644 --- a/ccw/src/tools/spec-loader.ts +++ b/ccw/src/tools/spec-loader.ts @@ -24,6 +24,7 @@ import { SPEC_DIMENSIONS, SPEC_CATEGORIES, type SpecDimension, + type SpecCategory, } from './spec-index-builder.js'; import { @@ -43,6 +44,8 @@ export interface SpecLoadOptions { projectPath: string; /** Specific dimension to load (loads all if omitted) */ dimension?: SpecDimension; + /** Workflow stage category filter (loads matching category specs) */ + category?: SpecCategory; /** Pre-extracted keywords (skips extraction if provided) */ keywords?: string[]; /** Output format: 'cli' for markdown, 'hook' for JSON */ @@ -138,7 +141,7 @@ const SPEC_PRIORITY_WEIGHT: Record = { * @returns SpecLoadResult with formatted content */ export async function loadSpecs(options: SpecLoadOptions): Promise { - const { projectPath, outputFormat, debug } = options; + const { projectPath, outputFormat, debug, category } = options; // Get injection control settings const maxLength = options.maxLength ?? 8000; @@ -149,6 +152,9 @@ export async function loadSpecs(options: SpecLoadOptions): Promise