feat(pipeline): G-pre1/2/3 — Object Clustering + Master Controls + API
G-pre1: 144k objects clustered into 7,466 groups via Mini-Batch K-Means
on bge-m3 embeddings. Two-stage: k=5000 base + sub-cluster groups >50.
G-pre2: 5,114 Master Controls from lifecycle phase chains
(define→implement→test→monitor), linking 172,504 atomic controls.
G-pre3: REST API for Master Controls
GET /v1/master-controls (list, search, filter)
GET /v1/master-controls/stats
GET /v1/master-controls/{mc_id} (detail with phase-controls)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,9 +4,11 @@ from api.control_generator_routes import router as generator_router
|
||||
from api.canonical_control_routes import router as canonical_router
|
||||
from api.document_compliance_routes import router as document_router
|
||||
from api.dependency_routes import router as dependency_router
|
||||
from api.master_control_routes import router as master_control_router
|
||||
|
||||
router = APIRouter()
|
||||
router.include_router(generator_router)
|
||||
router.include_router(canonical_router)
|
||||
router.include_router(document_router)
|
||||
router.include_router(dependency_router)
|
||||
router.include_router(master_control_router)
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
"""Master Control API — G-pre3.
|
||||
|
||||
Provides read access to Master Controls (lifecycle-grouped atomic controls).
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from sqlalchemy import text
|
||||
|
||||
from db.session import SessionLocal
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/v1/master-controls", tags=["master-controls"])
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_master_controls(
|
||||
limit: int = Query(50, ge=1, le=500),
|
||||
offset: int = Query(0, ge=0),
|
||||
search: Optional[str] = None,
|
||||
min_phases: Optional[int] = None,
|
||||
min_controls: Optional[int] = None,
|
||||
sort: str = Query("total_controls", regex="^(total_controls|phases|name|created_at)$"),
|
||||
):
|
||||
"""List Master Controls with optional filtering."""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
where_clauses = []
|
||||
params: dict = {"limit": limit, "offset": offset}
|
||||
|
||||
if search:
|
||||
where_clauses.append("mc.canonical_name ILIKE :search")
|
||||
params["search"] = f"%{search}%"
|
||||
if min_phases:
|
||||
where_clauses.append("jsonb_array_length(mc.phases_covered) >= :min_phases")
|
||||
params["min_phases"] = min_phases
|
||||
if min_controls:
|
||||
where_clauses.append("mc.total_controls >= :min_controls")
|
||||
params["min_controls"] = min_controls
|
||||
|
||||
where = "WHERE " + " AND ".join(where_clauses) if where_clauses else ""
|
||||
|
||||
sort_map = {
|
||||
"total_controls": "mc.total_controls DESC",
|
||||
"phases": "jsonb_array_length(mc.phases_covered) DESC",
|
||||
"name": "mc.canonical_name ASC",
|
||||
"created_at": "mc.created_at DESC",
|
||||
}
|
||||
order = sort_map.get(sort, "mc.total_controls DESC")
|
||||
|
||||
rows = db.execute(text(f"""
|
||||
SELECT mc.id, mc.master_control_id, mc.object_group_id,
|
||||
mc.canonical_name, mc.phases_covered,
|
||||
mc.phase_control_count, mc.total_controls,
|
||||
mc.created_at
|
||||
FROM master_controls mc
|
||||
{where}
|
||||
ORDER BY {order}
|
||||
LIMIT :limit OFFSET :offset
|
||||
"""), params).fetchall()
|
||||
|
||||
total = db.execute(text(f"""
|
||||
SELECT count(*) FROM master_controls mc {where}
|
||||
"""), params).scalar()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"master_controls": [
|
||||
{
|
||||
"id": str(r[0]),
|
||||
"master_control_id": r[1],
|
||||
"object_group_id": r[2],
|
||||
"canonical_name": r[3],
|
||||
"phases_covered": r[4],
|
||||
"phase_control_count": r[5],
|
||||
"total_controls": r[6],
|
||||
"created_at": str(r[7]),
|
||||
}
|
||||
for r in rows
|
||||
],
|
||||
}
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
async def master_control_stats():
|
||||
"""Aggregate statistics about Master Controls."""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
stats = db.execute(text("""
|
||||
SELECT
|
||||
count(*) AS total_master_controls,
|
||||
sum(total_controls) AS total_member_controls,
|
||||
avg(total_controls)::int AS avg_controls_per_mc,
|
||||
max(total_controls) AS max_controls,
|
||||
avg(jsonb_array_length(phases_covered))::numeric(3,1) AS avg_phases,
|
||||
max(jsonb_array_length(phases_covered)) AS max_phases
|
||||
FROM master_controls
|
||||
""")).fetchone()
|
||||
|
||||
phase_dist = db.execute(text("""
|
||||
SELECT phase, count(*) AS control_count
|
||||
FROM master_control_members
|
||||
GROUP BY phase
|
||||
ORDER BY control_count DESC
|
||||
""")).fetchall()
|
||||
|
||||
return {
|
||||
"total_master_controls": stats[0],
|
||||
"total_member_controls": stats[1],
|
||||
"avg_controls_per_mc": stats[2],
|
||||
"max_controls": stats[3],
|
||||
"avg_phases": float(stats[4]) if stats[4] else 0,
|
||||
"max_phases": stats[5],
|
||||
"phase_distribution": {r[0]: r[1] for r in phase_dist},
|
||||
}
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/{mc_id}")
|
||||
async def get_master_control(mc_id: str):
|
||||
"""Get a single Master Control with all phase-controls."""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
mc = db.execute(text("""
|
||||
SELECT mc.id, mc.master_control_id, mc.object_group_id,
|
||||
mc.canonical_name, mc.phases_covered,
|
||||
mc.phase_control_count, mc.total_controls
|
||||
FROM master_controls mc
|
||||
WHERE mc.master_control_id = :mc_id
|
||||
"""), {"mc_id": mc_id}).fetchone()
|
||||
|
||||
if not mc:
|
||||
raise HTTPException(status_code=404, detail="Master Control not found")
|
||||
|
||||
members = db.execute(text("""
|
||||
SELECT mcm.phase, mcm.action,
|
||||
cc.control_id, cc.title, cc.severity,
|
||||
cc.source_citation->>'source' AS source
|
||||
FROM master_control_members mcm
|
||||
JOIN canonical_controls cc ON cc.id = mcm.control_uuid
|
||||
WHERE mcm.master_control_uuid = CAST(:mc_uuid AS uuid)
|
||||
ORDER BY mcm.phase, cc.control_id
|
||||
"""), {"mc_uuid": str(mc[0])}).fetchall()
|
||||
|
||||
# Group by phase
|
||||
phases = {}
|
||||
for phase, action, ctrl_id, title, severity, source in members:
|
||||
if phase not in phases:
|
||||
phases[phase] = []
|
||||
phases[phase].append({
|
||||
"control_id": ctrl_id,
|
||||
"title": title,
|
||||
"action": action,
|
||||
"severity": severity,
|
||||
"source": source,
|
||||
})
|
||||
|
||||
return {
|
||||
"id": str(mc[0]),
|
||||
"master_control_id": mc[1],
|
||||
"object_group_id": mc[2],
|
||||
"canonical_name": mc[3],
|
||||
"phases_covered": mc[4],
|
||||
"phase_control_count": mc[5],
|
||||
"total_controls": mc[6],
|
||||
"phases": phases,
|
||||
}
|
||||
finally:
|
||||
db.close()
|
||||
Reference in New Issue
Block a user