59 KiB
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
AccountBaseModelorSiteSectorBaseModel - Automatic tenant filtering in querysets
- All new models inherit from
- Response Format: Unified across all endpoints
{ "success": true, "data": { /* model instance or list */ }, "message": "Optional message" } - ViewSet Pattern:
AccountModelViewSet(filters by account) andSiteSectorModelViewSet - 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:
- Attribute Discovery: Identify key dimensions (attributes) in a given sector
- Cluster Formation: Organize keywords and content around attribute intersections
- Blueprint Versioning: Create immutable snapshots of taxonomy and execution plans
- 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 2–3 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 2–3)
- 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 2–3)
│ ├── attribute_extraction.py # AI: Extract values from content (Phase 2–3)
│ ├── attribute_population.py # AI: Populate values into blueprint (Phase 2–3)
│ ├── cluster_formation.py # AI: Form clusters from keywords (Phase 2–3)
│ ├── keyword_generation.py # AI: Generate keywords per cluster (Phase 2–3)
│ └── content_planning.py # AI: Plan supporting content (Phase 2–3)
├── 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>§or=<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 2–3: 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 2–3: 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 2–3: 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 2–3 AI integration
4. IMPLEMENTATION STEPS
Phase 1A: Data Layer (This Sprint)
-
Create Django App
- Generate
sag/app inbackend/igny8_core/ - Register in
INSTALLED_APPS
- Generate
-
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)
- Implement
-
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
- Run
-
Implement Serializers
- SAGBlueprintDetailSerializer (nested attributes & clusters)
- SAGBlueprintListSerializer (lightweight)
- SAGAttributeSerializer, SAGClusterSerializer
- SectorAttributeTemplateSerializer
-
Implement ViewSets
- SAGBlueprintViewSet with confirm, archive, regenerate, health_check actions
- SAGAttributeViewSet (nested under blueprints)
- SAGClusterViewSet (nested under blueprints)
- SectorAttributeTemplateViewSet (admin-only)
-
Set Up URL Routing
- Create
sag/urls.pywith routers - Include in main API router at
/api/v1/sag/
- Create
-
Django Admin Registration
- Register all models with custom admin classes
- Configure list_display, filters, search, fieldsets
-
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
-
AI Function Stubs
- Create placeholder files in ai_functions/ (all raise NotImplementedError)
-
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=§or=
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.
- Read this document end-to-end to understand the architecture
- Copy model definitions exactly into
backend/igny8_core/sag/models.py - Copy serializer code exactly into
backend/igny8_core/sag/serializers.py - Copy viewset code exactly into
backend/igny8_core/sag/views.py - Copy URL routing into
backend/igny8_core/sag/urls.py - Copy admin.py exactly as-is
- Create service files with code from Section 3.7
- Create AI function stubs from Section 3.8
- Create migration for existing model changes (Site, Cluster, Task, Content, ContentIdea)
- Run migrations on development database
- Test endpoints with Postman or curl
- 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
- Models are self-contained — no circular imports
- Serializers follow DRF patterns already used in IGNY8
- ViewSets follow AccountModelViewSet pattern — inherit and override get_queryset()
- URL routing uses SimpleRouter — consistent with existing code
- All fields on existing models are nullable — no breaking changes
- All new endpoints return unified response format — {success, data, message}
- Service layer is optional in Phase 1 — focus on models & APIs; services can be minimal stubs
- 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 2–3 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.