# STAGE 4 REFACTOR PLAN — DDAY Completion

**Plan Date:** November 26, 2025  
**Author:** AI Agent (Claude Sonnet 4.5)  
**Objective:** Complete 100% of original DDAY refactor specifications  
**Based On:** `REFACTOR_GAP_ANALYSIS.md` — 20 identified gaps

---

## 🎯 STAGE 4 MISSION

**Primary Goal:** Close all gaps between original DDAY specs and current implementation to achieve **100% spec compliance**.

**Success Criteria:**
- ✅ All 20 identified gaps resolved
- ✅ Batch cluster/taxonomy assignment functional
- ✅ Sites module UI cleaned (builder buttons removed)
- ✅ All deprecated field references eliminated
- ✅ Content Manager is true "single source of truth"
- ✅ Full end-to-end workflow verified

**Estimated Effort:** 2-3 days  
**Priority:** HIGH (blocking complete refactor sign-off)

---

## 📋 PHASE BREAKDOWN

### Phase 1: Critical Features (Blocking Completion) — 1 day
**Goal:** Implement missing core spec requirements  
**Deliverables:**
- Batch cluster assignment
- Batch taxonomy assignment
- Sites module cleanup

### Phase 2: Backend Cleanup & Validation — 0.5 days
**Goal:** Remove deprecated references, add validations  
**Deliverables:**
- Filter cleanup
- Publish validation
- Management command updates

### Phase 3: Frontend Polish & UX — 0.5-1 day
**Goal:** Finalize UI, remove broken components  
**Deliverables:**
- PostEditor refactor
- ToggleTableRow cleanup
- Test updates

### Phase 4: Verification & Documentation — 0.5 days
**Goal:** Full E2E testing and docs update  
**Deliverables:**
- Complete workflow tests
- Updated STAGE_4_COMPLETE.md
- CHANGELOG.md entry

---

## 🔴 PHASE 1: CRITICAL FEATURES (Day 1)

### 1.1 Backend — Bulk Update Endpoint

**File:** `backend/igny8_core/modules/writer/serializers.py`

**Create new serializer:**
```python
class BulkUpdateContentSerializer(serializers.Serializer):
    """Serializer for bulk updating content records"""
    content_ids = serializers.ListField(
        child=serializers.IntegerField(),
        required=True,
        help_text="List of Content IDs to update"
    )
    cluster_id = serializers.IntegerField(
        required=False,
        allow_null=True,
        help_text="Cluster ID to assign to selected content"
    )
    taxonomy_term_ids = serializers.ListField(
        child=serializers.IntegerField(),
        required=False,
        help_text="Taxonomy term IDs to attach to selected content"
    )
    
    def validate_content_ids(self, value):
        if not value:
            raise serializers.ValidationError("At least one content ID required")
        if len(value) > 100:
            raise serializers.ValidationError("Maximum 100 items per batch")
        return value
    
    def validate(self, data):
        if not data.get('cluster_id') and not data.get('taxonomy_term_ids'):
            raise serializers.ValidationError(
                "Must provide cluster_id or taxonomy_term_ids"
            )
        return data
```

**File:** `backend/igny8_core/modules/writer/views.py`

**Add to ContentViewSet:**
```python
from rest_framework.decorators import action
from rest_framework.response import Response
from django.db import transaction

class ContentViewSet(SiteSectorModelViewSet):
    # ... existing code ...
    
    @action(detail=False, methods=['patch'], url_path='bulk-update')
    def bulk_update(self, request):
        """
        Bulk update cluster and/or taxonomy assignments
        
        POST /api/v1/writer/content/bulk-update/
        {
            "content_ids": [1, 2, 3],
            "cluster_id": 5,  // optional
            "taxonomy_term_ids": [10, 11]  // optional
        }
        """
        serializer = BulkUpdateContentSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        
        content_ids = serializer.validated_data['content_ids']
        cluster_id = serializer.validated_data.get('cluster_id')
        taxonomy_term_ids = serializer.validated_data.get('taxonomy_term_ids', [])
        
        # Verify user has access to this site's content
        queryset = self.get_queryset()
        content_items = queryset.filter(id__in=content_ids)
        
        if content_items.count() != len(content_ids):
            return Response({
                'success': False,
                'error': 'Some content items not found or not accessible'
            }, status=400)
        
        updated_count = 0
        errors = []
        
        with transaction.atomic():
            for content in content_items:
                try:
                    if cluster_id:
                        from igny8_core.business.planning.models import Clusters
                        cluster = Clusters.objects.get(id=cluster_id, site=content.site)
                        content.cluster = cluster
                    
                    if taxonomy_term_ids:
                        # Replace existing terms with new selection
                        content.taxonomy_terms.set(taxonomy_term_ids)
                    
                    content.save()
                    updated_count += 1
                    
                except Exception as e:
                    errors.append({
                        'content_id': content.id,
                        'error': str(e)
                    })
        
        return Response({
            'success': True,
            'updated': updated_count,
            'errors': errors,
            'message': f'Successfully updated {updated_count} content items'
        })
```

