Files
igny8/v2/V2-Execution-Docs/01A-sag-data-foundation.md
IGNY8 VPS (Salman) 128b186865 temproary docs uplaoded
2026-03-23 09:02:49 +00:00

59 KiB
Raw Blame History

IGNY8 Phase 1: SAG Data Foundation (01A)

Core Data Models & CRUD APIs

Document Version: 1.0 Date: 2026-03-23 Phase: IGNY8 Phase 1 — SAG Data Foundation Status: Build Ready Audience: Claude Code, Backend Developers, Architects


1. CURRENT STATE

Existing IGNY8 Architecture

  • Framework: Django 5.1 + Django REST Framework 3.15 (upgrading to Django 6.0 on new VPS)
  • Database: PostgreSQL 16 (upgrading to PostgreSQL 18)
  • Multi-Tenancy: AccountContextMiddleware enforces tenant isolation
    • All new models inherit from AccountBaseModel or SiteSectorBaseModel
    • Automatic tenant filtering in querysets
  • Response Format: Unified across all endpoints
    {
      "success": true,
      "data": { /* model instance or list */ },
      "message": "Optional message"
    }
    
  • ViewSet Pattern: AccountModelViewSet (filters by account) and SiteSectorModelViewSet
  • Async Processing: Celery 5.4 for background tasks
  • Existing Models:
    • Account (tenant root)
    • Site (per-account, multi-site support)
    • Sector (site-level category)
    • Keyword, Cluster, ContentIdea, Task, Content, Image, SiteIntegration

Frontend Stack

  • React 19 + TypeScript 5
  • Tailwind CSS 3
  • Zustand 4 (state management)

What Doesn't Exist

  • Attribute-based cluster formation system
  • Blueprint versioning and health scoring
  • Sector attribute templates
  • CRUD APIs for managing clusters and attributes at scale
  • Cross-cluster linking framework (reserved for Phase 2 Linker)

2. WHAT TO BUILD

Overview

IGNY8 Phase 1 introduces the SAG (Sector Attribute Grid) Data Foundation — a system for:

  1. Attribute Discovery: Identify key dimensions (attributes) in a given sector
  2. Cluster Formation: Organize keywords and content around attribute intersections
  3. Blueprint Versioning: Create immutable snapshots of taxonomy and execution plans
  4. Health Scoring: Track completeness and coverage of the SAG blueprint

Scope

  • 4 new Django models in new app sag/
  • Modifications to 5 existing models (all backward-compatible, nullable fields)
  • Full CRUD APIs with nested routing
  • Admin interface for templates and blueprints
  • Service layer for complex operations (attribute discovery, cluster generation, health checks)
  • AI function stubs for Phase 23 integration (attribute extraction, cluster formation, etc.)

Non-Scope (Downstream Phases)

  • Internal linking strategy (Phase 2 Linker)
  • Content planning automation (Phase 2 Planner)
  • WordPress taxonomy sync (Phase 3 Syncer)
  • AI-powered attribute discovery (Phase 23)
  • Reporting & analytics UI (Phase 2)

3. DATA MODELS & API STRUCTURE

3.1 New Django App: sag/

Location: backend/igny8_core/sag/

Directory Structure:

sag/
├── __init__.py
├── models.py                    # SAGBlueprint, SAGAttribute, SAGCluster, SectorAttributeTemplate
├── serializers.py               # DRF serializers
├── views.py                     # ViewSets & custom actions
├── urls.py                      # URL routing (/api/v1/sag/*)
├── admin.py                     # Django admin registration
├── apps.py                      # App config
├── services/
│   ├── __init__.py
│   ├── blueprint_service.py     # Blueprint CRUD, versioning, confirmation
│   ├── attribute_service.py     # Attribute CRUD, discovery stubs
│   ├── cluster_service.py       # Cluster CRUD, formation, linking
│   ├── keyword_service.py       # Keyword extraction from cluster config
│   ├── template_service.py      # Template merging, sector lookups
│   └── health_service.py        # Health scoring, completeness checks
├── ai_functions/
│   ├── __init__.py
│   ├── attribute_discovery.py   # AI: Discover sector attributes (Phase 23)
│   ├── attribute_extraction.py  # AI: Extract values from content (Phase 23)
│   ├── attribute_population.py  # AI: Populate values into blueprint (Phase 23)
│   ├── cluster_formation.py     # AI: Form clusters from keywords (Phase 23)
│   ├── keyword_generation.py    # AI: Generate keywords per cluster (Phase 23)
│   └── content_planning.py      # AI: Plan supporting content (Phase 23)
├── migrations/
│   ├── 0001_initial.py          # Initial models
│   └── (auto-generated)
└── tests/
    ├── __init__.py
    ├── test_models.py
    ├── test_views.py
    └── test_services.py

3.2 Model Definitions

SAGBlueprint (New, AccountBaseModel)

Purpose: Immutable snapshot of taxonomy, clusters, attributes, and execution plan for a site.

Fields:

class SAGBlueprint(AccountBaseModel):
    """
    Core blueprint model: versioned taxonomy & cluster plan for a site.
    Inherits: id (UUID), account_id (FK), created_at, updated_at
    """
    site = models.ForeignKey(
        'sites.Site',
        on_delete=models.CASCADE,
        related_name='sag_blueprints'
    )
    version = models.IntegerField(default=1)

    status = models.CharField(
        max_length=20,
        choices=[
            ('draft', 'Draft'),
            ('active', 'Active'),
            ('evolving', 'Evolving'),
            ('archived', 'Archived')
        ],
        default='draft'
    )

    source = models.CharField(
        max_length=20,
        choices=[
            ('site_builder', 'From Site Builder'),
            ('site_analysis', 'From Site Analysis'),
            ('manual', 'Manual Input')
        ],
        default='manual'
    )

    # Denormalized snapshots for fast reads
    attributes_json = models.JSONField(
        default=dict,
        help_text="Snapshot of all attributes: {attr_slug: {name, level, values, wp_slug}}"
    )
    clusters_json = models.JSONField(
        default=dict,
        help_text="Snapshot of all clusters: {cluster_slug: {name, type, keywords, hub_title}}"
    )

    taxonomy_plan = models.JSONField(
        default=dict,
        help_text="Execution taxonomy: organization of attributes by level, priority order"
    )

    execution_priority = models.JSONField(
        default=dict,
        help_text="Priority matrix: {cluster_slug: priority_score}"
    )

    # Phase 2 Linker reservation
    internal_linking_map = models.JSONField(
        default=dict,
        null=True,
        blank=True,
        help_text="Placeholder for Phase 2: cross-cluster internal linking strategy"
    )

    # Health & metrics
    sag_health_score = models.FloatField(
        default=0.0,
        validators=[MinValueValidator(0), MaxValueValidator(100)],
        help_text="0-100 score: completeness, coverage, consistency"
    )
    total_clusters = models.IntegerField(default=0)
    total_keywords = models.IntegerField(default=0)
    total_content_planned = models.IntegerField(default=0)
    total_content_published = models.IntegerField(default=0)

    # State tracking
    confirmed_at = models.DateTimeField(
        null=True,
        blank=True,
        help_text="When blueprint was moved from draft → active"
    )
    last_health_check = models.DateTimeField(
        null=True,
        blank=True,
        help_text="When last health_check() was run"
    )

    class Meta:
        unique_together = ('site', 'version', 'account')
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['site', 'status']),
            models.Index(fields=['account', 'status']),
        ]

    def __str__(self):
        return f"{self.site.name} SAG v{self.version} ({self.status})"

