# dsh-Kingdom 3.2.0 本地候选说明

3.2.0 只有两类产品变更：**主管与领地的完全绑定**（任命时必须同时指定领地、一个主管只主理一个
领地，席位与主理同一事务原子写入；退任时同时解除主理）与**交付区版面修复**（控制区不再抢宽导致
正文退化成竖排）。没有新增表、没有新增迁移，沿用 schema v4 与既有事件类型。

## 验证状态（本轮实际执行过的）

| 项 | 结果 |
|---|---|
| `npm run typecheck`（`tsc -p tsconfig.json --noEmit`） | 通过，0 错误 |
| 全量 `node --test tests/*.test.ts` | 596 项：**594 通过 / 0 失败 / 2 跳过**（2 项在 Windows 上跳过：1 项是 `process.platform === 'win32'` 的平台门，1 项是宿主无法创建符号链接时的运行时跳过） |
| 新增定向用例 `tests/v320-supervisor-territory-binding.test.ts` | 6/6 通过（AC1–AC5 + 事务嵌套） |
| `tests/owner-window.test.ts` | 28/28 通过（含"窗口解除主理"与"缺字段/范围外拒绝"两个新用例） |
| `scripts/p2-smoke.mjs` | 36/36 通过 |
| `scripts/p3-smoke.mjs` | 19/19 通过 |
| `scripts/hotplug-audit.mjs` | 26/26 通过 |
| 发布门 `pwsh -File scripts/release.ps1 -Version 3.2.0 -DryRun` | **P0–P3 全 PASS**（在提交 `075e410` 后的干净工作树运行；P3 产出 `dsh-kingdom-3.2.0.tgz`，722 669 字节，SHA-256 `6d40dc00d3457b223850f66cfa5a3ed0f2c42588d9e2042037607147ec7e3053`） |
| GitHub Actions（push 到 `main`，ubuntu-latest / Node 24） | 成功（typecheck + 全量测试 + 三个 smoke + `npm pack --dry-run`） |
| 独立审查 | `AUDIT_PASS`（AC1–AC7 全 PASS；AC8 的发布门在提交后补跑通过）。审查只读，未改动候选 |
| npm | `dsh-kingdom@3.2.0` 已发布（`latest`），shasum `0bc97ac894519774a4aaf773bdc343a6aa74a946`；从 registry 取回的 tgz 与上面冻结的 tgz **字节一致** |

**未执行（NOT_RUN）**：真实浏览器人工交互验收（新表单的"解除现任主理"勾选框与四种主题/窄屏下的
实际观感；交付区版面只有"同源 CSS + 同构 DOM"的等效渲染对比）；把本版 tgz 装进本机 profile 后的
DSH `0.1.7-rc.2` 实机加载回归；Linux 之外平台的产品行为；收益与成本测量；Owner 正式验收。
未运行过的项不得当作已通过。

## 1. 主管与领地完全绑定

**问题**：此前"任命主管"与"指定它的领地"是两条独立命令。中途放弃第二步，就会留下一个没有
领地的在任主管：席位显示已任命，GUI 里那块领地却仍显示"没有主管"，而治理操作按
`TERRITORY_SUPERVISOR_MISSING` fail-closed。

**现在的规则**（Owner 口径 2026-09-28 + 1:1 裁定 2026-09-29）：

- **任命主管必须同时给出领地**：`role.bind` 的 `role_type=SUPERVISOR` 需要 `territory_id`；
  缺省即拒绝，不写入任何事实。非主管角色携带 `territory_id` 同样拒绝（不静默忽略）。
- **同一事务原子写入**：席位与「该领地的主理」一起提交或一起回滚。领地侧被拒时，已经插入的
  席位随事务回滚，**不会**留下"有主管、没领地"的中间态。
- **不静默替换在任主理**：目标领地已有**在任 ACTIVE** 主理时，改派被拒绝并提示先解除；
  解除（`supervisor_binding_id: null`）后再指派。指向**已退任席位**的历史悬挂引用属于修复，
  可以直接改派（direct Slash 与 Owner GUI 窗口是同一条判据）。
- **一个主管席位只主理一个领地（1:1）**：席位已隶属其它未删除领地时，指派被拒绝并提示先解除
  原领地的主理。要在领地之间移动一个主管，必须"先解除原领地，再指派到新领地"。
  已被删除（tombstone）的领地不参与治理，其历史指针不锁住席位。
- **退任联动**：`role.unbind` 退任一个主管时，**同一事务**里解除它在领地上的主理关系并写出一条
  明确的解除事实（`TERRITORY_SUPERVISOR_UPDATED`，`unassigned: true`），不留下指向 RETIRED
  席位的悬挂引用。
- **存量未隶属席位**：3.2.0 之前建立的、没有领地的在任主管**不会被自动归属**。席位投影会给出
  它实际主理的领地列表，为空时由界面标注「未隶属领地（待处理）」，由 Owner 决定指派或解除。
  这类遗留席位同样受 1:1 约束（已经主理某领地的席位不会被自动改派）。