**File:** `backend/igny8_core/modules/writer/urls.py`

No changes needed (action decorator auto-registers route)

---

### 1.2 Frontend — Bulk Assignment UI

**File:** `frontend/src/config/pages/content.config.tsx`

**Add selection column:**
```typescript
import { Checkbox } from '@/components/ui/checkbox';

export const contentTableConfig = {
  columns: [
    {
      id: 'select',
      header: ({ table }) => (
        <Checkbox
          checked={table.getIsAllPageRowsSelected()}
          onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
          aria-label="Select all"
        />
      ),
      cell: ({ row }) => (
        <Checkbox
          checked={row.getIsSelected()}
          onCheckedChange={(value) => row.toggleSelected(!!value)}
          aria-label="Select row"
        />
      ),
      enableSorting: false,
      enableHiding: false,
    },
    // ... existing columns ...
  ],
  // ... rest of config ...
};
```

**File:** `frontend/src/services/api.ts`

**Add bulk update function:**
```typescript
export interface BulkUpdateContentRequest {
  content_ids: number[];
  cluster_id?: number;
  taxonomy_term_ids?: number[];
}

export interface BulkUpdateContentResponse {
  success: boolean;
  updated: number;
  errors: Array<{ content_id: number; error: string }>;
  message: string;
}

export const bulkUpdateContent = async (
  data: BulkUpdateContentRequest
): Promise<BulkUpdateContentResponse> => {
  const response = await apiClient.patch<BulkUpdateContentResponse>(
    '/api/v1/writer/content/bulk-update/',
    data
  );
  return response.data;
};
```

**File:** `frontend/src/pages/Writer/Content.tsx`

**Add bulk operations UI:**
```typescript
import { useState } from 'react';
import { bulkUpdateContent } from '@/services/api';
import { Button } from '@/components/ui/button';
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select';
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from '@/components/ui/dialog';

const ContentPage = () => {
  const [selectedRows, setSelectedRows] = useState<number[]>([]);
  const [bulkAction, setBulkAction] = useState<string>('');
  const [showBulkDialog, setShowBulkDialog] = useState(false);
  const [selectedCluster, setSelectedCluster] = useState<number | null>(null);
  const [selectedTaxonomies, setSelectedTaxonomies] = useState<number[]>([]);
  
  const handleBulkActionClick = () => {
    if (selectedRows.length === 0) {
      toast.error('Please select at least one content item');
      return;
    }
    
    if (bulkAction === 'assign_cluster') {
      setShowBulkDialog(true);
      // Load clusters for dropdown
    } else if (bulkAction === 'assign_taxonomy') {
      setShowBulkDialog(true);
      // Load taxonomies for dropdown
    }
  };
  
  const handleBulkUpdate = async () => {
    try {
      const result = await bulkUpdateContent({
        content_ids: selectedRows,
        cluster_id: bulkAction === 'assign_cluster' ? selectedCluster : undefined,
        taxonomy_term_ids: bulkAction === 'assign_taxonomy' ? selectedTaxonomies : undefined,
      });
      
      toast.success(result.message);
      setShowBulkDialog(false);
      setSelectedRows([]);
      // Refresh table data
      
    } catch (error) {
      toast.error('Bulk update failed');
    }
  };
  
  return (
    <div>
      {/* Bulk Actions Toolbar */}
      {selectedRows.length > 0 && (
        <div className="mb-4 flex items-center gap-2 p-4 bg-blue-50 rounded-lg">
          <span className="text-sm font-medium">
            {selectedRows.length} selected
          </span>
          <Select value={bulkAction} onValueChange={setBulkAction}>
            <SelectTrigger className="w-[200px]">
              <SelectValue placeholder="Bulk Actions" />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value="assign_cluster">Assign Cluster</SelectItem>
              <SelectItem value="assign_taxonomy">Assign Taxonomies</SelectItem>
            </SelectContent>
          </Select>
          <Button onClick={handleBulkActionClick}>Apply</Button>
        </div>
      )}
      
      {/* Bulk Assignment Dialog */}
      <Dialog open={showBulkDialog} onOpenChange={setShowBulkDialog}>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>
              {bulkAction === 'assign_cluster' ? 'Assign Cluster' : 'Assign Taxonomies'}
            </DialogTitle>
            <DialogDescription>
              Update {selectedRows.length} selected content items
            </DialogDescription>
          </DialogHeader>
          
          {bulkAction === 'assign_cluster' && (
            <ClusterSelector
              value={selectedCluster}
              onChange={setSelectedCluster}
            />
          )}
          
          {bulkAction === 'assign_taxonomy' && (
            <TaxonomyMultiSelector
              value={selectedTaxonomies}
              onChange={setSelectedTaxonomies}
            />
          )}
          
          <DialogFooter>
            <Button variant="outline" onClick={() => setShowBulkDialog(false)}>
              Cancel
            </Button>
            <Button onClick={handleBulkUpdate}>
              Update {selectedRows.length} Items
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>
      
      {/* Existing table component */}
    </div>
  );
};
```