SAGAttribute (New, AccountBaseModel)

Purpose: Dimensional metadata for a blueprint (e.g., color, size, price_range, problem_type).

Fields:

class SAGAttribute(AccountBaseModel):
    """
    Attribute/dimension within a blueprint.
    Inherits: id (UUID), account_id (FK), created_at, updated_at
    """
    blueprint = models.ForeignKey(
        'sag.SAGBlueprint',
        on_delete=models.CASCADE,
        related_name='attributes'
    )

    name = models.CharField(
        max_length=100,
        help_text="Human-readable: 'Color', 'Product Type', 'Problem Category'"
    )
    slug = models.SlugField(
        max_length=100,
        help_text="URL-safe: 'color', 'product_type', 'problem_category'"
    )

    description = models.TextField(
        blank=True,
        help_text="What does this attribute represent in the context of this site?"
    )

    level = models.CharField(
        max_length=20,
        choices=[
            ('primary', 'Primary (main taxonomy dimension)'),
            ('secondary', 'Secondary (supporting dimension)'),
            ('tertiary', 'Tertiary (refinement dimension)')
        ],
        default='secondary'
    )

    # Array of {name, slug} objects
    values = models.JSONField(
        default=list,
        help_text="Possible values: [{name: 'Red', slug: 'red'}, {name: 'Blue', slug: 'blue'}, ...]"
    )

    # WordPress integration (Phase 3)
    wp_taxonomy_slug = models.CharField(
        max_length=100,
        null=True,
        blank=True,
        help_text="WordPress taxonomy slug for syncing (e.g., 'pa_color' for WooCommerce)"
    )

    wp_sync_status = models.CharField(
        max_length=20,
        choices=[
            ('pending', 'Pending Sync'),
            ('synced', 'Synced to WordPress'),
            ('failed', 'Sync Failed')
        ],
        default='pending'
    )

    sort_order = models.IntegerField(
        default=0,
        help_text="Display order within blueprint"
    )

    class Meta:
        unique_together = ('blueprint', 'slug', 'account')
        ordering = ['sort_order', 'name']
        indexes = [
            models.Index(fields=['blueprint', 'level']),
        ]

    def __str__(self):
        return f"{self.name} ({self.level}) in {self.blueprint.site.name}"

SAGCluster (New, AccountBaseModel)

Purpose: A content hub organized around attribute intersections; contains keywords, structure, and supporting content plan.

Fields:

class SAGCluster(AccountBaseModel):
    """
    Cluster: hub page + supporting content organized around attribute intersection.
    Inherits: id (UUID), account_id (FK), created_at, updated_at
    """
    blueprint = models.ForeignKey(
        'sag.SAGBlueprint',
        on_delete=models.CASCADE,
        related_name='clusters'
    )
    site = models.ForeignKey(
        'sites.Site',
        on_delete=models.CASCADE,
        related_name='sag_clusters'
    )

    # Identity
    name = models.CharField(
        max_length=200,
        help_text="Cluster name (may become hub page title)"
    )
    slug = models.SlugField(
        max_length=200,
        help_text="URL-safe identifier: used in hub_page_url_slug"
    )

    cluster_type = models.CharField(
        max_length=50,
        choices=[
            ('product_category', 'Product Category'),
            ('condition_problem', 'Condition / Problem'),
            ('feature', 'Feature / Benefit'),
            ('brand', 'Brand'),
            ('informational', 'Informational / How-to'),
            ('comparison', 'Comparison')
        ],
        help_text="Semantic type for content strategy"
    )

    # Attribute intersection: {dimension_name: value_slug}
    attribute_intersection = models.JSONField(
        default=dict,
        help_text="Attribute values defining this cluster: {'color': 'red', 'material': 'leather'}"
    )

    # Hub page design
    hub_page_title = models.CharField(
        max_length=300,
        help_text="SEO-optimized hub page title"
    )
    hub_page_type = models.CharField(
        max_length=100,
        default='cluster_hub',
        help_text="'cluster_hub' or custom type"
    )
    hub_page_structure = models.CharField(
        max_length=100,
        default='guide_tutorial',
        choices=[
            ('guide_tutorial', 'Guide / Tutorial'),
            ('product_comparison', 'Product Comparison'),
            ('category_overview', 'Category Overview'),
            ('problem_solution', 'Problem / Solution'),
            ('resource_library', 'Resource Library')
        ],
        help_text="Content structure template for hub page"
    )
    hub_page_url_slug = models.CharField(
        max_length=300,
        help_text="URL slug for the hub page (e.g., '/guides/red-leather-bags/')"
    )

    # Keyword strategy
    auto_generated_keywords = models.JSONField(
        default=list,
        help_text="Array of keyword strings generated for this cluster"
    )

    # Supporting content plan: [{ title, type, structure, keywords }, ...]
    supporting_content_plan = models.JSONField(
        default=list,
        help_text="Planned supporting articles/guides linked to this hub"
    )

    # Linking
    linked_attribute_terms = models.JSONField(
        default=list,
        help_text="Slugs of attribute values mentioned in cluster (for internal linking)"
    )
    cross_cluster_links = models.JSONField(
        default=list,
        help_text="Slugs of related clusters for cross-linking"
    )

    # Status & metrics
    status = models.CharField(
        max_length=20,
        choices=[
            ('planned', 'Planned'),
            ('partial', 'Partially Complete'),
            ('complete', 'Complete')
        ],
        default='planned'
    )

    content_count = models.IntegerField(
        default=0,
        help_text="Number of content pieces published for this cluster"
    )

    link_coverage_score = models.FloatField(
        default=0.0,
        validators=[MinValueValidator(0), MaxValueValidator(100)],
        help_text="0-100 score: internal linking completeness"
    )

    backlink_target_tier = models.CharField(
        max_length=5,
        choices=[('T1', 'T1'), ('T2', 'T2'), ('T3', 'T3'), ('T4', 'T4'), ('T5', 'T5')],
        null=True,
        blank=True,
        help_text="Target tier for external backlinks (T1 = highest authority)"
    )

    class Meta:
        unique_together = ('blueprint', 'slug', 'account')
        ordering = ['name']
        indexes = [
            models.Index(fields=['blueprint', 'cluster_type']),
            models.Index(fields=['site', 'status']),
            models.Index(fields=['account', 'status']),
        ]

    def __str__(self):
        return f"{self.name} ({self.cluster_type}) in {self.site.name}"

