# Command: plan-fixes

> Deterministic grouping algorithm. Groups findings by file, merges dependent groups, topological sorts within groups, writes fix-plan.json.

## When to Use

- Phase 3A of Fixer, after context resolution
- Requires: `fixableFindings[]`, `sessionFolder`, `quickPath` from Phase 2

**Trigger conditions**:
- FIX-* task in Phase 3 with at least 1 fixable finding

## Strategy

**Mode**: Direct (inline execution, deterministic algorithm, no CLI needed)

## Execution Steps

### Step 1: Group Findings by Primary File

```javascript
const fileGroups = {}
for (const f of fixableFindings) {
  const file = f.location?.file || '_unknown'
  if (!fileGroups[file]) fileGroups[file] = []
  fileGroups[file].push(f)
}
```

### Step 2: Merge Groups with Cross-File Dependencies

```javascript
// Build adjacency: if finding A (group X) depends on finding B (group Y), merge X into Y
const findingFileMap = {}
for (const f of fixableFindings) {
  findingFileMap[f.id] = f.location?.file || '_unknown'
}

// Union-Find for group merging
const parent = {}
const find = (x) => parent[x] === x ? x : (parent[x] = find(parent[x]))
const union = (a, b) => { parent[find(a)] = find(b) }

const allFiles = Object.keys(fileGroups)
for (const file of allFiles) parent[file] = file

for (const f of fixableFindings) {
  const myFile = f.location?.file || '_unknown'
  for (const depId of (f.fix_dependencies || [])) {
    const depFile = findingFileMap[depId]
    if (depFile && depFile !== myFile) {
      union(myFile, depFile)
    }
  }
}

// Collect merged groups
const mergedGroupMap = {}
for (const file of allFiles) {
  const root = find(file)
  if (!mergedGroupMap[root]) mergedGroupMap[root] = { files: [], findings: [] }
  mergedGroupMap[root].files.push(file)
  mergedGroupMap[root].findings.push(...fileGroups[file])
}

// Deduplicate files
for (const g of Object.values(mergedGroupMap)) {
  g.files = [...new Set(g.files)]
}
```

### Step 3: Topological Sort Within Each Group

```javascript
function topoSort(findings) {
  const idSet = new Set(findings.map(f => f.id))
  const inDegree = {}
  const adj = {}
  for (const f of findings) {
    inDegree[f.id] = 0
    adj[f.id] = []
  }
  for (const f of findings) {
    for (const depId of (f.fix_dependencies || [])) {
      if (idSet.has(depId)) {
        adj[depId].push(f.id)
        inDegree[f.id]++
      }
    }
  }

  const queue = findings.filter(f => inDegree[f.id] === 0).map(f => f.id)
  const sorted = []
  while (queue.length > 0) {
    const id = queue.shift()
    sorted.push(id)
    for (const next of adj[id]) {
      inDegree[next]--
      if (inDegree[next] === 0) queue.push(next)
    }
  }

  // Handle cycles: append any unsorted findings at the end
  const sortedSet = new Set(sorted)
  for (const f of findings) {
    if (!sortedSet.has(f.id)) sorted.push(f.id)
  }

  const findingMap = Object.fromEntries(findings.map(f => [f.id, f]))
  return sorted.map(id => findingMap[id])
}

const groups = Object.entries(mergedGroupMap).map(([root, g], i) => {
  const sorted = topoSort(g.findings)
  const maxSev = sorted.reduce((max, f) => {
    const ord = { critical: 0, high: 1, medium: 2, low: 3 }
    return (ord[f.severity] ?? 4) < (ord[max] ?? 4) ? f.severity : max
  }, 'low')
  return {
    id: `G${i + 1}`,
    files: g.files,
    findings: sorted,
    max_severity: maxSev
  }
})
```

### Step 4: Sort Groups by Max Severity

```javascript
const SEV_ORDER = { critical: 0, high: 1, medium: 2, low: 3 }
groups.sort((a, b) => (SEV_ORDER[a.max_severity] ?? 4) - (SEV_ORDER[b.max_severity] ?? 4))

// Re-assign IDs after sort
groups.forEach((g, i) => { g.id = `G${i + 1}` })

const execution_order = groups.map(g => g.id)
```

### Step 5: Determine Execution Path

```javascript
const totalFindings = fixableFindings.length
const totalGroups = groups.length
const isQuickPath = totalFindings <= 5 && totalGroups <= 1
```

### Step 6: Write fix-plan.json

```javascript
const fixPlan = {
  plan_id: `fix-plan-${Date.now()}`,
  quick_path: isQuickPath,
  groups: groups.map(g => ({
    id: g.id,
    files: g.files,
    findings: g.findings.map(f => ({
      id: f.id, severity: f.severity, dimension: f.dimension,
      title: f.title, description: f.description,
      location: f.location, suggested_fix: f.suggested_fix,
      fix_strategy: f.fix_strategy, fix_complexity: f.fix_complexity,
      fix_dependencies: f.fix_dependencies,
      root_cause: f.root_cause, optimization: f.optimization
    })),
    max_severity: g.max_severity
  })),
  execution_order: execution_order,
  total_findings: totalFindings,
  total_groups: totalGroups
}

Bash(`mkdir -p "${sessionFolder}/fix"`)
Write(`${sessionFolder}/fix/fix-plan.json`, JSON.stringify(fixPlan, null, 2))

mcp__ccw-tools__team_msg({ operation:"log", team:"team-review", from:"fixer",
  to:"coordinator", type:"fix_progress",
  summary:`[fixer] Fix plan: ${totalGroups} groups, ${totalFindings} findings, path=${isQuickPath ? 'quick' : 'standard'}` })
```

## Error Handling

| Scenario | Resolution |
|----------|------------|
| All findings share one file | Single group, likely quick path |
| Dependency cycle detected | Topo sort appends cycle members at end |
| Finding references unknown dependency | Ignore that dependency edge |
| Empty fixableFindings | Should not reach this command (checked in Phase 2) |