**File:** `frontend/src/pages/Sites/Content.tsx`

Apply same bulk operations UI pattern.

---

### 1.3 Sites Module — Remove Builder Buttons

**File:** `frontend/src/pages/Sites/List.tsx`

**Remove buttons from site cards:**
```typescript
// REMOVE these imports and components:
// - BuilderButton
// - BlueprintsButton
// - PagesButton
// - SectorsButton (from card - move to Settings)

const SiteCard = ({ site }) => {
  return (
    <Card>
      <CardHeader>
        <CardTitle>{site.name}</CardTitle>
      </CardHeader>
      <CardContent>
        {/* KEEP these buttons */}
        <div className="flex gap-2">
          <Button asChild>
            <Link to={`/sites/${site.id}/dashboard`}>Dashboard</Link>
          </Button>
          <Button asChild>
            <Link to={`/sites/${site.id}/content`}>Content Manager</Link>
          </Button>
          <Button asChild>
            <Link to={`/sites/${site.id}/settings`}>Settings</Link>
          </Button>
        </div>
        
        {/* REMOVE these buttons:
        <Button>Builder</Button>
        <Button>Blueprints</Button>
        <Button>Pages</Button>
        <Button>Sectors</Button>
        */}
      </CardContent>
    </Card>
  );
};

// ADD active/inactive toggle at top of page
const SitesList = () => {
  const [showInactive, setShowInactive] = useState(false);
  
  return (
    <div>
      <div className="mb-6 flex justify-between items-center">
        <h1>Sites</h1>
        <div className="flex items-center gap-2">
          <Switch
            checked={showInactive}
            onCheckedChange={setShowInactive}
          />
          <span>Show Inactive Sites</span>
        </div>
      </div>
      
      {/* Site cards grid */}
    </div>
  );
};
```

**File:** `frontend/src/pages/Sites/Settings.tsx`

**Add Sectors tab:**
```typescript
const SettingsPage = () => {
  const tabs = [
    { id: 'general', label: 'General' },
    { id: 'wordpress', label: 'WordPress Integration' },
    { id: 'sectors', label: 'Sectors' },  // ADD THIS
    { id: 'advanced', label: 'Advanced' },
  ];
  
  return (
    <Tabs defaultValue="general">
      <TabsList>
        {tabs.map(tab => (
          <TabsTrigger key={tab.id} value={tab.id}>
            {tab.label}
          </TabsTrigger>
        ))}
      </TabsList>
      
      {/* ... existing tabs ... */}
      
      <TabsContent value="sectors">
        <Card>
          <CardHeader>
            <CardTitle>Sectors Management</CardTitle>
            <CardDescription>
              Organize content by business sectors/categories
            </CardDescription>
          </CardHeader>
          <CardContent>
            <SectorsManager siteId={siteId} />
          </CardContent>
        </Card>
      </TabsContent>
    </Tabs>
  );
};
```

