import Spec.Prelude /-! # ProjectGroup —— 飞书项目群作为协作空间(ADR-0001/0017) ADR-0001 的核心:一个 project 对应一个**长生命周期**飞书项目群;群是协作空间,**不是锁 owner**(锁归 `AgentRun`,见 `Lock` / ADR-0002),不是临时处理 session。群可在无 Claude 处理时保持开启;教师离群/静音与项目权限、与 Claude 生命周期相互独立。本模块钉死 project↔group 的**active**一对一绑定——likec4 画得出"project has group",画不出"恰好一个、 且群不持锁"。 **绑定历史(`PINNED`, ADR-0021):** active binding 严格 1:1;实现可以保留 archived historical binding rows 供审计/纠错,但 `GroupBinding` 谓词只刻画当前 active 快照。 群解散/不可达的自动化处理仍为 `OPEN`;pilot 纠错由 org admin 显式归档绑定。 **当前角色(`PINNED`, ADR-0017):** active binding 选择一个 Organization-scoped Agent role;普通群消息在接收时冻结该 role,随后即使群切换角色,已接收工作也不漂移。切换角色 只改变后续工作路由,不把不同 role 的 provider session 合并。每个 Organization 恰有一个 active 默认 role 用于初始化新 binding,role 名称不硬编码。切换共享 role 的 actor 必须同时 通过 project `agent.trigger` 与目标 role `role.trigger`;不额外要求 project `MANAGE`。 -/ namespace Spec.System variable (I : Identifiers) /-- 飞书项目群(`PINNED` 长生命周期协作空间, ADR-0001)。承载 project 与飞书 chat 的绑定; **不是锁 owner**(锁归 `AgentRun`,见 `Lock`);不是临时 session。 -/ structure ProjectGroup where /-- 群对应的飞书 chat(`PINNED` 关系, ADR-0001 "one project has one Feishu project group";chat 标识见 `Identifiers.ChatId`)。 -/ chat : I.ChatId /-- 项目↔active 群绑定表(`PINNED` 每项目至多一个 active 群, ADR-0001/0021)。 `ProjectId → Option ChatId` 的结构本身即"一个 project 至多绑一个 active 群"(由 `Option` 自带);良构补另一半——单射。 -/ def GroupBinding := I.ProjectId → Option I.ChatId /-- Active 绑定良构:**单射**——不同 project 不绑同一 active chat(`PINNED` 1:1 的另一半, ADR-0001/0021)。"每 project 至多一个群"由 `Option` 结构自带;这条钉死"每群至多属于一个 project"。archived historical bindings 不在本快照不变式内。 -/ def GroupBinding.WellFormed (b : GroupBinding I) : Prop := ∀ p₁ p₂ c, b p₁ = some c → b p₂ = some c → p₁ = p₂ /-- 每个 Organization 的 active 默认 Agent role(`PINNED`, ADR-0017)。函数形状钉死每个 Organization 恰好一个默认值;role 是否 active 及租户归属由 `WellScoped` 约束。 -/ def OrganizationDefaultRole := I.OrganizationId → I.AgentRoleId /-- 默认 role 必须属于作为其 key 的同一个 Organization(`PINNED`, ADR-0017)。 -/ def OrganizationDefaultRole.WellScoped (defaults : OrganizationDefaultRole I) (roleOrg : I.AgentRoleId → Option I.OrganizationId) : Prop := ∀ o, roleOrg (defaults o) = some o /-- Active 项目群选择的 Agent role(`PINNED`, ADR-0017)。role 属于项目 Organization; 具体外键表示是 plumbing,由 `WellScoped` 钉死租户边界。 -/ structure GroupSelectedRole where /-- 被选择 role 的项目。 -/ project : I.ProjectId /-- 作用于该项目 active 群的动态 Agent role。 -/ role : I.AgentRoleId /-- 群所选 role 必须与 project 同 Organization(`PINNED`, ADR-0017)。 -/ def GroupSelectedRole.WellScoped (selection : GroupSelectedRole I) (projectOrg : I.ProjectId → Option I.OrganizationId) (roleOrg : I.AgentRoleId → Option I.OrganizationId) : Prop := ∃ o, projectOrg selection.project = some o ∧ roleOrg selection.role = some o /-- 已接收群工作冻结其 role(`PINNED`, ADR-0017):调度时使用 admission 中记录的 role, 而不是重新读取可能已经变化的群当前 role。 -/ structure GroupRunRoleSnapshot where /-- 被接收的一次 run。 -/ run : I.RunId /-- 接收时冻结的 role。 -/ role : I.AgentRoleId end Spec.System