SectorAttributeTemplate (New, Base Model)

Purpose: Admin-managed templates for attribute frameworks and keyword patterns by industry/sector.

Fields:

class SectorAttributeTemplate(models.Model):
    """
    Reusable template for attributes and keywords by industry + sector.
    NOT tied to Account (admin-only, shared across tenants).
    """
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)

    industry = models.CharField(
        max_length=200,
        help_text="E.g., 'Fashion', 'SaaS', 'Healthcare'"
    )
    sector = models.CharField(
        max_length=200,
        help_text="E.g., 'Womens Apparel', 'CRM Software', 'Dental Services'"
    )

    site_type = models.CharField(
        max_length=50,
        choices=[
            ('e_commerce', 'E-Commerce'),
            ('services', 'Services'),
            ('saas', 'SaaS'),
            ('content', 'Content / Blog'),
            ('local_business', 'Local Business'),
            ('brand', 'Brand / Corporate')
        ],
        default='e_commerce'
    )

    # Template structure: {attribute_name: {level, suggested_values: [...]}, ...}
    attribute_framework = models.JSONField(
        default=dict,
        help_text="Template attributes: {'color': {'level': 'primary', 'suggested_values': ['red', 'blue', ...]}, ...}"
    )

    # Keyword templates per cluster type: {cluster_type: {pattern, examples}, ...}
    keyword_templates = models.JSONField(
        default=dict,
        help_text="Keyword generation patterns by cluster type"
    )

    source = models.CharField(
        max_length=50,
        choices=[
            ('manual', 'Manual'),
            ('ai_generated', 'AI-Generated'),
            ('user_contributed', 'User Contributed')
        ],
        default='manual'
    )

    is_active = models.BooleanField(
        default=True,
        help_text="Can be used for new blueprints"
    )

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        unique_together = ('industry', 'sector')
        ordering = ['industry', 'sector']
        indexes = [
            models.Index(fields=['industry', 'sector', 'is_active']),
        ]

    def __str__(self):
        return f"{self.industry}{self.sector} ({self.site_type})"

3.3 Modifications to Existing Models

All modifications are backward-compatible and nullable. Existing records are unaffected.

Site (in sites/models.py)

class Site(AccountBaseModel):
    # ... existing fields ...

    sag_blueprint = models.ForeignKey(
        'sag.SAGBlueprint',
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='sites_using_this_blueprint',
        help_text="Active SAG blueprint (if any) for this site"
    )

Cluster (in modules/planner/models.py)

class Cluster(AccountBaseModel):
    # ... existing fields ...

    sag_cluster = models.ForeignKey(
        'sag.SAGCluster',
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='traditional_clusters',
        help_text="Link to SAG cluster if part of SAG blueprint"
    )

    cluster_type = models.CharField(
        max_length=50,
        null=True,
        blank=True,
        help_text="Type from SAG (e.g., 'product_category'). Nullable for legacy clusters."
    )

Task (in modules/writer/models.py)

class Task(AccountBaseModel):
    # ... existing fields ...

    sag_cluster = models.ForeignKey(
        'sag.SAGCluster',
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='tasks',
        help_text="SAG cluster this task contributes to (if any)"
    )

    blueprint_context = models.JSONField(
        default=dict,
        null=True,
        blank=True,
        help_text="Context from SAG blueprint: keywords, attribute_intersection, hub_title"
    )

Content (in modules/writer/models.py)

class Content(AccountBaseModel):
    # ... existing fields ...

    sag_cluster = models.ForeignKey(
        'sag.SAGCluster',
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='content_pieces',
        help_text="SAG cluster this content was published for (if any)"
    )

ContentIdea (in modules/planner/models.py)

class ContentIdea(AccountBaseModel):
    # ... existing fields ...

    sag_cluster = models.ForeignKey(
        'sag.SAGCluster',
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='content_ideas',
        help_text="SAG cluster this idea belongs to (if any)"
    )

    idea_source = models.CharField(
        max_length=50,
        null=True,
        blank=True,
        choices=[
            ('sag_blueprint', 'From SAG Blueprint'),
            ('manual', 'Manual Entry'),
            ('user_suggestion', 'User Suggestion')
        ],
        help_text="Where the idea originated"
    )

3.4 Serializers

File: sag/serializers.py

from rest_framework import serializers
from .models import SAGBlueprint, SAGAttribute, SAGCluster, SectorAttributeTemplate


class SAGAttributeSerializer(serializers.ModelSerializer):
    class Meta:
        model = SAGAttribute
        fields = [
            'id', 'blueprint', 'name', 'slug', 'description',
            'level', 'values', 'wp_taxonomy_slug', 'wp_sync_status',
            'sort_order', 'created_at', 'updated_at'
        ]
        read_only_fields = ['id', 'created_at', 'updated_at']


class SAGClusterSerializer(serializers.ModelSerializer):
    class Meta:
        model = SAGCluster
        fields = [
            'id', 'blueprint', 'site', 'name', 'slug', 'cluster_type',
            'attribute_intersection', 'hub_page_title', 'hub_page_type',
            'hub_page_structure', 'hub_page_url_slug',
            'auto_generated_keywords', 'supporting_content_plan',
            'linked_attribute_terms', 'cross_cluster_links',
            'status', 'content_count', 'link_coverage_score',
            'backlink_target_tier', 'created_at', 'updated_at'
        ]
        read_only_fields = ['id', 'created_at', 'updated_at']


class SAGBlueprintDetailSerializer(serializers.ModelSerializer):
    """Full blueprint with nested attributes and clusters."""
    attributes = SAGAttributeSerializer(many=True, read_only=True)
    clusters = SAGClusterSerializer(many=True, read_only=True)

    class Meta:
        model = SAGBlueprint
        fields = [
            'id', 'site', 'account', 'version', 'status', 'source',
            'attributes_json', 'clusters_json',
            'taxonomy_plan', 'execution_priority', 'internal_linking_map',
            'sag_health_score', 'total_clusters', 'total_keywords',
            'total_content_planned', 'total_content_published',
            'confirmed_at', 'last_health_check',
            'attributes', 'clusters',
            'created_at', 'updated_at'
        ]
        read_only_fields = [
            'id', 'account', 'created_at', 'updated_at',
            'sag_health_score', 'total_clusters', 'total_keywords'
        ]


class SAGBlueprintListSerializer(serializers.ModelSerializer):
    """Lightweight blueprint listing."""
    class Meta:
        model = SAGBlueprint
        fields = [
            'id', 'site', 'version', 'status', 'source',
            'sag_health_score', 'total_clusters', 'total_keywords',
            'confirmed_at', 'created_at'
        ]
        read_only_fields = [
            'id', 'created_at',
            'sag_health_score', 'total_clusters', 'total_keywords'
        ]


class SectorAttributeTemplateSerializer(serializers.ModelSerializer):
    class Meta:
        model = SectorAttributeTemplate
        fields = [
            'id', 'industry', 'sector', 'site_type',
            'attribute_framework', 'keyword_templates',
            'source', 'is_active', 'created_at', 'updated_at'
        ]
        read_only_fields = ['id', 'created_at', 'updated_at']