---

### 1.4 Content Manager — Unassigned Cluster Filter

**File:** `frontend/src/config/pages/content.config.tsx`

**Add filter option:**
```typescript
export const contentFilters = {
  // ... existing filters ...
  cluster: {
    label: 'Cluster',
    options: [
      { value: 'null', label: 'Unassigned' },  // ADD THIS
      // ... dynamic cluster options loaded from API ...
    ],
  },
};
```

**File:** `frontend/src/pages/Writer/Content.tsx`

**Highlight unassigned rows:**
```typescript
const getRowClassName = (row: Content) => {
  if (!row.cluster_id) {
    return 'bg-yellow-50 border-l-4 border-l-yellow-500';
  }
  return '';
};

// Apply to table row rendering
<TableRow className={getRowClassName(content)}>
  ...
</TableRow>
```

---

## ⚠️ PHASE 2: BACKEND CLEANUP (Day 1.5-2)

### 2.1 Remove Deprecated Filter Fields

**File:** `backend/igny8_core/modules/writer/views.py`

**Before:**
```python
class ContentTaxonomyViewSet(SiteSectorModelViewSet):
    filterset_fields = ['taxonomy_type', 'sync_status', 'parent', 'external_id', 'external_taxonomy']
```

**After:**
```python
class ContentTaxonomyViewSet(SiteSectorModelViewSet):
    # Stage 1: Removed sync_status and parent fields
    filterset_fields = ['taxonomy_type', 'external_id', 'external_taxonomy']
```

---

### 2.2 Add Publish Validation

**File:** `backend/igny8_core/modules/writer/views.py`

**Update publish_content action:**
```python
@action(detail=True, methods=['post'])
def publish_content(self, request, pk=None):
    """Publish content to WordPress"""
    content = self.get_object()
    
    # Validation checks per spec
    if content.status != 'draft':
        return Response({
            'success': False,
            'error': f'Content must be in draft status (current: {content.status})'
        }, status=400)
    
    if not content.site.wordpress_endpoint:
        return Response({
            'success': False,
            'error': 'Site WordPress endpoint not configured'
        }, status=400)
    
    if not content.site.wordpress_auth_token:
        return Response({
            'success': False,
            'error': 'WordPress authentication not configured'
        }, status=400)
    
    if content.external_id:
        return Response({
            'success': False,
            'error': 'Content already published (external_id exists)'
        }, status=400)
    
    # Proceed with publish
    # ... existing publish logic ...
```

---

### 2.3 Clean Up Management Commands

**File:** `backend/igny8_core/modules/writer/management/commands/audit_site_metadata.py`

**Remove cluster_role references:**
```python
# REMOVE these lines:
tasks_with_cluster_role = tasks.filter(cluster_role__isnull=False).count()
self.stdout.write(f'  With Cluster Role: {tasks_with_cluster_role}/{total_tasks} ...')

# REMOVE this section:
roles = tasks.values('cluster_role').annotate(count=Count('id')).order_by('-count')
for role in roles:
    self.stdout.write(f'  {role["cluster_role"] or "NULL"}: {role["count"]} tasks')

# REPLACE WITH:
# Show content_type distribution instead
types = tasks.values('content_type').annotate(count=Count('id')).order_by('-count')
for type_obj in types:
    self.stdout.write(f'  {type_obj["content_type"]}: {type_obj["count"]} tasks')
```

---

## 🎨 PHASE 3: FRONTEND POLISH (Day 2-2.5)

### 3.1 PostEditor — Remove SEO/Metadata Tabs

**File:** `frontend/src/pages/Sites/PostEditor.tsx`

**Before (3 tabs):**
```typescript
const tabs = [
  { id: 'content', label: 'Content' },
  { id: 'seo', label: 'SEO' },  // REMOVE
  { id: 'metadata', label: 'Metadata' },  // REMOVE
];
```

