/// engine/engine_scope_perm.mbt —— 文件作用域权限门禁(军衔体制的第一层落地)
///
/// 背景:2026-10-21 40 个副机分支合并时发现,副机从老基线开分支、
/// 没 rebase、改了主机正在改的核心模块(store_sqlite.mbt / core_task.mbt / server.mbt),
/// 导致全仓 100% 冲突,自动解冲突还误丢了副机的 idiom 清理。
///
/// 根因:FIST-Mbt 有角色(HumanSteward/Leader/Executor)、军衔(L0-L7)、
/// 预订(reserve_scope)、锁(lock_acquire)——但**没有**目录→角色的权限映射,
/// 也**没有**在预订/锁获取时强制检查这个映射。
///
/// 本模块补上这个缺口:
///   1. 静态 scope→role 映射表(项目级"谁能改哪个目录"的宪法)
///   2. 权限检查函数 check_scope_permission(scope, agent_role)
///   3. 被 engine_rsv.rsv_try_set / locks_try_acquire 调用,
///      在"先到先得"之前,先过"你有没有资格改这个目录"的硬门
///
/// 这不是防并发冲突的唯一门禁——并发冲突靠预订/锁;这是**谁配不配改**的门禁。
/// 就像军队里炊事员不能进指挥中心,不是因为里面有人,是因为他没资格。

///|
/// 权限规则:pattern 是 glob 前缀("src/core/" 匹配该目录所有文件)
struct PermissionRule {
  pattern : String
  allowed_roles : Array[String]
  reason : String
}

///|
/// 项目级"谁能改哪个目录"的宪法——所有角色向上开放,向下封锁。
/// Executor(士兵)只能改测试/守卫门脚本/文档/流水账;
/// Leader(军官)再加 src/router/engine/display + cli/moon.pkg;
/// HumanSteward(统帅)独占 store_sqlite.mbt、server.mbt、moon.mod。
pub let scope_permissions : Array[PermissionRule] = [
  // —— 统帅独占 ——
  PermissionRule::{
    pattern: "src/store/store_sqlite.mbt",
    allowed_roles: ["HumanSteward"],
    reason: "store_sqlite 是数据库层唯一真源,迁移逻辑不可逆",
  },
  PermissionRule::{
    pattern: "src/store/store.mbt",
    allowed_roles: ["HumanSteward"],
    reason: "Store trait = 整个存储层的抽象",
  },
  PermissionRule::{
    pattern: "src/server/server.mbt",
    allowed_roles: ["HumanSteward"],
    reason: "所有 MCP 工具注册 + 核心路由 = 对外接口契约",
  },
  PermissionRule::{
    pattern: "moon.mod",
    allowed_roles: ["HumanSteward"],
    reason: "模块版本 = 全仓编译的地基",
  },

  // —— 军官可改(统帅 + Leader)——
  PermissionRule::{
    pattern: "src/core/",
    allowed_roles: ["HumanSteward", "Leader"],
    reason: "Task/Store/DAG 核心数据结构签名变更,改了全仓编译炸",
  },
  PermissionRule::{
    pattern: "src/engine/",
    allowed_roles: ["HumanSteward", "Leader"],
    reason: "引擎层 = 工具调用的骨架",
  },
  PermissionRule::{
    pattern: "src/router/",
    allowed_roles: ["HumanSteward", "Leader"],
    reason: "模型路由/Star 门 = 成本 + 安全",
  },
  PermissionRule::{
    pattern: "src/display/",
    allowed_roles: ["HumanSteward", "Leader"],
    reason: "军衔/兵种/参谋 = 展示层唯一真源",
  },
  PermissionRule::{
    pattern: "cmd/",
    allowed_roles: ["HumanSteward", "Leader"],
    reason: "CLI 入口 = 用户启动的第一格",
  },
  PermissionRule::{
    pattern: "plugins/",
    allowed_roles: ["HumanSteward", "Leader"],
    reason: "插件态 = gen_plugins 投影,副机改了也会被覆盖",
  },
  PermissionRule::{
    pattern: "moon.pkg",
    allowed_roles: ["HumanSteward", "Leader"],
    reason: "子包依赖声明 = 编译拓扑",
  },

  // —— 全角色可改(统帅 + 军官 + 士兵)——
  PermissionRule::{
    pattern: "scripts/",
    allowed_roles: ["HumanSteward", "Leader", "Executor"],
    reason: "判据脚本 = CI 流程肌肉层,副机修复守卫门 bug 的主战场",
  },
  PermissionRule::{
    pattern: "docs/",
    allowed_roles: ["HumanSteward", "Leader", "Executor"],
    reason: "文档 = 全员可以写",
  },
  PermissionRule::{
    pattern: "memory/",
    allowed_roles: ["HumanSteward", "Leader", "Executor"],
    reason: "流水账 = 全员可以记",
  },
  PermissionRule::{
    pattern: "templates/",
    allowed_roles: ["HumanSteward", "Leader", "Executor"],
    reason: "模式模板 = 任何角色可以扩展",
  },
  PermissionRule::{
    pattern: "patches/",
    allowed_roles: ["HumanSteward", "Leader", "Executor"],
    reason: "依赖侧补丁 = 副机修依赖 bug 的主战场",
  },
]