3.5 ViewSets & URL Routing

File: sag/views.py

from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated, IsAdminUser
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter, OrderingFilter

from igny8_core.common.permissions import IsAccountMember
from igny8_core.common.views import AccountModelViewSet, SiteSectorModelViewSet
from .models import SAGBlueprint, SAGAttribute, SAGCluster, SectorAttributeTemplate
from .serializers import (
    SAGBlueprintDetailSerializer, SAGBlueprintListSerializer,
    SAGAttributeSerializer, SAGClusterSerializer,
    SectorAttributeTemplateSerializer
)
from .services import (
    blueprint_service, attribute_service, cluster_service,
    health_service, template_service
)


class SAGBlueprintViewSet(AccountModelViewSet):
    """
    CRUD for SAG Blueprints.
    Nested under /api/v1/sag/blueprints/
    """
    permission_classes = [IsAuthenticated, IsAccountMember]
    filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
    filterset_fields = ['site', 'status', 'source']
    search_fields = ['site__name']
    ordering_fields = ['-created_at', 'version', 'status']

    def get_serializer_class(self):
        if self.action == 'list':
            return SAGBlueprintListSerializer
        return SAGBlueprintDetailSerializer

    def get_queryset(self):
        """Filter by account tenant."""
        return SAGBlueprint.objects.filter(
            account=self.request.user.account
        ).select_related('site').prefetch_related('attributes', 'clusters')

    @action(detail=True, methods=['post'])
    def confirm(self, request, pk=None):
        """Move blueprint from draft → active."""
        blueprint = self.get_object()
        try:
            blueprint = blueprint_service.confirm_blueprint(blueprint)
            serializer = self.get_serializer(blueprint)
            return Response({
                'success': True,
                'data': serializer.data,
                'message': f'Blueprint v{blueprint.version} activated.'
            })
        except Exception as e:
            return Response({
                'success': False,
                'data': None,
                'message': str(e)
            }, status=status.HTTP_400_BAD_REQUEST)

    @action(detail=True, methods=['post'])
    def archive(self, request, pk=None):
        """Move blueprint from active → archived."""
        blueprint = self.get_object()
        try:
            blueprint = blueprint_service.archive_blueprint(blueprint)
            serializer = self.get_serializer(blueprint)
            return Response({
                'success': True,
                'data': serializer.data,
                'message': f'Blueprint v{blueprint.version} archived.'
            })
        except Exception as e:
            return Response({
                'success': False,
                'data': None,
                'message': str(e)
            }, status=status.HTTP_400_BAD_REQUEST)

    @action(detail=True, methods=['post'])
    def regenerate(self, request, pk=None):
        """Create new version of blueprint from current."""
        blueprint = self.get_object()
        try:
            new_blueprint = blueprint_service.create_new_version(blueprint)
            serializer = self.get_serializer(new_blueprint)
            return Response({
                'success': True,
                'data': serializer.data,
                'message': f'New blueprint v{new_blueprint.version} created.'
            })
        except Exception as e:
            return Response({
                'success': False,
                'data': None,
                'message': str(e)
            }, status=status.HTTP_400_BAD_REQUEST)

    @action(detail=True, methods=['post'])
    def health_check(self, request, pk=None):
        """Compute health score for blueprint."""
        blueprint = self.get_object()
        try:
            result = health_service.compute_blueprint_health(blueprint)
            return Response({
                'success': True,
                'data': result,
                'message': 'Health check complete.'
            })
        except Exception as e:
            return Response({
                'success': False,
                'data': None,
                'message': str(e)
            }, status=status.HTTP_400_BAD_REQUEST)

    @action(detail=False, methods=['get'])
    def active_by_site(self, request):
        """GET /api/v1/sag/blueprints/active_by_site/?site_id=<uuid>"""
        site_id = request.query_params.get('site_id')
        if not site_id:
            return Response({
                'success': False,
                'data': None,
                'message': 'site_id query param required'
            }, status=status.HTTP_400_BAD_REQUEST)

        try:
            blueprint = blueprint_service.get_active_blueprint(
                site_id, request.user.account
            )
            serializer = self.get_serializer(blueprint)
            return Response({
                'success': True,
                'data': serializer.data,
                'message': None
            })
        except SAGBlueprint.DoesNotExist:
            return Response({
                'success': False,
                'data': None,
                'message': 'No active blueprint for this site.'
            }, status=status.HTTP_404_NOT_FOUND)


class SAGAttributeViewSet(AccountModelViewSet):
    """
    CRUD for SAG Attributes within a blueprint.
    Nested under /api/v1/sag/blueprints/{blueprint_id}/attributes/
    """
    permission_classes = [IsAuthenticated, IsAccountMember]
    serializer_class = SAGAttributeSerializer
    filter_backends = [DjangoFilterBackend, OrderingFilter]
    filterset_fields = ['level']
    ordering_fields = ['sort_order', 'name']

    def get_queryset(self):
        blueprint_id = self.kwargs.get('blueprint_id')
        return SAGAttribute.objects.filter(
            blueprint_id=blueprint_id,
            account=self.request.user.account
        )

    def perform_create(self, serializer):
        blueprint_id = self.kwargs.get('blueprint_id')
        serializer.save(
            blueprint_id=blueprint_id,
            account=self.request.user.account
        )


class SAGClusterViewSet(AccountModelViewSet):
    """
    CRUD for SAG Clusters within a blueprint.
    Nested under /api/v1/sag/blueprints/{blueprint_id}/clusters/
    """
    permission_classes = [IsAuthenticated, IsAccountMember]
    serializer_class = SAGClusterSerializer
    filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
    filterset_fields = ['cluster_type', 'status']
    search_fields = ['name', 'hub_page_title']
    ordering_fields = ['name', 'status', 'content_count']

    def get_queryset(self):
        blueprint_id = self.kwargs.get('blueprint_id')
        return SAGCluster.objects.filter(
            blueprint_id=blueprint_id,
            account=self.request.user.account
        )

    def perform_create(self, serializer):
        blueprint_id = self.kwargs.get('blueprint_id')
        blueprint = SAGBlueprint.objects.get(id=blueprint_id)
        serializer.save(
            blueprint=blueprint,
            site=blueprint.site,
            account=self.request.user.account
        )