**After (2 tabs):**
```typescript
const tabs = [
  { id: 'content', label: 'Content' },
  { id: 'taxonomy', label: 'Taxonomy & Cluster' },
];

// REMOVE entire SEO tab content (lines ~450-550):
// - meta_title input
// - meta_description textarea
// - primary_keyword input
// - secondary_keywords input

// REMOVE entire Metadata tab content (lines ~550-650):
// - tags selector (use taxonomy_terms instead)
// - categories selector (use taxonomy_terms instead)

// KEEP Taxonomy & Cluster tab:
<TabsContent value="taxonomy">
  <Card>
    <CardHeader>
      <CardTitle>Taxonomy & Cluster Assignment</CardTitle>
    </CardHeader>
    <CardContent>
      {/* Cluster selector */}
      <div className="mb-4">
        <Label>Cluster</Label>
        <ClusterSelect
          value={content.cluster_id}
          onChange={(id) => setContent({ ...content, cluster_id: id })}
        />
      </div>
      
      {/* Taxonomy terms (read-only display or multi-select) */}
      <div>
        <Label>Taxonomy Terms</Label>
        <TaxonomyMultiSelect
          value={content.taxonomy_terms?.map(t => t.id) || []}
          onChange={(ids) => updateTaxonomyTerms(ids)}
        />
      </div>
    </CardContent>
  </Card>
</TabsContent>
```

---

### 3.2 ToggleTableRow — Remove Deprecated Fallbacks

**File:** `frontend/src/components/common/ToggleTableRow.tsx`

**Remove these fallback patterns:**
```typescript
// REMOVE:
const primaryKeyword = row.primary_keyword || '';
const metaDescription = row.meta_description || row.description || '';
const tags = row.tags || [];
const categories = row.categories || [];

// REPLACE WITH new schema only:
const title = row.title;
const contentHtml = row.content_html;
const taxonomyTerms = row.taxonomy_terms || [];
const cluster = row.cluster_name || 'Unassigned';
```

---

### 3.3 Writer Tasks — Add Content Manager Link

**File:** `frontend/src/pages/Writer/Tasks.tsx`

**Add action button:**
```typescript
const taskActions = [
  {
    label: 'Generate Content',
    onClick: (task) => handleGenerateContent(task.id),
    show: (task) => task.status === 'queued',
  },
  {
    label: 'View in Content Manager',  // ADD THIS
    onClick: (task) => {
      // Navigate to Content Manager filtered by this task's cluster
      navigate(`/writer/content?cluster_id=${task.cluster_id}`);
    },
    show: (task) => task.status === 'completed',
  },
];
```

---

### 3.4 Update Test Mocks

**File:** `frontend/src/pages/Optimizer/__tests__/ContentSelector.test.tsx`

**Before:**
```typescript
const mockContent = [
  { id: 1, title: 'Test', source: 'igny8', sync_status: 'native', ... }
];
```

**After:**
```typescript
const mockContent = [
  { 
    id: 1, 
    title: 'Test', 
    source: 'igny8',
    content_type: 'post',
    content_structure: 'article',
    cluster_id: 1,
    cluster_name: 'Test Cluster',
    taxonomy_terms: [],
    status: 'draft',
    // REMOVED: sync_status, entity_type, cluster_role
  }
];
```

Apply same pattern to all test files.

---

### 3.5 Verify Cluster Detail Tabs

**File:** `frontend/src/pages/Planner/ClusterDetail.tsx`

**Ensure all 4 tabs exist:**
```typescript
const contentTypeTabs = [
  { id: 'post', label: 'Articles', icon: FileText },
  { id: 'page', label: 'Pages', icon: Layout },
  { id: 'product', label: 'Products', icon: Package },
  { id: 'category', label: 'Taxonomy Pages', icon: Tag },  // Verify this exists
];
```

---

## 🧪 PHASE 4: VERIFICATION & DOCS (Day 2.5-3)

### 4.1 End-to-End Testing

**Test Scenario 1: Full Pipeline**
```
1. Planner → Create keywords
2. Planner → Generate clusters
3. Planner → Generate ideas
4. Planner → Queue to Writer (bulk action)
5. Writer → Tasks → Verify tasks created with content_type/content_structure
6. Writer → Generate content for task
7. Writer → Content Manager → Verify content created with status=draft
8. Content Manager → Select 3 content items
9. Content Manager → Bulk assign cluster
10. Content Manager → Bulk assign taxonomies
11. Content Manager → Publish to WordPress
12. Verify external_id and external_url set
13. Try publishing again → Should get error
```

**Test Scenario 2: WordPress Import**
```
1. WordPress site with 10 posts
2. IGNY8 → Integration → Import from WordPress
3. Content Manager → Filter by source=wordpress
4. Content Manager → Filter by cluster=null
5. Content Manager → Bulk assign cluster to imported content
6. Verify cluster_id updated
```