///|
/// 权限判定三态(BUG-413)——**三种拒绝面必须能被调用方区分**:
///   · Allowed        已登记且角色够格 ⇒ 正常预订
///   · RoleDenied     已登记但角色不够(HumanSteward 保护等)⇒ **必须继续拒绝,绝不放行**
///   · Unregistered   根本没有匹配规则 ⇒ advisory 登记,不参与授权裁决
///
/// 为什么必须拆开:旧实现把两种 Err 混成同一个 String,调用方无法分辨
/// 「这条路我不能碰」(有规则、我不够格)与「这条路没人管、你自己负责字面不相交」。
/// 后果是 explore 模式的第 4 道并行门恒不可过——`reports/` 这类未登记路径永远拿不到 granted。
///
/// **`Unregistered` 不是放行授权**:它只表示「预订登记成功」;
/// 真实写权限仍由 `RoleDenied` 那一侧决定(HumanSteward 保护路径照旧拒绝)。
///
/// 为什么 `Allowed` 不带 `PermissionRule` 载荷:`PermissionRule` 是包内私有 struct,
/// 作为 `pub enum` 的 variant 载荷会触发可见性错误。需要规则详情时用 `matching_rule()`。
pub enum ScopeVerdict {
  Allowed
  RoleDenied(String)
  Unregistered(String)
}

///|
/// agent 原始名 → 落库主键指纹(BUG-418)。
///
/// **为什么 engine 里要有这一份**:`server.mbt` 的 `agent_fp` 是**包内私有**
/// (`fn agent_fp(raw)`,无 `pub`),engine 包看不到它;而 `reserve_scope` 收到的
/// `agent` 是调用方自由填的**原始名**(文档示例就是 `exec_1`),
/// 而 `agents.agent_id` 落的是 16 位指纹。
///
/// ⇒ 想查 `agents.role` 必须先自己算指纹。**直接传原始名永远查不到**
/// (那会让「查权威角色」静默失效,等于没修)。实现必须与 `server.mbt` 逐字同构:
/// `@omega.fnv1a64("agent_id:" + raw)`。
///
/// 本条被白盒锁 `rswb_4` 钉住:断言「原始名查不到、指纹查得到」。
pub fn agent_fp_of(raw : String) -> String {
  @omega.fnv1a64("agent_id:" + raw)
}

