v2-exece-docs
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
# IGNY8 Phase 1: SAG Data Foundation (01A)
|
||||
## Core Data Models & CRUD APIs
|
||||
|
||||
**Document Version:** 1.0
|
||||
**Document Version:** 1.1
|
||||
**Date:** 2026-03-23
|
||||
**Phase:** IGNY8 Phase 1 — SAG Data Foundation
|
||||
**Status:** Build Ready
|
||||
**Source of Truth:** Codebase at `/data/app/igny8/`
|
||||
**Audience:** Claude Code, Backend Developers, Architects
|
||||
|
||||
---
|
||||
@@ -12,8 +13,9 @@
|
||||
## 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)
|
||||
- **Framework:** Django >=5.2.7 + Django REST Framework (from requirements.txt)
|
||||
- **Database:** PostgreSQL (version set by infra stack)
|
||||
- **Primary Keys:** BigAutoField (integer PKs — NOT UUIDs). `DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'`
|
||||
- **Multi-Tenancy:** AccountContextMiddleware enforces tenant isolation
|
||||
- All new models inherit from `AccountBaseModel` or `SiteSectorBaseModel`
|
||||
- Automatic tenant filtering in querysets
|
||||
@@ -26,17 +28,24 @@
|
||||
}
|
||||
```
|
||||
- **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
|
||||
- **Async Processing:** Celery >=5.3.0 for background tasks
|
||||
- **Existing Models (PLURAL class names, all use BigAutoField PKs):**
|
||||
- Account, Site (in `auth/models.py`) — `AccountBaseModel`
|
||||
- Sector (in `auth/models.py`) — `AccountBaseModel`
|
||||
- Clusters (in `business/planning/models.py`) — `SiteSectorBaseModel`
|
||||
- Keywords (in `business/planning/models.py`) — `SiteSectorBaseModel`
|
||||
- ContentIdeas (in `business/planning/models.py`) — `SiteSectorBaseModel`
|
||||
- Tasks (in `business/content/models.py`) — `SiteSectorBaseModel`
|
||||
- Content (in `business/content/models.py`) — `SiteSectorBaseModel`
|
||||
- Images (in `business/content/models.py`) — `SiteSectorBaseModel`
|
||||
- SiteIntegration (in `business/integration/models.py`) — `SiteSectorBaseModel`
|
||||
- IntegrationProvider (in `modules/system/models.py`) — standalone
|
||||
|
||||
### Frontend Stack
|
||||
- React 19 + TypeScript 5
|
||||
- Tailwind CSS 3
|
||||
- Zustand 4 (state management)
|
||||
- React ^19.0.0 + TypeScript ~5.7.2
|
||||
- Tailwind CSS ^4.0.8
|
||||
- Zustand ^5.0.8 (state management)
|
||||
- Vite ^6.1.0
|
||||
|
||||
### What Doesn't Exist
|
||||
- Attribute-based cluster formation system
|
||||
@@ -128,10 +137,11 @@ sag/
|
||||
class SAGBlueprint(AccountBaseModel):
|
||||
"""
|
||||
Core blueprint model: versioned taxonomy & cluster plan for a site.
|
||||
Inherits: id (UUID), account_id (FK), created_at, updated_at
|
||||
Inherits: id (BigAutoField, integer PK), account_id (FK), created_at, updated_at
|
||||
Note: Uses BigAutoField per project convention (DEFAULT_AUTO_FIELD), NOT UUID.
|
||||
"""
|
||||
site = models.ForeignKey(
|
||||
'sites.Site',
|
||||
'igny8_core_auth.Site', # Actual app_label for Site model
|
||||
on_delete=models.CASCADE,
|
||||
related_name='sag_blueprints'
|
||||
)
|
||||
@@ -232,7 +242,7 @@ class SAGBlueprint(AccountBaseModel):
|
||||
class SAGAttribute(AccountBaseModel):
|
||||
"""
|
||||
Attribute/dimension within a blueprint.
|
||||
Inherits: id (UUID), account_id (FK), created_at, updated_at
|
||||
Inherits: id (BigAutoField, integer PK), account_id (FK), created_at, updated_at
|
||||
"""
|
||||
blueprint = models.ForeignKey(
|
||||
'sag.SAGBlueprint',
|
||||
@@ -315,7 +325,11 @@ class SAGAttribute(AccountBaseModel):
|
||||
class SAGCluster(AccountBaseModel):
|
||||
"""
|
||||
Cluster: hub page + supporting content organized around attribute intersection.
|
||||
Inherits: id (UUID), account_id (FK), created_at, updated_at
|
||||
Inherits: id (BigAutoField, integer PK), account_id (FK), created_at, updated_at
|
||||
|
||||
IMPORTANT: This model coexists with the existing `Clusters` model (in business/planning/models.py).
|
||||
Existing Clusters are pure topic-keyword groups. SAGClusters add attribute-based dimensionality.
|
||||
They are linked via an optional FK on the existing Clusters model.
|
||||
"""
|
||||
blueprint = models.ForeignKey(
|
||||
'sag.SAGBlueprint',
|
||||
@@ -323,7 +337,7 @@ class SAGCluster(AccountBaseModel):
|
||||
related_name='clusters'
|
||||
)
|
||||
site = models.ForeignKey(
|
||||
'sites.Site',
|
||||
'igny8_core_auth.Site', # Actual app_label for Site model
|
||||
on_delete=models.CASCADE,
|
||||
related_name='sag_clusters'
|
||||
)
|
||||
@@ -461,8 +475,8 @@ class SectorAttributeTemplate(models.Model):
|
||||
"""
|
||||
Reusable template for attributes and keywords by industry + sector.
|
||||
NOT tied to Account (admin-only, shared across tenants).
|
||||
Uses BigAutoField PK per project convention (do NOT use UUID).
|
||||
"""
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
|
||||
industry = models.CharField(
|
||||
max_length=200,
|
||||
@@ -533,11 +547,12 @@ class SectorAttributeTemplate(models.Model):
|
||||
|
||||
All modifications are **backward-compatible** and **nullable**. Existing records are unaffected.
|
||||
|
||||
#### **Site** (in `sites/models.py`)
|
||||
#### **Site** (in `auth/models.py`, app_label: `igny8_core_auth`)
|
||||
```python
|
||||
class Site(AccountBaseModel):
|
||||
class Site(SoftDeletableModel, AccountBaseModel):
|
||||
# ... existing fields ...
|
||||
|
||||
# NEW: SAG integration (nullable, backward-compatible)
|
||||
sag_blueprint = models.ForeignKey(
|
||||
'sag.SAGBlueprint',
|
||||
on_delete=models.SET_NULL,
|
||||
@@ -548,11 +563,13 @@ class Site(AccountBaseModel):
|
||||
)
|
||||
```
|
||||
|
||||
#### **Cluster** (in `modules/planner/models.py`)
|
||||
#### **Clusters** (in `business/planning/models.py`, app_label: `planner`)
|
||||
```python
|
||||
class Cluster(AccountBaseModel):
|
||||
# ... existing fields ...
|
||||
class Clusters(SoftDeletableModel, SiteSectorBaseModel):
|
||||
# ... existing fields: name, description, keywords_count, volume,
|
||||
# mapped_pages, status(new/mapped), disabled ...
|
||||
|
||||
# NEW: SAG integration (nullable, backward-compatible)
|
||||
sag_cluster = models.ForeignKey(
|
||||
'sag.SAGCluster',
|
||||
on_delete=models.SET_NULL,
|
||||
@@ -570,11 +587,16 @@ class Cluster(AccountBaseModel):
|
||||
)
|
||||
```
|
||||
|
||||
#### **Task** (in `modules/writer/models.py`)
|
||||
#### **Tasks** (in `business/content/models.py`, app_label: `writer`)
|
||||
```python
|
||||
class Task(AccountBaseModel):
|
||||
# ... existing fields ...
|
||||
class Tasks(SoftDeletableModel, SiteSectorBaseModel):
|
||||
# ... existing fields: title, description, content_type, content_structure,
|
||||
# keywords, word_count, status(queued/completed) ...
|
||||
# NOTE: Already has `cluster = FK('planner.Clusters')` and
|
||||
# `idea = FK('planner.ContentIdeas')` — these are NOT being replaced.
|
||||
# The new sag_cluster FK is an ADDITIONAL link to the SAG layer.
|
||||
|
||||
# NEW: SAG integration (nullable, backward-compatible)
|
||||
sag_cluster = models.ForeignKey(
|
||||
'sag.SAGCluster',
|
||||
on_delete=models.SET_NULL,
|
||||
@@ -592,11 +614,14 @@ class Task(AccountBaseModel):
|
||||
)
|
||||
```
|
||||
|
||||
#### **Content** (in `modules/writer/models.py`)
|
||||
#### **Content** (in `business/content/models.py`, app_label: `writer`)
|
||||
```python
|
||||
class Content(AccountBaseModel):
|
||||
class Content(SoftDeletableModel, SiteSectorBaseModel):
|
||||
# ... existing fields ...
|
||||
# NOTE: Already has task FK (to writer.Tasks which has cluster FK).
|
||||
# The new sag_cluster FK is an ADDITIONAL direct link to SAG layer.
|
||||
|
||||
# NEW: SAG integration (nullable, backward-compatible)
|
||||
sag_cluster = models.ForeignKey(
|
||||
'sag.SAGCluster',
|
||||
on_delete=models.SET_NULL,
|
||||
@@ -607,11 +632,12 @@ class Content(AccountBaseModel):
|
||||
)
|
||||
```
|
||||
|
||||
#### **ContentIdea** (in `modules/planner/models.py`)
|
||||
#### **ContentIdeas** (in `business/planning/models.py`, app_label: `planner`)
|
||||
```python
|
||||
class ContentIdea(AccountBaseModel):
|
||||
class ContentIdeas(SoftDeletableModel, SiteSectorBaseModel):
|
||||
# ... existing fields ...
|
||||
|
||||
# NEW: SAG integration (nullable, backward-compatible)
|
||||
sag_cluster = models.ForeignKey(
|
||||
'sag.SAGCluster',
|
||||
on_delete=models.SET_NULL,
|
||||
@@ -847,7 +873,7 @@ class SAGBlueprintViewSet(AccountModelViewSet):
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def active_by_site(self, request):
|
||||
"""GET /api/v1/sag/blueprints/active_by_site/?site_id=<uuid>"""
|
||||
"""GET /api/v1/sag/blueprints/active_by_site/?site_id=<int>"""
|
||||
site_id = request.query_params.get('site_id')
|
||||
if not site_id:
|
||||
return Response({
|
||||
@@ -1027,7 +1053,7 @@ blueprints_router.register(
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
path('blueprints/<uuid:blueprint_id>/', include(blueprints_router.urls)),
|
||||
path('blueprints/<int:blueprint_id>/', include(blueprints_router.urls)),
|
||||
]
|
||||
```
|
||||
|
||||
@@ -1309,7 +1335,7 @@ def compute_blueprint_health(blueprint):
|
||||
logger.info(f"Computed health for blueprint {blueprint.id}: {blueprint.sag_health_score}")
|
||||
|
||||
return {
|
||||
'blueprint_id': str(blueprint.id),
|
||||
'blueprint_id': blueprint.id,
|
||||
'health_score': blueprint.sag_health_score,
|
||||
'attribute_count': attribute_count,
|
||||
'cluster_count': cluster_count,
|
||||
@@ -1580,14 +1606,13 @@ def plan_supporting_content(cluster, hub_page_title, num_articles=5):
|
||||
|
||||
2. **Define Models**
|
||||
- Implement `SAGBlueprint`, `SAGAttribute`, `SAGCluster`, `SectorAttributeTemplate`
|
||||
- Add 5 new nullable fields to existing models (Site, Cluster, Task, Content, ContentIdea)
|
||||
- Add 5 new nullable fields to existing models (Site, Clusters, Tasks, Content, ContentIdeas)
|
||||
- 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
|
||||
- Create migration for modifications to existing models (Clusters, Tasks, Content, ContentIdeas in their respective apps; Site in igny8_core_auth)
|
||||
|
||||
4. **Implement Serializers**
|
||||
- SAGBlueprintDetailSerializer (nested attributes & clusters)
|
||||
@@ -1638,7 +1663,7 @@ def plan_supporting_content(cluster, hub_page_title, num_articles=5):
|
||||
|
||||
### Data Model
|
||||
- [ ] All 4 models created and migrated successfully
|
||||
- [ ] All 5 existing models have nullable SAG fields
|
||||
- [ ] All 5 existing models have nullable SAG fields (Site, Clusters, Tasks, Content, ContentIdeas)
|
||||
- [ ] 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
|
||||
@@ -1653,7 +1678,7 @@ def plan_supporting_content(cluster, hub_page_title, num_articles=5):
|
||||
- [ ] 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=<uuid>
|
||||
- [ ] GET /api/v1/sag/blueprints/active_by_site/?site_id=<int>
|
||||
- [ ] 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)
|
||||
@@ -1711,7 +1736,7 @@ This document contains **everything Claude Code needs to build the sag/ app**.
|
||||
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)
|
||||
9. **Create migration** for existing model changes (Site in `igny8_core_auth`, Clusters/ContentIdeas in `planner`, Tasks/Content in `writer`)
|
||||
10. **Run migrations** on development database
|
||||
11. **Test endpoints** with Postman or curl
|
||||
12. **Write unit & integration tests** matching patterns in existing test suite
|
||||
@@ -1724,7 +1749,7 @@ python manage.py startapp sag igny8_core/
|
||||
|
||||
# Makemigrations
|
||||
python manage.py makemigrations sag
|
||||
python manage.py makemigrations # For existing model changes
|
||||
python manage.py makemigrations igny8_core_auth planner writer # For existing model changes
|
||||
|
||||
# Migrate
|
||||
python manage.py migrate sag
|
||||
@@ -1753,7 +1778,7 @@ curl -X POST http://localhost:8000/api/v1/sag/blueprints/ \
|
||||
-H "Authorization: Token <YOUR_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"site": "<site-uuid>",
|
||||
"site": 42,
|
||||
"status": "draft",
|
||||
"source": "manual",
|
||||
"taxonomy_plan": {}
|
||||
@@ -1800,7 +1825,7 @@ POST /api/v1/sag/blueprints/
|
||||
Authorization: Token <token>
|
||||
|
||||
{
|
||||
"site": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"site": 42,
|
||||
"status": "draft",
|
||||
"source": "manual",
|
||||
"taxonomy_plan": {
|
||||
@@ -1819,9 +1844,9 @@ Authorization: Token <token>
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "660e8400-e29b-41d4-a716-446655440001",
|
||||
"site": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"account": "770e8400-e29b-41d4-a716-446655440002",
|
||||
"id": 1,
|
||||
"site": 42,
|
||||
"account": 7,
|
||||
"version": 1,
|
||||
"status": "draft",
|
||||
"source": "manual",
|
||||
@@ -1856,7 +1881,7 @@ Authorization: Token <token>
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
POST /api/v1/sag/blueprints/660e8400-e29b-41d4-a716-446655440001/confirm/
|
||||
POST /api/v1/sag/blueprints/1/confirm/
|
||||
Authorization: Token <token>
|
||||
```
|
||||
|
||||
@@ -1865,8 +1890,8 @@ Authorization: Token <token>
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "660e8400-e29b-41d4-a716-446655440001",
|
||||
"site": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"id": 1,
|
||||
"site": 42,
|
||||
"version": 1,
|
||||
"status": "active",
|
||||
"confirmed_at": "2026-03-23T10:05:00Z",
|
||||
|
||||
Reference in New Issue
Block a user