**Test Scenario 3: Cluster Detail**
```
1. Navigate to Cluster Detail page
2. Verify 4 tabs: Articles, Pages, Products, Taxonomy
3. Click each tab
4. Verify content filtered by content_type
5. Click content item → Opens in Content Manager
```

**Test Scenario 4: Sites Module**
```
1. Sites → List page
2. Verify NO builder/blueprints/pages buttons on cards
3. Verify active/inactive toggle at top
4. Click Settings on a site
5. Verify Sectors tab exists
6. Verify sectors can be managed
```

---

### 4.2 Documentation Updates

**File:** `STAGE_4_COMPLETE.md`

Create comprehensive completion document:
```markdown
# STAGE 4 IMPLEMENTATION — DDAY REFACTOR 100% COMPLETE

**Date:** [Completion Date]
**Status:** ✅ 100% COMPLETE (All Original Spec Requirements Met)

## Objective Achieved
- ✅ 20 gaps closed
- ✅ Batch operations implemented
- ✅ Sites module cleaned
- ✅ All deprecated fields removed
- ✅ 100% original DDAY spec compliance

## Files Modified
- Backend: 6 files
- Frontend: 10 files
- WordPress Plugin: 1 file
- Documentation: 3 files

## What Was Built
1. Bulk cluster assignment
2. Bulk taxonomy assignment
3. Sites UI cleanup (builder buttons removed)
4. Publish validation
5. PostEditor refactor
6. Deprecated field cleanup
7. Test updates

## Verification Results
- ✅ Full pipeline test passed
- ✅ WordPress import test passed
- ✅ Cluster detail test passed
- ✅ Sites module test passed
```

**File:** `CHANGELOG.md`

Add Stage 4 entry:
```markdown
### [2025-11-26] IGNY8 Stage 4 — DDAY Refactor Completion

**Status:** ✅ 100% COMPLETE (All Original Specifications Implemented)

**Summary:**
Stage 4 closed all remaining gaps from the original DDAY refactor specifications. The platform now has complete batch operations, cleaned Sites UI, removed all deprecated field references, and achieved 100% spec compliance.

#### Added
- Bulk cluster assignment for content
- Bulk taxonomy assignment for content
- Content Manager selection UI with checkboxes
- Publish validation (draft status, site config checks)
- Sectors management tab in Site Settings
- Active/inactive toggle for Sites list
- "View in Content Manager" link from Writer tasks
- Unassigned cluster filter and highlighting

#### Changed
- Sites module: Removed builder, blueprints, pages buttons
- PostEditor: Removed SEO and Metadata tabs
- ToggleTableRow: Removed deprecated field fallbacks
- ContentTaxonomy filters: Removed sync_status and parent

#### Fixed
- Management command: Removed cluster_role references
- Frontend tests: Updated mocks to new schema
- All deprecated field references eliminated

#### Removed
- SEO tab from PostEditor
- Metadata tab from PostEditor
- Builder-related buttons from Sites cards
- All cluster_role, sync_status, entity_type references
```

**File:** `original-specs of-refactor`

Add completion notice at top:
```
✅ IMPLEMENTATION STATUS: 100% COMPLETE (Stage 4 - November 26, 2025)

All specifications in this document have been fully implemented across Stages 1-4.
See STAGE_4_COMPLETE.md for final implementation summary.

---
[Original specs follow below...]
```

---

## 📦 COMPLETE FILE CHECKLIST

### Backend Files (6)
- [ ] `backend/igny8_core/modules/writer/views.py`
  - [ ] Remove `sync_status` and `parent` from ContentTaxonomyViewSet filters
  - [ ] Add `bulk_update` action to ContentViewSet
  - [ ] Add publish validation checks
  
- [ ] `backend/igny8_core/modules/writer/serializers.py`
  - [ ] Add `BulkUpdateContentSerializer`
  
- [ ] `backend/igny8_core/modules/writer/management/commands/audit_site_metadata.py`
  - [ ] Remove `cluster_role` references
  - [ ] Replace with `content_type` stats
  
- [ ] `backend/igny8_core/business/publishing/services/adapters/wordpress_adapter.py`
  - [ ] Verify cluster_id sent to WordPress plugin