///|
/// 从 agent 名字推断角色(**BUG-418 收窄 + BUG-421 二次收窄:只做缺省提示,不是授权依据**)。
///
/// ⚠️ BUG-418 前的实现用 `contains("commander")` 做**子串**匹配 ⇒
/// 任何自称 `commander-fake` 的 agent 都能拿到 HumanSteward,进而过 HumanSteward 保护门。
/// **那不是「文档不准」,是可主动利用的越权面。**
///
/// 收窄规则(BUG-418 裁决 (b) + BUG-421 收口,按「整名精确匹配」):
///   · **禁止**「前缀/子串命中即给高角色」—— 这才是攻击面
///   · **只允许**人类指挥官的整名:`human_steward` / `human` ⇒ HumanSteward
///   · **其余一律 Executor**,**包括整名 `leader` / `lead`**(BUG-421):
///     那两条白名单写于 BUG-418 修法期,理由是「关掉会把军官自报这条路关死」——
///     但「军官自报」本身就是「名字即身份」,与 BUG-418 要堵的是同一个东西,
///     只是从「任意子串」收窄成了「一个单词」。实测 `agent=leader` 可订
///     `src/engine/` / `src/core/` / `src/router/` / `cmd/`(军官面),
///     与「只有 human_steward/human 两个精确匹配算特权」的口径不一致。
///
/// ⇒ 现在 `leader` / `lead` / `LEADER` → Executor(与 `commander-fake` 同一档)
/// ⇒ `human_steward` / `human` → HumanSteward(人类指挥官唯一路径,保留)
///
/// ⚠️ **硬约束:角色不可从名字推断 Leader。Leader 只来自 `agents.role`(已登记身份)。**
/// 需要 Leader 面的 lane 必须在 `agents` 表被显式登记成 `role="Leader"`
/// (唯一入口是 server 启动注册;当前**没有**面向 agent 的自助登记工具面,
/// 这是 BUG-421 关闭后的既定代价 —— 见 memory/bugs.md BUG-421「风险」段与
/// reports/2026-10-27-bug421-decision-brief.md Q2 的步数推算)。
///
/// **权威面仍是 `agents.role`**(见 `resolve_agent_role`):本函数只在查不到声明时做兜底,
/// 且回执会带 `role_source="inferred"` 让调用方知道这是猜的。
pub fn infer_role_from_agent(agent_name : String) -> String {
  let a = agent_name.to_lower()
  // 整名精确匹配(大小写已归一)。子串/前缀/军官单词一律不给高角色。
  if a == "human_steward" || a == "human" {
    "HumanSteward"
  } else {
    // ⚠️ 硬约束:此处**不得**再把军官单词(leader / lead)加回整名白名单 —— BUG-421 已删。
    // 判据:`engine_role_resolve_wbtest.mbt::role_resolve_wb3`(纯函数)+
    //       `engine_scope_perm_role_wbtest.mbt::scope_role_wb1/wb7`(端到端)+
    //       `scope_role_wb6` 的函数体扫描(源码级 tripwire)。
    "Executor"
  }
}

///|
/// 角色档位(数值越大权越大)。**未知/拼错的值一律 0(= Executor 档)—— fail-closed。**
///
/// 为什么需要它(BUG-422):`resolve_agent_role` 要把「名字推断」当作**下限**,
/// 就不能再用字符串相等判「谁更高」。档位表是唯一真源,别处不要再手写比较。
pub fn role_rank(role : String) -> Int32 {
  match role {
    "HumanSteward" => 2
    "Leader" => 1
    "Executor" => 0
    _ => 0
  }
}

///|
/// **角色 → 军衔数字**(BUG-432 正解:`role → rank` 取代 `name → rank`)。
///
/// ⚠️ 别把本函数与 `role_rank` 混成一套:`role_rank` 是**角色档位**(数值越大权越大,2/1/0),
/// 本函数是 **claim 门用的军衔数字**(数值越小权越大,口径见 `rank_can_claim` 的
/// `arank == 0 || arank <= tlevel`)。两者方向**相反**,这正是 BUG-432 立案时被绕进去的地方
/// (抬头写「可能拿到更高档」,实测 rank 6 比默认档 5 **更低权**)。
///
/// | role | rank | 军衔 | 依据 |
/// |---|---|---|---|
/// | `HumanSteward` | 0 | 司令/统帅 | 人类指挥官通吃(与整名白名单 `human_steward`/`human` 同源) |
/// | `Leader` | 4 | 团长 | 军官线:**必须比默认执行者档(5)更高**。旧表把 `leader*` 判成 6(连长)⇒ 军官比普通执行者**更受限**,方向反转 |
/// | `Executor` | 5 | 营长 | 现网默认档,**逐字沿用** `estimate_rank` 的 else 分支 ⇒ 零回归 |
/// | 未知 / 空串 / 拼错 | 5 | — | fail-closed 到默认档:未知值**不提权**(也不误伤) |
///
/// **为什么 Executor 不是 7(排长)/8(士兵)**:门是 `arank <= tlevel`,现网 100% 任务 level=6
/// ⇒ rank 7 会让**所有** Executor 连 level=6 都领不了(门直接焊死,全仓 claim 全线 Err)。
/// 5 是现网实际生效的默认档,平移过来即零回归。
///
/// ⚠️ **硬约束:本函数只接受角色,永不接受「名字」**。`has_prefix("leader") ⇒ 6` 已随 BUG-432 删除;
/// claim 侧要拿军衔,必须先经 `judged_role`(登记优先 + 收窄后推断兜底)拿到角色。
/// 判据见 `claim_rank_wbtest.mbt` 的 G5(行为翻转锁)+ 入档回执里的变异对照(判例-29/30:
/// **不许**写「grep 源码有没有 has_prefix」那种会命中注释的自指判据 —— 它是恒绿的)。
pub fn rank_of_role(role : String) -> Int32 {
  match role {
    "HumanSteward" => 0
    "Leader" => 4
    "Executor" => 5
    // 未知值 ⇒ 默认档:不比今天更宽,绝不因未知/空值提权
    _ => 5
  }
}

