chore: update commands, specs, and ccw tools

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.
This commit is contained in:
catlog22
2026-03-09 23:20:39 +08:00
parent cbd1813ea7
commit 663620955c
13 changed files with 1942 additions and 455 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -1,3 +1,13 @@
---
title: Architecture Constraints
readMode: optional
priority: medium
category: general
scope: project
dimension: specs
keywords: [architecture, constraint, schema, compatibility, portability, design, arch]
---
# Architecture Constraints
## Schema Evolution

View File

@@ -1,3 +1,13 @@
---
title: Coding Conventions
readMode: optional
priority: medium
category: general
scope: project
dimension: specs
keywords: [coding, convention, style, naming, pattern, navigation, schema, error-handling, implementation, validation, clarity, doc]
---
# Coding Conventions
## Navigation & Path Handling
@@ -9,6 +19,7 @@
## Document Generation
- [architecture] For document generation systems, adopt Layer 3→2→1 pattern (components → features → indexes) for efficient incremental updates. (learned: 2026-03-07)
- [tools] When commands need to generate files with deterministic paths and frontmatter, use dedicated ccw tool endpoints (`ccw tool exec`) instead of raw `ccw cli -p` calls. Endpoints control output path, file naming, and structural metadata; CLI tools only generate prose content. (learned: 2026-03-09)
## Implementation Quality

View File

@@ -48,8 +48,9 @@ doc-index.json → tech-registry/*.md (L3) → feature-maps/*.md (L2) → _index
├── tech-registry/ ← Component documentation (Layer 3)
│ ├── _index.md
│ └── {component-slug}.md
└── sessions/
── _index.md ← Planning sessions index (Layer 1)
└── planning/ ← Planning sessions (Layer 1)
── _index.md ← Planning sessions index
└── {task-slug}-{date}/ ← Individual session folders
```
## Phase 1: Load & Validate
@@ -87,147 +88,82 @@ IF docs already exist AND NOT --force:
Ask user (unless -y → overwrite)
```
## Phase 2: Layer 3 Component Documentation
## Phase 2: Layer 3 -- Component Documentation
For each component in `technicalComponents[]`:
For each component in `technicalComponents[]`, call the generate_ddd_docs endpoint:
```bash
ccw cli -p "PURPOSE: Generate component documentation for {component.name}
TASK:
• Document component purpose and responsibility
• List exported symbols (classes, functions, types)
• Document dependencies (internal and external)
• Include code examples for key APIs
• Document integration points with other components
MODE: write
CONTEXT: @{component.codeLocations[].path}
EXPECTED: Markdown file with: Overview, API Reference, Dependencies, Usage Examples
CONSTRAINTS: Focus on public API | Include type signatures
" --tool gemini --mode write --cd .workflow/.doc-index/tech-registry/
for COMPONENT_ID in "${technicalComponents[@]}"; do
ccw tool exec generate_ddd_docs '{"strategy":"component","entityId":"'"$COMPONENT_ID"'","tool":"gemini"}'
done
```
The endpoint handles:
- Loading the component entity from doc-index.json
- Building YAML frontmatter (layer: 3, component_id, name, type, features, code_locations, generated_at)
- Constructing the CLI prompt with code context paths
- **Including Change History section**: Pull related entries from `doc-index.json.actions[]` where `affectedComponents` includes this component ID. Display as timeline (date, action type, description)
- Writing output to `.workflow/.doc-index/tech-registry/{slug}.md`
- Tool fallback (gemini -> qwen -> codex) on failure
Output: `.workflow/.doc-index/tech-registry/{component-slug}.md`
Frontmatter:
```markdown
---
layer: 3
component_id: tech-{slug}
name: ComponentName
type: service|controller|model|...
features: [feat-auth]
code_locations:
- path: src/services/auth.ts
symbols: [AuthService, AuthService.login]
generated_at: ISO8601
---
```
## Phase 3: Layer 2 -- Feature Documentation
Sections: Responsibility, Code Locations, Related Requirements, Architecture Decisions, Dependencies (in/out)
## Phase 3: Layer 2 — Feature Documentation
For each feature in `features[]`:
For each feature in `features[]`, call the generate_ddd_docs endpoint:
```bash
ccw cli -p "PURPOSE: Generate feature documentation for {feature.name}
TASK:
• Describe feature purpose and business value
• List requirements (from requirementIds)
• Document components involved (from techComponentIds)
• Include architecture decisions (from adrIds)
• Provide integration guide
MODE: write
CONTEXT: @.workflow/.doc-index/tech-registry/{related-components}.md
EXPECTED: Markdown file with: Overview, Requirements, Components, Architecture, Integration
CONSTRAINTS: Reference Layer 3 component docs | Business-focused language
" --tool gemini --mode write --cd .workflow/.doc-index/feature-maps/
for FEATURE_ID in "${features[@]}"; do
ccw tool exec generate_ddd_docs '{"strategy":"feature","entityId":"'"$FEATURE_ID"'","tool":"gemini"}'
done
```
The endpoint handles:
- Loading the feature entity from doc-index.json
- Building YAML frontmatter (layer: 2, feature_id, name, epic_id, status, requirements, components, tags, generated_at)
- Constructing the CLI prompt referencing Layer 3 component docs
- **Including Change History section**: Pull related entries from `doc-index.json.actions[]` where `affectedFeatures` includes this feature ID. Display as timeline (date, action type, description)
- Writing output to `.workflow/.doc-index/feature-maps/{slug}.md`
- Tool fallback (gemini -> qwen -> codex) on failure
Output: `.workflow/.doc-index/feature-maps/{feature-slug}.md`
Frontmatter:
```markdown
---
layer: 2
feature_id: feat-{slug}
name: Feature Name
epic_id: EPIC-NNN|null
status: implemented|in-progress|planned|partial
requirements: [REQ-001, REQ-002]
components: [tech-auth-service, tech-user-model]
depends_on_layer3: [tech-auth-service, tech-user-model]
tags: [auth, security]
generated_at: ISO8601
---
```
Sections: Overview, Requirements (with mapping status), Technical Components, Architecture Decisions, Change History
## Phase 4: Layer 1 — Index & Overview Documentation
## Phase 4: Layer 1 -- Index & Overview Documentation
### 4.1 Index Documents
Generate catalog files:
Generate catalog files for each subdirectory:
- **feature-maps/_index.md** — Feature overview table with status
- **tech-registry/_index.md** — Component registry table with types
- **action-logs/_index.md** — Action history table (empty initially for new projects)
```bash
# Feature maps index
ccw tool exec generate_ddd_docs '{"strategy":"index","entityId":"feature-maps","tool":"gemini"}'
# Tech registry index
ccw tool exec generate_ddd_docs '{"strategy":"index","entityId":"tech-registry","tool":"gemini"}'
# Action logs index
ccw tool exec generate_ddd_docs '{"strategy":"index","entityId":"action-logs","tool":"gemini"}'
# Planning sessions index
ccw tool exec generate_ddd_docs '{"strategy":"index","entityId":"planning","tool":"gemini"}'
```
Or generate all indexes at once (omit entityId):
```bash
ccw tool exec generate_ddd_docs '{"strategy":"index","tool":"gemini"}'
```
### 4.2 README.md (unless --skip-overview)
```bash
ccw cli -p "PURPOSE: Generate project README with overview and navigation
TASK:
• Project summary and purpose
• Quick start guide
• Navigation to features, components, and architecture
• Link to doc-index.json
MODE: write
CONTEXT: @.workflow/.doc-index/doc-index.json @.workflow/.doc-index/feature-maps/_index.md
EXPECTED: README.md with: Overview, Quick Start, Navigation, Links
CONSTRAINTS: High-level only | Entry point for new developers
" --tool gemini --mode write --cd .workflow/.doc-index/
ccw tool exec generate_ddd_docs '{"strategy":"overview","tool":"gemini"}'
```
### 4.3 ARCHITECTURE.md (unless --skip-overview)
```bash
ccw cli -p "PURPOSE: Generate architecture overview document
TASK:
• System design overview
• Component relationships and dependencies
• Key architecture decisions (from ADRs)
• Technology stack
MODE: write
CONTEXT: @.workflow/.doc-index/doc-index.json @.workflow/.doc-index/tech-registry/*.md
EXPECTED: ARCHITECTURE.md with: System Design, Component Diagram, ADRs, Tech Stack
CONSTRAINTS: Architecture-focused | Reference component docs for details
" --tool gemini --mode write --cd .workflow/.doc-index/
```
### 4.4 sessions/_index.md (unless --skip-overview)
```bash
ccw cli -p "PURPOSE: Generate planning sessions index
TASK:
• List all planning session folders chronologically
• Link to each session's plan.json
• Show session status and task count
MODE: write
CONTEXT: @.workflow/.doc-index/planning/*/plan.json
EXPECTED: sessions/_index.md with: Session List, Links, Status
CONSTRAINTS: Chronological order | Link to session folders
" --tool gemini --mode write --cd .workflow/.doc-index/sessions/
```
Layer 1 frontmatter:
```markdown
---
layer: 1
depends_on_layer2: [feat-auth, feat-orders]
generated_at: ISO8601
---
ccw tool exec generate_ddd_docs '{"strategy":"overview","entityId":"architecture","tool":"gemini"}'
```
## Phase 5: SCHEMA.md (unless --skip-schema)
@@ -235,17 +171,7 @@ generated_at: ISO8601
### 5.1 Generate Schema Documentation
```bash
ccw cli -p "PURPOSE: Document doc-index.json schema structure and versioning
TASK:
• Document current schema structure (all fields)
• Define versioning policy (semver: major.minor)
• Document migration protocol for version upgrades
• Provide examples for each schema section
MODE: write
CONTEXT: @.workflow/.doc-index/doc-index.json
EXPECTED: SCHEMA.md with: Schema Structure, Versioning Policy, Migration Protocol, Examples
CONSTRAINTS: Complete field documentation | Clear migration steps
" --tool gemini --mode write --cd .workflow/.doc-index/
ccw tool exec generate_ddd_docs '{"strategy":"schema","tool":"gemini"}'
```
### 5.2 Versioning Policy
@@ -284,7 +210,7 @@ Total: {N} documents generated
| `-y, --yes` | Auto-confirm all decisions |
| `--layer <3\|2\|1\|all>` | Generate specific layer only (default: all) |
| `--force` | Overwrite existing documents |
| `--skip-overview` | Skip README.md, ARCHITECTURE.md, sessions/_index.md |
| `--skip-overview` | Skip README.md, ARCHITECTURE.md, planning/_index.md |
| `--skip-schema` | Skip SCHEMA.md generation |
## Integration Points
@@ -293,3 +219,4 @@ Total: {N} documents generated
- **Called by**: `/ddd:scan` (after index assembly), `/ddd:index-build` (after index assembly)
- **Standalone**: Can be run independently on any project with existing doc-index.json
- **Output**: Complete document tree in `.workflow/.doc-index/`
- **Endpoint**: `ccw tool exec generate_ddd_docs` handles prompt construction, frontmatter, tool fallback, and file creation

View File

@@ -163,7 +163,7 @@ ccw cli -p "PURPOSE: Update project overview docs after feature changes
TASK:
• Update README.md feature list
• Update ARCHITECTURE.md if new components added
• Update sessions/_index.md with new planning sessions
• Update planning/_index.md with new planning sessions
MODE: write
CONTEXT: @.workflow/.doc-index/feature-maps/*.md @.workflow/.doc-index/doc-index.json
EXPECTED: Updated overview docs with current project state

View File

@@ -37,11 +37,42 @@ After completing a development task, synchronize the document index with actual
- `doc-index.json` must exist
- Git repository with committed or staged changes
## Phase 0: Consistency Validation
Before processing changes, verify that `doc-index.json` entries are consistent with actual code state.
### 0.1 Validate Code Locations
For each `technicalComponents[].codeLocations[]`:
- Verify file exists on disk
- If file was deleted/moved → flag for removal or update
- If file exists → verify listed `symbols[]` still exist (quick grep/AST check)
### 0.2 Validate Symbols
For components with `codeLocations[].symbols[]`:
- Check each symbol still exists in the referenced file
- Detect new exported symbols not yet tracked
- Report: `{N} stale symbols, {N} untracked symbols`
### 0.3 Validation Report
```
Consistency Check:
Components validated: {N}
Files verified: {N}
Stale references: {N} (files missing or symbols removed)
Untracked symbols: {N} (new exports not in index)
```
If stale references found: warn and auto-fix during Phase 3 updates.
If `--dry-run`: report only, no fixes.
## Phase 1: Change Detection
### 0.1 Schema Version Check (TASK-006)
### 1.0.1 Schema Version Check
Before processing changes, verify doc-index schema compatibility:
Before processing changes, verify doc-index.json schema compatibility:
```javascript
const docIndex = JSON.parse(Read('.workflow/.doc-index/doc-index.json'));
@@ -201,6 +232,7 @@ For each affected component in `doc-index.json`:
- Update `codeLocations` if file paths or line ranges changed
- Update `symbols` if new exports were added
- Add new `actionIds` entry
- **Auto-update `responsibility`**: If symbols changed (new methods/exports added or removed), re-infer responsibility from current symbols list using Gemini analysis. This prevents stale descriptions (e.g., responsibility still says "登录、注册" after adding logout support)
### 3.2 Register New Components