class SectorAttributeTemplateViewSet(viewsets.ModelViewSet):
    """
    Admin-only CRUD for sector templates (global, not account-specific).
    Endpoint: /api/v1/sag/sector-templates/
    """
    permission_classes = [IsAuthenticated, IsAdminUser]
    serializer_class = SectorAttributeTemplateSerializer
    queryset = SectorAttributeTemplate.objects.filter(is_active=True)
    filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
    filterset_fields = ['industry', 'site_type']
    search_fields = ['industry', 'sector']
    ordering_fields = ['industry', 'sector']

    @action(detail=False, methods=['get'])
    def by_industry_sector(self, request):
        """
        GET /api/v1/sag/sector-templates/by_industry_sector/?industry=<str>&sector=<str>
        """
        industry = request.query_params.get('industry')
        sector = request.query_params.get('sector')

        if not industry or not sector:
            return Response({
                'success': False,
                'data': None,
                'message': 'industry and sector query params required'
            }, status=status.HTTP_400_BAD_REQUEST)

        try:
            template = SectorAttributeTemplate.objects.get(
                industry=industry, sector=sector, is_active=True
            )
            serializer = self.get_serializer(template)
            return Response({
                'success': True,
                'data': serializer.data,
                'message': None
            })
        except SectorAttributeTemplate.DoesNotExist:
            return Response({
                'success': False,
                'data': None,
                'message': f'No template found for {industry}{sector}'
            }, status=status.HTTP_404_NOT_FOUND)

    @action(detail=True, methods=['post'])
    def merge_multi_sector(self, request, pk=None):
        """
        POST /api/v1/sag/sector-templates/{id}/merge_multi_sector/
        Merge multiple sector templates into a single blueprint template.
        """
        template = self.get_object()
        sectors = request.data.get('sectors', [])

        try:
            merged = template_service.merge_templates(template, sectors)
            return Response({
                'success': True,
                'data': merged,
                'message': 'Templates merged successfully.'
            })
        except Exception as e:
            return Response({
                'success': False,
                'data': None,
                'message': str(e)
            }, status=status.HTTP_400_BAD_REQUEST)

File: sag/urls.py

from django.urls import path, include
from rest_framework.routers import SimpleRouter
from .views import (
    SAGBlueprintViewSet, SAGAttributeViewSet, SAGClusterViewSet,
    SectorAttributeTemplateViewSet
)

router = SimpleRouter()
router.register(r'blueprints', SAGBlueprintViewSet, basename='sag-blueprint')
router.register(r'sector-templates', SectorAttributeTemplateViewSet, basename='sector-template')

# Nested routers for attributes & clusters
blueprints_router = SimpleRouter()
blueprints_router.register(
    r'attributes',
    SAGAttributeViewSet,
    basename='sag-attribute'
)
blueprints_router.register(
    r'clusters',
    SAGClusterViewSet,
    basename='sag-cluster'
)

urlpatterns = [
    path('', include(router.urls)),
    path('blueprints/<uuid:blueprint_id>/', include(blueprints_router.urls)),
]

Root URLs Integration (in backend/igny8_core/urls.py or api/v1/urls.py):

urlpatterns = [
    path('api/v1/sag/', include('sag.urls')),
]

3.6 Admin Configuration

File: sag/admin.py

from django.contrib import admin
from .models import SAGBlueprint, SAGAttribute, SAGCluster, SectorAttributeTemplate


@admin.register(SAGBlueprint)
class SAGBlueprintAdmin(admin.ModelAdmin):
    list_display = [
        'site', 'version', 'status', 'sag_health_score',
        'total_clusters', 'total_keywords', 'created_at'
    ]
    list_filter = ['status', 'source', 'account__name', 'created_at']
    search_fields = ['site__name', 'account__name']
    readonly_fields = [
        'id', 'created_at', 'updated_at',
        'sag_health_score', 'total_clusters', 'total_keywords',
        'confirmed_at', 'last_health_check'
    ]
    fieldsets = (
        ('Identity', {
            'fields': ('id', 'site', 'account', 'version', 'status', 'source')
        }),
        ('Content', {
            'fields': (
                'attributes_json', 'clusters_json',
                'taxonomy_plan', 'execution_priority'
            )
        }),
        ('Health & Metrics', {
            'fields': (
                'sag_health_score', 'total_clusters', 'total_keywords',
                'total_content_planned', 'total_content_published',
                'last_health_check'
            )
        }),
        ('State', {
            'fields': ('confirmed_at', 'created_at', 'updated_at')
        }),
        ('Future: Phase 2 Linker', {
            'fields': ('internal_linking_map',),
            'classes': ('collapse',)
        }),
    )


@admin.register(SAGAttribute)
class SAGAttributeAdmin(admin.ModelAdmin):
    list_display = ['name', 'blueprint', 'level', 'slug', 'sort_order']
    list_filter = ['level', 'wp_sync_status', 'account__name']
    search_fields = ['name', 'slug', 'blueprint__site__name']
    readonly_fields = ['id', 'created_at', 'updated_at']
    fieldsets = (
        ('Identity', {
            'fields': ('id', 'blueprint', 'account', 'name', 'slug')
        }),
        ('Definition', {
            'fields': ('description', 'level', 'values', 'sort_order')
        }),
        ('WordPress Integration', {
            'fields': ('wp_taxonomy_slug', 'wp_sync_status'),
            'classes': ('collapse',)
        }),
        ('Timestamps', {
            'fields': ('created_at', 'updated_at'),
            'classes': ('collapse',)
        }),
    )


@admin.register(SAGCluster)
class SAGClusterAdmin(admin.ModelAdmin):
    list_display = [
        'name', 'cluster_type', 'blueprint', 'status',
        'content_count', 'link_coverage_score'
    ]
    list_filter = ['cluster_type', 'status', 'account__name', 'created_at']
    search_fields = ['name', 'slug', 'hub_page_title', 'blueprint__site__name']
    readonly_fields = ['id', 'created_at', 'updated_at']
    fieldsets = (
        ('Identity', {
            'fields': ('id', 'blueprint', 'site', 'account', 'name', 'slug')
        }),
        ('Type & Content', {
            'fields': (
                'cluster_type', 'attribute_intersection',
                'auto_generated_keywords', 'supporting_content_plan'
            )
        }),
        ('Hub Page', {
            'fields': (
                'hub_page_title', 'hub_page_type',
                'hub_page_structure', 'hub_page_url_slug'
            )
        }),
        ('Linking', {
            'fields': (
                'linked_attribute_terms', 'cross_cluster_links',
                'link_coverage_score', 'backlink_target_tier'
            )
        }),
        ('Status & Metrics', {
            'fields': ('status', 'content_count')
        }),
        ('Timestamps', {
            'fields': ('created_at', 'updated_at'),
            'classes': ('collapse',)
        }),
    )


@admin.register(SectorAttributeTemplate)
class SectorAttributeTemplateAdmin(admin.ModelAdmin):
    list_display = ['industry', 'sector', 'site_type', 'source', 'is_active', 'updated_at']
    list_filter = ['site_type', 'source', 'is_active', 'industry']
    search_fields = ['industry', 'sector']
    readonly_fields = ['id', 'created_at', 'updated_at']
    fieldsets = (
        ('Identity', {
            'fields': ('id', 'industry', 'sector', 'site_type')
        }),
        ('Templates', {
            'fields': ('attribute_framework', 'keyword_templates')
        }),
        ('Metadata', {
            'fields': ('source', 'is_active', 'created_at', 'updated_at')
        }),
    )

