"""Answer-library freshness: control backing + staleness marking. Revision ID: 501 Revises: 500 安全问卷的答案库今天有 ``last_reviewed_at`` 和 ``review_interval_months``, 也就是「多久该复核一次」——但没有「**它依据的事实变了没有**」。一条说 「我们对所有特权账号强制 MFA」的答案,在 MFA 控制项翻红之后仍然会安静地 躺到下一个复核周期,然后被原样发给客户。 本迁移给答案加三列: * ``backing_control_keys`` —— 这条答案依据哪些控制项(JSON 数组,可为空); * ``stale_reason`` / ``stale_since`` —— 为什么、从什么时候起被标记为过时。 **只标记,不自动重写。** 自动重起草会把没人看过的 AI 文本推进一份要发给 客户的合规材料里;答案库的既有纪律是「起草可以自动,发送永远留人」, 过时标记的作用是把「该复核了」变成一个可见的信号,而不是替人做决定。 """ import uuid import sqlalchemy as sa from alembic import op revision = "501" down_revision = "500" branch_labels = None depends_on = None TABLE = "auditor_answer_library_entries" def _has_column(bind, column: str) -> bool: return bool( bind.execute( sa.text( "SELECT 1 FROM information_schema.COLUMNS " "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :t " "AND COLUMN_NAME = :c" ), {"t": TABLE, "c": column}, ).fetchone() ) def upgrade() -> None: bind = op.get_bind() if not _has_column(bind, "backing_control_keys"): op.add_column( TABLE, sa.Column("backing_control_keys", sa.JSON(), nullable=True), ) if not _has_column(bind, "stale_reason"): op.add_column( TABLE, sa.Column("stale_reason", sa.String(64), nullable=True), ) if not _has_column(bind, "stale_since"): op.add_column( TABLE, sa.Column("stale_since", sa.DateTime(timezone=True), nullable=True), ) def downgrade() -> None: bind = op.get_bind() for column in ("stale_since", "stale_reason", "backing_control_keys"): if _has_column(bind, column): op.drop_column(TABLE, column)