feat(control-library): document-grouped batching, generation strategy tracking, sort by source
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 31s
CI/CD / test-python-backend-compliance (push) Successful in 31s
CI/CD / test-python-document-crawler (push) Successful in 21s
CI/CD / test-python-dsms-gateway (push) Successful in 18s
CI/CD / validate-canonical-controls (push) Successful in 11s
CI/CD / Deploy (push) Successful in 2s
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 31s
CI/CD / test-python-backend-compliance (push) Successful in 31s
CI/CD / test-python-document-crawler (push) Successful in 21s
CI/CD / test-python-dsms-gateway (push) Successful in 18s
CI/CD / validate-canonical-controls (push) Successful in 11s
CI/CD / Deploy (push) Successful in 2s
- Group chunks by regulation_code before batching for better LLM context - Add generation_strategy column (ungrouped=v1, document_grouped=v2) - Add v1/v2 badge to control cards in frontend - Add sort-by-source option with visual group headers - Add frontend page tests (18 tests) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -80,6 +80,7 @@ class ControlResponse(BaseModel):
|
||||
category: Optional[str] = None
|
||||
target_audience: Optional[str] = None
|
||||
generation_metadata: Optional[dict] = None
|
||||
generation_strategy: Optional[str] = "ungrouped"
|
||||
created_at: str
|
||||
updated_at: str
|
||||
|
||||
@@ -161,7 +162,7 @@ _CONTROL_COLS = """id, framework_id, control_id, title, objective, rationale,
|
||||
evidence_confidence, open_anchors, release_state, tags,
|
||||
license_rule, source_original_text, source_citation,
|
||||
customer_visible, verification_method, category,
|
||||
target_audience, generation_metadata,
|
||||
target_audience, generation_metadata, generation_strategy,
|
||||
created_at, updated_at"""
|
||||
|
||||
|
||||
@@ -297,8 +298,14 @@ async def list_controls(
|
||||
verification_method: Optional[str] = Query(None),
|
||||
category: Optional[str] = Query(None),
|
||||
target_audience: Optional[str] = Query(None),
|
||||
source: Optional[str] = Query(None, description="Filter by source_citation->source"),
|
||||
search: Optional[str] = Query(None, description="Full-text search in control_id, title, objective"),
|
||||
sort: Optional[str] = Query("control_id", description="Sort field: control_id, created_at, severity"),
|
||||
order: Optional[str] = Query("asc", description="Sort order: asc or desc"),
|
||||
limit: Optional[int] = Query(None, ge=1, le=5000, description="Max results"),
|
||||
offset: Optional[int] = Query(None, ge=0, description="Offset for pagination"),
|
||||
):
|
||||
"""List all canonical controls, with optional filters."""
|
||||
"""List canonical controls with filters, search, sorting and pagination."""
|
||||
query = f"""
|
||||
SELECT {_CONTROL_COLS}
|
||||
FROM canonical_controls
|
||||
@@ -324,8 +331,35 @@ async def list_controls(
|
||||
if target_audience:
|
||||
query += " AND target_audience = :ta"
|
||||
params["ta"] = target_audience
|
||||
if source:
|
||||
if source == "__none__":
|
||||
query += " AND (source_citation IS NULL OR source_citation->>'source' IS NULL OR source_citation->>'source' = '')"
|
||||
else:
|
||||
query += " AND source_citation->>'source' = :src"
|
||||
params["src"] = source
|
||||
if search:
|
||||
query += " AND (control_id ILIKE :q OR title ILIKE :q OR objective ILIKE :q)"
|
||||
params["q"] = f"%{search}%"
|
||||
|
||||
query += " ORDER BY control_id"
|
||||
# Sorting
|
||||
sort_col = "control_id"
|
||||
if sort in ("created_at", "updated_at", "severity", "control_id"):
|
||||
sort_col = sort
|
||||
elif sort == "source":
|
||||
sort_col = "source_citation->>'source'"
|
||||
sort_dir = "DESC" if order and order.lower() == "desc" else "ASC"
|
||||
if sort == "source":
|
||||
# Group by source first, then by control_id within each source
|
||||
query += f" ORDER BY {sort_col} {sort_dir} NULLS LAST, control_id ASC"
|
||||
else:
|
||||
query += f" ORDER BY {sort_col} {sort_dir}"
|
||||
|
||||
if limit is not None:
|
||||
query += " LIMIT :lim"
|
||||
params["lim"] = limit
|
||||
if offset is not None:
|
||||
query += " OFFSET :off"
|
||||
params["off"] = offset
|
||||
|
||||
with SessionLocal() as db:
|
||||
rows = db.execute(text(query), params).fetchall()
|
||||
@@ -333,6 +367,87 @@ async def list_controls(
|
||||
return [_control_row(r) for r in rows]
|
||||
|
||||
|
||||
@router.get("/controls-count")
|
||||
async def count_controls(
|
||||
severity: Optional[str] = Query(None),
|
||||
domain: Optional[str] = Query(None),
|
||||
release_state: Optional[str] = Query(None),
|
||||
verification_method: Optional[str] = Query(None),
|
||||
category: Optional[str] = Query(None),
|
||||
target_audience: Optional[str] = Query(None),
|
||||
source: Optional[str] = Query(None),
|
||||
search: Optional[str] = Query(None),
|
||||
):
|
||||
"""Count controls matching filters (for pagination)."""
|
||||
query = "SELECT count(*) FROM canonical_controls WHERE 1=1"
|
||||
params: dict[str, Any] = {}
|
||||
|
||||
if severity:
|
||||
query += " AND severity = :sev"
|
||||
params["sev"] = severity
|
||||
if domain:
|
||||
query += " AND LEFT(control_id, LENGTH(:dom)) = :dom"
|
||||
params["dom"] = domain.upper()
|
||||
if release_state:
|
||||
query += " AND release_state = :rs"
|
||||
params["rs"] = release_state
|
||||
if verification_method:
|
||||
query += " AND verification_method = :vm"
|
||||
params["vm"] = verification_method
|
||||
if category:
|
||||
query += " AND category = :cat"
|
||||
params["cat"] = category
|
||||
if target_audience:
|
||||
query += " AND target_audience = :ta"
|
||||
params["ta"] = target_audience
|
||||
if source:
|
||||
if source == "__none__":
|
||||
query += " AND (source_citation IS NULL OR source_citation->>'source' IS NULL OR source_citation->>'source' = '')"
|
||||
else:
|
||||
query += " AND source_citation->>'source' = :src"
|
||||
params["src"] = source
|
||||
if search:
|
||||
query += " AND (control_id ILIKE :q OR title ILIKE :q OR objective ILIKE :q)"
|
||||
params["q"] = f"%{search}%"
|
||||
|
||||
with SessionLocal() as db:
|
||||
total = db.execute(text(query), params).scalar()
|
||||
|
||||
return {"total": total}
|
||||
|
||||
|
||||
@router.get("/controls-meta")
|
||||
async def controls_meta():
|
||||
"""Return aggregated metadata for filter dropdowns (domains, sources, counts)."""
|
||||
with SessionLocal() as db:
|
||||
total = db.execute(text("SELECT count(*) FROM canonical_controls")).scalar()
|
||||
|
||||
domains = db.execute(text("""
|
||||
SELECT UPPER(SPLIT_PART(control_id, '-', 1)) as domain, count(*) as cnt
|
||||
FROM canonical_controls
|
||||
GROUP BY domain ORDER BY domain
|
||||
""")).fetchall()
|
||||
|
||||
sources = db.execute(text("""
|
||||
SELECT source_citation->>'source' as src, count(*) as cnt
|
||||
FROM canonical_controls
|
||||
WHERE source_citation->>'source' IS NOT NULL AND source_citation->>'source' != ''
|
||||
GROUP BY src ORDER BY cnt DESC
|
||||
""")).fetchall()
|
||||
|
||||
no_source = db.execute(text("""
|
||||
SELECT count(*) FROM canonical_controls
|
||||
WHERE source_citation IS NULL OR source_citation->>'source' IS NULL OR source_citation->>'source' = ''
|
||||
""")).scalar()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"domains": [{"domain": r[0], "count": r[1]} for r in domains],
|
||||
"sources": [{"source": r[0], "count": r[1]} for r in sources],
|
||||
"no_source_count": no_source,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/controls/{control_id}")
|
||||
async def get_control(control_id: str):
|
||||
"""Get a single canonical control by its control_id (e.g. AUTH-001)."""
|
||||
@@ -661,6 +776,7 @@ def _control_row(r) -> dict:
|
||||
"category": r.category,
|
||||
"target_audience": r.target_audience,
|
||||
"generation_metadata": r.generation_metadata,
|
||||
"generation_strategy": getattr(r, "generation_strategy", "ungrouped"),
|
||||
"created_at": r.created_at.isoformat() if r.created_at else None,
|
||||
"updated_at": r.updated_at.isoformat() if r.updated_at else None,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user