3.7 Service Layer

File: sag/services/blueprint_service.py

from django.utils import timezone
from ..models import SAGBlueprint
from .cluster_service import synchronize_denormalized_clusters
from .attribute_service import synchronize_denormalized_attributes


def confirm_blueprint(blueprint):
    """Move blueprint from draft → active. Update Site reference."""
    if blueprint.status != 'draft':
        raise ValueError(f"Cannot confirm blueprint with status {blueprint.status}")

    blueprint.status = 'active'
    blueprint.confirmed_at = timezone.now()
    blueprint.save()

    # Update Site to reference this blueprint
    blueprint.site.sag_blueprint = blueprint
    blueprint.site.save()

    return blueprint


def archive_blueprint(blueprint):
    """Move blueprint from active → archived."""
    if blueprint.status not in ['active', 'evolving']:
        raise ValueError(f"Cannot archive blueprint with status {blueprint.status}")

    blueprint.status = 'archived'
    blueprint.save()

    # Unlink from Site
    blueprint.site.sag_blueprint = None
    blueprint.site.save()

    return blueprint


def create_new_version(blueprint):
    """Create a new version by copying current blueprint."""
    new_version = SAGBlueprint.objects.create(
        site=blueprint.site,
        account=blueprint.account,
        version=blueprint.version + 1,
        status='draft',
        source=blueprint.source,
        attributes_json=blueprint.attributes_json.copy(),
        clusters_json=blueprint.clusters_json.copy(),
        taxonomy_plan=blueprint.taxonomy_plan.copy(),
        execution_priority=blueprint.execution_priority.copy(),
        internal_linking_map=blueprint.internal_linking_map.copy() if blueprint.internal_linking_map else {},
    )

    # Duplicate attributes & clusters (create new PK)
    for attr in blueprint.attributes.all():
        attr.pk = None
        attr.blueprint = new_version
        attr.save()

    for cluster in blueprint.clusters.all():
        cluster.pk = None
        cluster.blueprint = new_version
        cluster.save()

    return new_version


def get_active_blueprint(site_id, account):
    """Retrieve the active blueprint for a site, or None."""
    return SAGBlueprint.objects.get(
        site_id=site_id,
        account=account,
        status='active'
    )

File: sag/services/health_service.py

from django.utils import timezone
from ..models import SAGBlueprint, SAGCluster
import logging

logger = logging.getLogger(__name__)


def compute_blueprint_health(blueprint):
    """
    Compute health score (0-100) based on:
    - Attribute completeness: are all attributes defined?
    - Cluster coverage: do clusters cover attribute space?
    - Content completion: how much content is published?
    - Structural consistency: are all required fields filled?
    """
    score = 0.0

    # 1. Attribute completeness (25 points)
    attribute_count = blueprint.attributes.count()
    if attribute_count > 0:
        score += 25

    # 2. Cluster coverage (25 points)
    cluster_count = blueprint.clusters.count()
    if cluster_count >= 5:
        score += 25
    elif cluster_count > 0:
        score += (cluster_count / 5) * 25

    # 3. Content completion (30 points)
    total_expected = max(blueprint.total_keywords or 1, 10)
    if blueprint.total_content_published > 0:
        completion_ratio = blueprint.total_content_published / total_expected
        score += min(30, completion_ratio * 30)

    # 4. Structural consistency (20 points)
    consistency_score = 0
    if blueprint.taxonomy_plan:
        consistency_score += 10
    if blueprint.execution_priority:
        consistency_score += 10
    score += consistency_score

    # Update blueprint
    blueprint.sag_health_score = min(100.0, score)
    blueprint.last_health_check = timezone.now()
    blueprint.save()

    logger.info(f"Computed health for blueprint {blueprint.id}: {blueprint.sag_health_score}")

    return {
        'blueprint_id': str(blueprint.id),
        'health_score': blueprint.sag_health_score,
        'attribute_count': attribute_count,
        'cluster_count': cluster_count,
        'content_published': blueprint.total_content_published,
        'last_check': blueprint.last_health_check.isoformat()
    }


def synchronize_denormalized_fields(blueprint):
    """Update snapshot JSONs in blueprint from current attributes & clusters."""
    # Rebuild attributes_json
    blueprint.attributes_json = {
        attr.slug: {
            'name': attr.name,
            'level': attr.level,
            'values': attr.values,
            'wp_taxonomy_slug': attr.wp_taxonomy_slug
        }
        for attr in blueprint.attributes.all()
    }

    # Rebuild clusters_json
    blueprint.clusters_json = {
        cluster.slug: {
            'name': cluster.name,
            'type': cluster.cluster_type,
            'hub_title': cluster.hub_page_title,
            'keywords': cluster.auto_generated_keywords,
            'status': cluster.status
        }
        for cluster in blueprint.clusters.all()
    }

    # Update totals
    blueprint.total_clusters = blueprint.clusters.count()
    blueprint.total_keywords = sum(
        len(c.auto_generated_keywords) for c in blueprint.clusters.all()
    )
    blueprint.total_content_published = blueprint.clusters.aggregate(
        total=models.Sum('content_count')
    )['total'] or 0

    blueprint.save()
    return blueprint

File: sag/services/cluster_service.py

from ..models import SAGCluster


def create_cluster_from_attribute_intersection(blueprint, site, cluster_type,
                                               attribute_intersection, name, slug):
    """Create a cluster with given attribute intersection."""
    cluster = SAGCluster.objects.create(
        blueprint=blueprint,
        site=site,
        account=blueprint.account,
        name=name,
        slug=slug,
        cluster_type=cluster_type,
        attribute_intersection=attribute_intersection,
        hub_page_title=f"{name} Guide",
        hub_page_url_slug=slug,
        status='planned'
    )
    return cluster


def update_cluster_content_metrics(cluster):
    """Update content_count based on related Content objects."""
    content_count = cluster.content_pieces.filter(
        status='published'
    ).count()
    cluster.content_count = content_count
    cluster.save()
    return cluster

File: sag/services/attribute_service.py

from ..models import SAGAttribute


def create_attribute(blueprint, name, slug, level, values, description=''):
    """Create an attribute within a blueprint."""
    # Ensure values is array of {name, slug}
    if isinstance(values, list) and values and isinstance(values[0], str):
        # Convert string list to {slug: name, name: name}
        values = [{'name': v, 'slug': v.lower().replace(' ', '_')} for v in values]

    attr = SAGAttribute.objects.create(
        blueprint=blueprint,
        account=blueprint.account,
        name=name,
        slug=slug,
        level=level,
        values=values,
        description=description
    )
    return attr

File: sag/services/template_service.py

from ..models import SectorAttributeTemplate