///|
/// **哪些角色允许从「自报名字 → agents 登记表」这条路继承**(BUG-422 加固)。
///
///   · `Executor` / `Leader` ⇒ 可继承(Leader 是「派下来的军官」的唯一正道)
///   · `HumanSteward` 与任何未知值 ⇒ **None,永不继承**
///
/// 为什么把 HumanSteward 挡在继承面之外:HS 是**人类指挥官**的唯一身份,
/// 独占 4 条不可逆路径(`store_sqlite.mbt` / `store.mbt` / `server.mbt` / `moon.mod`)。
/// 「报一个已登记名字 ⇒ 继承它的 role」如果能把 HS 交出去,等于把 BUG-418
/// 刚关掉的统帅面从登记表那一侧又开回来,而且这次连 `human_steward` 这个整名都不用猜。
/// 现网 `agents` 表**没有任何 HS 记录**(`agent_touch_first_seen` 只写 Executor,
/// server 启动只写一条 Leader)⇒ 本限制的现实代价为 0,纯属把未来的口先堵上。
pub fn inheritable_role(declared_role : String) -> String? {
  match declared_role {
    "Executor" => Some("Executor")
    "Leader" => Some("Leader")
    _ => None
  }
}

///|
/// 解析 agent 的**裁决角色**与**来源**(BUG-418 裁决 (a) + BUG-422 B 案收口)。
///
/// **BUG-422 前的语义(已废)**:`agents.role` 无条件优先(`declared` > `inferred`)。
/// 那条规则的致命之处不在「读表」,而在**查表键是调用方自报的名字**:
/// 报 `agent="fist-mbt"` 就继承了 `server.mbt` 启动时写下的 `role="Leader"`(实测 ALLOWED)。
///
/// **现在的语义(三件硬约束,缺一不可)**:
///   1. **`inferred` 是下限** —— 声明**不许降级**:名字已经推出 HumanSteward 时,
///      一条 `role="Executor"` 的登记记录不能把它压回 Executor(BUG-423 的注册面毒化同族)。
///   2. **只有可继承角色生效** —— 见 `inheritable_role`:`Executor`/`Leader` 可继承,
///      `HumanSteward` 与未知值一律**忽略声明**(fail-closed 到推断)。
///   3. **声明必须来自「非本进程自身份」的登记记录** —— 这一条不在本纯函数里做,
///      而在它的**取数侧**:`engine_rsv.mbt` 的 `declared_role_for_judgment` 走
///      `SqliteStore::agents_role_for_judgment`,后者把 `agent_type == "server"` 的记录
///      整条剔除(那是**服务器自己**的身份,任何外部 agent 报它的名字都不该继承它)。
///      ⇒ 本函数保持纯、无 IO、可单测;「谁能被冒用」的政策落在 store 那一层,且有单测钉住。
///
/// 返回 `(role, source)`:
///   · `"declared"` = 裁决角色来自一条**通过上述过滤**的登记记录
///   · `"inferred"` = 没有可用声明(未登记 / 被过滤 / 角色不可继承 / 低于推断)
pub fn resolve_agent_role(
  agent_name : String,
  declared : String?,
) -> (String, String) {
  let inferred = infer_role_from_agent(agent_name)
  match declared {
    Some(r) =>
      match inheritable_role(r) {
        // 下限语义:声明只有**不低于**推断时才生效,永不允许把推断结果降级
        Some(d) =>
          if role_rank(d) >= role_rank(inferred) {
            (d, "declared")
          } else {
            (inferred, "inferred")
          }
        // HumanSteward / 未知值 / 空串:不可继承 ⇒ 退推断
        None => (inferred, "inferred")
      }
    None => (inferred, "inferred")
  }
}