### Frontend Files (10)
- [ ] `frontend/src/pages/Writer/Content.tsx`
  - [ ] Add bulk operations toolbar
  - [ ] Add selection state management
  - [ ] Add bulk assignment dialogs
  
- [ ] `frontend/src/pages/Sites/Content.tsx`
  - [ ] Add same bulk operations UI
  
- [ ] `frontend/src/config/pages/content.config.tsx`
  - [ ] Add selection column
  - [ ] Add "Unassigned" cluster filter option
  
- [ ] `frontend/src/services/api.ts`
  - [ ] Add `bulkUpdateContent` function
  - [ ] Add interfaces
  
- [ ] `frontend/src/pages/Sites/List.tsx`
  - [ ] Remove builder/blueprints/pages buttons
  - [ ] Add active/inactive toggle
  
- [ ] `frontend/src/pages/Sites/Settings.tsx`
  - [ ] Add Sectors tab
  - [ ] Create SectorsManager component
  
- [ ] `frontend/src/pages/Sites/PostEditor.tsx`
  - [ ] Remove SEO tab
  - [ ] Remove Metadata tab
  - [ ] Keep only Content and Taxonomy tabs
  
- [ ] `frontend/src/components/common/ToggleTableRow.tsx`
  - [ ] Remove deprecated field fallbacks
  
- [ ] `frontend/src/pages/Writer/Tasks.tsx`
  - [ ] Add "View in Content Manager" action
  
- [ ] `frontend/src/pages/Optimizer/__tests__/ContentSelector.test.tsx`
  - [ ] Update test mocks to new schema

### WordPress Plugin (1)
- [ ] `sync/igny8-to-wp.php`
  - [ ] Verify `_igny8_cluster_id` meta field saved

### Documentation (4)
- [ ] `STAGE_4_COMPLETE.md` (create)
- [ ] `CHANGELOG.md` (update)
- [ ] `original-specs of-refactor` (add completion notice)
- [ ] `REFACTOR_GAP_ANALYSIS.md` (already created)

---

## 🎯 SUCCESS METRICS

### Technical Compliance
- ✅ 0 deprecated field references in active code
- ✅ 0 TypeScript compilation errors
- ✅ All backend ViewSet filters use only Stage 1 schema
- ✅ All frontend components use only Stage 1 schema

### Feature Completeness
- ✅ Batch cluster assignment works for 1-100 items
- ✅ Batch taxonomy assignment works for 1-100 items
- ✅ Sites module shows only Dashboard/Content/Settings buttons
- ✅ PostEditor has only 2 tabs (Content, Taxonomy & Cluster)
- ✅ Publish validation prevents invalid operations

### User Experience
- ✅ Content Manager is single source of truth
- ✅ Unassigned imported content clearly visible
- ✅ Bulk operations intuitive and fast
- ✅ No confusing deprecated UI elements

### Code Quality
- ✅ No console errors in browser
- ✅ All tests passing
- ✅ Build time < 15s
- ✅ No unused imports or dead code

---

## 🚀 DEPLOYMENT STEPS

### Pre-Deployment
1. Run backend tests: `python manage.py test`
2. Run frontend build: `npm run build`
3. Check for TypeScript errors
4. Review all changed files

### Deployment
1. Deploy backend to staging
2. Run migrations (none expected)
3. Deploy frontend to staging
4. Run E2E tests on staging
5. Deploy to production
6. Monitor for errors

### Post-Deployment
1. Verify bulk operations work
2. Verify Sites UI clean
3. Verify publish validation
4. Update user documentation
5. Announce Stage 4 completion

---

## 💡 FUTURE ENHANCEMENTS (Post-Stage 4)

### Short Term (Next 2-4 weeks)
- Auto-cluster assignment for imported content (ML-based)
- Scheduled bulk publishing
- Content preview before publish
- Advanced content search

### Medium Term (Next 1-3 months)
- Full Optimizer module refactor
- Linker module alignment
- Content versioning
- A/B testing for content

### Long Term (Next 3-6 months)
- AI-powered content suggestions
- Performance analytics dashboard
- Multi-site content sharing
- Advanced taxonomy auto-tagging

---

**Stage 4 Completion Target:** 100% DDAY Refactor Spec Compliance  
**Estimated Completion Date:** [3 days from start]  
**Status:** Ready for Implementation ✅
