mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-11 17:21:03 +08:00
Update DDD commands (doc-generate, doc-refresh, sync), workflow commands (session/sync, spec/add, spec/setup, spec/load), ccw specs, personal preferences, and add generate-ddd-docs tool.
895 lines
28 KiB
Markdown
895 lines
28 KiB
Markdown
---
|
||
name: add
|
||
description: Add knowledge entries (bug fixes, code patterns, decisions, rules) to project specs interactively or automatically
|
||
argument-hint: "[-y|--yes] [--type <bug|pattern|decision|rule>] [--tag <tag>] [--dimension <specs|personal>] [--scope <global|project>] [--interactive] \"summary text\""
|
||
examples:
|
||
- /workflow:spec:add "Use functional components for all React code"
|
||
- /workflow:spec:add -y "No direct DB access from controllers" --type rule
|
||
- /workflow:spec:add --type bug --tag api "API 返回 502 Bad Gateway"
|
||
- /workflow:spec:add --type pattern --tag routing "添加新 API 路由标准流程"
|
||
- /workflow:spec:add --type decision --tag db "选用 PostgreSQL 作为主数据库"
|
||
- /workflow:spec:add --interactive
|
||
---
|
||
|
||
## Auto Mode
|
||
|
||
When `--yes` or `-y`: Auto-categorize and add entry without confirmation.
|
||
|
||
# Spec Add Command (/workflow:spec:add)
|
||
|
||
## Overview
|
||
|
||
Unified command for adding structured knowledge entries one at a time. Supports 4 knowledge types with optional extended fields for complex entries (bug debugging, code patterns, architecture decisions).
|
||
|
||
**Key Features**:
|
||
- 4 knowledge types: `bug`, `pattern`, `decision`, `rule`
|
||
- Unified entry format: `- [type:tag] summary (date)`
|
||
- Extended fields for complex types (bug/pattern/decision)
|
||
- Interactive wizard with type-specific field prompts
|
||
- Direct CLI mode with auto-detection
|
||
- Backward compatible: `[tag]` = `[rule:tag]` shorthand
|
||
- Auto-confirm mode (`-y`/`--yes`) for scripted usage
|
||
|
||
## Knowledge Type System
|
||
|
||
| Type | Purpose | Format | Target File |
|
||
|------|---------|--------|-------------|
|
||
| `bug` | Debugging experience (symptoms → cause → fix) | Extended | `learnings.md` |
|
||
| `pattern` | Reusable code patterns / reference implementations | Extended | `coding-conventions.md` |
|
||
| `decision` | Architecture / design decisions (ADR-lite) | Extended | `architecture-constraints.md` |
|
||
| `rule` | Hard constraints, conventions, general insights | Simple (single line) | By content (conventions / constraints) |
|
||
|
||
### Extended Fields Per Type
|
||
|
||
**bug** (core: 原因, 修复 | optional: 症状, 参考):
|
||
```markdown
|
||
- [bug:api] API 返回 502 Bad Gateway (2026-03-06)
|
||
- 原因: 路由处理器未在 server.ts 路由分发中注册
|
||
- 修复: 在路由分发逻辑中导入并调用 app.use(newRouter)
|
||
- 参考: src/server.ts:45
|
||
```
|
||
|
||
**pattern** (core: 场景, 代码 | optional: 步骤):
|
||
```markdown
|
||
- [pattern:routing] 添加新 API 路由标准流程 (2026-03-06)
|
||
- 场景: Express 应用新增业务接口
|
||
- 步骤: 1.创建 routes/xxx.ts → 2.server.ts import → 3.app.use() 挂载
|
||
- 代码:
|
||
```typescript
|
||
if (pathname.startsWith('/api/xxx')) {
|
||
if (await handleXxxRoutes(routeContext)) return;
|
||
}
|
||
```
|
||
```
|
||
|
||
**decision** (core: 决策, 理由 | optional: 背景, 备选, 状态):
|
||
```markdown
|
||
- [decision:db] 选用 PostgreSQL 作为主数据库 (2026-03-01)
|
||
- 决策: 使用 PostgreSQL 15
|
||
- 理由: JSONB 支持完善,PostGIS 扩展成熟
|
||
- 备选: MySQL(JSON弱) / SQLite(不适合并发)
|
||
- 状态: accepted
|
||
```
|
||
|
||
**rule** (no extended fields):
|
||
```markdown
|
||
- [rule:security] 禁止在代码中硬编码密钥或密码
|
||
```
|
||
|
||
### Entry Format Specification
|
||
|
||
```
|
||
Entry Line: - [type:tag] 摘要描述 (YYYY-MM-DD)
|
||
Extended: - key: value
|
||
Code Block: ```lang
|
||
code here
|
||
```
|
||
```
|
||
|
||
- **`type`**: Required. One of `bug`, `pattern`, `decision`, `rule`
|
||
- **`tag`**: Required. Domain tag (api, routing, schema, react, security, etc.)
|
||
- **`(date)`**: Required for bug/pattern/decision. Optional for rule.
|
||
- **Backward compat**: `- [tag] text` = `- [rule:tag] text`
|
||
|
||
### Parsing Regex
|
||
|
||
```javascript
|
||
// Entry line extraction
|
||
/^- \[(\w+):([\w-]+)\] (.*?)(?: \((\d{4}-\d{2}-\d{2})\))?$/
|
||
|
||
// Extended field extraction (per indented line)
|
||
/^\s{4}-\s([\w-]+):\s?(.*)/
|
||
```
|
||
|
||
## Use Cases
|
||
|
||
1. **Bug Fix**: Capture debugging experience immediately after fixing a bug
|
||
2. **Code Pattern**: Record reusable coding patterns discovered during implementation
|
||
3. **Architecture Decision**: Document important technical decisions with rationale
|
||
4. **Rule/Convention**: Add team conventions or hard constraints
|
||
5. **Interactive**: Guided wizard with type-specific field prompts
|
||
|
||
## Usage
|
||
```bash
|
||
/workflow:spec:add # Interactive wizard
|
||
/workflow:spec:add --interactive # Explicit interactive wizard
|
||
/workflow:spec:add "Use async/await instead of callbacks" # Direct mode (auto-detect → rule)
|
||
/workflow:spec:add --type bug --tag api "API 返回 502" # Bug with tag
|
||
/workflow:spec:add --type pattern --tag react "带状态函数组件" # Pattern with tag
|
||
/workflow:spec:add --type decision --tag db "选用 PostgreSQL" # Decision with tag
|
||
/workflow:spec:add -y "No direct DB access" --type rule --tag arch # Auto-confirm rule
|
||
/workflow:spec:add --scope global --dimension personal # Global personal spec
|
||
```
|
||
|
||
## Parameters
|
||
|
||
| Parameter | Type | Required | Default | Description |
|
||
|-----------|------|----------|---------|-------------|
|
||
| `summary` | string | Yes (unless `--interactive`) | - | Summary text for the knowledge entry |
|
||
| `--type` | enum | No | auto-detect | Type: `bug`, `pattern`, `decision`, `rule` |
|
||
| `--tag` | string | No | auto-detect | Domain tag (api, routing, schema, react, security, etc.) |
|
||
| `--dimension` | enum | No | Interactive | `specs` (project) or `personal` |
|
||
| `--scope` | enum | No | `project` | `global` or `project` (only for personal dimension) |
|
||
| `--interactive` | flag | No | - | Launch full guided wizard |
|
||
| `-y` / `--yes` | flag | No | - | Auto-categorize and add without confirmation |
|
||
|
||
### Legacy Parameter Mapping
|
||
|
||
For backward compatibility, old parameter values are internally mapped:
|
||
|
||
| Old Parameter | Old Value | Maps To |
|
||
|---------------|-----------|---------|
|
||
| `--type` | `convention` | `rule` |
|
||
| `--type` | `constraint` | `rule` |
|
||
| `--type` | `learning` | `bug` (if has cause/fix indicators) or `rule` (otherwise) |
|
||
| `--category` | `<value>` | `--tag <value>` |
|
||
|
||
### Suggested Tags
|
||
|
||
| Domain | Tags |
|
||
|--------|------|
|
||
| Backend | `api`, `routing`, `db`, `auth`, `middleware` |
|
||
| Frontend | `react`, `ui`, `state`, `css`, `a11y` |
|
||
| Infra | `deploy`, `ci`, `docker`, `perf`, `build` |
|
||
| Quality | `security`, `testing`, `lint`, `typing` |
|
||
| Architecture | `arch`, `schema`, `migration`, `pattern` |
|
||
|
||
Tags are freeform — any `[\w-]+` value is accepted.
|
||
|
||
## Execution Process
|
||
|
||
```
|
||
Input Parsing:
|
||
|- Parse: summary text (positional argument, optional if --interactive)
|
||
|- Parse: --type (bug|pattern|decision|rule)
|
||
|- Parse: --tag (domain tag)
|
||
|- Parse: --dimension (specs|personal)
|
||
|- Parse: --scope (global|project)
|
||
|- Parse: --interactive (flag)
|
||
+- Parse: -y / --yes (flag)
|
||
|
||
Step 1: Parse Input (with legacy mapping)
|
||
|
||
Step 2: Determine Mode
|
||
|- If --interactive OR no summary text → Full Interactive Wizard (Path A)
|
||
+- If summary text provided → Direct Mode (Path B)
|
||
|
||
Path A: Interactive Wizard
|
||
|- Step A1: Ask dimension (if not specified)
|
||
|- Step A2: Ask scope (if personal + scope not specified)
|
||
|- Step A3: Ask type (bug|pattern|decision|rule)
|
||
|- Step A4: Ask tag (domain tag)
|
||
|- Step A5: Ask summary (entry text)
|
||
|- Step A6: Ask extended fields (if bug/pattern/decision)
|
||
+- Continue to Step 3
|
||
|
||
Path B: Direct Mode
|
||
|- Step B1: Auto-detect type (if not specified) using detectType()
|
||
|- Step B2: Auto-detect tag (if not specified) using detectTag()
|
||
|- Step B3: Default dimension to 'specs' if not specified
|
||
+- Continue to Step 3
|
||
|
||
Step 3: Determine Target File
|
||
|- bug → .ccw/specs/learnings.md
|
||
|- pattern → .ccw/specs/coding-conventions.md
|
||
|- decision → .ccw/specs/architecture-constraints.md
|
||
|- rule → .ccw/specs/coding-conventions.md or architecture-constraints.md
|
||
+- personal → ~/.ccw/personal/ or .ccw/personal/
|
||
|
||
Step 4: Build Entry (entry line + extended fields)
|
||
|
||
Step 5: Validate and Write
|
||
|- Ensure target directory and file exist
|
||
|- Check for duplicates
|
||
|- Append entry to file
|
||
+- Run ccw spec rebuild
|
||
|
||
Step 6: Display Confirmation
|
||
+- If -y/--yes: Minimal output
|
||
+- Otherwise: Full confirmation with location details
|
||
```
|
||
|
||
## Implementation
|
||
|
||
### Step 1: Parse Input
|
||
|
||
```javascript
|
||
// Parse arguments
|
||
const args = $ARGUMENTS
|
||
const argsLower = args.toLowerCase()
|
||
|
||
// Extract flags
|
||
const autoConfirm = argsLower.includes('--yes') || argsLower.includes('-y')
|
||
const isInteractive = argsLower.includes('--interactive')
|
||
|
||
// Extract named parameters (support both new and legacy names)
|
||
const hasType = argsLower.includes('--type')
|
||
const hasTag = argsLower.includes('--tag') || argsLower.includes('--category')
|
||
const hasDimension = argsLower.includes('--dimension')
|
||
const hasScope = argsLower.includes('--scope')
|
||
|
||
let type = hasType ? args.match(/--type\s+(\w+)/i)?.[1]?.toLowerCase() : null
|
||
let tag = hasTag ? args.match(/--(?:tag|category)\s+([\w-]+)/i)?.[1]?.toLowerCase() : null
|
||
let dimension = hasDimension ? args.match(/--dimension\s+(\w+)/i)?.[1]?.toLowerCase() : null
|
||
let scope = hasScope ? args.match(/--scope\s+(\w+)/i)?.[1]?.toLowerCase() : null
|
||
|
||
// Extract summary text (everything before flags, or quoted string)
|
||
let summaryText = args
|
||
.replace(/--type\s+\w+/gi, '')
|
||
.replace(/--(?:tag|category)\s+[\w-]+/gi, '')
|
||
.replace(/--dimension\s+\w+/gi, '')
|
||
.replace(/--scope\s+\w+/gi, '')
|
||
.replace(/--interactive/gi, '')
|
||
.replace(/--yes/gi, '')
|
||
.replace(/-y\b/gi, '')
|
||
.replace(/^["']|["']$/g, '')
|
||
.trim()
|
||
|
||
// Legacy type mapping
|
||
if (type) {
|
||
const legacyMap = { 'convention': 'rule', 'constraint': 'rule' }
|
||
if (legacyMap[type]) {
|
||
type = legacyMap[type]
|
||
} else if (type === 'learning') {
|
||
// Defer to detectType() for finer classification
|
||
type = null
|
||
}
|
||
}
|
||
|
||
// Validate values
|
||
if (scope && !['global', 'project'].includes(scope)) {
|
||
console.log("Invalid scope. Use 'global' or 'project'.")
|
||
return
|
||
}
|
||
if (dimension && !['specs', 'personal'].includes(dimension)) {
|
||
console.log("Invalid dimension. Use 'specs' or 'personal'.")
|
||
return
|
||
}
|
||
if (type && !['bug', 'pattern', 'decision', 'rule'].includes(type)) {
|
||
console.log("Invalid type. Use 'bug', 'pattern', 'decision', or 'rule'.")
|
||
return
|
||
}
|
||
// Tags are freeform [\w-]+, no validation needed
|
||
```
|
||
|
||
### Step 2: Determine Mode
|
||
|
||
```javascript
|
||
const useInteractiveWizard = isInteractive || !summaryText
|
||
```
|
||
|
||
### Path A: Interactive Wizard
|
||
|
||
**If dimension not specified**:
|
||
```javascript
|
||
if (!dimension) {
|
||
const dimensionAnswer = AskUserQuestion({
|
||
questions: [{
|
||
question: "What type of spec do you want to create?",
|
||
header: "Dimension",
|
||
multiSelect: false,
|
||
options: [
|
||
{
|
||
label: "Project Spec",
|
||
description: "Knowledge entries for this project (stored in .ccw/specs/)"
|
||
},
|
||
{
|
||
label: "Personal Spec",
|
||
description: "Personal preferences across projects (stored in ~/.ccw/personal/)"
|
||
}
|
||
]
|
||
}]
|
||
})
|
||
dimension = dimensionAnswer.answers["Dimension"] === "Project Spec" ? "specs" : "personal"
|
||
}
|
||
```
|
||
|
||
**If personal dimension and scope not specified**:
|
||
```javascript
|
||
if (dimension === 'personal' && !scope) {
|
||
const scopeAnswer = AskUserQuestion({
|
||
questions: [{
|
||
question: "Where should this personal spec be stored?",
|
||
header: "Scope",
|
||
multiSelect: false,
|
||
options: [
|
||
{
|
||
label: "Global (Recommended)",
|
||
description: "Apply to ALL projects (~/.ccw/personal/)"
|
||
},
|
||
{
|
||
label: "Project-only",
|
||
description: "Apply only to this project (.ccw/personal/)"
|
||
}
|
||
]
|
||
}]
|
||
})
|
||
scope = scopeAnswer.answers["Scope"].includes("Global") ? "global" : "project"
|
||
}
|
||
```
|
||
|
||
**Ask type (if not specified)**:
|
||
```javascript
|
||
if (!type) {
|
||
const typeAnswer = AskUserQuestion({
|
||
questions: [{
|
||
question: "What type of knowledge entry is this?",
|
||
header: "Type",
|
||
multiSelect: false,
|
||
options: [
|
||
{
|
||
label: "Bug",
|
||
description: "Debugging experience: symptoms, root cause, fix (e.g., API 502 caused by...)"
|
||
},
|
||
{
|
||
label: "Pattern",
|
||
description: "Reusable code pattern or reference implementation (e.g., adding API routes)"
|
||
},
|
||
{
|
||
label: "Decision",
|
||
description: "Architecture or design decision with rationale (e.g., chose PostgreSQL because...)"
|
||
},
|
||
{
|
||
label: "Rule",
|
||
description: "Hard constraint, convention, or general insight (e.g., no direct DB access)"
|
||
}
|
||
]
|
||
}]
|
||
})
|
||
const typeLabel = typeAnswer.answers["Type"]
|
||
type = typeLabel.includes("Bug") ? "bug"
|
||
: typeLabel.includes("Pattern") ? "pattern"
|
||
: typeLabel.includes("Decision") ? "decision"
|
||
: "rule"
|
||
}
|
||
```
|
||
|
||
**Ask tag (if not specified)**:
|
||
```javascript
|
||
if (!tag) {
|
||
const tagAnswer = AskUserQuestion({
|
||
questions: [{
|
||
question: "What domain does this entry belong to?",
|
||
header: "Tag",
|
||
multiSelect: false,
|
||
options: [
|
||
{ label: "api", description: "API endpoints, HTTP, REST, routing" },
|
||
{ label: "arch", description: "Architecture, design patterns, module structure" },
|
||
{ label: "security", description: "Authentication, authorization, input validation" },
|
||
{ label: "perf", description: "Performance, caching, optimization" }
|
||
]
|
||
}]
|
||
})
|
||
tag = tagAnswer.answers["Tag"].toLowerCase().replace(/\s+/g, '-')
|
||
}
|
||
```
|
||
|
||
**Ask summary (entry text)**:
|
||
```javascript
|
||
if (!summaryText) {
|
||
const contentAnswer = AskUserQuestion({
|
||
questions: [{
|
||
question: "Enter the summary text for this entry:",
|
||
header: "Summary",
|
||
multiSelect: false,
|
||
options: [
|
||
{ label: "Custom text", description: "Type your summary using the 'Other' option below" },
|
||
{ label: "Skip", description: "Cancel adding an entry" }
|
||
]
|
||
}]
|
||
})
|
||
if (contentAnswer.answers["Summary"] === "Skip") return
|
||
summaryText = contentAnswer.answers["Summary"]
|
||
}
|
||
```
|
||
|
||
**Ask extended fields (if bug/pattern/decision)**:
|
||
```javascript
|
||
let extendedFields = {}
|
||
|
||
if (type === 'bug') {
|
||
// Core fields: 原因, 修复
|
||
const bugAnswer = AskUserQuestion({
|
||
questions: [
|
||
{
|
||
question: "Root cause of the bug (原因):",
|
||
header: "Cause",
|
||
multiSelect: false,
|
||
options: [
|
||
{ label: "Enter cause", description: "Type root cause via 'Other' option" },
|
||
{ label: "Skip", description: "Add later by editing the file" }
|
||
]
|
||
},
|
||
{
|
||
question: "How was it fixed (修复):",
|
||
header: "Fix",
|
||
multiSelect: false,
|
||
options: [
|
||
{ label: "Enter fix", description: "Type fix description via 'Other' option" },
|
||
{ label: "Skip", description: "Add later by editing the file" }
|
||
]
|
||
}
|
||
]
|
||
})
|
||
if (bugAnswer.answers["Cause"] !== "Skip") extendedFields['原因'] = bugAnswer.answers["Cause"]
|
||
if (bugAnswer.answers["Fix"] !== "Skip") extendedFields['修复'] = bugAnswer.answers["Fix"]
|
||
|
||
} else if (type === 'pattern') {
|
||
// Core field: 场景
|
||
const patternAnswer = AskUserQuestion({
|
||
questions: [{
|
||
question: "When should this pattern be used (场景):",
|
||
header: "UseCase",
|
||
multiSelect: false,
|
||
options: [
|
||
{ label: "Enter use case", description: "Type applicable scenario via 'Other' option" },
|
||
{ label: "Skip", description: "Add later by editing the file" }
|
||
]
|
||
}]
|
||
})
|
||
if (patternAnswer.answers["UseCase"] !== "Skip") extendedFields['场景'] = patternAnswer.answers["UseCase"]
|
||
|
||
} else if (type === 'decision') {
|
||
// Core fields: 决策, 理由
|
||
const decisionAnswer = AskUserQuestion({
|
||
questions: [
|
||
{
|
||
question: "What was decided (决策):",
|
||
header: "Decision",
|
||
multiSelect: false,
|
||
options: [
|
||
{ label: "Enter decision", description: "Type the decision via 'Other' option" },
|
||
{ label: "Skip", description: "Add later by editing the file" }
|
||
]
|
||
},
|
||
{
|
||
question: "Why was this chosen (理由):",
|
||
header: "Rationale",
|
||
multiSelect: false,
|
||
options: [
|
||
{ label: "Enter rationale", description: "Type the reasoning via 'Other' option" },
|
||
{ label: "Skip", description: "Add later by editing the file" }
|
||
]
|
||
}
|
||
]
|
||
})
|
||
if (decisionAnswer.answers["Decision"] !== "Skip") extendedFields['决策'] = decisionAnswer.answers["Decision"]
|
||
if (decisionAnswer.answers["Rationale"] !== "Skip") extendedFields['理由'] = decisionAnswer.answers["Rationale"]
|
||
}
|
||
```
|
||
|
||
### Path B: Direct Mode
|
||
|
||
**Auto-detect type if not specified**:
|
||
```javascript
|
||
function detectType(text) {
|
||
const t = text.toLowerCase()
|
||
|
||
// Bug indicators
|
||
if (/\b(bug|fix|错误|报错|502|404|500|crash|失败|异常|undefined|null pointer)\b/.test(t)) {
|
||
return 'bug'
|
||
}
|
||
|
||
// Pattern indicators
|
||
if (/\b(pattern|模式|模板|标准流程|how to|步骤|参考)\b/.test(t)) {
|
||
return 'pattern'
|
||
}
|
||
|
||
// Decision indicators
|
||
if (/\b(决定|选用|采用|decision|chose|选择|替代|vs|比较)\b/.test(t)) {
|
||
return 'decision'
|
||
}
|
||
|
||
// Default to rule
|
||
return 'rule'
|
||
}
|
||
|
||
function detectTag(text) {
|
||
const t = text.toLowerCase()
|
||
|
||
if (/\b(api|http|rest|endpoint|路由|routing|proxy)\b/.test(t)) return 'api'
|
||
if (/\b(security|auth|permission|密钥|xss|sql|注入)\b/.test(t)) return 'security'
|
||
if (/\b(database|db|sql|postgres|mysql|mongo|数据库)\b/.test(t)) return 'db'
|
||
if (/\b(react|component|hook|组件|jsx|tsx)\b/.test(t)) return 'react'
|
||
if (/\b(performance|perf|cache|缓存|slow|慢|优化)\b/.test(t)) return 'perf'
|
||
if (/\b(test|testing|jest|vitest|测试|coverage)\b/.test(t)) return 'testing'
|
||
if (/\b(architecture|arch|layer|模块|module|依赖)\b/.test(t)) return 'arch'
|
||
if (/\b(build|webpack|vite|compile|构建|打包)\b/.test(t)) return 'build'
|
||
if (/\b(deploy|ci|cd|docker|部署)\b/.test(t)) return 'deploy'
|
||
if (/\b(style|naming|命名|格式|lint|eslint)\b/.test(t)) return 'style'
|
||
if (/\b(schema|migration|迁移|版本)\b/.test(t)) return 'schema'
|
||
if (/\b(error|exception|错误处理|异常处理)\b/.test(t)) return 'error'
|
||
if (/\b(ui|css|layout|样式|界面)\b/.test(t)) return 'ui'
|
||
if (/\b(file|path|路径|目录|文件)\b/.test(t)) return 'file'
|
||
if (/\b(doc|comment|文档|注释)\b/.test(t)) return 'doc'
|
||
|
||
return 'general'
|
||
}
|
||
|
||
if (!type) {
|
||
type = detectType(summaryText)
|
||
}
|
||
if (!tag) {
|
||
tag = detectTag(summaryText)
|
||
}
|
||
if (!dimension) {
|
||
dimension = 'specs' // Default to project specs in direct mode
|
||
}
|
||
```
|
||
|
||
### Step 3: Ensure Guidelines File Exists
|
||
|
||
**Uses .ccw/specs/ directory (same as frontend/backend spec-index-builder)**
|
||
|
||
```bash
|
||
bash(test -f .ccw/specs/coding-conventions.md && echo "EXISTS" || echo "NOT_FOUND")
|
||
```
|
||
|
||
**If NOT_FOUND**, initialize spec system:
|
||
|
||
```bash
|
||
Bash('ccw spec init')
|
||
Bash('ccw spec rebuild')
|
||
```
|
||
|
||
### Step 4: Determine Target File
|
||
|
||
```javascript
|
||
const path = require('path')
|
||
const os = require('os')
|
||
|
||
let targetFile
|
||
let targetDir
|
||
|
||
if (dimension === 'specs') {
|
||
targetDir = '.ccw/specs'
|
||
|
||
if (type === 'bug') {
|
||
targetFile = path.join(targetDir, 'learnings.md')
|
||
} else if (type === 'decision') {
|
||
targetFile = path.join(targetDir, 'architecture-constraints.md')
|
||
} else if (type === 'pattern') {
|
||
targetFile = path.join(targetDir, 'coding-conventions.md')
|
||
} else {
|
||
// rule: route by content and tag
|
||
const isConstraint = /\b(禁止|no|never|must not|forbidden|不得|不允许)\b/i.test(summaryText)
|
||
const isQuality = /\b(test|coverage|lint|eslint|质量|测试覆盖|pre-commit|tsc|type.check)\b/i.test(summaryText)
|
||
|| ['testing', 'quality', 'lint'].includes(tag)
|
||
if (isQuality) {
|
||
targetFile = path.join(targetDir, 'quality-rules.md')
|
||
} else if (isConstraint) {
|
||
targetFile = path.join(targetDir, 'architecture-constraints.md')
|
||
} else {
|
||
targetFile = path.join(targetDir, 'coding-conventions.md')
|
||
}
|
||
}
|
||
} else {
|
||
// Personal specs
|
||
if (scope === 'global') {
|
||
targetDir = path.join(os.homedir(), '.ccw', 'personal')
|
||
} else {
|
||
targetDir = path.join('.ccw', 'personal')
|
||
}
|
||
|
||
// Type-based filename
|
||
const fileMap = { bug: 'learnings', pattern: 'conventions', decision: 'constraints', rule: 'conventions' }
|
||
targetFile = path.join(targetDir, `${fileMap[type]}.md`)
|
||
}
|
||
```
|
||
|
||
### Step 5: Build Entry
|
||
|
||
```javascript
|
||
function buildEntry(summary, type, tag, extendedFields) {
|
||
const date = new Date().toISOString().split('T')[0]
|
||
const needsDate = ['bug', 'pattern', 'decision'].includes(type)
|
||
|
||
// Entry line
|
||
let entry = `- [${type}:${tag}] ${summary}`
|
||
if (needsDate) {
|
||
entry += ` (${date})`
|
||
}
|
||
|
||
// Extended fields (indented with 4 spaces)
|
||
if (extendedFields && Object.keys(extendedFields).length > 0) {
|
||
for (const [key, value] of Object.entries(extendedFields)) {
|
||
entry += `\n - ${key}: ${value}`
|
||
}
|
||
}
|
||
|
||
return entry
|
||
}
|
||
```
|
||
|
||
### Step 6: Write Spec
|
||
|
||
```javascript
|
||
const fs = require('fs')
|
||
const matter = require('gray-matter') // YAML frontmatter parser
|
||
|
||
// Ensure directory exists
|
||
if (!fs.existsSync(targetDir)) {
|
||
fs.mkdirSync(targetDir, { recursive: true })
|
||
}
|
||
|
||
// ── Frontmatter check & repair ──
|
||
// Handles 3 cases:
|
||
// A) File doesn't exist → create with frontmatter
|
||
// B) File exists but no frontmatter → prepend frontmatter
|
||
// C) File exists with frontmatter → ensure keywords include current tag
|
||
|
||
const titleMap = {
|
||
'coding-conventions': 'Coding Conventions',
|
||
'architecture-constraints': 'Architecture Constraints',
|
||
'learnings': 'Learnings',
|
||
'quality-rules': 'Quality Rules',
|
||
'conventions': 'Personal Conventions',
|
||
'constraints': 'Personal Constraints'
|
||
}
|
||
|
||
function ensureFrontmatter(filePath, dim, sc, t, ty) {
|
||
const basename = path.basename(filePath, '.md')
|
||
const title = titleMap[basename] || basename
|
||
|
||
if (!fs.existsSync(filePath)) {
|
||
// Case A: Create new file with frontmatter
|
||
const content = `---
|
||
title: ${title}
|
||
readMode: optional
|
||
priority: medium
|
||
scope: ${dim === 'personal' ? sc : 'project'}
|
||
dimension: ${dim}
|
||
keywords: [${t}, ${ty}]
|
||
---
|
||
|
||
# ${title}
|
||
|
||
`
|
||
fs.writeFileSync(filePath, content, 'utf8')
|
||
return
|
||
}
|
||
|
||
// File exists — read and check frontmatter
|
||
const raw = fs.readFileSync(filePath, 'utf8')
|
||
let parsed
|
||
try {
|
||
parsed = matter(raw)
|
||
} catch {
|
||
parsed = { data: {}, content: raw }
|
||
}
|
||
|
||
const hasFrontmatter = raw.trimStart().startsWith('---')
|
||
|
||
if (!hasFrontmatter) {
|
||
// Case B: File exists but no frontmatter → prepend
|
||
const fm = `---
|
||
title: ${title}
|
||
readMode: optional
|
||
priority: medium
|
||
scope: ${dim === 'personal' ? sc : 'project'}
|
||
dimension: ${dim}
|
||
keywords: [${t}, ${ty}]
|
||
---
|
||
|
||
`
|
||
fs.writeFileSync(filePath, fm + raw, 'utf8')
|
||
return
|
||
}
|
||
|
||
// Case C: Frontmatter exists → ensure keywords include current tag
|
||
const existingKeywords = parsed.data.keywords || []
|
||
const newKeywords = [...new Set([...existingKeywords, t, ty])]
|
||
|
||
if (newKeywords.length !== existingKeywords.length) {
|
||
// Keywords changed — update frontmatter
|
||
parsed.data.keywords = newKeywords
|
||
const updated = matter.stringify(parsed.content, parsed.data)
|
||
fs.writeFileSync(filePath, updated, 'utf8')
|
||
}
|
||
}
|
||
|
||
ensureFrontmatter(targetFile, dimension, scope, tag, type)
|
||
|
||
// Read existing content
|
||
let content = fs.readFileSync(targetFile, 'utf8')
|
||
|
||
// Deduplicate: skip if summary text already exists in the file
|
||
if (content.includes(summaryText)) {
|
||
console.log(`
|
||
Entry already exists in ${targetFile}
|
||
Text: "${summaryText}"
|
||
`)
|
||
return
|
||
}
|
||
|
||
// Build the entry
|
||
const newEntry = buildEntry(summaryText, type, tag, extendedFields)
|
||
|
||
// Append the entry
|
||
content = content.trimEnd() + '\n' + newEntry + '\n'
|
||
fs.writeFileSync(targetFile, content, 'utf8')
|
||
|
||
// Rebuild spec index
|
||
Bash('ccw spec rebuild')
|
||
```
|
||
|
||
### Step 7: Display Confirmation
|
||
|
||
**If `-y`/`--yes` (auto mode)**:
|
||
```
|
||
Spec added: [${type}:${tag}] "${summaryText}" -> ${targetFile}
|
||
```
|
||
|
||
**Otherwise (full confirmation)**:
|
||
```
|
||
Entry created successfully
|
||
|
||
Type: ${type}
|
||
Tag: ${tag}
|
||
Summary: "${summaryText}"
|
||
Dimension: ${dimension}
|
||
Scope: ${dimension === 'personal' ? scope : 'project'}
|
||
${Object.keys(extendedFields).length > 0 ? `Extended fields: ${Object.keys(extendedFields).join(', ')}` : ''}
|
||
|
||
Location: ${targetFile}
|
||
|
||
Use 'ccw spec list' to view all specs
|
||
Tip: Edit ${targetFile} to add code examples or additional details
|
||
```
|
||
|
||
## Target File Resolution
|
||
|
||
### Project Specs (dimension: specs)
|
||
```
|
||
.ccw/specs/
|
||
|- coding-conventions.md <- pattern, rule (conventions)
|
||
|- architecture-constraints.md <- decision, rule (constraints)
|
||
|- learnings.md <- bug (debugging experience)
|
||
+- quality-rules.md <- quality rules
|
||
```
|
||
|
||
### Personal Specs (dimension: personal)
|
||
```
|
||
# Global (~/.ccw/personal/)
|
||
~/.ccw/personal/
|
||
|- conventions.md <- pattern, rule (all projects)
|
||
|- constraints.md <- decision, rule (all projects)
|
||
+- learnings.md <- bug (all projects)
|
||
|
||
# Project-local (.ccw/personal/)
|
||
.ccw/personal/
|
||
|- conventions.md <- pattern, rule (this project only)
|
||
|- constraints.md <- decision, rule (this project only)
|
||
+- learnings.md <- bug (this project only)
|
||
```
|
||
|
||
## Examples
|
||
|
||
### Interactive Wizard
|
||
```bash
|
||
/workflow:spec:add --interactive
|
||
# Prompts for: dimension -> scope (if personal) -> type -> tag -> summary -> extended fields
|
||
```
|
||
|
||
### Add a Bug Fix Experience
|
||
```bash
|
||
/workflow:spec:add --type bug --tag api "API 返回 502 Bad Gateway"
|
||
```
|
||
|
||
Result in `.ccw/specs/learnings.md`:
|
||
```markdown
|
||
- [bug:api] API 返回 502 Bad Gateway (2026-03-09)
|
||
```
|
||
|
||
With interactive extended fields:
|
||
```markdown
|
||
- [bug:api] API 返回 502 Bad Gateway (2026-03-09)
|
||
- 原因: 路由处理器未在 server.ts 路由分发中注册
|
||
- 修复: 在路由分发逻辑中导入并调用 app.use(newRouter)
|
||
```
|
||
|
||
### Add a Code Pattern
|
||
```bash
|
||
/workflow:spec:add --type pattern --tag routing "添加新 API 路由标准流程"
|
||
```
|
||
|
||
Result in `.ccw/specs/coding-conventions.md`:
|
||
```markdown
|
||
- [pattern:routing] 添加新 API 路由标准流程 (2026-03-09)
|
||
- 场景: Express 应用新增业务接口
|
||
```
|
||
|
||
### Add an Architecture Decision
|
||
```bash
|
||
/workflow:spec:add --type decision --tag db "选用 PostgreSQL 作为主数据库"
|
||
```
|
||
|
||
Result in `.ccw/specs/architecture-constraints.md`:
|
||
```markdown
|
||
- [decision:db] 选用 PostgreSQL 作为主数据库 (2026-03-09)
|
||
- 决策: 使用 PostgreSQL 15
|
||
- 理由: JSONB 支持完善,PostGIS 扩展成熟
|
||
```
|
||
|
||
### Add a Rule (Direct, Auto-detect)
|
||
```bash
|
||
/workflow:spec:add "Use async/await instead of callbacks"
|
||
```
|
||
|
||
Result in `.ccw/specs/coding-conventions.md`:
|
||
```markdown
|
||
- [rule:style] Use async/await instead of callbacks
|
||
```
|
||
|
||
### Add a Constraint Rule
|
||
```bash
|
||
/workflow:spec:add -y "No direct DB access from controllers" --type rule --tag arch
|
||
```
|
||
|
||
Result in `.ccw/specs/architecture-constraints.md`:
|
||
```markdown
|
||
- [rule:arch] No direct DB access from controllers
|
||
```
|
||
|
||
### Legacy Compatibility
|
||
```bash
|
||
# Old syntax still works
|
||
/workflow:spec:add "No ORM allowed" --type constraint --category architecture
|
||
# Internally maps to: --type rule --tag architecture
|
||
```
|
||
|
||
Result:
|
||
```markdown
|
||
- [rule:architecture] No ORM allowed
|
||
```
|
||
|
||
### Personal Spec
|
||
```bash
|
||
/workflow:spec:add --scope global --dimension personal --type rule --tag style "Prefer descriptive variable names"
|
||
```
|
||
|
||
Result in `~/.ccw/personal/conventions.md`:
|
||
```markdown
|
||
- [rule:style] Prefer descriptive variable names
|
||
```
|
||
|
||
## Error Handling
|
||
|
||
- **Duplicate Entry**: Warn and skip if summary text already exists in target file
|
||
- **Invalid Type**: Exit with error - must be 'bug', 'pattern', 'decision', or 'rule'
|
||
- **Invalid Scope**: Exit with error - must be 'global' or 'project'
|
||
- **Invalid Dimension**: Exit with error - must be 'specs' or 'personal'
|
||
- **Legacy Type**: Auto-map convention→rule, constraint→rule, learning→auto-detect
|
||
- **File not writable**: Check permissions, suggest manual creation
|
||
- **File Corruption**: Backup existing file before modification
|
||
|
||
## Related Commands
|
||
|
||
- `/workflow:spec:setup` - Initialize project with specs scaffold
|
||
- `/workflow:session:sync` - Quick-sync session work to specs and project-tech
|
||
- `/workflow:session:start` - Start a session
|
||
- `/workflow:session:complete` - Complete session (prompts for learnings)
|
||
- `ccw spec list` - View all specs
|
||
- `ccw spec load --category <cat>` - Load filtered specs
|
||
- `ccw spec rebuild` - Rebuild spec index
|