8월 7일까지 워킹트리에만 남아 있던 미커밋 작업을 커밋한다. 여러 사본 폴더(worktree·clone)에 흩어져 있던 중간 스냅샷을 정리하기 전에 원본을 git 이력으로 고정하는 것이 목적이다. - contracts/routes/services: measurement, outcome_trajectory, rupture_repair, deliberate_practice, calibration_transfer, supervision_research, multimodal_alliance, continuous_improvement 계열 신규 모듈과 테스트 - infra/db/init: 07~16 마이그레이션(측정 기반~calibration transfer 실행) - apps/web: 세션 리뷰 카드·관리 화면·E2E 스펙 추가 - docs/ops: G0~G8 라이브 통합·배포·롤백 증거 문서와 evidence JSON/PNG - scripts: smoke·ledger·릴리스 에이전트·NAS 프리뷰 운영 스크립트 engine.public 로그 .bak과 apps/web/test-results 산출물은 커밋에서 제외했다.
592 lines
27 KiB
Python
592 lines
27 KiB
Python
"""런타임 스키마 준비 상태 정책.
|
|
|
|
권위 스키마는 ``infra/db/init/*.sql``이다. 앱 기동 중 불완전한 스키마 보정은 로컬 개발
|
|
환경에서만 허용하고, 스테이징과 운영은 owner migration을 요구하며 fail-closed한다.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Any, Protocol
|
|
|
|
from .config import settings
|
|
|
|
|
|
class SchemaConnection(Protocol):
|
|
async def fetchrow(self, query: str, *args: Any) -> Any: ...
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class RuntimeSchemaContract:
|
|
component: str
|
|
relations: tuple[str, ...]
|
|
columns: tuple[str, ...] = ()
|
|
policies: tuple[str, ...] = ()
|
|
triggers: tuple[str, ...] = ()
|
|
indexes: tuple[str, ...] = ()
|
|
|
|
|
|
REVIEW_SCHEMA_CONTRACT = RuntimeSchemaContract(
|
|
component="review/evaluation",
|
|
relations=(
|
|
"app.session_evaluation",
|
|
"app.case_worksheet",
|
|
"app.live_coach_events",
|
|
"app.safety_events",
|
|
"app.session_review_status",
|
|
"app.session_share_link",
|
|
"app.session_archive_state",
|
|
),
|
|
columns=(
|
|
"app.live_coach_events.event_type",
|
|
"app.live_coach_events.credit_delta",
|
|
"app.live_coach_events.credit_balance",
|
|
"app.live_coach_events.reason",
|
|
"app.session_review_status.worksheet_status",
|
|
"app.session_review_status.worksheet_note",
|
|
"app.session_review_status.worksheet_reviewed_at",
|
|
),
|
|
policies=(
|
|
"app.session_evaluation.p_session_evaluation_select",
|
|
"app.case_worksheet.p_case_worksheet_select",
|
|
"app.live_coach_events.p_live_coach_events_select",
|
|
"app.safety_events.p_safety_events_select",
|
|
"app.session_review_status.p_session_review_status_select",
|
|
"app.session_share_link.p_session_share_select",
|
|
"app.session_archive_state.p_session_archive_select",
|
|
),
|
|
)
|
|
|
|
NOTIFICATION_SCHEMA_CONTRACT = RuntimeSchemaContract(
|
|
component="notification",
|
|
relations=("app.notification_event", "app.notification_delivery"),
|
|
policies=(
|
|
"app.notification_event.p_notification_event_admin_all",
|
|
"app.notification_delivery.p_notification_delivery_admin_all",
|
|
),
|
|
)
|
|
|
|
MEASUREMENT_SCHEMA_CONTRACT = RuntimeSchemaContract(
|
|
component="outcome/alliance measurement",
|
|
relations=(
|
|
"app.measurement_instrument",
|
|
"app.measurement_event",
|
|
"app.alliance_pulse",
|
|
"app.self_assessment",
|
|
"audit.alliance_pulse_status_event",
|
|
"audit.model_run",
|
|
"ds.benchmark_case",
|
|
"ds.benchmark_observation",
|
|
),
|
|
columns=(
|
|
"app.measurement_event.source_kind",
|
|
"app.measurement_event.perspective",
|
|
"app.measurement_event.instrument_id",
|
|
"app.measurement_event.model_run_id",
|
|
"app.measurement_event.supersedes_id",
|
|
"app.measurement_event.pulse_id",
|
|
"app.alliance_pulse.checkpoint",
|
|
"app.alliance_pulse.status",
|
|
"app.self_assessment.scores",
|
|
"audit.alliance_pulse_status_event.from_status",
|
|
"audit.alliance_pulse_status_event.to_status",
|
|
"audit.alliance_pulse_status_event.changed_at",
|
|
"audit.model_run.prompt_bundle_hash",
|
|
"audit.model_run.input_evidence_hash",
|
|
),
|
|
policies=(
|
|
"app.measurement_instrument.p_measurement_instrument_select",
|
|
"app.measurement_event.p_measurement_event_select",
|
|
"app.alliance_pulse.p_alliance_pulse_select",
|
|
"app.self_assessment.p_self_assessment_select",
|
|
"audit.alliance_pulse_status_event.p_alliance_pulse_status_event_select",
|
|
"audit.model_run.p_model_run_select",
|
|
"ds.benchmark_case.p_benchmark_case_select",
|
|
"ds.benchmark_observation.p_benchmark_observation_select",
|
|
),
|
|
triggers=(
|
|
"app.measurement_event.trg_measurement_event_append_only",
|
|
"app.alliance_pulse.trg_alliance_pulse_transition_guard",
|
|
"app.alliance_pulse.trg_alliance_pulse_delete_guard",
|
|
"app.alliance_pulse.trg_alliance_pulse_status_audit",
|
|
"app.self_assessment.trg_self_assessment_append_only",
|
|
"audit.alliance_pulse_status_event.trg_alliance_pulse_status_event_append_only",
|
|
"audit.model_run.trg_model_run_append_only",
|
|
),
|
|
)
|
|
|
|
|
|
OUTCOME_TRAJECTORY_SCHEMA_CONTRACT = RuntimeSchemaContract(
|
|
component="longitudinal outcome trajectory",
|
|
relations=(
|
|
"ds.synthetic_outcome_arc",
|
|
"app.outcome_trajectory_revision",
|
|
"app.outcome_trajectory_observation",
|
|
"app.relationship_memory_event",
|
|
"app.relationship_memory_projection",
|
|
),
|
|
columns=(
|
|
"ds.synthetic_outcome_arc.data_classification",
|
|
"ds.synthetic_outcome_arc.clinical_claim_allowed",
|
|
"app.outcome_trajectory_revision.supersedes_revision_id",
|
|
"app.outcome_trajectory_revision.source_fingerprint",
|
|
"app.outcome_trajectory_revision.assessment",
|
|
"app.outcome_trajectory_observation.measurement_id",
|
|
"app.outcome_trajectory_observation.status",
|
|
"app.outcome_trajectory_observation.missing_reason",
|
|
"app.relationship_memory_event.resolves_event_id",
|
|
"app.relationship_memory_event.visible_to",
|
|
"app.relationship_memory_projection.ai_view",
|
|
),
|
|
policies=(
|
|
"ds.synthetic_outcome_arc.p_synthetic_outcome_arc_select",
|
|
"app.outcome_trajectory_revision.p_outcome_trajectory_revision_select",
|
|
"app.outcome_trajectory_observation.p_outcome_trajectory_observation_select",
|
|
"app.relationship_memory_event.p_relationship_memory_event_select",
|
|
"app.relationship_memory_projection.p_relationship_memory_projection_select",
|
|
),
|
|
triggers=(
|
|
"app.measurement_event.trg_outcome_submission_turn_ownership",
|
|
"ds.synthetic_outcome_arc.trg_synthetic_outcome_arc_append_only",
|
|
"app.outcome_trajectory_revision.trg_outcome_trajectory_revision_append_only",
|
|
"app.outcome_trajectory_observation.trg_outcome_trajectory_observation_append_only",
|
|
"app.relationship_memory_event.trg_relationship_memory_link",
|
|
"app.relationship_memory_event.trg_relationship_memory_event_append_only",
|
|
"app.relationship_memory_projection.trg_relationship_projection_view",
|
|
"app.relationship_memory_projection.trg_relationship_memory_projection_append_only",
|
|
),
|
|
indexes=("app.measurement_event.uq_measurement_event_outcome_submission_axis",),
|
|
)
|
|
|
|
|
|
RUPTURE_REPAIR_SCHEMA_CONTRACT = RuntimeSchemaContract(
|
|
component="rupture and repair evidence ledger",
|
|
relations=(
|
|
"app.rupture_episode",
|
|
"app.rupture_observation_event",
|
|
"app.rupture_reconciliation_revision",
|
|
"app.rupture_safety_reference",
|
|
),
|
|
columns=(
|
|
"app.rupture_episode.case_id",
|
|
"app.rupture_episode.visible_to",
|
|
"app.rupture_observation_event.sequence_no",
|
|
"app.rupture_observation_event.evidence_turn_ids",
|
|
"app.rupture_observation_event.model_run_id",
|
|
"app.rupture_observation_event.supersedes_observation_id",
|
|
"app.rupture_reconciliation_revision.supersedes_revision_id",
|
|
"app.rupture_reconciliation_revision.disposition",
|
|
"app.rupture_safety_reference.safety_event_id",
|
|
),
|
|
policies=(
|
|
"app.rupture_episode.p_rupture_episode_select",
|
|
"app.rupture_episode.p_rupture_episode_insert",
|
|
"app.rupture_observation_event.p_rupture_observation_select",
|
|
"app.rupture_observation_event.p_rupture_observation_insert",
|
|
"app.rupture_reconciliation_revision.p_rupture_reconciliation_select",
|
|
"app.rupture_reconciliation_revision.p_rupture_reconciliation_insert",
|
|
"app.rupture_safety_reference.p_rupture_safety_select",
|
|
"app.rupture_safety_reference.p_rupture_safety_insert",
|
|
),
|
|
triggers=(
|
|
"app.rupture_episode.trg_rupture_episode_anchor",
|
|
"app.rupture_episode.trg_rupture_episode_append_only",
|
|
"app.rupture_observation_event.trg_rupture_observation_contract",
|
|
"app.rupture_observation_event.trg_rupture_observation_append_only",
|
|
"app.rupture_reconciliation_revision.trg_rupture_reconciliation_contract",
|
|
"app.rupture_reconciliation_revision.trg_rupture_reconciliation_append_only",
|
|
"app.rupture_safety_reference.trg_rupture_safety_contract",
|
|
"app.rupture_safety_reference.trg_rupture_safety_append_only",
|
|
),
|
|
indexes=("app.rupture_observation_event.uq_rupture_observation_episode_sequence",),
|
|
)
|
|
|
|
|
|
DELIBERATE_PRACTICE_SCHEMA_CONTRACT = RuntimeSchemaContract(
|
|
component="deliberate practice evidence ledger",
|
|
relations=(
|
|
"app.practice_prescription_submission",
|
|
"app.practice_coaching_card",
|
|
"app.practice_prescription",
|
|
"app.practice_episode_submission",
|
|
"app.practice_attempt_evidence",
|
|
"app.competency_graph_snapshot",
|
|
"app.practice_curriculum_decision_event",
|
|
"app.practice_teacher_correction",
|
|
),
|
|
columns=(
|
|
"app.practice_prescription_submission.content_hash",
|
|
"app.practice_coaching_card.evidence_turn_ids",
|
|
"app.practice_coaching_card.uncertainty",
|
|
"app.practice_prescription.activity_mode",
|
|
"app.practice_prescription.scenario_novelty",
|
|
"app.practice_episode_submission.mastery_allowed",
|
|
"app.practice_episode_submission.mastery_blockers",
|
|
"app.practice_attempt_evidence.client_response",
|
|
"app.practice_attempt_evidence.utterance_template_id",
|
|
"app.competency_graph_snapshot.supersedes_snapshot_id",
|
|
"app.practice_curriculum_decision_event.selected_prescription_record_id",
|
|
"app.practice_teacher_correction.supersedes_correction_id",
|
|
),
|
|
policies=(
|
|
"app.practice_prescription_submission.p_practice_prescription_submission_select",
|
|
"app.practice_prescription_submission.p_practice_prescription_submission_insert",
|
|
"app.practice_teacher_correction.p_practice_teacher_correction_select",
|
|
"app.practice_teacher_correction.p_practice_teacher_correction_insert",
|
|
),
|
|
triggers=(
|
|
"app.practice_episode_submission.trg_practice_episode_transfer_gate",
|
|
"app.practice_attempt_evidence.trg_practice_attempt_contract",
|
|
"app.competency_graph_snapshot.trg_competency_snapshot_chain",
|
|
"app.practice_curriculum_decision_event.trg_practice_curriculum_decision_contract",
|
|
"app.practice_teacher_correction.trg_practice_teacher_correction_contract",
|
|
),
|
|
indexes=(
|
|
"app.practice_prescription.uq_practice_prescription_record_learner",
|
|
"app.practice_attempt_evidence.idx_practice_attempt_episode_sequence",
|
|
"app.competency_graph_snapshot.idx_competency_graph_snapshot_latest",
|
|
"app.practice_teacher_correction.idx_practice_teacher_correction_attempt",
|
|
),
|
|
)
|
|
|
|
|
|
CALIBRATION_TRANSFER_SCHEMA_CONTRACT = RuntimeSchemaContract(
|
|
component="calibration mirror and unseen transfer ledger",
|
|
relations=(
|
|
"app.calibration_prediction_history",
|
|
"app.calibration_prediction_revision",
|
|
"app.calibration_prediction_lock",
|
|
"app.calibration_performance_observation",
|
|
"app.calibration_assessment_snapshot",
|
|
"app.calibration_metacognitive_prescription",
|
|
"app.calibration_transfer_suite",
|
|
"app.calibration_transfer_trial",
|
|
"app.calibration_transfer_assessment",
|
|
"app.calibration_subgroup_drift_report",
|
|
"app.calibration_teacher_review_event",
|
|
"app.calibration_transfer_execution_event",
|
|
),
|
|
columns=(
|
|
"app.calibration_prediction_revision.supersedes_prediction_revision_id",
|
|
"app.calibration_prediction_revision.predicted_success_probability",
|
|
"app.calibration_prediction_lock.locked_sequence",
|
|
"app.calibration_performance_observation.revealed_sequence",
|
|
"app.calibration_performance_observation.uncertainty",
|
|
"app.calibration_assessment_snapshot.assessment_payload",
|
|
"app.calibration_metacognitive_prescription.prescription_payload",
|
|
"app.calibration_transfer_suite.clinical_claim_allowed",
|
|
"app.calibration_transfer_trial.relationship_style",
|
|
"app.calibration_transfer_trial.phrase_family_id",
|
|
"app.calibration_transfer_assessment.assessment_payload",
|
|
"app.calibration_subgroup_drift_report.data_classification",
|
|
"app.calibration_teacher_review_event.supersedes_review_id",
|
|
"app.calibration_transfer_execution_event.original_transfer_trial_record_id",
|
|
"app.calibration_transfer_execution_event.practice_session_id",
|
|
"app.calibration_transfer_execution_event.normalized_evaluator_labels",
|
|
"app.calibration_transfer_execution_event.source_kind",
|
|
"app.calibration_transfer_execution_event.perspective",
|
|
"app.calibration_transfer_execution_event.model_run_id",
|
|
"app.calibration_transfer_execution_event.instrument_id",
|
|
"app.calibration_transfer_execution_event.instrument_version",
|
|
"app.calibration_transfer_execution_event.observer_version",
|
|
"app.calibration_transfer_execution_event.evidence_turn_ids",
|
|
),
|
|
policies=(
|
|
"app.calibration_teacher_review_event.p_calibration_teacher_review_event_select",
|
|
"app.calibration_teacher_review_event.p_calibration_teacher_review_event_insert",
|
|
"app.calibration_transfer_execution_event.p_calibration_transfer_execution_event_select",
|
|
"app.calibration_transfer_execution_event.p_calibration_transfer_execution_event_insert",
|
|
),
|
|
triggers=(
|
|
"app.calibration_prediction_revision.trg_calibration_prediction_revision_contract",
|
|
"app.calibration_prediction_lock.trg_calibration_prediction_lock_contract",
|
|
"app.calibration_performance_observation.trg_calibration_observation_reveal_contract",
|
|
"app.calibration_assessment_snapshot.trg_calibration_assessment_chain",
|
|
"app.calibration_transfer_assessment.trg_calibration_transfer_assessment_contract",
|
|
"app.calibration_subgroup_drift_report.trg_calibration_subgroup_drift_contract",
|
|
"app.calibration_teacher_review_event.trg_calibration_teacher_review_contract",
|
|
"app.calibration_transfer_execution_event.trg_calibration_transfer_execution_contract",
|
|
"app.calibration_transfer_execution_event.trg_calibration_transfer_execution_append_only",
|
|
),
|
|
indexes=(
|
|
"app.calibration_prediction_revision.idx_calibration_prediction_revision_latest",
|
|
"app.calibration_performance_observation.idx_calibration_observation_learner_competency",
|
|
"app.calibration_transfer_trial.idx_calibration_transfer_trial_suite_competency",
|
|
"app.calibration_teacher_review_event.idx_calibration_teacher_review_target",
|
|
"app.calibration_transfer_execution_event.idx_calibration_transfer_execution_learner_competency",
|
|
"app.calibration_transfer_execution_event.idx_calibration_transfer_execution_trial",
|
|
),
|
|
)
|
|
|
|
|
|
SUPERVISION_RESEARCH_SCHEMA_CONTRACT = RuntimeSchemaContract(
|
|
component="supervision and research evidence ledger",
|
|
relations=(
|
|
"app.supervision_evidence_pointer",
|
|
"app.supervision_attention_snapshot",
|
|
"app.supervision_attention_item",
|
|
"app.supervision_attention_reason",
|
|
"app.supervision_teacher_ai_disagreement",
|
|
"app.supervision_calibration_dataset_row",
|
|
"audit.supervision_teacher_event",
|
|
"app.supervision_curriculum_gap_snapshot",
|
|
"app.supervision_evaluation_batch",
|
|
"app.supervision_evaluation_observation",
|
|
"app.supervision_drift_report",
|
|
"app.supervision_drift_subgroup_metric",
|
|
"app.supervision_phase3_manifest",
|
|
"app.supervision_phase3_artifact",
|
|
),
|
|
columns=(
|
|
"app.supervision_evidence_pointer.consumer_view",
|
|
"app.supervision_evidence_pointer.content_hash",
|
|
"app.supervision_attention_item.evidence_pointer_ids",
|
|
"app.supervision_attention_item.drilldown_routes",
|
|
"app.supervision_teacher_ai_disagreement.raw_transcript_included",
|
|
"app.supervision_calibration_dataset_row.row_hash",
|
|
"audit.supervision_teacher_event.content_hash",
|
|
"app.supervision_curriculum_gap_snapshot.status",
|
|
"app.supervision_evaluation_batch.prompt_version",
|
|
"app.supervision_drift_report.status",
|
|
"app.supervision_phase3_manifest.artifact_count",
|
|
"app.supervision_phase3_artifact.source_pointer_id",
|
|
),
|
|
policies=(
|
|
"app.supervision_evidence_pointer.p_supervision_evidence_pointer_select",
|
|
"app.supervision_evidence_pointer.p_supervision_evidence_pointer_insert",
|
|
"app.supervision_attention_snapshot.p_supervision_attention_snapshot_select",
|
|
"app.supervision_attention_snapshot.p_supervision_attention_snapshot_insert",
|
|
"app.supervision_attention_item.p_supervision_attention_item_select",
|
|
"app.supervision_attention_item.p_supervision_attention_item_insert",
|
|
"app.supervision_curriculum_gap_snapshot.p_supervision_curriculum_gap_select",
|
|
"app.supervision_curriculum_gap_snapshot.p_supervision_curriculum_gap_insert",
|
|
"app.supervision_teacher_ai_disagreement.p_supervision_teacher_ai_disagreement_insert",
|
|
"app.supervision_calibration_dataset_row.p_supervision_calibration_dataset_row_insert",
|
|
"audit.supervision_teacher_event.p_supervision_teacher_event_select",
|
|
"audit.supervision_teacher_event.p_supervision_teacher_event_insert",
|
|
),
|
|
triggers=(
|
|
"app.supervision_evidence_pointer.trg_supervision_evidence_pointer_contract",
|
|
"app.supervision_attention_item.trg_supervision_attention_item_contract",
|
|
"app.supervision_attention_reason.trg_supervision_attention_reason_contract",
|
|
"app.supervision_teacher_ai_disagreement.trg_supervision_teacher_ai_actor",
|
|
"audit.supervision_teacher_event.trg_supervision_teacher_event_append_only",
|
|
"audit.supervision_teacher_event.trg_supervision_teacher_event_pointer_array",
|
|
),
|
|
)
|
|
|
|
|
|
CONTINUOUS_IMPROVEMENT_SCHEMA_CONTRACT = RuntimeSchemaContract(
|
|
component="continuous improvement approval ledger",
|
|
relations=(
|
|
"app.ci_ingestion_submission",
|
|
"app.ci_source_artifact",
|
|
"app.ci_agentic_job",
|
|
"app.ci_content_pipeline",
|
|
"app.ci_red_team_review",
|
|
"app.ci_red_team_finding",
|
|
"app.ci_content_benchmark",
|
|
"app.ci_content_qualification",
|
|
"app.ci_gate_artifact",
|
|
"app.ci_model_calibration_snapshot",
|
|
"app.ci_model_change_gate",
|
|
"app.ci_release_gate",
|
|
"audit.ci_human_approval_event",
|
|
"app.ci_catalog_entry",
|
|
"audit.ci_lifecycle_event",
|
|
"app.ci_operational_incident",
|
|
"app.ci_regression_dag_node",
|
|
),
|
|
columns=(
|
|
"app.ci_ingestion_submission.data_classification",
|
|
"app.ci_agentic_job.data_classification",
|
|
"app.ci_agentic_job.status",
|
|
"app.ci_content_pipeline.state",
|
|
"app.ci_content_pipeline.draft_payload",
|
|
"app.ci_content_qualification.gate_state",
|
|
"app.ci_gate_artifact.artifact_kind",
|
|
"app.ci_model_change_gate.gate_decision",
|
|
"app.ci_model_change_gate.state",
|
|
"app.ci_release_gate.state",
|
|
"audit.ci_human_approval_event.decision",
|
|
"app.ci_catalog_entry.approval_event_id",
|
|
"audit.ci_lifecycle_event.event_status",
|
|
"audit.ci_lifecycle_event.executor_receipt_id",
|
|
"audit.ci_lifecycle_event.executor_evidence_refs",
|
|
"app.ci_operational_incident.pii_included",
|
|
"app.ci_regression_dag_node.depends_on_record_ids",
|
|
),
|
|
policies=(
|
|
"app.ci_ingestion_submission.p_ci_ingestion_submission_admin_insert",
|
|
"app.ci_agentic_job.p_ci_agentic_job_select",
|
|
"app.ci_agentic_job.p_ci_agentic_job_insert",
|
|
"app.ci_agentic_job.p_ci_agentic_job_update",
|
|
"app.ci_catalog_entry.p_ci_catalog_entry_select",
|
|
"app.ci_catalog_entry.p_ci_catalog_entry_insert",
|
|
"audit.ci_human_approval_event.p_ci_human_approval_select",
|
|
"audit.ci_human_approval_event.p_ci_human_approval_insert",
|
|
"audit.ci_lifecycle_event.p_ci_lifecycle_select",
|
|
"audit.ci_lifecycle_event.p_ci_lifecycle_monitor_insert",
|
|
"audit.ci_lifecycle_event.p_ci_lifecycle_admin_insert",
|
|
),
|
|
triggers=(
|
|
"app.ci_content_pipeline.trg_ci_content_pipeline_sources",
|
|
"app.ci_agentic_job.trg_ci_agentic_job_update_contract",
|
|
"app.ci_content_qualification.trg_ci_content_qualification_sources",
|
|
"app.ci_model_change_gate.trg_ci_model_gate_artifacts",
|
|
"app.ci_release_gate.trg_ci_release_gate_artifacts",
|
|
"app.ci_regression_dag_node.trg_ci_dag_dependencies",
|
|
"app.ci_red_team_review.trg_ci_red_team_review_contract",
|
|
"app.ci_red_team_finding.trg_ci_red_team_finding_contract",
|
|
"audit.ci_human_approval_event.trg_ci_human_approval_contract",
|
|
"app.ci_catalog_entry.trg_ci_catalog_approval",
|
|
"audit.ci_lifecycle_event.trg_ci_lifecycle_contract",
|
|
"app.ci_model_change_gate.trg_ci_model_snapshot_contract",
|
|
),
|
|
indexes=(
|
|
"app.ci_agentic_job.idx_ci_agentic_job_ready",
|
|
),
|
|
)
|
|
|
|
|
|
MULTIMODAL_ALLIANCE_SCHEMA_CONTRACT = RuntimeSchemaContract(
|
|
component="multimodal alliance consent and evidence ledger",
|
|
relations=(
|
|
"app.multimodal_ingestion_request",
|
|
"app.multimodal_consent_snapshot",
|
|
"app.multimodal_audio_asset",
|
|
"app.multimodal_audio_timeline",
|
|
"app.multimodal_word_timestamp",
|
|
"app.multimodal_voice_event",
|
|
"app.multimodal_axis_measurement",
|
|
"app.multimodal_fusion_decision",
|
|
"app.multimodal_deletion_request",
|
|
"audit.multimodal_deletion_tombstone",
|
|
),
|
|
columns=(
|
|
"app.multimodal_ingestion_request.content_hash",
|
|
"app.multimodal_consent_snapshot.consent_status",
|
|
"app.multimodal_audio_asset.retained_until",
|
|
"app.multimodal_audio_timeline.clock_version",
|
|
"app.multimodal_word_timestamp.token_hash",
|
|
"app.multimodal_voice_event.claim_scope",
|
|
"app.multimodal_axis_measurement.source_kind",
|
|
"app.multimodal_fusion_decision.fusion_applied",
|
|
"app.multimodal_fusion_decision.incremental_gain",
|
|
"app.multimodal_deletion_request.scopes",
|
|
"audit.multimodal_deletion_tombstone.deletion_proof",
|
|
),
|
|
policies=(
|
|
"app.multimodal_audio_asset.p_multimodal_audio_asset_select",
|
|
"app.multimodal_ingestion_request.p_multimodal_human_ingestion_insert",
|
|
"app.multimodal_consent_snapshot.p_multimodal_consent_insert",
|
|
"app.multimodal_deletion_request.p_multimodal_deletion_request_insert",
|
|
"audit.multimodal_deletion_tombstone.p_multimodal_tombstone_select",
|
|
"audit.multimodal_deletion_tombstone.p_multimodal_tombstone_insert",
|
|
),
|
|
triggers=(
|
|
"app.multimodal_consent_snapshot.trg_multimodal_consent_sequence",
|
|
"app.multimodal_audio_asset.trg_multimodal_audio_consent",
|
|
"app.multimodal_audio_timeline.trg_multimodal_timeline_consent",
|
|
"app.multimodal_word_timestamp.trg_multimodal_word_clock",
|
|
"app.multimodal_voice_event.trg_multimodal_event_clock",
|
|
"app.multimodal_fusion_decision.trg_multimodal_fusion_contract",
|
|
"audit.multimodal_deletion_tombstone.trg_multimodal_deletion_tombstone_append_only",
|
|
),
|
|
indexes=(
|
|
"app.multimodal_consent_snapshot.idx_multimodal_consent_latest",
|
|
"app.multimodal_audio_timeline.idx_multimodal_timeline_session",
|
|
"app.multimodal_word_timestamp.idx_multimodal_word_clock",
|
|
"app.multimodal_voice_event.idx_multimodal_event_clock",
|
|
"app.multimodal_axis_measurement.idx_multimodal_measurement_session",
|
|
"app.multimodal_deletion_request.idx_multimodal_deletion_pending",
|
|
),
|
|
)
|
|
|
|
|
|
async def schema_contract_ready(
|
|
conn: SchemaConnection,
|
|
contract: RuntimeSchemaContract,
|
|
) -> bool:
|
|
row = await conn.fetchrow(
|
|
"""
|
|
SELECT
|
|
NOT EXISTS (
|
|
SELECT 1
|
|
FROM unnest($1::text[]) AS required(relation_name)
|
|
WHERE to_regclass(required.relation_name) IS NULL
|
|
)
|
|
AND NOT EXISTS (
|
|
SELECT 1
|
|
FROM unnest($2::text[]) AS required(qualified_name)
|
|
WHERE NOT EXISTS (
|
|
SELECT 1
|
|
FROM information_schema.columns c
|
|
WHERE c.table_schema = split_part(required.qualified_name, '.', 1)
|
|
AND c.table_name = split_part(required.qualified_name, '.', 2)
|
|
AND c.column_name = split_part(required.qualified_name, '.', 3)
|
|
)
|
|
)
|
|
AND NOT EXISTS (
|
|
SELECT 1
|
|
FROM unnest($3::text[]) AS required(qualified_name)
|
|
WHERE NOT EXISTS (
|
|
SELECT 1
|
|
FROM pg_policies p
|
|
WHERE p.schemaname = split_part(required.qualified_name, '.', 1)
|
|
AND p.tablename = split_part(required.qualified_name, '.', 2)
|
|
AND p.policyname = split_part(required.qualified_name, '.', 3)
|
|
)
|
|
)
|
|
AND NOT EXISTS (
|
|
SELECT 1
|
|
FROM unnest($4::text[]) AS required(qualified_name)
|
|
WHERE NOT EXISTS (
|
|
SELECT 1
|
|
FROM pg_trigger t
|
|
JOIN pg_class c ON c.oid = t.tgrelid
|
|
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
WHERE n.nspname = split_part(required.qualified_name, '.', 1)
|
|
AND c.relname = split_part(required.qualified_name, '.', 2)
|
|
AND t.tgname = split_part(required.qualified_name, '.', 3)
|
|
AND NOT t.tgisinternal
|
|
)
|
|
)
|
|
AND NOT EXISTS (
|
|
SELECT 1
|
|
FROM unnest($5::text[]) AS required(qualified_name)
|
|
WHERE NOT EXISTS (
|
|
SELECT 1
|
|
FROM pg_indexes i
|
|
WHERE i.schemaname = split_part(required.qualified_name, '.', 1)
|
|
AND i.tablename = split_part(required.qualified_name, '.', 2)
|
|
AND i.indexname = split_part(required.qualified_name, '.', 3)
|
|
)
|
|
) AS ready
|
|
""",
|
|
list(contract.relations),
|
|
list(contract.columns),
|
|
list(contract.policies),
|
|
list(contract.triggers),
|
|
list(contract.indexes),
|
|
)
|
|
return bool(row and row["ready"])
|
|
|
|
|
|
def runtime_schema_bootstrap_required(
|
|
contract: RuntimeSchemaContract | str,
|
|
*,
|
|
ready: bool,
|
|
) -> bool:
|
|
"""로컬 보정 필요 여부를 반환하고, dev 외 환경에서는 불완전 스키마를 차단한다."""
|
|
if ready:
|
|
return False
|
|
component = (
|
|
contract.component if isinstance(contract, RuntimeSchemaContract) else contract
|
|
)
|
|
if settings.environment != "dev":
|
|
raise RuntimeError(
|
|
f"{component} runtime DB schema is incomplete; run owner migration/init and "
|
|
"scripts/check-deploy-preflight.py before starting the API"
|
|
)
|
|
return True
|