Core changes: - Add @defgroup/@ingroup to 1791 C2+ contracts (555 files) for HCA 128× pre-training DSA grouping - Add §0.1 Pre-Training Frequency matrix to semantics-core - Add §VIII Attention Architecture rules (ATTN_1-4) with MLA/CSA/HCA/DSA mechanics - Add @defgroup/@ingroup to canonical syntax (§II) and all contract examples Agent prompts (5 files): - Add ZERO-STATE RATIONALE with MLA/CSA/HCA/DSA compression mechanics - Add pre-training note: @RATIONALE/@REJECTED are in-context learned tags - svelte-coder: add missing #region contract, fix Svelte rule violations - python-coder/fullstack-coder: honor function contracts from speckit plan - qa-tester: add attention compliance audit (P3 ATTN_1-4 checks) Skills (6 files): - Translate all axiom_config descriptions to English - Fix doc_dirs to index .opencode/ and .specify/ - Deduplicate 5× complexity_rules → single global_tags catalog - Reduce semantics-svelte 591→485 lines (remove duplicate code blocks) - Fix semantics-testing: 'Short IDs' → 'Short hierarchical IDs' - Fix all examples: flat IDs → hierarchical Domain.Name format - Fix Svelte examples: replace raw Tailwind + <button> with semantic tokens + /ui Speckit workflow (commands + templates): - speckit.plan: add Function-Level Contracts for C3+ with @PRE/@POST/@TEST_EDGE - speckit.plan: add Attention Compliance Gate (ATTN_1-4 before contract generation) - speckit.tasks: add function contract inlining format (constraints in task description) - speckit.specify: load semantics-core for spec density rules - spec-template: add #region contract, @SEMANTICS grouping, hierarchical IDs - ux-reference-template: add #region wrapper - plan-template: add attention gate, @defgroup/@ingroup guidance - tasks-template: add attention audit + rebuild + orphan check tasks - constitution.md: translate to English, add Principle VIII (attention-optimized contracts) Reference modules rewritten (hierarchical IDs + full contracts): - Auth.Jwt: 6 child contracts with @RATIONALE/@REJECTED/@TEST_EDGE - Api.Auth: 5 endpoints with @TEST_EDGE + molecular CoT markers - Migration.Model: @defgroup Migration with 18 @ACTION + 6 @INVARIANT Scripts: - add_defgroup_ingroup.py: zero-risk additive @ingroup migration (1791 insertions) - migrate_hierarchical.py: flat→hierarchical ID dry-run analysis (792 contracts) - merge_prompts.py: merge all prompts/skills/commands into one review file Config: - axiom_config.yaml: 749→395 lines (-47%), English, doc_dirs include prompts - Fix test_datasets.py import collision (rename → test_datasets_routes.py) - Fix test_preview.py: SupersetClient→get_superset_client, AsyncMock, logger f-string
332 lines
11 KiB
Python
332 lines
11 KiB
Python
# #region GitHelpers [C:3] [TYPE Module] [SEMANTICS fastapi, git, api]
|
|
# @defgroup Api Module group.
|
|
# @BRIEF Shared helper functions for Git route modules.
|
|
# @LAYER API
|
|
# @RELATION CALLS -> [GitDeps]
|
|
# @RELATION CALLS -> [SupersetClient]
|
|
# @RELATION CALLS -> [UserDashboardPreference]
|
|
|
|
import os
|
|
|
|
from fastapi import HTTPException
|
|
from sqlalchemy.orm import Session
|
|
|
|
from src.core.logger import logger
|
|
from src.core.superset_client import SupersetClient
|
|
from src.models.auth import User
|
|
from src.models.git import GitServerConfig
|
|
from src.models.profile import UserDashboardPreference
|
|
|
|
from ._deps import get_git_service
|
|
|
|
|
|
# #region _build_no_repo_status_payload [C:1] [TYPE Function]
|
|
# @BRIEF Build a consistent status payload for dashboards without initialized repositories.
|
|
# @POST Returns a stable payload compatible with frontend repository status parsing.
|
|
def _build_no_repo_status_payload() -> dict:
|
|
return {
|
|
"is_dirty": False,
|
|
"untracked_files": [],
|
|
"modified_files": [],
|
|
"staged_files": [],
|
|
"current_branch": None,
|
|
"upstream_branch": None,
|
|
"has_upstream": False,
|
|
"ahead_count": 0,
|
|
"behind_count": 0,
|
|
"is_diverged": False,
|
|
"sync_state": "NO_REPO",
|
|
"sync_status": "NO_REPO",
|
|
"has_repo": False,
|
|
}
|
|
# #endregion _build_no_repo_status_payload
|
|
|
|
|
|
# #region _handle_unexpected_git_route_error [C:1] [TYPE Function]
|
|
# @BRIEF Convert unexpected route-level exceptions to stable 500 API responses.
|
|
# @PRE `error` is a non-HTTPException instance.
|
|
# @POST Raises HTTPException(500) with route-specific context.
|
|
def _handle_unexpected_git_route_error(route_name: str, error: Exception) -> None:
|
|
logger.error(f"[{route_name}][Coherence:Failed] {error}")
|
|
raise HTTPException(status_code=500, detail=f"{route_name} failed: {error!s}")
|
|
# #endregion _handle_unexpected_git_route_error
|
|
|
|
|
|
# #region _resolve_repository_status [C:2] [TYPE Function]
|
|
# @BRIEF Resolve repository status for one dashboard with graceful NO_REPO semantics.
|
|
# @PRE `dashboard_id` is a valid integer.
|
|
# @POST Returns standard status payload or `NO_REPO` payload when repository path is absent.
|
|
async def _resolve_repository_status(dashboard_id: int) -> dict:
|
|
git_service = get_git_service()
|
|
repo_path = await git_service._get_repo_path(dashboard_id)
|
|
if not os.path.exists(repo_path):
|
|
logger.reason(
|
|
f"Repository is not initialized for dashboard {dashboard_id}",
|
|
extra={"src": "get_repository_status"},
|
|
)
|
|
return _build_no_repo_status_payload()
|
|
|
|
try:
|
|
return await git_service.get_status(dashboard_id)
|
|
except HTTPException as e:
|
|
if e.status_code == 404:
|
|
logger.reason(
|
|
f"Repository is not initialized for dashboard {dashboard_id}",
|
|
extra={"src": "get_repository_status"},
|
|
)
|
|
return _build_no_repo_status_payload()
|
|
raise
|
|
# #endregion _resolve_repository_status
|
|
|
|
|
|
# #region _get_git_config_or_404 [C:2] [TYPE Function]
|
|
# @BRIEF Resolve GitServerConfig by id or raise 404.
|
|
def _get_git_config_or_404(db: Session, config_id: str) -> GitServerConfig:
|
|
config = db.query(GitServerConfig).filter(GitServerConfig.id == config_id).first()
|
|
if not config:
|
|
raise HTTPException(status_code=404, detail="Git configuration not found")
|
|
return config
|
|
# #endregion _get_git_config_or_404
|
|
|
|
|
|
# #region _find_dashboard_id_by_slug [C:2] [TYPE Function]
|
|
# @BRIEF Resolve dashboard numeric ID by slug in a specific environment.
|
|
async def _find_dashboard_id_by_slug(
|
|
client: SupersetClient,
|
|
dashboard_slug: str,
|
|
) -> int | None:
|
|
query_variants = [
|
|
{
|
|
"filters": [{"col": "slug", "opr": "eq", "value": dashboard_slug}],
|
|
"page": 0,
|
|
"page_size": 1,
|
|
},
|
|
{
|
|
"filters": [{"col": "slug", "op": "eq", "value": dashboard_slug}],
|
|
"page": 0,
|
|
"page_size": 1,
|
|
},
|
|
]
|
|
|
|
for query in query_variants:
|
|
try:
|
|
_count, dashboards = await client.get_dashboards_page(query=query)
|
|
if dashboards:
|
|
resolved_id = dashboards[0].get("id")
|
|
if resolved_id is not None:
|
|
return int(resolved_id)
|
|
except Exception:
|
|
continue
|
|
return None
|
|
# #endregion _find_dashboard_id_by_slug
|
|
|
|
|
|
# #region _resolve_dashboard_id_from_ref [C:2] [TYPE Function]
|
|
# @BRIEF Resolve dashboard ID from slug-or-id reference for Git routes.
|
|
async def _resolve_dashboard_id_from_ref(
|
|
dashboard_ref: str,
|
|
config_manager,
|
|
env_id: str | None = None,
|
|
) -> int:
|
|
normalized_ref = str(dashboard_ref or "").strip()
|
|
if not normalized_ref:
|
|
raise HTTPException(status_code=400, detail="dashboard_ref is required")
|
|
|
|
if normalized_ref.isdigit():
|
|
return int(normalized_ref)
|
|
|
|
if not env_id:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="env_id is required for slug-based Git operations",
|
|
)
|
|
|
|
environments = config_manager.get_environments()
|
|
env = next((e for e in environments if e.id == env_id), None)
|
|
if not env:
|
|
raise HTTPException(status_code=404, detail="Environment not found")
|
|
|
|
dashboard_id = await _find_dashboard_id_by_slug(SupersetClient(env), normalized_ref)
|
|
if dashboard_id is None:
|
|
raise HTTPException(
|
|
status_code=404, detail=f"Dashboard slug '{normalized_ref}' not found"
|
|
)
|
|
return dashboard_id
|
|
# #endregion _resolve_dashboard_id_from_ref
|
|
|
|
|
|
# #region _find_dashboard_id_by_slug_async [C:2] [TYPE Function]
|
|
# @BRIEF Resolve dashboard numeric ID by slug asynchronously for hot-path Git routes.
|
|
async def _find_dashboard_id_by_slug_async(
|
|
client: "AsyncSupersetClient",
|
|
dashboard_slug: str,
|
|
) -> int | None:
|
|
query_variants = [
|
|
{
|
|
"filters": [{"col": "slug", "opr": "eq", "value": dashboard_slug}],
|
|
"page": 0,
|
|
"page_size": 1,
|
|
},
|
|
{
|
|
"filters": [{"col": "slug", "op": "eq", "value": dashboard_slug}],
|
|
"page": 0,
|
|
"page_size": 1,
|
|
},
|
|
]
|
|
|
|
for query in query_variants:
|
|
try:
|
|
_count, dashboards = await client.get_dashboards_page(query=query)
|
|
if dashboards:
|
|
resolved_id = dashboards[0].get("id")
|
|
if resolved_id is not None:
|
|
return int(resolved_id)
|
|
except Exception:
|
|
continue
|
|
return None
|
|
# #endregion _find_dashboard_id_by_slug_async
|
|
|
|
|
|
# #region _resolve_dashboard_id_from_ref_async [C:2] [TYPE Function]
|
|
# @BRIEF Resolve dashboard ID asynchronously from slug-or-id reference for hot Git routes.
|
|
async def _resolve_dashboard_id_from_ref_async(
|
|
dashboard_ref: str,
|
|
config_manager,
|
|
env_id: str | None = None,
|
|
) -> int:
|
|
normalized_ref = str(dashboard_ref or "").strip()
|
|
if not normalized_ref:
|
|
raise HTTPException(status_code=400, detail="dashboard_ref is required")
|
|
|
|
if normalized_ref.isdigit():
|
|
return int(normalized_ref)
|
|
|
|
if not env_id:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="env_id is required for slug-based Git operations",
|
|
)
|
|
|
|
environments = config_manager.get_environments()
|
|
env = next((e for e in environments if e.id == env_id), None)
|
|
if not env:
|
|
raise HTTPException(status_code=404, detail="Environment not found")
|
|
|
|
from src.core.async_superset_client import AsyncSupersetClient
|
|
|
|
client = AsyncSupersetClient(env)
|
|
try:
|
|
dashboard_id = await _find_dashboard_id_by_slug_async(client, normalized_ref)
|
|
if dashboard_id is None:
|
|
raise HTTPException(
|
|
status_code=404, detail=f"Dashboard slug '{normalized_ref}' not found"
|
|
)
|
|
return dashboard_id
|
|
finally:
|
|
await client.aclose()
|
|
# #endregion _resolve_dashboard_id_from_ref_async
|
|
|
|
|
|
# #region _resolve_repo_key_from_ref [C:2] [TYPE Function]
|
|
# @BRIEF Resolve repository folder key with slug-first strategy and deterministic fallback.
|
|
async def _resolve_repo_key_from_ref(
|
|
dashboard_ref: str,
|
|
dashboard_id: int,
|
|
config_manager,
|
|
env_id: str | None = None,
|
|
) -> str:
|
|
normalized_ref = str(dashboard_ref or "").strip()
|
|
if normalized_ref and not normalized_ref.isdigit():
|
|
return normalized_ref
|
|
|
|
if env_id:
|
|
try:
|
|
environments = config_manager.get_environments()
|
|
env = next((e for e in environments if e.id == env_id), None)
|
|
if env:
|
|
payload = await SupersetClient(env).get_dashboard(dashboard_id)
|
|
dashboard_data = (
|
|
payload.get("result", payload) if isinstance(payload, dict) else {}
|
|
)
|
|
dashboard_slug = dashboard_data.get("slug")
|
|
if dashboard_slug:
|
|
return str(dashboard_slug)
|
|
except Exception:
|
|
logger.debug("Could not resolve dashboard slug for dashboard_id=%s", dashboard_id)
|
|
|
|
return f"dashboard-{dashboard_id}"
|
|
# #endregion _resolve_repo_key_from_ref
|
|
|
|
|
|
# #region _sanitize_optional_identity_value [C:1] [TYPE Function]
|
|
# @BRIEF Normalize optional identity value into trimmed string or None.
|
|
def _sanitize_optional_identity_value(value: str | None) -> str | None:
|
|
normalized = str(value or "").strip()
|
|
if not normalized:
|
|
return None
|
|
return normalized
|
|
# #endregion _sanitize_optional_identity_value
|
|
|
|
|
|
# #region _resolve_current_user_git_identity [C:2] [TYPE Function]
|
|
# @BRIEF Resolve configured Git username/email from current user's profile preferences.
|
|
def _resolve_current_user_git_identity(
|
|
db: Session,
|
|
current_user: User | None,
|
|
) -> tuple[str, str] | None:
|
|
if db is None or not hasattr(db, "query"):
|
|
return None
|
|
|
|
user_id = _sanitize_optional_identity_value(getattr(current_user, "id", None))
|
|
if not user_id:
|
|
return None
|
|
|
|
try:
|
|
preference = (
|
|
db.query(UserDashboardPreference)
|
|
.filter(UserDashboardPreference.user_id == user_id)
|
|
.first()
|
|
)
|
|
except Exception as resolve_error:
|
|
logger.explore(
|
|
"Failed to load profile preference for resolving git identity",
|
|
extra={"src": "_resolve_current_user_git_identity", "payload": {"user_id": user_id}, "error": str(resolve_error)},
|
|
)
|
|
return None
|
|
|
|
if not preference:
|
|
return None
|
|
|
|
git_username = _sanitize_optional_identity_value(
|
|
getattr(preference, "git_username", None)
|
|
)
|
|
git_email = _sanitize_optional_identity_value(
|
|
getattr(preference, "git_email", None)
|
|
)
|
|
if not git_username or not git_email:
|
|
return None
|
|
return git_username, git_email
|
|
# #endregion _resolve_current_user_git_identity
|
|
|
|
|
|
# #region _apply_git_identity_from_profile [C:2] [TYPE Function]
|
|
# @BRIEF Apply user-scoped Git identity to repository-local config before write/pull operations.
|
|
async def _apply_git_identity_from_profile(
|
|
dashboard_id: int,
|
|
db: Session,
|
|
current_user: User | None,
|
|
) -> None:
|
|
identity = _resolve_current_user_git_identity(db, current_user)
|
|
if not identity:
|
|
return
|
|
|
|
git_service = get_git_service()
|
|
configure_identity_fn = getattr(git_service, "configure_identity", None)
|
|
if not callable(configure_identity_fn):
|
|
return
|
|
|
|
git_username, git_email = identity
|
|
await configure_identity_fn(dashboard_id, git_username, git_email)
|
|
# #endregion _apply_git_identity_from_profile
|
|
# #endregion GitHelpers
|