///|
/// 判断 agent(按角色)是否有权限编辑 scope。
/// **不再用单一 `Result[_, String]` 表达三种不同结论** —— 见 `ScopeVerdict`(BUG-413)。
pub fn check_scope_permission(
  scope : String,
  agent_role : String,
) -> ScopeVerdict {
  match find_matching_rule(scope) {
    None =>
      Unregistered(
        "未登记:scope '" +
        scope +
        "' 无匹配权限规则 ⇒ advisory 登记(不参与授权裁决);" +
        "并行时须自行保证与其他 lane 的文件字面不相交。",
      )
    Some(rule) =>
      if rule.allowed_roles.contains(agent_role) {
        Allowed
      } else {
        RoleDenied(
          "拒绝:角色 '" +
          agent_role +
          "' 无权编辑 '" +
          scope +
          "'。允许的角色:" +
          rule.allowed_roles.join(", ") +
          ";规则原因:" +
          rule.reason,
        )
      }
  }
}

///|
/// 取 scope 命中的规则(`Allowed` 之外的调用方若需要规则详情,走这里)。
pub fn matching_rule(scope : String) -> PermissionRule? {
  find_matching_rule(scope)
}

///|
/// 匹配规则——**最长 pattern 优先**(最具体的覆盖最宽泛的,BUG-419)。
///
/// 旧实现是「遍历 `scope_permissions`,第一条匹配即返回」,**结果依赖规则表顺序**:
/// 任何人把一条更宽的前缀规则(如 `src/` 保护整棵源码树)插到前面,
/// `src/store/store_sqlite.mbt`、`src/server/server.mbt` 这类 HumanSteward 独占规则
/// 就会被**静默遮蔽**成「Leader 也能改」——两个方向都无报错、无日志、无测试红。
///
/// 实测(BUG-419):注入 `src/` 到表首后,旧实现让上述两个文件从 HumanSteward 独占
/// 降级为 Leader 可改(与 BUG-418 叠加 = 双重越权口)。
///
/// 现实现按 `pattern.length()` **降序遍历**,只取第一条(= 最长)匹配 ⇒ **对规则表顺序免疫**。
/// 用「只认最长那一条」而不是「收集全部再选」,是为了让语义与注释一致:
/// 最具体的规则**独占**裁决,不做多规则合并(合并会让权限面变成并集,更宽)。
fn find_matching_rule(scope : String) -> PermissionRule? {
  find_matching_rule_in(scope, scope_permissions)
}

///|
/// 同 `find_matching_rule`,但**接受显式规则表** —— 只供白盒锁构造「宽规则排在窄规则前面」
/// 的现场(判例-29:判据必须能测到实现,而不是只测现网那一份恰好正确的表)。
///
/// 生产调用一律只传 `scope_permissions`(见上一函数)⇒ 对现网零回归。
fn find_matching_rule_in(
  scope : String,
  rules : Array[PermissionRule],
) -> PermissionRule? {
  // 按 pattern 长度降序:最长 = 最具体。稳定排序保证同长度时保持原相对顺序。
  let ordered = rules.copy()
  ordered.sort_by(fn(a, b) {
    let la = a.pattern.length()
    let lb = b.pattern.length()
    if la > lb {
      -1
    } else if la < lb {
      1
    } else {
      0
    }
  })
  for rule in ordered {
    if scope_matches_pattern(scope, rule.pattern) {
      return Some(rule)
    }
  }
  None
}

///|
/// glob 前缀匹配:pattern 带 "/" 后缀 → 匹配目录及子目录;
/// pattern 是文件路径 → scope 精确等于或以它开头。
fn scope_matches_pattern(scope : String, pattern : String) -> Bool {
  if pattern.has_suffix("/") {
    let prefix = pattern[0:pattern.length() - 1].to_owned()
    scope == pattern || scope.has_prefix(prefix)
  } else {
    scope == pattern || scope.has_prefix(pattern + "/")
  }
}