Files
gh-duongdev-ccpm/commands/spec:sync.md
2025-11-29 18:24:24 +08:00

19 KiB

description, allowed-tools, argument-hint
description allowed-tools argument-hint
Sync spec document with implementation reality
LinearMCP
Read
Glob
Grep
AskUserQuestion
<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

If $1 is an issue ID:

// 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:

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

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

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

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

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:

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:

{
  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":

// 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":

// 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":

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

{
  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

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