View File

@@ -65,11 +65,14 @@ Analyze context and produce two update payloads. Use LLM reasoning (current agen
```javascript
// ── Guidelines extraction ──
// Scan git diff + session for:
// - New patterns adopted → convention
// - Restrictions discovered → constraint
// - Surprises / gotchas → learning
// - Debugging experiences → bug
// - Reusable code patterns → pattern
// - Architecture/design decisions → decision
// - Conventions, constraints, insights → rule
//
// Output: array of { type, category, text }
// Output: array of { type, tag, text }
// type: 'bug' | 'pattern' | 'decision' | 'rule'
// tag: domain tag (api, routing, schema, security, etc.)
// RULE: Only extract genuinely reusable insights. Skip trivial/obvious items.
// RULE: Deduplicate against existing guidelines before adding.
@@ -118,7 +121,7 @@ console.log(`
── Sync Preview ──
Guidelines (${guidelineUpdates.length} items):
${guidelineUpdates.map(g => ` [${g.type}/${g.category}] ${g.text}`).join('\n') || ' (none)'}
${guidelineUpdates.map(g => ` [${g.type}:${g.tag}] ${g.text}`).join('\n') || ' (none)'}
Tech [${detectCategory(summary)}]:
${techEntry.title}
@@ -137,26 +140,102 @@ if (!autoYes) {
## Step 4: Write
```javascript
// ── Update specs/*.md ──
// Uses .ccw/specs/ directory (same as frontend/backend spec-index-builder)
if (guidelineUpdates.length > 0) {
// Map guideline types to spec files
const specFileMap = {
convention: '.ccw/specs/coding-conventions.md',
constraint: '.ccw/specs/architecture-constraints.md',
learning: '.ccw/specs/coding-conventions.md' // learnings appended to conventions
const matter = require('gray-matter') // YAML frontmatter parser
// ── Frontmatter check & repair helper ──
// Ensures target spec file has valid YAML frontmatter with keywords
// Uses gray-matter for robust parsing (handles malformed frontmatter, missing fields)
function ensureFrontmatter(filePath, tag, type) {
const titleMap = {
'coding-conventions': 'Coding Conventions',
'architecture-constraints': 'Architecture Constraints',
'learnings': 'Learnings',
'quality-rules': 'Quality Rules'
}
const basename = filePath.split('/').pop().replace('.md', '')
const title = titleMap[basename] || basename
const defaultFm = {
title,
readMode: 'optional',
priority: 'medium',
scope: 'project',
dimension: 'specs',
keywords: [tag, type]
}
if (!file_exists(filePath)) {
// Case A: Create new file with frontmatter
Write(filePath, matter.stringify(`\n# ${title}\n\n`, defaultFm))
return
}
const raw = Read(filePath)
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
Write(filePath, matter.stringify(raw, defaultFm))
return
}
// Case C: Frontmatter exists → ensure keywords include current tag
const existingKeywords = parsed.data.keywords || []
const newKeywords = [...new Set([...existingKeywords, tag, type])]
if (newKeywords.length !== existingKeywords.length) {
parsed.data.keywords = newKeywords
Write(filePath, matter.stringify(parsed.content, parsed.data))
}
}
// ── Update specs/*.md ──
// Uses .ccw/specs/ directory - unified [type:tag] entry format
if (guidelineUpdates.length > 0) {
// Map knowledge types to spec files
const specFileMap = {
bug: '.ccw/specs/learnings.md',
pattern: '.ccw/specs/coding-conventions.md',
decision: '.ccw/specs/architecture-constraints.md',
rule: null // determined by content below
}
const date = new Date().toISOString().split('T')[0]
const needsDate = { bug: true, pattern: true, decision: true, rule: false }
for (const g of guidelineUpdates) {
const targetFile = specFileMap[g.type]
// For rule type, route by content and tag
let targetFile = specFileMap[g.type]
if (!targetFile) {
const isQuality = /\b(test|coverage|lint|eslint|质量|测试覆盖|pre-commit|tsc|type.check)\b/i.test(g.text)
|| ['testing', 'quality', 'lint'].includes(g.tag)
const isConstraint = /\b(禁止|no|never|must not|forbidden|不得|不允许)\b/i.test(g.text)
if (isQuality) {
targetFile = '.ccw/specs/quality-rules.md'
} else if (isConstraint) {
targetFile = '.ccw/specs/architecture-constraints.md'
} else {
targetFile = '.ccw/specs/coding-conventions.md'
}
}
// Ensure frontmatter exists and keywords are up-to-date
ensureFrontmatter(targetFile, g.tag, g.type)
const existing = Read(targetFile)
const ruleText = g.type === 'learning'
? `- [${g.category}] ${g.text} (learned: ${new Date().toISOString().split('T')[0]})`
: `- [${g.category}] ${g.text}`
const entryLine = needsDate[g.type]
? `- [${g.type}:${g.tag}] ${g.text} (${date})`
: `- [${g.type}:${g.tag}] ${g.text}`
// Deduplicate: skip if text already in file
if (!existing.includes(g.text)) {
const newContent = existing.trimEnd() + '\n' + ruleText + '\n'
const newContent = existing.trimEnd() + '\n' + entryLine + '\n'
Write(targetFile, newContent)
}
}
@@ -198,4 +277,5 @@ Write(techPath, JSON.stringify(tech, null, 2))
## Related Commands
- `/workflow:spec:setup` - Initialize project with specs scaffold
- `/workflow:spec:add` - Interactive wizard to create individual specs with scope selection
- `/workflow:spec:add` - Add knowledge entries (bug/pattern/decision/rule) with unified [type:tag] format
- `/workflow:spec:load` - Interactive spec loader with keyword/type/tag filtering

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,392 @@
---
name: load
description: Interactive spec loader - ask what user needs, then load relevant specs by keyword routing
argument-hint: "[--all] [--type <bug|pattern|decision|rule>] [--tag <tag>] [\"keyword query\"]"
examples:
- /workflow:spec:load
- /workflow:spec:load "api routing"
- /workflow:spec:load --type bug
- /workflow:spec:load --all
- /workflow:spec:load --tag security
---
# Spec Load Command (/workflow:spec:load)
## Overview
Interactive entry point for loading and browsing project specs. Asks the user what they need, then routes to the appropriate spec content based on keywords, type filters, or tag filters.
**Design**: Menu-driven → keyword match → load & display. No file modifications.
**Note**: This command may be called by other workflow commands. Upon completion, return immediately to continue the calling workflow.
## Usage
```bash
/workflow:spec:load # Interactive menu
/workflow:spec:load "api routing" # Direct keyword search
/workflow:spec:load --type bug # Filter by knowledge type
/workflow:spec:load --tag security # Filter by domain tag
/workflow:spec:load --all # Load all specs
```
## Execution Process
```
Input Parsing:
├─ Parse --all flag → loadAll = true | false
├─ Parse --type (bug|pattern|decision|rule)
├─ Parse --tag (domain tag)
└─ Parse keyword query (positional text)
Decision:
├─ --all → Load all specs (Path C)
├─ --type or --tag or keyword → Direct filter (Path B)
└─ No args → Interactive menu (Path A)
Path A: Interactive Menu
├─ Step A1: Ask user intent
├─ Step A2: Route to action
└─ Step A3: Display results
Path B: Direct Filter
├─ Step B1: Build filter from args
├─ Step B2: Search specs
└─ Step B3: Display results
Path C: Load All
└─ Display all spec contents
Output:
└─ Formatted spec entries matching user query
```
## Implementation
### Step 1: Parse Input
```javascript
const args = $ARGUMENTS
const argsLower = args.toLowerCase()
const loadAll = argsLower.includes('--all')
const hasType = argsLower.includes('--type')
const hasTag = argsLower.includes('--tag')
let type = hasType ? args.match(/--type\s+(\w+)/i)?.[1]?.toLowerCase() : null
let tag = hasTag ? args.match(/--tag\s+([\w-]+)/i)?.[1]?.toLowerCase() : null
// Extract keyword query (everything that's not a flag)
let keyword = args
.replace(/--type\s+\w+/gi, '')
.replace(/--tag\s+[\w-]+/gi, '')
.replace(/--all/gi, '')
.replace(/^["']|["']$/g, '')
.trim()
// Validate type
if (type && !['bug', 'pattern', 'decision', 'rule'].includes(type)) {
console.log("Invalid type. Use 'bug', 'pattern', 'decision', or 'rule'.")
return
}
```
### Step 2: Determine Mode
```javascript
const useInteractive = !loadAll && !hasType && !hasTag && !keyword
```
### Path A: Interactive Menu
```javascript
if (useInteractive) {
const answer = AskUserQuestion({
questions: [{
question: "What specs would you like to load?",
header: "Action",
multiSelect: false,
options: [
{
label: "Browse all specs",
description: "Load and display all project spec entries"
},
{
label: "Search by keyword",
description: "Find specs matching a keyword (e.g., api, security, routing)"
},
{
label: "View bug experiences",
description: "Load all [bug:*] debugging experience entries"
},
{
label: "View code patterns",
description: "Load all [pattern:*] reusable code pattern entries"
}
]
}]
})
const choice = answer.answers["Action"]
if (choice === "Browse all specs") {
loadAll = true
} else if (choice === "View bug experiences") {
type = "bug"
} else if (choice === "View code patterns") {
type = "pattern"
} else if (choice === "Search by keyword") {
// Ask for keyword
const kwAnswer = AskUserQuestion({
questions: [{
question: "Enter keyword(s) to search for:",
header: "Keyword",
multiSelect: false,
options: [
{ label: "api", description: "API endpoints, HTTP, REST, routing" },
{ label: "security", description: "Authentication, authorization, input validation" },
{ label: "arch", description: "Architecture, design patterns, module structure" },
{ label: "perf", description: "Performance, caching, optimization" }
]
}]
})
keyword = kwAnswer.answers["Keyword"].toLowerCase()
} else {
// "Other" — user typed custom input, use as keyword
keyword = choice.toLowerCase()
}
}
```
### Step 3: Load Spec Files
```javascript
// Discover all spec files
const specFiles = [
'.ccw/specs/coding-conventions.md',
'.ccw/specs/architecture-constraints.md',
'.ccw/specs/learnings.md',
'.ccw/specs/quality-rules.md'
]
// Also check personal specs
const personalFiles = [
'~/.ccw/personal/conventions.md',
'~/.ccw/personal/constraints.md',
'~/.ccw/personal/learnings.md',
'.ccw/personal/conventions.md',
'.ccw/personal/constraints.md',
'.ccw/personal/learnings.md'
]
// Read all existing spec files
const allEntries = []
for (const file of [...specFiles, ...personalFiles]) {
if (!file_exists(file)) continue
const content = Read(file)
// Extract entries using unified format regex
// Entry line: - [type:tag] summary (date)
// Extended: - key: value
const lines = content.split('\n')
let currentEntry = null
for (const line of lines) {
const entryMatch = line.match(/^- \[(\w+):([\w-]+)\] (.*?)(?:\s+\((\d{4}-\d{2}-\d{2})\))?$/)
if (entryMatch) {
if (currentEntry) allEntries.push(currentEntry)
currentEntry = {
type: entryMatch[1],
tag: entryMatch[2],
summary: entryMatch[3],
date: entryMatch[4] || null,
extended: {},
source: file,
raw: line
}
} else if (currentEntry && /^\s{4}- ([\w-]+):\s?(.*)/.test(line)) {
const fieldMatch = line.match(/^\s{4}- ([\w-]+):\s?(.*)/)
currentEntry.extended[fieldMatch[1]] = fieldMatch[2]
} else if (currentEntry && !/^\s{4}/.test(line) && line.trim() !== '') {
// Non-indented non-empty line = end of current entry
allEntries.push(currentEntry)
currentEntry = null
}
// Also handle legacy format: - [tag] text (learned: date)
const legacyMatch = line.match(/^- \[([\w-]+)\] (.+?)(?:\s+\(learned: (\d{4}-\d{2}-\d{2})\))?$/)
if (!entryMatch && legacyMatch) {
if (currentEntry) allEntries.push(currentEntry)
currentEntry = {
type: 'rule',
tag: legacyMatch[1],
summary: legacyMatch[2],
date: legacyMatch[3] || null,
extended: {},
source: file,
raw: line,
legacy: true
}
}
}
if (currentEntry) allEntries.push(currentEntry)
}
```
### Step 4: Filter Entries
```javascript
let filtered = allEntries
// Filter by type
if (type) {
filtered = filtered.filter(e => e.type === type)
}
// Filter by tag
if (tag) {
filtered = filtered.filter(e => e.tag === tag)
}
// Filter by keyword (search in tag, summary, and extended fields)
if (keyword) {
const kw = keyword.toLowerCase()
const kwTerms = kw.split(/\s+/)
filtered = filtered.filter(e => {
const searchText = [
e.type, e.tag, e.summary,
...Object.values(e.extended)
].join(' ').toLowerCase()
return kwTerms.every(term => searchText.includes(term))
})
}
// If --all, keep everything (no filter)
```
### Step 5: Display Results
```javascript
if (filtered.length === 0) {
const filterDesc = []
if (type) filterDesc.push(`type=${type}`)
if (tag) filterDesc.push(`tag=${tag}`)
if (keyword) filterDesc.push(`keyword="${keyword}"`)
console.log(`
No specs found matching: ${filterDesc.join(', ') || '(all)'}
Available spec files:
${specFiles.filter(f => file_exists(f)).map(f => ` - ${f}`).join('\n') || ' (none)'}
Suggestions:
- Use /workflow:spec:setup to initialize specs
- Use /workflow:spec:add to add new entries
- Use /workflow:spec:load --all to see everything
`)
return
}
// Group by source file
const grouped = {}
for (const entry of filtered) {
if (!grouped[entry.source]) grouped[entry.source] = []
grouped[entry.source].push(entry)
}
// Display
console.log(`
## Specs Loaded (${filtered.length} entries)
${type ? `Type: ${type}` : ''}${tag ? ` Tag: ${tag}` : ''}${keyword ? ` Keyword: "${keyword}"` : ''}
`)
for (const [source, entries] of Object.entries(grouped)) {
console.log(`### ${source}`)
console.log('')
for (const entry of entries) {
// Render entry
const datePart = entry.date ? ` (${entry.date})` : ''
console.log(`- [${entry.type}:${entry.tag}] ${entry.summary}${datePart}`)
// Render extended fields
for (const [key, value] of Object.entries(entry.extended)) {
console.log(` - ${key}: ${value}`)
}
}
console.log('')
}
// Summary footer
const typeCounts = {}
for (const e of filtered) {
typeCounts[e.type] = (typeCounts[e.type] || 0) + 1
}
const typeBreakdown = Object.entries(typeCounts)
.map(([t, c]) => `${t}: ${c}`)
.join(', ')
console.log(`---`)
console.log(`Total: ${filtered.length} entries (${typeBreakdown})`)
console.log(`Sources: ${Object.keys(grouped).join(', ')}`)
```
## Examples
### Interactive Browse
```bash
/workflow:spec:load
# → Menu: "What specs would you like to load?"
# → User selects "Browse all specs"
# → Displays all entries grouped by file
```
### Keyword Search
```bash
/workflow:spec:load "api routing"
# → Filters entries where tag/summary/extended contains "api" AND "routing"
# → Displays matching entries
```
### Type Filter
```bash
/workflow:spec:load --type bug
# → Shows all [bug:*] entries from learnings.md
```
### Tag Filter
```bash
/workflow:spec:load --tag security
# → Shows all [*:security] entries across all spec files
```
### Combined Filters
```bash
/workflow:spec:load --type rule --tag api
# → Shows all [rule:api] entries
```
### Load All
```bash
/workflow:spec:load --all
# → Displays every entry from every spec file
```
## Error Handling
| Error | Resolution |
|-------|------------|
| No spec files found | Suggest `/workflow:spec:setup` to initialize |
| No matching entries | Show available files and suggest alternatives |
| Invalid type | Exit with valid type list |
| Corrupt entry format | Skip unparseable lines, continue loading |
## Related Commands
- `/workflow:spec:setup` - Initialize project with specs scaffold
- `/workflow:spec:add` - Add knowledge entries (bug/pattern/decision/rule) with unified [type:tag] format
- `/workflow:session:sync` - Quick-sync session work to specs and project-tech
- `ccw spec list` - View spec file index
- `ccw spec load` - CLI-level spec loading (used by hooks)

View File

@@ -471,70 +471,129 @@ For each category of collected answers, append rules to the corresponding spec M
- Round 5 (quality): `category: execution` (testing phase)
```javascript
const matter = require('gray-matter') // YAML frontmatter parser
// ── Frontmatter check & repair helper ──
// Ensures target spec file has valid YAML frontmatter with keywords
// Uses gray-matter for robust parsing (handles malformed frontmatter, missing fields)
function ensureSpecFrontmatter(filePath, extraKeywords = []) {
const titleMap = {
'coding-conventions': 'Coding Conventions',
'architecture-constraints': 'Architecture Constraints',
'learnings': 'Learnings',
'quality-rules': 'Quality Rules'
}
const basename = filePath.split('/').pop().replace('.md', '')
const title = titleMap[basename] || basename
const defaultKw = filePath.includes('conventions') ? 'convention'
: filePath.includes('constraints') ? 'constraint' : 'quality'
const defaultFm = {
title,
readMode: 'optional',
priority: 'medium',
category: 'general',
scope: 'project',
dimension: 'specs',
keywords: [...new Set([defaultKw, ...extraKeywords])]
}
if (!file_exists(filePath)) {
// Case A: Create new file with frontmatter
const specDir = path.dirname(filePath)
if (!fs.existsSync(specDir)) {
fs.mkdirSync(specDir, { recursive: true })
}
Write(filePath, matter.stringify(`\n# ${title}\n\n`, defaultFm))
return
}
const raw = Read(filePath)
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
Write(filePath, matter.stringify(raw, defaultFm))
return
}
// Case C: Frontmatter exists → ensure keywords include extras
const existingKeywords = parsed.data.keywords || []
const newKeywords = [...new Set([...existingKeywords, defaultKw, ...extraKeywords])]
if (newKeywords.length !== existingKeywords.length) {
parsed.data.keywords = newKeywords
Write(filePath, matter.stringify(parsed.content, parsed.data))
}
}
// Helper: append rules to a spec MD file with category support
// Uses .ccw/specs/ directory (same as frontend/backend spec-index-builder)
function appendRulesToSpecFile(filePath, rules, defaultCategory = 'general') {
if (rules.length === 0) return
// Ensure .ccw/specs/ directory exists
const specDir = path.dirname(filePath)
if (!fs.existsSync(specDir)) {
fs.mkdirSync(specDir, { recursive: true })
}
// Extract domain tags from rules for keyword accumulation
const ruleTags = rules
.map(r => r.match(/\[[\w]+:([\w-]+)\]/)?.[1])
.filter(Boolean)
// Check if file exists
if (!file_exists(filePath)) {
// Create file with frontmatter including category
const frontmatter = `---
title: ${filePath.includes('conventions') ? 'Coding Conventions' : filePath.includes('constraints') ? 'Architecture Constraints' : 'Quality Rules'}
readMode: optional
priority: medium
category: ${defaultCategory}
scope: project
dimension: specs
keywords: [${defaultCategory}, ${filePath.includes('conventions') ? 'convention' : filePath.includes('constraints') ? 'constraint' : 'quality'}]
---
# ${filePath.includes('conventions') ? 'Coding Conventions' : filePath.includes('constraints') ? 'Architecture Constraints' : 'Quality Rules'}
`
Write(filePath, frontmatter)
}
// Ensure frontmatter exists and keywords include rule tags
ensureSpecFrontmatter(filePath, [...new Set(ruleTags)])
const existing = Read(filePath)
// Append new rules as markdown list items after existing content
const newContent = existing.trimEnd() + '\n' + rules.map(r => `- ${r}`).join('\n') + '\n'
// Append new rules as markdown list items - rules are already in [type:tag] format from caller
const newContent = existing.trimEnd() + '\n' + rules.map(r => {
// If rule already has - prefix or [type:tag] format, use as-is
if (/^- /.test(r)) return r
if (/^\[[\w]+:[\w-]+\]/.test(r)) return `- ${r}`
return `- [rule:${defaultCategory}] ${r}`
}).join('\n') + '\n'
Write(filePath, newContent)
}
// Write conventions (general category) - use .ccw/specs/ (same as frontend/backend)
appendRulesToSpecFile('.ccw/specs/coding-conventions.md',
[...newCodingStyle, ...newNamingPatterns, ...newFileStructure, ...newDocumentation],
'general')
// Helper: infer domain tag from rule content
function inferTag(text) {
const t = text.toLowerCase()
if (/\b(api|http|rest|endpoint|routing)\b/.test(t)) return 'api'
if (/\b(security|auth|permission|xss|sql|sanitize)\b/.test(t)) return 'security'
if (/\b(database|db|sql|postgres|mysql)\b/.test(t)) return 'db'
if (/\b(react|component|hook|jsx|tsx)\b/.test(t)) return 'react'
if (/\b(performance|cache|lazy|async|slow)\b/.test(t)) return 'perf'
if (/\b(test|coverage|mock|jest|vitest)\b/.test(t)) return 'testing'
if (/\b(architecture|layer|module|dependency)\b/.test(t)) return 'arch'
if (/\b(naming|camel|pascal|prefix|suffix)\b/.test(t)) return 'naming'
if (/\b(file|folder|directory|structure)\b/.test(t)) return 'file'
if (/\b(doc|comment|jsdoc|readme)\b/.test(t)) return 'doc'
if (/\b(build|webpack|vite|compile)\b/.test(t)) return 'build'
if (/\b(deploy|ci|cd|docker)\b/.test(t)) return 'deploy'
if (/\b(lint|eslint|prettier|format)\b/.test(t)) return 'lint'
if (/\b(type|typescript|strict|any)\b/.test(t)) return 'typing'
return 'style' // fallback for coding conventions
}
// Write constraints (planning category)
// Write conventions - infer domain tags from content
appendRulesToSpecFile('.ccw/specs/coding-conventions.md',
[...newCodingStyle, ...newNamingPatterns, ...newFileStructure, ...newDocumentation]
.map(r => /^\[[\w]+:[\w-]+\]/.test(r) ? r : `[rule:${inferTag(r)}] ${r}`),
'style')
// Write constraints - infer domain tags from content
appendRulesToSpecFile('.ccw/specs/architecture-constraints.md',
[...newArchitecture, ...newTechStack, ...newPerformance, ...newSecurity],
'planning')
[...newArchitecture, ...newTechStack, ...newPerformance, ...newSecurity]
.map(r => /^\[[\w]+:[\w-]+\]/.test(r) ? r : `[rule:${inferTag(r)}] ${r}`),
'arch')
// Write quality rules (execution category)
if (newQualityRules.length > 0) {
const qualityPath = '.ccw/specs/quality-rules.md'
if (!file_exists(qualityPath)) {
Write(qualityPath, `---
title: Quality Rules
readMode: required
priority: high
category: execution
scope: project
dimension: specs
keywords: [execution, quality, testing, coverage, lint]
---
# Quality Rules
`)
}
// ensureSpecFrontmatter handles create/repair/keyword-update
ensureSpecFrontmatter(qualityPath, ['quality', 'testing', 'coverage', 'lint'])
appendRulesToSpecFile(qualityPath,
newQualityRules.map(q => `${q.rule} (scope: ${q.scope}, enforced by: ${q.enforced_by})`),
'execution')
@@ -644,7 +703,8 @@ Next steps:
## Related Commands
- `/workflow:spec:add` - Interactive wizard to create individual specs with scope selection
- `/workflow:spec:add` - Add knowledge entries (bug/pattern/decision/rule) with unified [type:tag] format
- `/workflow:spec:load` - Interactive spec loader with keyword/type/tag filtering
- `/workflow:session:sync` - Quick-sync session work to specs and project-tech
- `workflow-plan` skill - Start planning with initialized project context
- `/workflow:status --project` - View project state and guidelines

View File

@@ -0,0 +1,671 @@
/**
* Generate DDD Docs Tool
* Generate DDD documentation from doc-index.json with deterministic output paths.
* Supports 5 strategies: component (L3), feature (L2), index, overview, schema
*/
import { z } from 'zod';
import type { ToolSchema, ToolResult } from '../types/tool.js';
import { existsSync, readFileSync, mkdirSync, writeFileSync, unlinkSync } from 'fs';
import { join, resolve, dirname } from 'path';
import { execSync } from 'child_process';
import { tmpdir } from 'os';
import { getSecondaryModel } from './cli-config-manager.js';
// Default doc-index path relative to project root
const DEFAULT_DOC_INDEX_PATH = '.workflow/.doc-index/doc-index.json';
// Define Zod schema for validation
const ParamsSchema = z.object({
strategy: z.enum(['component', 'feature', 'index', 'overview', 'schema']),
entityId: z.string().optional(),
docIndexPath: z.string().default(DEFAULT_DOC_INDEX_PATH),
tool: z.enum(['gemini', 'qwen', 'codex']).default('gemini'),
model: z.string().optional(),
});
type Params = z.infer<typeof ParamsSchema>;
interface ToolOutput {
success: boolean;
strategy: string;
entity_id?: string;
output_path: string;
tool: string;
model?: string;
duration_seconds?: number;
message?: string;
error?: string;
}
// --- doc-index.json type definitions ---
interface CodeLocation {
path: string;
symbols?: string[];
lineRange?: [number, number];
}
interface TechnicalComponent {
id: string;
name: string;
type: string;
responsibility?: string;
adrId?: string | null;
docPath?: string;
codeLocations?: CodeLocation[];
dependsOn?: string[];
featureIds?: string[];
actionIds?: string[];
}
interface Feature {
id: string;
name: string;
epicId?: string | null;
status?: string;
docPath?: string;
requirementIds?: string[];
techComponentIds?: string[];
tags?: string[];
}
interface DocIndex {
version?: string;
schema_version?: string;
project?: string;
build_path?: string;
last_updated?: string;
features?: Feature[];
technicalComponents?: TechnicalComponent[];
requirements?: Array<{ id: string; title?: string; priority?: string }>;
architectureDecisions?: Array<{ id: string; title?: string; componentIds?: string[] }>;
actions?: Array<{ id: string; description?: string; type?: string; timestamp?: string; affectedComponents?: string[]; affectedFeatures?: string[] }>;
glossary?: Array<{ id: string; term: string; definition?: string }>;
[key: string]: unknown;
}
// --- Core functions ---
/**
* Load and parse doc-index.json
*/
function loadDocIndex(indexPath: string): DocIndex {
const absPath = resolve(process.cwd(), indexPath);
if (!existsSync(absPath)) {
throw new Error(`doc-index.json not found at: ${absPath}. Run /ddd:scan or /ddd:index-build first.`);
}
const raw = readFileSync(absPath, 'utf8');
return JSON.parse(raw) as DocIndex;
}
/**
* Calculate deterministic output path based on strategy and entityId.
* All paths are relative to the doc-index directory.
*/
function calculateDddOutputPath(
strategy: string,
entityId: string | undefined,
docIndexDir: string
): string {
switch (strategy) {
case 'component': {
if (!entityId) throw new Error('entityId is required for component strategy');
// tech-{slug} -> {slug}.md
const slug = entityId.replace(/^tech-/, '');
return join(docIndexDir, 'tech-registry', `${slug}.md`);
}
case 'feature': {
if (!entityId) throw new Error('entityId is required for feature strategy');
// feat-{slug} -> {slug}.md
const slug = entityId.replace(/^feat-/, '');
return join(docIndexDir, 'feature-maps', `${slug}.md`);
}
case 'index':
// Generate _index.md files - entityId determines which subdirectory
if (entityId) {
return join(docIndexDir, entityId, '_index.md');
}
// Default: generate all index files (return the doc-index dir itself)
return docIndexDir;
case 'overview':
if (entityId === 'architecture') {
return join(docIndexDir, 'ARCHITECTURE.md');
}
return join(docIndexDir, 'README.md');
case 'schema':
return join(docIndexDir, 'SCHEMA.md');
default:
throw new Error(`Unknown strategy: ${strategy}`);
}
}
/**
* Build YAML frontmatter string from entity metadata
*/
function buildFrontmatter(
strategy: string,
entity: TechnicalComponent | Feature | null,
docIndex: DocIndex
): string {
const now = new Date().toISOString();
switch (strategy) {
case 'component': {
const comp = entity as TechnicalComponent;
if (!comp) return '';
const featureIds = comp.featureIds || [];
const codeLocations = (comp.codeLocations || []).map(loc => {
const symbolsStr = loc.symbols && loc.symbols.length > 0
? `\n symbols: [${loc.symbols.join(', ')}]`
: '';
return ` - path: ${loc.path}${symbolsStr}`;
}).join('\n');
return [
'---',
'layer: 3',
`component_id: ${comp.id}`,
`name: ${comp.name}`,
`type: ${comp.type || 'unknown'}`,
`features: [${featureIds.join(', ')}]`,
codeLocations ? `code_locations:\n${codeLocations}` : 'code_locations: []',
`generated_at: ${now}`,
'---',
].join('\n');
}
case 'feature': {
const feat = entity as Feature;
if (!feat) return '';
const reqIds = feat.requirementIds || [];
const techIds = feat.techComponentIds || [];
const tags = feat.tags || [];
return [
'---',
'layer: 2',
`feature_id: ${feat.id}`,
`name: ${feat.name}`,
`epic_id: ${feat.epicId || 'null'}`,
`status: ${feat.status || 'planned'}`,
`requirements: [${reqIds.join(', ')}]`,
`components: [${techIds.join(', ')}]`,
`depends_on_layer3: [${techIds.join(', ')}]`,
`tags: [${tags.join(', ')}]`,
`generated_at: ${now}`,
'---',
].join('\n');
}
case 'index':
case 'overview': {
const featureIds = (docIndex.features || []).map(f => f.id);
return [
'---',
'layer: 1',
`depends_on_layer2: [${featureIds.join(', ')}]`,
`generated_at: ${now}`,
'---',
].join('\n');
}
case 'schema':
return [
'---',
`schema_version: ${docIndex.schema_version || docIndex.version || '1.0'}`,
`generated_at: ${now}`,
'---',
].join('\n');
default:
return '';
}
}
/**
* Build CLI prompt combining frontmatter, content instructions, and code context
*/
function buildDddPrompt(
strategy: string,
entity: TechnicalComponent | Feature | null,
frontmatter: string,
docIndex: DocIndex,
outputPath: string
): string {
const absOutputPath = resolve(process.cwd(), outputPath);
switch (strategy) {
case 'component': {
const comp = entity as TechnicalComponent;
const contextPaths = (comp.codeLocations || []).map(loc => `@${loc.path}`).join(' ');
// Build change history from actions
const compActions = (docIndex.actions || [])
.filter(a => (a.affectedComponents || []).includes(comp.id))
.map(a => `- ${a.timestamp?.split('T')[0] || 'unknown'} | ${a.type || 'change'} | ${a.description || a.id}`)
.join('\n');
const changeHistoryBlock = compActions
? `\n\nChange History (include as "## Change History" section):\n${compActions}`
: '';
return `PURPOSE: Generate component documentation for ${comp.name}
TASK:
- Document component purpose and responsibility
- List exported symbols (classes, functions, types)
- Document dependencies (internal and external)
- Include code examples for key APIs
- Document integration points with other components
- Include change history timeline
MODE: write
CONTEXT: ${contextPaths || '@**/*'}
EXPECTED: Markdown file with: Overview, API Reference, Dependencies, Usage Examples, Change History
CONSTRAINTS: Focus on public API | Include type signatures
OUTPUT FILE: ${absOutputPath}
The file MUST start with this exact frontmatter:
${frontmatter}
Sections to include after frontmatter:
- Responsibility
- Code Locations
- Related Requirements
- Architecture Decisions
- Dependencies (in/out)
- Change History${changeHistoryBlock}`;
}
case 'feature': {
const feat = entity as Feature;
const techIds = feat.techComponentIds || [];
const componentDocs = techIds
.map(id => {
const slug = id.replace(/^tech-/, '');
return `@.workflow/.doc-index/tech-registry/${slug}.md`;
})
.join(' ');
// Build change history from actions
const featActions = (docIndex.actions || [])
.filter(a => (a.affectedFeatures || []).includes(feat.id))
.map(a => `- ${a.timestamp?.split('T')[0] || 'unknown'} | ${a.type || 'change'} | ${a.description || a.id}`)
.join('\n');
const featChangeHistoryBlock = featActions
? `\n\nChange History (include as "## Change History" section):\n${featActions}`
: '';
return `PURPOSE: Generate feature documentation for ${feat.name}
TASK:
- Describe feature purpose and business value
- List requirements (from requirementIds)
- Document components involved (from techComponentIds)
- Include architecture decisions (from adrIds)
- Provide integration guide
- Include change history timeline
MODE: write
CONTEXT: ${componentDocs || '@.workflow/.doc-index/tech-registry/*.md'}
EXPECTED: Markdown file with: Overview, Requirements, Components, Architecture, Integration, Change History
CONSTRAINTS: Reference Layer 3 component docs | Business-focused language
OUTPUT FILE: ${absOutputPath}
The file MUST start with this exact frontmatter:
${frontmatter}
Sections to include after frontmatter:
- Overview
- Requirements (with mapping status)
- Technical Components
- Architecture Decisions
- Change History${featChangeHistoryBlock}`;
}
case 'index': {
const docIndexDir = dirname(resolve(process.cwd(), outputPath));
const parentDir = dirname(docIndexDir);
return `PURPOSE: Generate index document for ${docIndexDir}
TASK:
- List all entries in this directory with brief descriptions
- Create a navigable catalog with links to each document
- Include status/type columns where applicable
MODE: write
CONTEXT: @${parentDir}/doc-index.json
EXPECTED: Markdown index file with: table of entries, descriptions, links
CONSTRAINTS: Catalog format | Link to sibling documents
OUTPUT FILE: ${absOutputPath}
The file MUST start with this exact frontmatter:
${frontmatter}`;
}
case 'overview': {
const isArchitecture = outputPath.endsWith('ARCHITECTURE.md');
if (isArchitecture) {
return `PURPOSE: Generate architecture overview document
TASK:
- System design overview
- Component relationships and dependencies
- Key architecture decisions (from ADRs)
- Technology stack
MODE: write
CONTEXT: @.workflow/.doc-index/doc-index.json @.workflow/.doc-index/tech-registry/*.md
EXPECTED: ARCHITECTURE.md with: System Design, Component Diagram, ADRs, Tech Stack
CONSTRAINTS: Architecture-focused | Reference component docs for details
OUTPUT FILE: ${absOutputPath}
The file MUST start with this exact frontmatter:
${frontmatter}`;
}
return `PURPOSE: Generate project README with overview and navigation
TASK:
- Project summary and purpose
- Quick start guide
- Navigation to features, components, and architecture
- Link to doc-index.json
MODE: write
CONTEXT: @.workflow/.doc-index/doc-index.json @.workflow/.doc-index/feature-maps/_index.md
EXPECTED: README.md with: Overview, Quick Start, Navigation, Links
CONSTRAINTS: High-level only | Entry point for new developers
OUTPUT FILE: ${absOutputPath}
The file MUST start with this exact frontmatter:
${frontmatter}`;
}
case 'schema': {
return `PURPOSE: Document doc-index.json schema structure and versioning
TASK:
- Document current schema structure (all fields)
- Define versioning policy (semver: major.minor)
- Document migration protocol for version upgrades
- Provide examples for each schema section
MODE: write
CONTEXT: @.workflow/.doc-index/doc-index.json
EXPECTED: SCHEMA.md with: Schema Structure, Versioning Policy, Migration Protocol, Examples
CONSTRAINTS: Complete field documentation | Clear migration steps
OUTPUT FILE: ${absOutputPath}
The file MUST start with this exact frontmatter:
${frontmatter}`;
}
default:
throw new Error(`Unknown strategy: ${strategy}`);
}
}
/**
* Create temporary prompt file and return path
*/
function createPromptFile(prompt: string): string {
const timestamp = Date.now();
const randomSuffix = Math.random().toString(36).substring(2, 8);
const promptFile = join(tmpdir(), `ddd-docs-prompt-${timestamp}-${randomSuffix}.txt`);
writeFileSync(promptFile, prompt, 'utf8');
return promptFile;
}
/**
* Build CLI command using stdin piping
*/
function buildCliCommand(tool: string, promptFile: string, model: string): string {
const normalizedPath = promptFile.replace(/\\/g, '/');
const isWindows = process.platform === 'win32';
const catCmd = isWindows ? `Get-Content -Raw "${normalizedPath}" | ` : `cat "${normalizedPath}" | `;
const modelFlag = model ? ` -m "${model}"` : '';
switch (tool) {
case 'qwen':
return `${catCmd}qwen${modelFlag} --yolo`;
case 'codex':
if (isWindows) {
return `codex --full-auto exec (Get-Content -Raw "${normalizedPath}")${modelFlag} --skip-git-repo-check -s danger-full-access`;
}
return `codex --full-auto exec "$(cat "${normalizedPath}")"${modelFlag} --skip-git-repo-check -s danger-full-access`;
case 'gemini':
default:
return `${catCmd}gemini${modelFlag} --yolo`;
}
}
/**
* Resolve entity from doc-index based on strategy and entityId
*/
function resolveEntity(
strategy: string,
entityId: string | undefined,
docIndex: DocIndex
): TechnicalComponent | Feature | null {
if (strategy === 'component') {
if (!entityId) throw new Error('entityId is required for component strategy');
const comp = (docIndex.technicalComponents || []).find(c => c.id === entityId);
if (!comp) throw new Error(`Component not found in doc-index: ${entityId}`);
return comp;
}
if (strategy === 'feature') {
if (!entityId) throw new Error('entityId is required for feature strategy');
const feat = (docIndex.features || []).find(f => f.id === entityId);
if (!feat) throw new Error(`Feature not found in doc-index: ${entityId}`);
return feat;
}
// index, overview, schema do not require a specific entity
return null;
}
/**
* For the index strategy, generate _index.md for multiple directories
*/
function getIndexTargets(entityId: string | undefined): string[] {
if (entityId) {
return [entityId];
}
// Default: all standard subdirectories
return ['feature-maps', 'tech-registry', 'action-logs', 'planning'];
}
// Tool schema for MCP
export const schema: ToolSchema = {
name: 'generate_ddd_docs',
description: `Generate DDD documentation from doc-index.json with deterministic output paths.
Strategies:
- component: Layer 3 technical component doc (tech-registry/{slug}.md)
- feature: Layer 2 feature map doc (feature-maps/{slug}.md)
- index: Layer 1 _index.md catalog files for subdirectories
- overview: Layer 1 README.md or ARCHITECTURE.md
- schema: SCHEMA.md documenting doc-index.json structure
Requires doc-index.json from /ddd:scan or /ddd:index-build.
Output: .workflow/.doc-index/...`,
inputSchema: {
type: 'object',
properties: {
strategy: {
type: 'string',
enum: ['component', 'feature', 'index', 'overview', 'schema'],
description: 'Document generation strategy: component (L3), feature (L2), index, overview, schema (L1)'
},
entityId: {
type: 'string',
description: 'Entity ID from doc-index.json (required for component/feature, optional for index/overview). For overview: "architecture" to generate ARCHITECTURE.md, omit for README.md. For index: subdirectory name or omit for all.'
},
docIndexPath: {
type: 'string',
description: 'Path to doc-index.json (default: .workflow/.doc-index/doc-index.json)',
default: '.workflow/.doc-index/doc-index.json'
},
tool: {
type: 'string',
enum: ['gemini', 'qwen', 'codex'],
description: 'CLI tool to use (default: gemini)',
default: 'gemini'
},
model: {
type: 'string',
description: 'Model name (optional, uses tool defaults)'
}
},
required: ['strategy']
}
};
// Handler function
export async function handler(params: Record<string, unknown>): Promise<ToolResult<ToolOutput>> {
const parsed = ParamsSchema.safeParse(params);
if (!parsed.success) {
return { success: false, error: `Invalid params: ${parsed.error.message}` };
}
const { strategy, entityId, docIndexPath, tool, model } = parsed.data;
try {
// Load doc-index.json
const docIndex = loadDocIndex(docIndexPath);
const docIndexDir = dirname(resolve(process.cwd(), docIndexPath));
// Resolve model
let actualModel = model || '';
if (!actualModel) {
try {
actualModel = getSecondaryModel(process.cwd(), tool);
} catch {
actualModel = '';
}
}
// Handle index strategy separately (may generate multiple files)
if (strategy === 'index') {
const targets = getIndexTargets(entityId);
const results: string[] = [];
for (const target of targets) {
const outputPath = join(docIndexDir, target, '_index.md');
const absOutputDir = dirname(resolve(process.cwd(), outputPath));
// Ensure directory exists
mkdirSync(absOutputDir, { recursive: true });
const frontmatter = buildFrontmatter('index', null, docIndex);
const prompt = buildDddPrompt('index', null, frontmatter, docIndex, outputPath);
const promptFile = createPromptFile(prompt);
const command = buildCliCommand(tool, promptFile, actualModel);
console.log(`[DDD] Generating index: ${target}/_index.md`);
try {
const startTime = Date.now();
execSync(command, {
cwd: docIndexDir,
encoding: 'utf8',
stdio: 'inherit',
timeout: 600000,
shell: process.platform === 'win32' ? 'powershell.exe' : '/bin/bash'
});
const duration = Math.round((Date.now() - startTime) / 1000);
results.push(`${target}/_index.md (${duration}s)`);
} finally {
try { unlinkSync(promptFile); } catch { /* ignore */ }
}
}
return {
success: true,
result: {
success: true,
strategy,
entity_id: entityId,
output_path: docIndexDir,
tool,
model: actualModel,
message: `Generated index files: ${results.join(', ')}`
}
};
}
// Single-file strategies: component, feature, overview, schema
const entity = resolveEntity(strategy, entityId, docIndex);
const outputPath = calculateDddOutputPath(strategy, entityId, docIndexDir);
const absOutputDir = dirname(resolve(process.cwd(), outputPath));
// Ensure output directory exists
mkdirSync(absOutputDir, { recursive: true });
// Build frontmatter and prompt
const frontmatter = buildFrontmatter(strategy, entity, docIndex);
const prompt = buildDddPrompt(strategy, entity, frontmatter, docIndex, outputPath);
// Create temp prompt file
const promptFile = createPromptFile(prompt);
// Build CLI command
const command = buildCliCommand(tool, promptFile, actualModel);
console.log(`[DDD] Generating ${strategy}: ${outputPath}`);
console.log(`[DDD] Tool: ${tool} | Model: ${actualModel || 'default'}`);
try {
const startTime = Date.now();
execSync(command, {
cwd: docIndexDir,
encoding: 'utf8',
stdio: 'inherit',
timeout: 600000,
shell: process.platform === 'win32' ? 'powershell.exe' : '/bin/bash'
});
const duration = Math.round((Date.now() - startTime) / 1000);
// Cleanup
try { unlinkSync(promptFile); } catch { /* ignore */ }
console.log(`[DDD] Completed in ${duration}s: ${outputPath}`);
return {
success: true,
result: {
success: true,
strategy,
entity_id: entityId,
output_path: outputPath,
tool,
model: actualModel,
duration_seconds: duration,
message: `Documentation generated successfully in ${duration}s`
}
};
} catch (error) {
// Cleanup on error
try { unlinkSync(promptFile); } catch { /* ignore */ }
// Tool fallback: gemini -> qwen -> codex
const fallbackChain = ['gemini', 'qwen', 'codex'];
const currentIdx = fallbackChain.indexOf(tool);
if (currentIdx >= 0 && currentIdx < fallbackChain.length - 1) {
const nextTool = fallbackChain[currentIdx + 1];
console.log(`[DDD] ${tool} failed, falling back to ${nextTool}`);
return handler({ ...params, tool: nextTool });
}
return {
success: false,
error: `Documentation generation failed: ${(error as Error).message}`
};
}
} catch (error) {
return {
success: false,
error: `Tool execution failed: ${(error as Error).message}`
};
}
}

View File

@@ -14,6 +14,7 @@ import * as classifyFoldersMod from './classify-folders.js';
import * as detectChangedModulesMod from './detect-changed-modules.js';
import * as discoverDesignFilesMod from './discover-design-files.js';
import * as generateModuleDocsMod from './generate-module-docs.js';
import * as generateDddDocsMod from './generate-ddd-docs.js';
import * as convertTokensToCssMod from './convert-tokens-to-css.js';
import * as sessionManagerMod from './session-manager.js';
import * as cliExecutorMod from './cli-executor.js';
@@ -358,6 +359,7 @@ registerTool(toLegacyTool(classifyFoldersMod));
registerTool(toLegacyTool(detectChangedModulesMod));
registerTool(toLegacyTool(discoverDesignFilesMod));
registerTool(toLegacyTool(generateModuleDocsMod));
registerTool(toLegacyTool(generateDddDocsMod));
registerTool(toLegacyTool(convertTokensToCssMod));
registerTool(toLegacyTool(sessionManagerMod));
registerTool(toLegacyTool(cliExecutorMod));