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
329 lines
10 KiB
Python
329 lines
10 KiB
Python
# #region CleanReleaseV2Api [C:4] [TYPE Module] [SEMANTICS fastapi, clean-release, candidate, lifecycle, api]
|
|
# @defgroup Api Module group.
|
|
# @BRIEF Redesigned clean release API for headless candidate lifecycle.
|
|
# @LAYER API
|
|
# @RELATION DEPENDS_ON -> [CleanReleaseRepository]
|
|
# @RELATION CALLS -> [approve_candidate]
|
|
# @RELATION CALLS -> [publish_candidate]
|
|
# @PRE Clean release repository dependency is available for candidate lifecycle endpoints.
|
|
# @POST Candidate registration, approval, publication, and revocation routes are registered without behavior changes.
|
|
# @SIDE_EFFECT Persists candidate lifecycle state through clean release services and repository adapters.
|
|
|
|
from datetime import UTC, datetime
|
|
from typing import Any
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
|
|
from ...dependencies import get_clean_release_repository
|
|
from ...models.clean_release import (
|
|
CandidateArtifact,
|
|
DistributionManifest,
|
|
ReleaseCandidate,
|
|
)
|
|
from ...services.clean_release.approval_service import (
|
|
approve_candidate,
|
|
reject_candidate,
|
|
)
|
|
from ...services.clean_release.dto import CandidateDTO, ManifestDTO
|
|
from ...services.clean_release.enums import CandidateStatus
|
|
from ...services.clean_release.publication_service import (
|
|
publish_candidate,
|
|
revoke_publication,
|
|
)
|
|
from ...services.clean_release.repository import CleanReleaseRepository
|
|
|
|
router = APIRouter(prefix="/api/v2/clean-release", tags=["Clean Release V2"])
|
|
|
|
|
|
# #region ApprovalRequest [C:1] [TYPE Class]
|
|
# @BRIEF Schema for approval request payload.
|
|
class ApprovalRequest(dict):
|
|
pass
|
|
|
|
|
|
# #endregion ApprovalRequest
|
|
|
|
|
|
# #region PublishRequest [C:1] [TYPE Class]
|
|
# @BRIEF Schema for publication request payload.
|
|
class PublishRequest(dict):
|
|
pass
|
|
|
|
|
|
# #endregion PublishRequest
|
|
|
|
|
|
# #region RevokeRequest [C:1] [TYPE Class]
|
|
# @BRIEF Schema for revocation request payload.
|
|
class RevokeRequest(dict):
|
|
pass
|
|
|
|
|
|
# #endregion RevokeRequest
|
|
|
|
|
|
# #region register_candidate [C:3] [TYPE Function]
|
|
# @ingroup Api
|
|
# @BRIEF Register a new release candidate.
|
|
# @PRE Payload contains required fields (id, version, source_snapshot_ref, created_by).
|
|
# @POST Candidate is saved in repository.
|
|
# @RELATION DEPENDS_ON -> [CleanReleaseRepository]
|
|
# @RELATION DEPENDS_ON -> [clean_release_dto]
|
|
@router.post(
|
|
"/candidates", response_model=CandidateDTO, status_code=status.HTTP_201_CREATED
|
|
)
|
|
async def register_candidate(
|
|
payload: dict[str, Any],
|
|
repository: CleanReleaseRepository = Depends(get_clean_release_repository),
|
|
):
|
|
candidate = ReleaseCandidate(
|
|
id=payload["id"],
|
|
version=payload["version"],
|
|
source_snapshot_ref=payload["source_snapshot_ref"],
|
|
created_by=payload["created_by"],
|
|
created_at=datetime.now(UTC),
|
|
status=CandidateStatus.DRAFT.value,
|
|
)
|
|
repository.save_candidate(candidate)
|
|
return CandidateDTO(
|
|
id=candidate.id,
|
|
version=candidate.version,
|
|
source_snapshot_ref=candidate.source_snapshot_ref,
|
|
created_at=candidate.created_at,
|
|
created_by=candidate.created_by,
|
|
status=CandidateStatus(candidate.status),
|
|
)
|
|
|
|
|
|
# #endregion register_candidate
|
|
|
|
|
|
# #region import_artifacts [C:3] [TYPE Function]
|
|
# @ingroup Api
|
|
# @BRIEF Associate artifacts with a release candidate.
|
|
# @PRE Candidate exists.
|
|
# @POST Artifacts are processed (placeholder).
|
|
# @RELATION DEPENDS_ON -> [CleanReleaseRepository]
|
|
@router.post("/candidates/{candidate_id}/artifacts")
|
|
async def import_artifacts(
|
|
candidate_id: str,
|
|
payload: dict[str, Any],
|
|
repository: CleanReleaseRepository = Depends(get_clean_release_repository),
|
|
):
|
|
candidate = repository.get_candidate(candidate_id)
|
|
if not candidate:
|
|
raise HTTPException(status_code=404, detail="Candidate not found")
|
|
|
|
for art_data in payload.get("artifacts", []):
|
|
artifact = CandidateArtifact(
|
|
id=art_data["id"],
|
|
candidate_id=candidate_id,
|
|
path=art_data["path"],
|
|
sha256=art_data["sha256"],
|
|
size=art_data["size"],
|
|
)
|
|
# In a real repo we'd have save_artifact
|
|
# repository.save_artifact(artifact)
|
|
pass
|
|
|
|
return {"status": "success"}
|
|
|
|
|
|
# #endregion import_artifacts
|
|
|
|
|
|
# #region build_manifest [C:3] [TYPE Function]
|
|
# @ingroup Api
|
|
# @BRIEF Generate distribution manifest for a candidate.
|
|
# @PRE Candidate exists.
|
|
# @POST Manifest is created and saved.
|
|
# @RELATION DEPENDS_ON -> [CleanReleaseRepository]
|
|
@router.post(
|
|
"/candidates/{candidate_id}/manifests",
|
|
response_model=ManifestDTO,
|
|
status_code=status.HTTP_201_CREATED,
|
|
)
|
|
async def build_manifest(
|
|
candidate_id: str,
|
|
repository: CleanReleaseRepository = Depends(get_clean_release_repository),
|
|
):
|
|
candidate = repository.get_candidate(candidate_id)
|
|
if not candidate:
|
|
raise HTTPException(status_code=404, detail="Candidate not found")
|
|
|
|
manifest = DistributionManifest(
|
|
id=f"manifest-{candidate_id}",
|
|
candidate_id=candidate_id,
|
|
manifest_version=1,
|
|
manifest_digest="hash-123",
|
|
artifacts_digest="art-hash-123",
|
|
created_by="system",
|
|
created_at=datetime.now(UTC),
|
|
source_snapshot_ref=candidate.source_snapshot_ref,
|
|
content_json={"items": [], "summary": {}},
|
|
)
|
|
repository.save_manifest(manifest)
|
|
|
|
return ManifestDTO(
|
|
id=manifest.id,
|
|
candidate_id=manifest.candidate_id,
|
|
manifest_version=manifest.manifest_version,
|
|
manifest_digest=manifest.manifest_digest,
|
|
artifacts_digest=manifest.artifacts_digest,
|
|
created_at=manifest.created_at,
|
|
created_by=manifest.created_by,
|
|
source_snapshot_ref=manifest.source_snapshot_ref,
|
|
content_json=manifest.content_json,
|
|
)
|
|
|
|
|
|
# #endregion build_manifest
|
|
|
|
|
|
# #region approve_candidate_endpoint [C:3] [TYPE Function]
|
|
# @ingroup Api
|
|
# @BRIEF Endpoint to record candidate approval.
|
|
# @RELATION CALLS -> [approve_candidate]
|
|
@router.post("/candidates/{candidate_id}/approve")
|
|
async def approve_candidate_endpoint(
|
|
candidate_id: str,
|
|
payload: dict[str, Any],
|
|
repository: CleanReleaseRepository = Depends(get_clean_release_repository),
|
|
):
|
|
try:
|
|
decision = approve_candidate(
|
|
repository=repository,
|
|
candidate_id=candidate_id,
|
|
report_id=str(payload["report_id"]),
|
|
decided_by=str(payload["decided_by"]),
|
|
comment=payload.get("comment"),
|
|
)
|
|
except Exception as exc:
|
|
raise HTTPException(
|
|
status_code=409, detail={"message": str(exc), "code": "APPROVAL_GATE_ERROR"}
|
|
)
|
|
|
|
return {"status": "ok", "decision": decision.decision, "decision_id": decision.id}
|
|
|
|
|
|
# #endregion approve_candidate_endpoint
|
|
|
|
|
|
# #region reject_candidate_endpoint [C:3] [TYPE Function]
|
|
# @ingroup Api
|
|
# @BRIEF Endpoint to record candidate rejection.
|
|
# @RELATION CALLS -> [reject_candidate]
|
|
@router.post("/candidates/{candidate_id}/reject")
|
|
async def reject_candidate_endpoint(
|
|
candidate_id: str,
|
|
payload: dict[str, Any],
|
|
repository: CleanReleaseRepository = Depends(get_clean_release_repository),
|
|
):
|
|
try:
|
|
decision = reject_candidate(
|
|
repository=repository,
|
|
candidate_id=candidate_id,
|
|
report_id=str(payload["report_id"]),
|
|
decided_by=str(payload["decided_by"]),
|
|
comment=payload.get("comment"),
|
|
)
|
|
except Exception as exc:
|
|
raise HTTPException(
|
|
status_code=409, detail={"message": str(exc), "code": "APPROVAL_GATE_ERROR"}
|
|
)
|
|
|
|
return {"status": "ok", "decision": decision.decision, "decision_id": decision.id}
|
|
|
|
|
|
# #endregion reject_candidate_endpoint
|
|
|
|
|
|
# #region publish_candidate_endpoint [C:3] [TYPE Function]
|
|
# @ingroup Api
|
|
# @BRIEF Endpoint to publish an approved candidate.
|
|
# @RELATION CALLS -> [publish_candidate]
|
|
@router.post("/candidates/{candidate_id}/publish")
|
|
async def publish_candidate_endpoint(
|
|
candidate_id: str,
|
|
payload: dict[str, Any],
|
|
repository: CleanReleaseRepository = Depends(get_clean_release_repository),
|
|
):
|
|
try:
|
|
publication = publish_candidate(
|
|
repository=repository,
|
|
candidate_id=candidate_id,
|
|
report_id=str(payload["report_id"]),
|
|
published_by=str(payload["published_by"]),
|
|
target_channel=str(payload["target_channel"]),
|
|
publication_ref=payload.get("publication_ref"),
|
|
)
|
|
except Exception as exc:
|
|
raise HTTPException(
|
|
status_code=409,
|
|
detail={"message": str(exc), "code": "PUBLICATION_GATE_ERROR"},
|
|
)
|
|
|
|
return {
|
|
"status": "ok",
|
|
"publication": {
|
|
"id": publication.id,
|
|
"candidate_id": publication.candidate_id,
|
|
"report_id": publication.report_id,
|
|
"published_by": publication.published_by,
|
|
"published_at": publication.published_at.isoformat()
|
|
if publication.published_at
|
|
else None,
|
|
"target_channel": publication.target_channel,
|
|
"publication_ref": publication.publication_ref,
|
|
"status": publication.status,
|
|
},
|
|
}
|
|
|
|
|
|
# #endregion publish_candidate_endpoint
|
|
|
|
|
|
# #region revoke_publication_endpoint [C:3] [TYPE Function]
|
|
# @ingroup Api
|
|
# @BRIEF Endpoint to revoke a previous publication.
|
|
# @RELATION CALLS -> [revoke_publication]
|
|
@router.post("/publications/{publication_id}/revoke")
|
|
async def revoke_publication_endpoint(
|
|
publication_id: str,
|
|
payload: dict[str, Any],
|
|
repository: CleanReleaseRepository = Depends(get_clean_release_repository),
|
|
):
|
|
try:
|
|
publication = revoke_publication(
|
|
repository=repository,
|
|
publication_id=publication_id,
|
|
revoked_by=str(payload["revoked_by"]),
|
|
comment=payload.get("comment"),
|
|
)
|
|
except Exception as exc:
|
|
raise HTTPException(
|
|
status_code=409,
|
|
detail={"message": str(exc), "code": "PUBLICATION_GATE_ERROR"},
|
|
)
|
|
|
|
return {
|
|
"status": "ok",
|
|
"publication": {
|
|
"id": publication.id,
|
|
"candidate_id": publication.candidate_id,
|
|
"report_id": publication.report_id,
|
|
"published_by": publication.published_by,
|
|
"published_at": publication.published_at.isoformat()
|
|
if publication.published_at
|
|
else None,
|
|
"target_channel": publication.target_channel,
|
|
"publication_ref": publication.publication_ref,
|
|
"status": publication.status,
|
|
},
|
|
}
|
|
|
|
|
|
# #endregion revoke_publication_endpoint
|
|
|
|
# #endregion CleanReleaseV2Api
|