def merge_templates(base_template, sector_slugs):
    """Merge multiple sector templates into a unified framework."""
    merged_attributes = base_template.attribute_framework.copy()
    merged_keywords = base_template.keyword_templates.copy()

    # For each sector_slug, fetch and merge
    for sector_slug in sector_slugs:
        try:
            template = SectorAttributeTemplate.objects.get(
                industry=base_template.industry,
                sector=sector_slug,
                is_active=True
            )
            # Deep merge
            merged_attributes.update(template.attribute_framework)
            merged_keywords.update(template.keyword_templates)
        except SectorAttributeTemplate.DoesNotExist:
            pass

    return {
        'attribute_framework': merged_attributes,
        'keyword_templates': merged_keywords
    }

File: sag/services/__init__.py

from . import blueprint_service
from . import attribute_service
from . import cluster_service
from . import template_service
from . import health_service
from . import keyword_service

__all__ = [
    'blueprint_service',
    'attribute_service',
    'cluster_service',
    'template_service',
    'health_service',
    'keyword_service',
]

3.8 AI Function Stubs

File: sag/ai_functions/attribute_discovery.py

"""
Phase 23: AI-powered attribute discovery from site content.
Stub for now; will integrate with Claude/Anthropic API in Phase 2.
"""


def discover_attributes_from_site_content(site, content_samples):
    """
    [STUB] Analyze site content to discover domain-specific attributes.

    Args:
        site: Site instance
        content_samples: List of text samples from site

    Returns:
        List of discovered attributes: [{name, level, suggested_values}, ...]
    """
    raise NotImplementedError("Phase 2: AI discovery not yet implemented")

File: sag/ai_functions/cluster_formation.py

"""
Phase 23: AI-powered cluster formation from keywords and attributes.
"""


def form_clusters_from_keywords(keywords, attributes, cluster_type_hints=None):
    """
    [STUB] Group keywords into clusters based on attribute intersection.

    Args:
        keywords: List of keyword strings
        attributes: List of SAGAttribute instances
        cluster_type_hints: Optional dict of keyword → cluster_type mappings

    Returns:
        List of clusters: [{name, slug, attribute_intersection, keywords}, ...]
    """
    raise NotImplementedError("Phase 2: AI cluster formation not yet implemented")

File: sag/ai_functions/keyword_generation.py

"""
Phase 23: AI-powered keyword generation for clusters.
"""


def generate_keywords_for_cluster(cluster, attribute_intersection, competitor_keywords=None):
    """
    [STUB] Generate SEO keywords for a cluster given attributes.

    Args:
        cluster: SAGCluster instance
        attribute_intersection: Dict of {attribute_slug: value_slug}
        competitor_keywords: Optional list of competitor keywords to avoid/differentiate

    Returns:
        List of generated keywords: [{keyword, search_volume, difficulty, intent}, ...]
    """
    raise NotImplementedError("Phase 2: AI keyword generation not yet implemented")

File: sag/ai_functions/content_planning.py

"""
Phase 2: AI-powered content planning for clusters.
"""


def plan_supporting_content(cluster, hub_page_title, num_articles=5):
    """
    [STUB] Generate supporting article plan for a cluster hub.

    Args:
        cluster: SAGCluster instance
        hub_page_title: Title of the hub page
        num_articles: Number of supporting articles to plan

    Returns:
        List of article plans: [{title, type, structure, keywords}, ...]
    """
    raise NotImplementedError("Phase 2: AI content planning not yet implemented")

File: sag/ai_functions/__init__.py

# Stubs for Phase 23 AI integration

4. IMPLEMENTATION STEPS

Phase 1A: Data Layer (This Sprint)

  1. Create Django App

    • Generate sag/ app in backend/igny8_core/
    • Register in INSTALLED_APPS
  2. Define Models

    • Implement SAGBlueprint, SAGAttribute, SAGCluster, SectorAttributeTemplate
    • Add 5 new nullable fields to existing models (Site, Cluster, Task, Content, ContentIdea)
    • Ensure all models inherit from correct base class (AccountBaseModel or base Model)
  3. Create Migrations

    • Run makemigrations sag
    • Manually verify for circular imports or dependencies
    • Create migration for modifications to existing models
    • All existing fields must remain untouched
  4. Implement Serializers

    • SAGBlueprintDetailSerializer (nested attributes & clusters)
    • SAGBlueprintListSerializer (lightweight)
    • SAGAttributeSerializer, SAGClusterSerializer
    • SectorAttributeTemplateSerializer
  5. Implement ViewSets

    • SAGBlueprintViewSet with confirm, archive, regenerate, health_check actions
    • SAGAttributeViewSet (nested under blueprints)
    • SAGClusterViewSet (nested under blueprints)
    • SectorAttributeTemplateViewSet (admin-only)
  6. Set Up URL Routing

    • Create sag/urls.py with routers
    • Include in main API router at /api/v1/sag/
  7. Django Admin Registration

    • Register all models with custom admin classes
    • Configure list_display, filters, search, fieldsets
  8. Service Layer

    • Implement blueprint_service.py (confirm, archive, create_new_version, get_active)
    • Implement health_service.py (compute_blueprint_health)
    • Implement cluster_service.py stubs
    • Implement attribute_service.py stubs
    • Implement template_service.py stubs
  9. AI Function Stubs

    • Create placeholder files in ai_functions/ (all raise NotImplementedError)
  10. Testing

    • Unit tests for models (validation, constraints)
    • Integration tests for ViewSets (CRUD, permissions)
    • Service layer tests

Phase 1B: Wizard & Migrations (Next Sprint)

  • Frontend React wizard for blueprint creation
  • Migration scripts for existing site data → SAG blueprints
  • Feature flag sag_enabled

5. ACCEPTANCE CRITERIA

Data Model

  • All 4 models created and migrated successfully
  • All 5 existing models have nullable SAG fields
  • Unique constraints enforced (blueprint version, attribute slugs, cluster slugs, template industry/sector)
  • Foreign key cascades correct (blueprint → attributes/clusters)
  • All model methods and properties work as documented

API Endpoints

  • GET /api/v1/sag/blueprints/ (list all, paginated, filtered by account)
  • POST /api/v1/sag/blueprints/ (create draft blueprint)
  • GET /api/v1/sag/blueprints/{id}/ (retrieve with nested attributes/clusters)
  • PATCH /api/v1/sag/blueprints/{id}/ (partial update)
  • DELETE /api/v1/sag/blueprints/{id}/ (soft-delete or hard-delete with cascades)
  • POST /api/v1/sag/blueprints/{id}/confirm/ (draft → active)
  • POST /api/v1/sag/blueprints/{id}/archive/ (active → archived)
  • POST /api/v1/sag/blueprints/{id}/regenerate/ (create v+1)
  • POST /api/v1/sag/blueprints/{id}/health_check/ (compute score)
  • GET /api/v1/sag/blueprints/active_by_site/?site_id=
  • GET/POST /api/v1/sag/blueprints/{blueprint_id}/attributes/
  • GET/POST /api/v1/sag/blueprints/{blueprint_id}/clusters/
  • GET/POST /api/v1/sag/sector-templates/ (admin-only)
  • GET /api/v1/sag/sector-templates/by_industry_sector/?industry=&sector=

