666 lines
19 KiB
Markdown
666 lines
19 KiB
Markdown
---
|
|
description: Sync spec document with implementation reality
|
|
allowed-tools: [LinearMCP, Read, Glob, Grep, AskUserQuestion]
|
|
argument-hint: <doc-id-or-issue-id>
|
|
---
|
|
|
|
# Sync Spec: $1
|
|
|
|
## 🚨 CRITICAL: Safety Rules
|
|
|
|
**READ FIRST**: ``$CCPM_COMMANDS_DIR/SAFETY_RULES.md``
|
|
|
|
**NEVER** submit, post, or update anything to Jira, Confluence, BitBucket, or Slack without explicit user confirmation, even in bypass permission mode.
|
|
|
|
---
|
|
|
|
## Argument
|
|
|
|
- **$1** - Document ID or Issue ID (will find linked spec doc)
|
|
|
|
## Workflow
|
|
|
|
### Step 1: Fetch Spec and Related Issues
|
|
|
|
If `$1` is an issue ID:
|
|
|
|
```javascript
|
|
// 1. Get issue
|
|
const issue = await getLinearIssue($1)
|
|
|
|
// 2. Extract spec doc link from description
|
|
const docLinkPattern = /\[(?:Epic Spec|Feature Design): .+?\]\((.+?)\)/
|
|
const match = issue.description.match(docLinkPattern)
|
|
|
|
if (match) {
|
|
const docUrl = match[1]
|
|
const docId = extractDocId(docUrl)
|
|
}
|
|
|
|
// 3. Get all sub-issues (tasks)
|
|
const tasks = await getLinearSubIssues(issue.id)
|
|
```
|
|
|
|
If `$1` is a doc ID:
|
|
|
|
```javascript
|
|
// 1. Get document
|
|
const doc = await getLinearDocument($1)
|
|
|
|
// 2. Find linked issue from document content or metadata
|
|
// Look for "Linear Epic:" or "Linear Feature:" links
|
|
const issueLinkPattern = /\[WORK-\d+\]\((.+?)\)/
|
|
const match = doc.content.match(issueLinkPattern)
|
|
|
|
if (match) {
|
|
const issueUrl = match[1]
|
|
const issueId = extractIssueId(issueUrl)
|
|
const issue = await getLinearIssue(issueId)
|
|
const tasks = await getLinearSubIssues(issue.id)
|
|
}
|
|
```
|
|
|
|
### Step 2: Analyze Spec vs Reality
|
|
|
|
**Compare spec sections with actual implementation:**
|
|
|
|
#### 2.1: Requirements Drift
|
|
|
|
```javascript
|
|
function checkRequirementsDrift(specDoc, tasks) {
|
|
const specRequirements = extractRequirements(specDoc)
|
|
const implementedFeatures = extractImplementedFeatures(tasks)
|
|
|
|
const drift = {
|
|
missing: [], // In spec but not implemented
|
|
extra: [], // Implemented but not in spec
|
|
changed: [] // Different from spec
|
|
}
|
|
|
|
// Compare
|
|
for (const req of specRequirements) {
|
|
const implemented = implementedFeatures.find(f => matches(f, req))
|
|
|
|
if (!implemented) {
|
|
drift.missing.push(req)
|
|
} else if (!exactMatch(implemented, req)) {
|
|
drift.changed.push({ spec: req, actual: implemented })
|
|
}
|
|
}
|
|
|
|
for (const feature of implementedFeatures) {
|
|
if (!specRequirements.find(r => matches(feature, r))) {
|
|
drift.extra.push(feature)
|
|
}
|
|
}
|
|
|
|
return drift
|
|
}
|
|
```
|
|
|
|
#### 2.2: Implementation Tasks Drift
|
|
|
|
```javascript
|
|
function checkTasksDrift(specDoc, linearTasks) {
|
|
const specTasks = extractTasksFromSpec(specDoc)
|
|
// From "## Implementation Plan" or "## Task Breakdown"
|
|
|
|
const drift = {
|
|
inSpecNotLinear: [], // Tasks in spec but no Linear issue
|
|
inLinearNotSpec: [], // Linear tasks not documented in spec
|
|
statusMismatch: [] // Different completion status
|
|
}
|
|
|
|
// Compare task lists
|
|
for (const specTask of specTasks) {
|
|
const linearTask = linearTasks.find(lt => matches(lt, specTask))
|
|
|
|
if (!linearTask) {
|
|
drift.inSpecNotLinear.push(specTask)
|
|
} else {
|
|
// Check if status matches
|
|
const specCompleted = specTask.checked
|
|
const linearCompleted = linearTask.status === 'Done'
|
|
|
|
if (specCompleted !== linearCompleted) {
|
|
drift.statusMismatch.push({
|
|
task: specTask,
|
|
specStatus: specCompleted ? 'Done' : 'Pending',
|
|
linearStatus: linearTask.status
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const linearTask of linearTasks) {
|
|
if (!specTasks.find(st => matches(linearTask, st))) {
|
|
drift.inLinearNotSpec.push(linearTask)
|
|
}
|
|
}
|
|
|
|
return drift
|
|
}
|
|
```
|
|
|
|
#### 2.3: API Design Drift
|
|
|
|
```javascript
|
|
function checkApiDrift(specDoc, codebase) {
|
|
const specApis = extractApiDesign(specDoc)
|
|
const implementedApis = searchCodebaseForApis(codebase)
|
|
|
|
const drift = {
|
|
endpointsMissing: [],
|
|
endpointsExtra: [],
|
|
signatureChanged: []
|
|
}
|
|
|
|
// Compare endpoints
|
|
for (const specApi of specApis) {
|
|
const impl = implementedApis.find(api => api.path === specApi.path)
|
|
|
|
if (!impl) {
|
|
drift.endpointsMissing.push(specApi)
|
|
} else if (!signaturesMatch(impl, specApi)) {
|
|
drift.signatureChanged.push({
|
|
spec: specApi,
|
|
actual: impl,
|
|
differences: compareSignatures(specApi, impl)
|
|
})
|
|
}
|
|
}
|
|
|
|
return drift
|
|
}
|
|
```
|
|
|
|
#### 2.4: Data Model Drift
|
|
|
|
```javascript
|
|
function checkDataModelDrift(specDoc, codebase) {
|
|
const specModels = extractDataModel(specDoc)
|
|
const implementedModels = searchCodebaseForModels(codebase)
|
|
|
|
const drift = {
|
|
tablesMissing: [],
|
|
tablesExtra: [],
|
|
fieldsMissing: [],
|
|
fieldsChanged: []
|
|
}
|
|
|
|
// Compare schemas
|
|
for (const specModel of specModels) {
|
|
const impl = implementedModels.find(m => m.name === specModel.name)
|
|
|
|
if (!impl) {
|
|
drift.tablesMissing.push(specModel)
|
|
} else {
|
|
// Compare fields
|
|
for (const specField of specModel.fields) {
|
|
const implField = impl.fields.find(f => f.name === specField.name)
|
|
|
|
if (!implField) {
|
|
drift.fieldsMissing.push({
|
|
table: specModel.name,
|
|
field: specField
|
|
})
|
|
} else if (specField.type !== implField.type) {
|
|
drift.fieldsChanged.push({
|
|
table: specModel.name,
|
|
field: specField.name,
|
|
specType: specField.type,
|
|
actualType: implField.type
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return drift
|
|
}
|
|
```
|
|
|
|
### Step 3: Search Codebase for Implementation
|
|
|
|
Use **Glob** and **Grep** to find implemented code:
|
|
|
|
```javascript
|
|
async function searchCodebaseForApis(projectPath) {
|
|
// Search for API route files
|
|
const apiFiles = await glob(`${projectPath}/**/api/**/*.{ts,js}`)
|
|
|
|
const apis = []
|
|
|
|
for (const file of apiFiles) {
|
|
const content = await read(file)
|
|
|
|
// Extract API endpoints
|
|
// Look for: app.post('/api/endpoint', ...) or export async function POST(...)
|
|
const endpointPattern = /(?:app\.(get|post|put|delete|patch)|export async function (GET|POST|PUT|DELETE|PATCH))\s*\(\s*['"`](.+?)['"`]/g
|
|
|
|
let match
|
|
while ((match = endpointPattern.exec(content)) !== null) {
|
|
const method = match[1] || match[2]
|
|
const path = match[3]
|
|
|
|
apis.push({
|
|
method: method.toUpperCase(),
|
|
path: path,
|
|
file: file,
|
|
// Could extract request/response types if TypeScript
|
|
})
|
|
}
|
|
}
|
|
|
|
return apis
|
|
}
|
|
|
|
async function searchCodebaseForModels(projectPath) {
|
|
// Search for database schema files
|
|
const schemaPattern = `${projectPath}/**/schema/**/*.{ts,js,sql}`
|
|
const schemaFiles = await glob(schemaPattern)
|
|
|
|
const models = []
|
|
|
|
for (const file of schemaFiles) {
|
|
const content = await read(file)
|
|
|
|
// Extract table definitions
|
|
// Drizzle: pgTable('table_name', { ... })
|
|
// Prisma: model TableName { ... }
|
|
// SQL: CREATE TABLE table_name ( ... )
|
|
|
|
// Parse and extract models
|
|
// This is simplified - actual implementation would parse properly
|
|
models.push(...parseSchema(content))
|
|
}
|
|
|
|
return models
|
|
}
|
|
```
|
|
|
|
### Step 4: Generate Sync Report
|
|
|
|
```
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
🔄 Spec Sync Report: [Document Title]
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
📄 Spec Doc: [DOC-456](link)
|
|
🎯 Issue: [WORK-123 - Feature Title](link)
|
|
📅 Last Synced: [Never / Date]
|
|
📊 Drift Score: [XX]% ([Grade])
|
|
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
📊 Overall Drift Analysis
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
**Drift Score**: 85% ← Lower is better (0% = perfect sync)
|
|
|
|
Legend: ✅ In Sync | ⚠️ Minor Drift | ❌ Major Drift
|
|
|
|
✅ Requirements: 2 missing, 1 extra (10% drift)
|
|
⚠️ Implementation Tasks: 3 status mismatches (25% drift)
|
|
❌ API Design: 2 endpoints differ (40% drift)
|
|
✅ Data Model: All tables match (0% drift)
|
|
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
📋 Requirements Drift
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
**In Spec, Not Implemented:**
|
|
1. FR3: Password reset via email
|
|
- Spec says: "Users can reset password via email link"
|
|
- Reality: No implementation found
|
|
- Action: Implement or remove from spec
|
|
|
|
2. NFR2: Response time < 200ms
|
|
- Spec says: "95th percentile response time under 200ms"
|
|
- Reality: Not measured/verified
|
|
- Action: Add performance monitoring
|
|
|
|
**Implemented, Not in Spec:**
|
|
1. Social Login (Google OAuth)
|
|
- Found: POST /api/auth/google endpoint
|
|
- Not documented in spec
|
|
- Action: Add to spec or mark as scope creep
|
|
|
|
**Changed from Spec:**
|
|
1. Login Rate Limiting
|
|
- Spec: "10 attempts per hour"
|
|
- Actual: 5 attempts per 15 minutes (stricter)
|
|
- Action: Update spec to reflect current implementation
|
|
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
✅ Implementation Tasks Drift
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
**Status Mismatches:**
|
|
1. Task 2: API endpoints
|
|
- Spec Checklist: ✅ Marked complete
|
|
- Linear Status: In Progress
|
|
- Action: Update spec checklist or complete Linear task
|
|
|
|
2. Task 4: Testing
|
|
- Spec Checklist: ⏳ Not checked
|
|
- Linear Status: Done
|
|
- Action: Check off in spec
|
|
|
|
3. Task 5: Documentation
|
|
- Spec Checklist: ⏳ Not checked
|
|
- Linear Status: Done
|
|
- Action: Check off in spec
|
|
|
|
**In Spec, No Linear Task:**
|
|
1. Task 6: Performance optimization
|
|
- Missing Linear task
|
|
- Action: Create Linear task or remove from spec
|
|
|
|
**In Linear, Not in Spec:**
|
|
1. WORK-125: Fix login bug (subtask)
|
|
- Unplanned task added during implementation
|
|
- Action: Add to spec as "Bug Fixes" section
|
|
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
🔌 API Design Drift
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
**Signature Changed:**
|
|
1. POST /api/auth/login
|
|
- Spec Request:
|
|
```typescript
|
|
{ email: string, password: string }
|
|
```
|
|
- Actual Request:
|
|
```typescript
|
|
{ email: string, password: string, rememberMe?: boolean }
|
|
```
|
|
- Change: Added optional `rememberMe` field
|
|
- Action: Update spec with new signature
|
|
|
|
2. POST /api/auth/refresh
|
|
- Spec Response:
|
|
```typescript
|
|
{ token: string }
|
|
```
|
|
- Actual Response:
|
|
```typescript
|
|
{ accessToken: string, refreshToken: string, expiresIn: number }
|
|
```
|
|
- Change: More detailed response
|
|
- Action: Update spec to match implementation
|
|
|
|
**Endpoints Missing:**
|
|
- None ✅
|
|
|
|
**Extra Endpoints (Not in Spec):**
|
|
1. GET /api/auth/session
|
|
- Found in code but not documented
|
|
- Action: Add to spec
|
|
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
🗄️ Data Model Drift
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
✅ All tables and fields match spec! No drift detected.
|
|
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
💡 Recommended Actions
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
**Critical (Fix Immediately):**
|
|
1. Update API signatures in spec (2 endpoints changed)
|
|
2. Implement missing requirements or remove from spec (2 items)
|
|
|
|
**Important (Fix Soon):**
|
|
1. Sync task statuses between spec checklist and Linear (3 mismatches)
|
|
2. Document unplanned features added during implementation (1 endpoint)
|
|
|
|
**Nice to Have:**
|
|
1. Add performance monitoring for NFR validation
|
|
2. Create missing Linear tasks for spec items
|
|
|
|
**Estimated Time to Sync**: 2-3 hours
|
|
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
```
|
|
|
|
### Step 5: Ask User How to Resolve Drift
|
|
|
|
Use **AskUserQuestion**:
|
|
|
|
```javascript
|
|
{
|
|
questions: [{
|
|
question: "Drift detected! How would you like to resolve it?",
|
|
header: "Sync Action",
|
|
multiSelect: false,
|
|
options: [
|
|
{
|
|
label: "Update Spec to Match Reality",
|
|
description: "Modify spec doc to reflect current implementation (recommended)"
|
|
},
|
|
{
|
|
label: "Update Implementation to Match Spec",
|
|
description: "Modify code to match original spec design"
|
|
},
|
|
{
|
|
label: "Hybrid Approach",
|
|
description: "Update spec for some items, code for others (I'll choose)"
|
|
},
|
|
{
|
|
label: "Review Only",
|
|
description: "Just show the report, I'll fix manually"
|
|
}
|
|
]
|
|
}]
|
|
}
|
|
```
|
|
|
|
### Step 6: Apply Sync Changes
|
|
|
|
#### If "Update Spec to Match Reality":
|
|
|
|
```javascript
|
|
// Update spec document with actual implementation
|
|
|
|
// 1. Update Requirements section
|
|
updateSpecSection(doc, 'requirements', {
|
|
add: drift.requirements.extra,
|
|
remove: drift.requirements.missing,
|
|
modify: drift.requirements.changed
|
|
})
|
|
|
|
// 2. Update API Design section
|
|
updateSpecSection(doc, 'api-design', {
|
|
updateSignatures: drift.api.signatureChanged,
|
|
addEndpoints: drift.api.endpointsExtra
|
|
})
|
|
|
|
// 3. Update Task Checklist
|
|
updateSpecChecklist(doc, {
|
|
check: drift.tasks.statusMismatch.filter(t => t.linearStatus === 'Done'),
|
|
uncheck: drift.tasks.statusMismatch.filter(t => t.linearStatus !== 'Done'),
|
|
add: drift.tasks.inLinearNotSpec
|
|
})
|
|
|
|
// 4. Add "Change Log" entry
|
|
appendToChangeLog(doc, {
|
|
date: new Date().toISOString().split('T')[0],
|
|
change: 'Synced spec with implementation reality',
|
|
details: `Updated ${changesCount} sections to match current implementation`,
|
|
author: currentUser
|
|
})
|
|
|
|
// 5. Update "Last Synced" timestamp in spec
|
|
updateMetadata(doc, {
|
|
lastSynced: new Date().toISOString()
|
|
})
|
|
```
|
|
|
|
#### If "Update Implementation to Match Spec":
|
|
|
|
```javascript
|
|
// Show what needs to be implemented to match spec
|
|
|
|
const implementationPlan = {
|
|
missingRequirements: drift.requirements.missing,
|
|
missingEndpoints: drift.api.endpointsMissing,
|
|
changedSignatures: drift.api.signatureChanged,
|
|
missingTasks: drift.tasks.inSpecNotLinear
|
|
}
|
|
|
|
// Show plan and ask if user wants to create Linear tasks for fixes
|
|
const tasks = generateFixTasks(implementationPlan)
|
|
|
|
// Offer to create tasks via /ccpm:spec:break-down or manually
|
|
```
|
|
|
|
#### If "Hybrid Approach":
|
|
|
|
```javascript
|
|
// Show each drift item and ask user decision
|
|
|
|
for (const item of allDriftItems) {
|
|
const choice = await askUserQuestion({
|
|
question: `Drift: ${item.description}. How to resolve?`,
|
|
options: [
|
|
"Update Spec",
|
|
"Update Code",
|
|
"Keep As Is"
|
|
]
|
|
})
|
|
|
|
if (choice === "Update Spec") {
|
|
updateSpec(item)
|
|
} else if (choice === "Update Code") {
|
|
addToImplementationBacklog(item)
|
|
}
|
|
// else: skip
|
|
}
|
|
```
|
|
|
|
### Step 7: Display Sync Results
|
|
|
|
```
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
✅ Spec Synced Successfully!
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
📄 Document: [DOC-456](link)
|
|
🔄 Sync Method: [Update Spec to Match Reality]
|
|
📊 Changes Applied: 12
|
|
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
📋 Changes Summary
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
**Requirements Section:**
|
|
- ✅ Added: 1 new requirement (Social Login)
|
|
- ✅ Removed: 2 unimplemented requirements
|
|
- ✅ Updated: 1 changed requirement (Rate Limiting)
|
|
|
|
**API Design Section:**
|
|
- ✅ Updated: 2 endpoint signatures
|
|
- ✅ Added: 1 undocumented endpoint (GET /api/auth/session)
|
|
|
|
**Task Checklist:**
|
|
- ✅ Checked: 2 completed tasks
|
|
- ✅ Added: 1 new task (Bug Fixes)
|
|
|
|
**Metadata:**
|
|
- ✅ Last Synced: ${new Date().toISOString().split('T')[0]}
|
|
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
💡 New Drift Score: 5% (was 85%)
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
```
|
|
|
|
### Step 8: Interactive Next Actions
|
|
|
|
```javascript
|
|
{
|
|
questions: [{
|
|
question: "Sync complete! What would you like to do next?",
|
|
header: "Next Step",
|
|
multiSelect: false,
|
|
options: [
|
|
{
|
|
label: "Review Updated Spec",
|
|
description: "Open spec document in Linear to review changes"
|
|
},
|
|
{
|
|
label: "Review Spec",
|
|
description: "Run /ccpm:spec:review to validate updated spec"
|
|
},
|
|
{
|
|
label: "View Project Status",
|
|
description: "Check overall project status (/ccpm:utils:status)"
|
|
},
|
|
{
|
|
label: "Done",
|
|
description: "Finish for now"
|
|
}
|
|
]
|
|
}]
|
|
}
|
|
```
|
|
|
|
## Drift Score Calculation
|
|
|
|
```javascript
|
|
function calculateDriftScore(drift) {
|
|
let totalItems = 0
|
|
let driftItems = 0
|
|
|
|
// Requirements
|
|
totalItems += drift.requirements.total
|
|
driftItems += drift.requirements.missing.length +
|
|
drift.requirements.extra.length +
|
|
drift.requirements.changed.length
|
|
|
|
// Tasks
|
|
totalItems += drift.tasks.total
|
|
driftItems += drift.tasks.statusMismatch.length +
|
|
drift.tasks.inSpecNotLinear.length +
|
|
drift.tasks.inLinearNotSpec.length
|
|
|
|
// API
|
|
totalItems += drift.api.total
|
|
driftItems += drift.api.endpointsMissing.length +
|
|
drift.api.endpointsExtra.length +
|
|
drift.api.signatureChanged.length
|
|
|
|
// Data Model
|
|
totalItems += drift.dataModel.total
|
|
driftItems += drift.dataModel.tablesMissing.length +
|
|
drift.dataModel.tablesExtra.length +
|
|
drift.dataModel.fieldsMissing.length +
|
|
drift.dataModel.fieldsChanged.length
|
|
|
|
// Calculate percentage
|
|
if (totalItems === 0) return 0
|
|
|
|
const driftPercentage = Math.round((driftItems / totalItems) * 100)
|
|
|
|
return driftPercentage
|
|
}
|
|
|
|
function getDriftGrade(score) {
|
|
if (score <= 10) return { grade: 'A', label: 'Excellent Sync' }
|
|
if (score <= 25) return { grade: 'B', label: 'Minor Drift' }
|
|
if (score <= 50) return { grade: 'C', label: 'Moderate Drift' }
|
|
if (score <= 75) return { grade: 'D', label: 'Major Drift' }
|
|
return { grade: 'F', label: 'Significant Drift' }
|
|
}
|
|
```
|
|
|
|
## Notes
|
|
|
|
- Non-destructive: Always creates backups before updating
|
|
- Bidirectional: Can sync spec → code or code → spec
|
|
- Smart Detection: Uses codebase analysis to find actual implementation
|
|
- Preserves Intent: Asks user before resolving ambiguous drift
|
|
- Change Log: Tracks all sync operations in spec document
|
|
- Drift Score: Quantifies how much spec diverged from reality
|