chore(repo): initialize team collaboration repository
This commit is contained in:
@@ -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_events(append-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))
|
||||
Reference in New Issue
Block a user