chore(repo): initialize team collaboration repository
CI / Python 3.12 (push) Waiting to run
CI / Python 3.9 (push) Waiting to run

This commit is contained in:
2026-07-27 20:40:12 +08:00
commit c91a64fddb
109 changed files with 21121 additions and 0 deletions
@@ -0,0 +1,31 @@
"""11 张表 → 方案 §5.3 的 7 个核心领域对象。映射见 docs/domain-model.md。"""
from .analysis import AuditLog, EvidenceBundle, FailureSignature, RunFailure
from .assets import Dut, FirmwareArtifact, OobController, TestHost
from .base import Base, iso_z, new_id, utcnow
from .run import RecoveryAction, ResourceLock, Run, RunEvent, RunStep
from .workflow import Workflow
__all__ = [
"Base",
"utcnow",
"new_id",
"iso_z",
# 资产
"TestHost",
"OobController",
"Dut",
"FirmwareArtifact",
# 工作流
"Workflow",
# Run
"Run",
"RunStep",
"RunEvent",
"RecoveryAction",
"ResourceLock",
# 分析
"FailureSignature",
"RunFailure",
"EvidenceBundle",
"AuditLog",
]
@@ -0,0 +1,112 @@
"""失败签名、失败实例、Evidence Bundle、审计。"""
from __future__ import annotations
import datetime as _dt
from typing import Any, Dict, List, Optional
from sqlalchemy import (
DateTime,
Float,
ForeignKey,
Index,
Integer,
JSON,
String,
Text,
UniqueConstraint,
)
from sqlalchemy.orm import Mapped, mapped_column
from ..enums import FailureClass, IntegrityState
from .base import Base, TimestampMixin, pk
class FailureSignature(Base, TimestampMixin):
"""失败签名 —— 聚类锚点。
8 要素哈希(方案 §8.3)。只用错误码会把"掉盘""脚本超时"归成一类,
聚类就废了;用原始日志则每条日志都是新签名。所以用模板化后的日志指纹。
"""
__tablename__ = "failure_signatures"
__table_args__ = (UniqueConstraint("hash", name="uq_signature_hash"),)
id: Mapped[str] = pk("sig")
hash: Mapped[str] = mapped_column(String(64), nullable=False)
title: Mapped[str] = mapped_column(String(255), nullable=False)
# 8 要素
workflow_step: Mapped[str] = mapped_column(String(64), nullable=False)
error_codes: Mapped[List[Any]] = mapped_column(JSON, default=list)
log_templates: Mapped[List[Any]] = mapped_column(JSON, default=list)
host_state: Mapped[str] = mapped_column(String(32), default="UNKNOWN")
dut_enumeration_state: Mapped[str] = mapped_column(String(32), default="UNKNOWN")
data_integrity_state: Mapped[str] = mapped_column(String(16), default=IntegrityState.NOT_CHECKED.value)
recovery_outcome: Mapped[Optional[str]] = mapped_column(String(16))
environment_fingerprint_hash: Mapped[Optional[str]] = mapped_column(String(64))
failure_class: Mapped[str] = mapped_column(String(24), default=FailureClass.UNKNOWN.value)
occurrences: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
first_seen_at: Mapped[Optional[_dt.datetime]] = mapped_column(DateTime)
last_seen_at: Mapped[Optional[_dt.datetime]] = mapped_column(DateTime)
# 人工确认后的处置:如已提单号、已知问题、误报
triage_note: Mapped[Optional[str]] = mapped_column(Text)
issue_ref: Mapped[Optional[str]] = mapped_column(String(64))
class RunFailure(Base):
"""一次具体的失败实例,挂在签名下面。"""
__tablename__ = "run_failures"
__table_args__ = (Index("ix_failure_run", "run_id"),)
id: Mapped[str] = pk("fail")
run_id: Mapped[str] = mapped_column(ForeignKey("runs.id"), nullable=False)
signature_id: Mapped[Optional[str]] = mapped_column(ForeignKey("failure_signatures.id"))
step_id: Mapped[Optional[str]] = mapped_column(String(32))
loop_index: Mapped[Optional[int]] = mapped_column(Integer)
ts: Mapped[_dt.datetime] = mapped_column(DateTime, nullable=False)
failure_class: Mapped[str] = mapped_column(String(24), default=FailureClass.UNKNOWN.value)
summary: Mapped[str] = mapped_column(Text, nullable=False)
detail: Mapped[Dict[str, Any]] = mapped_column(JSON, default=dict)
# 记录固件版本,A/B 对比直接从这里聚合
firmware_label: Mapped[Optional[str]] = mapped_column(String(64))
class EvidenceBundle(Base, TimestampMixin):
"""证据包。completeness < 1 说明有关键字段缺失 —— MVP 硬指标是 ≥95%"""
__tablename__ = "evidence_bundles"
id: Mapped[str] = pk("bundle")
run_id: Mapped[str] = mapped_column(ForeignKey("runs.id"), nullable=False)
uri: Mapped[str] = mapped_column(String(512), nullable=False)
manifest: Mapped[Dict[str, Any]] = mapped_column(JSON, default=dict)
size_bytes: Mapped[int] = mapped_column(Integer, default=0)
completeness: Mapped[float] = mapped_column(Float, default=0.0)
missing_fields: Mapped[List[Any]] = mapped_column(JSON, default=list)
class AuditLog(Base):
"""审计:谁、什么时候、用哪个模板版本、对什么、做了什么、结果如何。
方案 §6.6 的硬要求。与决策放在同一处 —— 见 ADR-0004。
"""
__tablename__ = "audit_log"
__table_args__ = (Index("ix_audit_ts", "ts"),)
id: Mapped[str] = pk("audit")
ts: Mapped[_dt.datetime] = mapped_column(DateTime, nullable=False)
actor: Mapped[str] = mapped_column(String(64), nullable=False)
action: Mapped[str] = mapped_column(String(64), nullable=False)
target_type: Mapped[Optional[str]] = mapped_column(String(32))
target_id: Mapped[Optional[str]] = mapped_column(String(32))
command_template: Mapped[Optional[str]] = mapped_column(String(64))
template_version: Mapped[Optional[str]] = mapped_column(String(16))
params: Mapped[Dict[str, Any]] = mapped_column(JSON, default=dict)
result: Mapped[Optional[str]] = mapped_column(String(32))
approved_by: Mapped[Optional[str]] = mapped_column(String(64))
reason: Mapped[Optional[str]] = mapped_column(Text)
@@ -0,0 +1,111 @@
"""资产:测试主机、带外控制器、DUT、固件包。"""
from __future__ import annotations
import datetime as _dt
from typing import Any, Dict, List, Optional
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, JSON, String, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from ..enums import DutStatus, HostStatus, PowerState
from .base import Base, TimestampMixin, pk
class OobController(Base, TimestampMixin):
"""带外控制器。
独立成表而不是挂在 TestHost 上,因为它的核心价值就是**主机死后独活**:
它有自己的网络、自己的心跳、自己的生命周期。主机 OFFLINE 时它必须还 ONLINE
这个"矛盾"状态是恢复阶梯 L3-L5 可用性的判断依据。
"""
__tablename__ = "oob_controllers"
id: Mapped[str] = pk("oob")
name: Mapped[str] = mapped_column(String(128), nullable=False)
kind: Mapped[str] = mapped_column(String(32), default="simulated") # raspberry_pi / jetkvm / pdu / simulated
endpoint: Mapped[Optional[str]] = mapped_column(String(255))
# 能力位:{"power": true, "reset": true, "atx": true, "ac": true, "hdmi": true, "temp": true}
capabilities: Mapped[Dict[str, Any]] = mapped_column(JSON, default=dict)
status: Mapped[str] = mapped_column(String(16), default=HostStatus.UNKNOWN.value)
power_state: Mapped[str] = mapped_column(String(16), default=PowerState.UNKNOWN.value)
last_seen_at: Mapped[Optional[_dt.datetime]] = mapped_column(DateTime)
temperature_c: Mapped[Optional[float]] = mapped_column()
class TestHost(Base, TimestampMixin):
"""测试主机。环境指纹的主要来源 —— A/B 对比的可比性靠它固定。"""
__tablename__ = "test_hosts"
id: Mapped[str] = pk("host")
name: Mapped[str] = mapped_column(String(128), nullable=False, unique=True)
os_family: Mapped[str] = mapped_column(String(32), default="linux") # linux / windows
os_version: Mapped[Optional[str]] = mapped_column(String(128))
kernel: Mapped[Optional[str]] = mapped_column(String(128))
cpu: Mapped[Optional[str]] = mapped_column(String(128))
motherboard: Mapped[Optional[str]] = mapped_column(String(128))
bios_version: Mapped[Optional[str]] = mapped_column(String(64))
agent_version: Mapped[Optional[str]] = mapped_column(String(32))
ip_address: Mapped[Optional[str]] = mapped_column(String(64))
status: Mapped[str] = mapped_column(String(16), default=HostStatus.UNKNOWN.value)
last_heartbeat_at: Mapped[Optional[_dt.datetime]] = mapped_column(DateTime)
agent_id: Mapped[Optional[str]] = mapped_column(String(32))
# 只保存 bearer token 的 SHA-256 摘要;原始 token 仅在注册响应中返回一次。
agent_token: Mapped[Optional[str]] = mapped_column(String(64))
oob_controller_id: Mapped[Optional[str]] = mapped_column(ForeignKey("oob_controllers.id"))
# 已安装工具与版本,来自 Agent 的 discover(){"nvme-cli": "2.8", "fio": "3.36"}
toolchain: Mapped[Dict[str, Any]] = mapped_column(JSON, default=dict)
labels: Mapped[Dict[str, Any]] = mapped_column(JSON, default=dict)
class Dut(Base, TimestampMixin):
"""被测设备。
allow_destructive 是防误盘的第二道信号(第一道是序列号,第三道是系统盘检测)。
它不给普通 API 改 —— 只能走带审批记录的资产接口,见 ADR-0004。
"""
__tablename__ = "duts"
__table_args__ = (UniqueConstraint("serial", name="uq_dut_serial"),)
id: Mapped[str] = pk("dut")
serial: Mapped[str] = mapped_column(String(64), nullable=False)
model: Mapped[str] = mapped_column(String(128), nullable=False)
vendor: Mapped[Optional[str]] = mapped_column(String(64))
capacity_gb: Mapped[Optional[int]] = mapped_column(Integer)
form_factor: Mapped[str] = mapped_column(String(32), default="M.2") # M.2 / U.2 / E1.S
interface: Mapped[str] = mapped_column(String(32), default="NVMe")
current_firmware: Mapped[Optional[str]] = mapped_column(String(64))
bdf: Mapped[Optional[str]] = mapped_column(String(32)) # PCIe 地址,如 0000:03:00.0
device_path: Mapped[Optional[str]] = mapped_column(String(64)) # /dev/nvme0n1
test_host_id: Mapped[Optional[str]] = mapped_column(ForeignKey("test_hosts.id"))
status: Mapped[str] = mapped_column(String(16), default=DutStatus.IDLE.value)
# 破坏性测试白名单标记。默认 False —— 安全默认拒绝。
allow_destructive: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
health: Mapped[Dict[str, Any]] = mapped_column(JSON, default=dict) # SMART 关键项、温度、寿命
labels: Mapped[Dict[str, Any]] = mapped_column(JSON, default=dict)
class FirmwareArtifact(Base, TimestampMixin):
"""固件包。哈希 + 签名 + 适用型号,缺一样就不可审计。"""
__tablename__ = "firmware_artifacts"
id: Mapped[str] = pk("fw")
version: Mapped[str] = mapped_column(String(64), nullable=False)
label: Mapped[Optional[str]] = mapped_column(String(64)) # "A" / "B" / "候选版"
sha256: Mapped[str] = mapped_column(String(64), nullable=False)
signature: Mapped[Optional[str]] = mapped_column(String(512))
applicable_models: Mapped[List[Any]] = mapped_column(JSON, default=list)
flash_tool: Mapped[str] = mapped_column(String(64), default="nvme_cli")
file_uri: Mapped[Optional[str]] = mapped_column(String(512))
uploaded_by: Mapped[Optional[str]] = mapped_column(String(64))
@@ -0,0 +1,64 @@
"""ORM 基类与通用字段。
刻意保持"贫血模型":这些类只管持久化,不放业务逻辑。
业务逻辑在 engine/(纯函数)与 services/(编排),这样才能不起数据库单测。
"""
from __future__ import annotations
import datetime as _dt
import uuid
from typing import Any, Dict
from sqlalchemy import DateTime, JSON, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
"""所有表的基类。
type_annotation_map 里只用方言中立的类型:SQLite(开发)与 PostgreSQL(生产)
行为一致。不要引入 JSONB / ARRAY / UUID 这些 PG 特有类型 —— 见 ADR-0001。
"""
type_annotation_map = {
Dict[str, Any]: JSON,
dict: JSON,
list: JSON,
}
def utcnow() -> _dt.datetime:
"""统一的当前时间:naive UTC。
SQLite 不保存时区,所以全仓库统一存 naive UTC,序列化时再补 Z。
禁止在任何地方用 datetime.now()(本地时区)—— 实验室里跨时区协作会出事。
"""
return _dt.datetime.utcnow()
def new_id(prefix: str) -> str:
"""带前缀的 ID,如 run_7f3a9c2e...。
前缀让日志和时间线一眼可读 —— 排障时不用回表查这个 ID 是什么东西。
"""
return "{0}_{1}".format(prefix, uuid.uuid4().hex[:20])
def iso_z(value: Any) -> Any:
"""naive UTC datetime → ISO8601 带 Z。非 datetime 原样返回。"""
if isinstance(value, _dt.datetime):
return value.replace(microsecond=value.microsecond).isoformat() + "Z"
return value
class TimestampMixin:
created_at: Mapped[_dt.datetime] = mapped_column(DateTime, default=utcnow, nullable=False)
updated_at: Mapped[_dt.datetime] = mapped_column(
DateTime, default=utcnow, onupdate=utcnow, nullable=False
)
def pk(prefix: str) -> Mapped[str]:
return mapped_column(
String(32), primary_key=True, default=lambda: new_id(prefix)
)
@@ -0,0 +1,188 @@
"""Run 及其行为记录。
runs / run_steps / recovery_actions 是**读模型**(投影)。
唯一真相是 run_eventsappend-only)—— 见 ADR-0002。
仓储层刻意不提供 update_run_state():改状态的唯一入口是 events/recorder.py。
"""
from __future__ import annotations
import datetime as _dt
from typing import Any, Dict, List, Optional
from sqlalchemy import (
Boolean,
DateTime,
Float,
ForeignKey,
Index,
Integer,
JSON,
String,
Text,
UniqueConstraint,
)
from sqlalchemy.orm import Mapped, mapped_column
from ..enums import EventSource, RunState, Severity, StepState
from .base import Base, TimestampMixin, pk
class Run(Base, TimestampMixin):
__tablename__ = "runs"
id: Mapped[str] = pk("run")
name: Mapped[Optional[str]] = mapped_column(String(255))
workflow_id: Mapped[str] = mapped_column(ForeignKey("workflows.id"), nullable=False)
workflow_key: Mapped[str] = mapped_column(String(64), nullable=False)
workflow_version: Mapped[int] = mapped_column(Integer, nullable=False)
spec_hash: Mapped[str] = mapped_column(String(64), nullable=False)
state: Mapped[str] = mapped_column(String(16), default=RunState.QUEUED.value, nullable=False)
verdict: Mapped[Optional[str]] = mapped_column(String(16))
dut_id: Mapped[Optional[str]] = mapped_column(ForeignKey("duts.id"))
test_host_id: Mapped[Optional[str]] = mapped_column(ForeignKey("test_hosts.id"))
firmware_a_id: Mapped[Optional[str]] = mapped_column(ForeignKey("firmware_artifacts.id"))
firmware_b_id: Mapped[Optional[str]] = mapped_column(ForeignKey("firmware_artifacts.id"))
params: Mapped[Dict[str, Any]] = mapped_column(JSON, default=dict)
# 任务开始时冻结的不可变快照。固件之外任何一项变了,A/B 对比结论就不成立。
env_fingerprint: Mapped[Dict[str, Any]] = mapped_column(JSON, default=dict)
env_fingerprint_hash: Mapped[Optional[str]] = mapped_column(String(64))
loop_target: Mapped[int] = mapped_column(Integer, default=1)
loop_done: Mapped[int] = mapped_column(Integer, default=0)
# 最近一次检查点:{"loop_index": 37, "next_step_key": "...", "dut_fw": "A"}
checkpoint: Mapped[Dict[str, Any]] = mapped_column(JSON, default=dict)
started_at: Mapped[Optional[_dt.datetime]] = mapped_column(DateTime)
ended_at: Mapped[Optional[_dt.datetime]] = mapped_column(DateTime)
# ROI 凭据(方案 §9.3):这两个字段是销售武器,从第一行代码就记。
unattended_completion: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
human_touches: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
recovery_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
# 事件序号分配器(单点递增,保证时间线可重放,见 ADR-0002)
event_seq: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
created_by: Mapped[Optional[str]] = mapped_column(String(64))
freeze_reason: Mapped[Optional[str]] = mapped_column(Text)
class RunStep(Base, TimestampMixin):
"""物化后的步骤实例。循环体在物化时按 loop_index 展开。"""
__tablename__ = "run_steps"
__table_args__ = (
UniqueConstraint("run_id", "seq", name="uq_step_run_seq"),
Index("ix_step_run_state", "run_id", "state"),
)
id: Mapped[str] = pk("step")
run_id: Mapped[str] = mapped_column(ForeignKey("runs.id"), nullable=False)
seq: Mapped[int] = mapped_column(Integer, nullable=False)
loop_index: Mapped[Optional[int]] = mapped_column(Integer)
step_key: Mapped[str] = mapped_column(String(64), nullable=False)
step_type: Mapped[str] = mapped_column(String(32), nullable=False)
adapter: Mapped[str] = mapped_column(String(32), nullable=False)
template: Mapped[Optional[str]] = mapped_column(String(64))
params: Mapped[Dict[str, Any]] = mapped_column(JSON, default=dict)
state: Mapped[str] = mapped_column(String(16), default=StepState.PENDING.value, nullable=False)
attempt: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
max_retry: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
timeout_s: Mapped[float] = mapped_column(Float, default=600.0, nullable=False)
# 非幂等步骤(默认)中断后不自动重跑 —— "自动重试把盘刷坏"是最容易砸招牌的失败模式
idempotent: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
danger: Mapped[str] = mapped_column(String(16), default="none")
on_fail: Mapped[str] = mapped_column(String(16), default="retry")
checkpoint_after: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
# 租约:Agent 领了活就死了 → 租约到期 → 步骤回 PENDING 重新派发
leased_by: Mapped[Optional[str]] = mapped_column(String(32))
lease_expires_at: Mapped[Optional[_dt.datetime]] = mapped_column(DateTime)
started_at: Mapped[Optional[_dt.datetime]] = mapped_column(DateTime)
ended_at: Mapped[Optional[_dt.datetime]] = mapped_column(DateTime)
exit_code: Mapped[Optional[int]] = mapped_column(Integer)
error_class: Mapped[Optional[str]] = mapped_column(String(32))
result: Mapped[Dict[str, Any]] = mapped_column(JSON, default=dict) # UnifiedResult
log_uri: Mapped[Optional[str]] = mapped_column(String(512))
class RunEvent(Base):
"""Append-only 事件流 —— 唯一真相。
seq 由控制平面单点分配(不按时间戳排序):主机断电、时钟漂移、
带外与主机时钟不同步都会让时间戳乱序。Agent 事件与带外事件进同一条序列,
双通道观测的交叉校验靠它。
"""
__tablename__ = "run_events"
__table_args__ = (
UniqueConstraint("run_id", "seq", name="uq_event_run_seq"),
# 网络抖动重发不产生重复事件(见 agent-protocol.md
UniqueConstraint("run_id", "idempotency_key", name="uq_event_idem"),
Index("ix_event_run_seq", "run_id", "seq"),
)
id: Mapped[str] = pk("ev")
run_id: Mapped[str] = mapped_column(ForeignKey("runs.id"), nullable=False)
seq: Mapped[int] = mapped_column(Integer, nullable=False)
ts: Mapped[_dt.datetime] = mapped_column(DateTime, nullable=False)
source: Mapped[str] = mapped_column(String(16), default=EventSource.CONTROL_PLANE.value)
kind: Mapped[str] = mapped_column(String(48), nullable=False)
severity: Mapped[str] = mapped_column(String(16), default=Severity.INFO.value)
step_id: Mapped[Optional[str]] = mapped_column(String(32))
loop_index: Mapped[Optional[int]] = mapped_column(Integer)
message: Mapped[Optional[str]] = mapped_column(Text)
payload: Mapped[Dict[str, Any]] = mapped_column(JSON, default=dict)
idempotency_key: Mapped[Optional[str]] = mapped_column(String(64))
class RecoveryAction(Base):
"""每一级恢复的留证记录。恢复前抓画面/温度,恢复后验证 DUT 重新枚举。"""
__tablename__ = "recovery_actions"
id: Mapped[str] = pk("rec")
run_id: Mapped[str] = mapped_column(ForeignKey("runs.id"), nullable=False)
step_id: Mapped[Optional[str]] = mapped_column(String(32))
loop_index: Mapped[Optional[int]] = mapped_column(Integer)
level: Mapped[str] = mapped_column(String(24), nullable=False)
trigger: Mapped[str] = mapped_column(String(32), nullable=False)
outcome: Mapped[Optional[str]] = mapped_column(String(16))
started_at: Mapped[_dt.datetime] = mapped_column(DateTime, nullable=False)
ended_at: Mapped[Optional[_dt.datetime]] = mapped_column(DateTime)
detail: Mapped[Dict[str, Any]] = mapped_column(JSON, default=dict)
evidence_uris: Mapped[List[Any]] = mapped_column(JSON, default=list)
class ResourceLock(Base):
"""DUT / 主机独占锁。PREFLIGHT 获取,终态释放。
唯一约束在 (resource_type, resource_id) —— 数据库来保证互斥,不靠应用层自觉。
"""
__tablename__ = "resource_locks"
__table_args__ = (
UniqueConstraint("resource_type", "resource_id", name="uq_lock_resource"),
)
id: Mapped[str] = pk("lock")
resource_type: Mapped[str] = mapped_column(String(16), nullable=False) # dut / test_host / oob
resource_id: Mapped[str] = mapped_column(String(32), nullable=False)
run_id: Mapped[str] = mapped_column(ForeignKey("runs.id"), nullable=False)
acquired_at: Mapped[_dt.datetime] = mapped_column(DateTime, nullable=False)
expires_at: Mapped[Optional[_dt.datetime]] = mapped_column(DateTime)
@@ -0,0 +1,35 @@
"""工作流:SOP 的可执行形态。"""
from __future__ import annotations
from typing import Any, Dict, Optional
from sqlalchemy import Boolean, Integer, JSON, String, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from ..enums import DangerLevel
from .base import Base, TimestampMixin, pk
class Workflow(Base, TimestampMixin):
"""工作流定义。
spec 存原文,spec_hash 存规范化后的哈希。spec_hash 变了而 version 没变 →
拒绝发布(见 workflow-spec.md)。理由:A/B 对比的结论必须能追溯到
具体哪一版流程,否则"上周跑的和这周跑的是不是同一个流程"说不清。
"""
__tablename__ = "workflows"
__table_args__ = (UniqueConstraint("key", "version", name="uq_workflow_key_version"),)
id: Mapped[str] = pk("wf")
key: Mapped[str] = mapped_column(String(64), nullable=False)
name: Mapped[str] = mapped_column(String(128), nullable=False)
version: Mapped[int] = mapped_column(Integer, default=1, nullable=False)
spec: Mapped[Dict[str, Any]] = mapped_column(JSON, nullable=False)
spec_hash: Mapped[str] = mapped_column(String(64), nullable=False)
danger_level: Mapped[str] = mapped_column(String(16), default=DangerLevel.NONE.value)
source_sop: Mapped[Optional[str]] = mapped_column(String(255))
published: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
approved_by: Mapped[Optional[str]] = mapped_column(String(64))