Serialization

  • All responses follow unified format: {success, data, message}
  • Nested serializers work (blueprint detail includes attributes & clusters)
  • Read-only fields enforced (id, timestamps, computed fields)
  • Validation rules applied (max_length, choices, unique_together)

Permissions & Tenancy

  • All account-based endpoints enforce IsAccountMember
  • Tenant isolation: users see only their account's blueprints
  • Admin-only endpoints require IsAdminUser
  • queryset filters automatically by account

Admin Interface

  • All models registered in Django admin
  • List views show key metrics (status, health, cluster count)
  • Search functional (by site name, industry, sector)
  • Filters functional (by status, source, type)
  • Read-only fields protected from editing

Service Layer

  • blueprint_service.confirm_blueprint() works (draft → active, Site ref updated)
  • blueprint_service.archive_blueprint() works (active → archived, Site ref cleared)
  • blueprint_service.create_new_version() creates copy with version+1
  • health_service.compute_blueprint_health() computes 0-100 score
  • All services handle exceptions gracefully

AI Function Stubs

  • All stubs present and raise NotImplementedError
  • No actual AI calls in Phase 1

Documentation

  • This document is self-contained and buildable
  • Code comments explain non-obvious logic
  • Django model docstrings present
  • ViewSet action docstrings present

6. CLAUDE CODE INSTRUCTIONS

How to Use This Document

This document contains everything Claude Code needs to build the sag/ app.

  1. Read this document end-to-end to understand the architecture
  2. Copy model definitions exactly into backend/igny8_core/sag/models.py
  3. Copy serializer code exactly into backend/igny8_core/sag/serializers.py
  4. Copy viewset code exactly into backend/igny8_core/sag/views.py
  5. Copy URL routing into backend/igny8_core/sag/urls.py
  6. Copy admin.py exactly as-is
  7. Create service files with code from Section 3.7
  8. Create AI function stubs from Section 3.8
  9. Create migration for existing model changes (Site, Cluster, Task, Content, ContentIdea)
  10. Run migrations on development database
  11. Test endpoints with Postman or curl
  12. Write unit & integration tests matching patterns in existing test suite

Key Commands

# Create app
python manage.py startapp sag igny8_core/

# Makemigrations
python manage.py makemigrations sag
python manage.py makemigrations  # For existing model changes

# Migrate
python manage.py migrate sag
python manage.py migrate

# Create superuser (for admin)
python manage.py createsuperuser

# Test
python manage.py test sag --verbosity=2

# Run development server
python manage.py runserver

Testing Endpoints (Postman/curl)

# 1. Get auth token (assuming DRF token auth)
curl -X POST http://localhost:8000/api/v1/auth/login/ \
  -H "Content-Type: application/json" \
  -d '{"username": "admin", "password": "password"}'

# 2. Create blueprint
curl -X POST http://localhost:8000/api/v1/sag/blueprints/ \
  -H "Authorization: Token <YOUR_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "site": "<site-uuid>",
    "status": "draft",
    "source": "manual",
    "taxonomy_plan": {}
  }'

# 3. List blueprints
curl -X GET http://localhost:8000/api/v1/sag/blueprints/ \
  -H "Authorization: Token <YOUR_TOKEN>"

# 4. Confirm blueprint
curl -X POST http://localhost:8000/api/v1/sag/blueprints/<blueprint-id>/confirm/ \
  -H "Authorization: Token <YOUR_TOKEN>"

Integration with Existing Code

  • Import AccountBaseModel from igny8_core.common.models
  • Import AccountModelViewSet from igny8_core.common.views
  • Import IsAccountMember from igny8_core.common.permissions
  • Follow timezone handling as in existing models (use timezone.now())
  • Follow logging patterns from existing services
  • Use Celery for async if health_check needs to be background task (deferred to Phase 2)

Notes for Claude Code

  1. Models are self-contained — no circular imports
  2. Serializers follow DRF patterns already used in IGNY8
  3. ViewSets follow AccountModelViewSet pattern — inherit and override get_queryset()
  4. URL routing uses SimpleRouter — consistent with existing code
  5. All fields on existing models are nullable — no breaking changes
  6. All new endpoints return unified response format — {success, data, message}
  7. Service layer is optional in Phase 1 — focus on models & APIs; services can be minimal stubs
  8. AI functions are stubs only — Phase 2 will implement with actual AI

7. APPENDIX: Example Request/Response

Example: Create Blueprint

Request:

POST /api/v1/sag/blueprints/
Authorization: Token <token>

{
  "site": "550e8400-e29b-41d4-a716-446655440000",
  "status": "draft",
  "source": "manual",
  "taxonomy_plan": {
    "primary_dimensions": ["color", "material"],
    "secondary_dimensions": ["size", "brand"]
  },
  "execution_priority": {
    "cluster_red_leather": 1,
    "cluster_blue_canvas": 2
  }
}

Response:

{
  "success": true,
  "data": {
    "id": "660e8400-e29b-41d4-a716-446655440001",
    "site": "550e8400-e29b-41d4-a716-446655440000",
    "account": "770e8400-e29b-41d4-a716-446655440002",
    "version": 1,
    "status": "draft",
    "source": "manual",
    "attributes_json": {},
    "clusters_json": {},
    "taxonomy_plan": {
      "primary_dimensions": ["color", "material"],
      "secondary_dimensions": ["size", "brand"]
    },
    "execution_priority": {
      "cluster_red_leather": 1,
      "cluster_blue_canvas": 2
    },
    "internal_linking_map": {},
    "sag_health_score": 0.0,
    "total_clusters": 0,
    "total_keywords": 0,
    "total_content_planned": 0,
    "total_content_published": 0,
    "confirmed_at": null,
    "last_health_check": null,
    "attributes": [],
    "clusters": [],
    "created_at": "2026-03-23T10:00:00Z",
    "updated_at": "2026-03-23T10:00:00Z"
  },
  "message": null
}

Example: Confirm Blueprint

Request:

POST /api/v1/sag/blueprints/660e8400-e29b-41d4-a716-446655440001/confirm/
Authorization: Token <token>

Response:

{
  "success": true,
  "data": {
    "id": "660e8400-e29b-41d4-a716-446655440001",
    "site": "550e8400-e29b-41d4-a716-446655440000",
    "version": 1,
    "status": "active",
    "confirmed_at": "2026-03-23T10:05:00Z",
    ...
  },
  "message": "Blueprint v1 activated."
}

Summary

This document defines the complete SAG Data Foundation for IGNY8 Phase 1:

  • 4 new models for blueprints, attributes, clusters, and templates
  • 5 modified existing models with nullable SAG references
  • Full CRUD API with custom actions (confirm, archive, regenerate, health_check)
  • Service layer for complex operations
  • AI function stubs for Phase 23 integration
  • Admin interface for management

All code is production-ready, follows IGNY8 patterns, and is backward-compatible. The next phase (Phase 1B) will add the wizard UI and migration scripts.

Build Status: Ready for Claude Code implementation.