命令与界面：

```
/kingdom role.bind {"role_type":"SUPERVISOR","role_name":"主理","territory_id":"<领地 id>","session_id":"<真实 DSH session>"}
/kingdom role.bind {"role_type":"WORKER","role_name":"执行者"}                 # 非主管不带 territory_id
/kingdom territory.supervisor {"territory_id":"...","supervisor_binding_id":null}   # 解除该领地主理
/kingdom role.unbind {"binding_id":"<主管 binding id>","reason":"换届"}        # 同时解除其领地主理
```

Owner GUI 管理窗口的 `role.bind` 表单在角色为主管时出现**必选**领地下拉（内容来自本次授权范围
目录），其它角色隐藏该字段；`kingdom_draft_owner_binding_intent` 的自然语言草案对主管改为
**单步**可执行命令（`role.bind` 自带 `territory_id`），不再是旧的两步。

**领地范围判据**：主管任命的目标领地必须**显式**出现在本次管理窗口的授权范围内
（`catalog.territories` 与提交判定完全一致，不用 `kingdomWide` 放宽）。若要在窗口里给新建的
领地任命主管，请重新激活一个包含该领地的窗口。

**管理窗口现在也能解除主理**：`territory.supervisor` 的 `supervisor_binding_id` 接受**显式 `null`**
（表单上是"解除现任主理（不指派新主管）"勾选框；省略该字段仍是输入错误，不会被当成解除）。
因此 1:1 之下"把主理从领地 A 换到领地 B"可以完全在窗口里完成：先在 A 上解除、再到 B 上指派。
解除同样受未结算执行守卫约束（有在途工作时不允许解除责任），预览会写清"解除后该领地无主理 →
fail-closed 直到指派新的"。

**存量数据的说明**：3.2.0 之前可能留下两类与上面规则不符的存量——① 没有隶属领地的在任主管席位；
② 一个席位同时挂在多个领地（旧能力允许）。本版**不会自动收敛**它们：① 只作可见标注，② 只约束
**新的**指派。两者在退任时都会被一并解除（退任联动遍历该席位名下的所有未删除领地）。

**事务嵌套**：直接 Slash 的 `ownerWrite` 与 Owner GUI 窗口的 `apply` 各自已经开了一层事务，
而这条原子通道还要求自己的边界。`KingdomStore.withImmediateTransaction` 因此改为真正的嵌套：
外层 `BEGIN IMMEDIATE`/`COMMIT`/`ROLLBACK`，内层 `SAVEPOINT`/`RELEASE`/`ROLLBACK TO`。
只做"加入外层"是不够的——内层把拒绝变成返回文案时，它已写下的行会留在外层事务里被提交。
`schema`、表结构与事件类型均未改动。

**边界**：`bindRole()` 仍是低层原语，本身不承载"主管必须有领地"这条策略；策略在全部 Owner 入口
（direct Slash、Owner GUI 窗口、GUI 表单）强制执行，并统一由 `bindSupervisorForTerritory` 完成
原子写入。这样既保证产品层面不存在"有主管、没领地"的创建路径，又不改动只用于构造测试夹具的
低层调用。Agent Tool（`kingdom_bind_role` 等）依旧是 zero-write。

**未结算执行的拦截范围（3.2.0 审计 r1 更正）**：`UNSETTLED_EXECUTION` / `UNSETTLED_LEASE` /
`UNSETTLED_DISPATCH` 守卫只覆盖 **Owner 管理窗口**这条路径（`guardUnsettled`）。direct Slash 与
core 的 `role.bind` / `territory.supervisor` **没有**等价守卫——这是 3.1.0 的既有语义，本版没有
改动它，文档也不得声称"旧主管的未结算执行一定会拦住改派"。需要该守卫时请走 Owner 管理窗口。

## 2. 交付区版面修复

交付条目的控制区里有一条 `flex: 1 0 100%` 的状态文本，而它在 `grid-template-columns:
minmax(0, 1fr) auto` 的右列（`auto` = max-content）里。右轨被撑到最宽，左轨被压成 0 宽，
正文因此退化成每行一两个字、竖排显示。现在改成分行 flex：正文占弹性主列（20rem 起），
控制区按自身内容窄排，放不下时整体换行。这是纯 CSS 改动，不改变任何数据、事件或判断。
交付条目里显示的 `UNKNOWN` 是**数据**（v1.0.0 旧格式的弱接受证据），产品按设计不推断。

## 不适用范围

- 不改 schema，不新增迁移；不改 Owner Control Plane 的授权模型。
- 不改 `resolveTrustedToolSession`，不改 3.1.0 的 peer 分线声明。
- 不处理 3.1.0 审计遗留的文档残留（另行处理）。
