@minonl/dia 0.0.14 → 0.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,61 +1,165 @@
1
- # dia graph Web 视觉设计稿(v0.1)
1
+ # dia graph Web 视觉设计稿(v0.3)
2
2
 
3
- > 静态 mockup:[`graph-web-mockup.html`](graph-web-mockup.html)(直接用浏览器打开,无网络/无依赖)
4
- > 数据契约:`dia graph --json`(schema 1);本稿只做视觉与信息层级,不是实现。
3
+ > **落地状态(2026-11-06)**:M1 已实现 `dia graph --export-html`(真实账本 → 单文件静态 HTML,无网络);
4
+ > 数据模型/契约/安全边界见 [graph-web-m1.md](graph-web-m1.md)。live 服务(M2)未做。
5
5
 
6
- ## 1. 布局
6
+ > 静态 mockup:[`graph-web-mockup.html`](graph-web-mockup.html)(浏览器直接打开,无网络/无依赖)
7
+ > 数据契约:`dia graph --json`(schema 1);时间线所需 start/end 为后续扩展字段。
7
8
 
8
- ```
9
- ┌ header: dia graph · 只读 · 更新时间 ──────────────────────────┐
10
- ├ 工具栏:状态过滤 | 密度 | 提示 │
11
- ├ 主画布(卡片 + SVG 边) ┌ 右侧 320px ────┐
12
- │ sessions | todo | doing | review | paths │ 概览 / 需要人 │
13
- │ │ 图例 / 节点详情 │
14
- └ footer: 数据来源 / 约束 ───────────────────┴────────────────┘
15
- ```
9
+ ## 1. 两个视图
16
10
 
17
- - 主画布按状态分列:session → todo → doing → review → path;边的方向从左到右。
18
- - 右侧固定 320px:概览计数、attention(阻塞/claim/待评审)、图例、节点详情。
19
- - 节点卡片颜色只表达 **kind + status**,不用于装饰。
11
+ 顶部 tab:
20
12
 
21
- ## 2. 信息层级
13
+ - **关系图**:回答“谁依赖谁、先后顺序、优先级”;
14
+ - **时间线**:独立甘特图,回答“什么时候做、做到哪、是否延期”。
22
15
 
23
- | 层 | 内容 | 设计 |
24
- |---|---|---|
25
- | 第一层 | task 标题 + status | 加粗 13px;状态色只出现在左边框和 badge |
26
- | 第二层 | task id / owner | 11px muted;owner 显示短 id(`@a1b2c3d4`) |
27
- | 第三层 | 依赖/触碰/claim | SVG 边 + 右侧详情;不在卡片里堆文字 |
28
- | attention | 阻塞 / claim 冲突 / 待评审 | 右上角固定区,不藏在 hover 里 |
16
+ ### 关系图:按依赖深度分层,不按状态分列
17
+
18
+ - 纵轴 = 优先级,**P0 最高、P1 中间、P2 最下**;每个优先级是一条**通栏泳道色带**,泳道之间用虚线分隔,不再用闭合方框;
19
+ - P0/P1/P2 标签固定在左侧泳道栏、垂直居中;**横向滚动时标签吸住左边缘**(JS 同步 scrollLeft),泳道底色按级别计算(P0 最深,向下递减),可自然扩展 P3/P4;
20
+ - 组内从左到右 = 依赖顺序(按依赖深度排序);
21
+ - **状态不占布局**:用内联图标/文本 + 进度条表达;
22
+ - `简约`:只显示 task + `depends_on`;
23
+ - `详细`:展开 owner、session、path、touches、claims,并在卡片里列具体关联(`depends_on / depended_by / touches / claims / collaborates_with`)。
24
+ - 图例固定在画布右下角:只显示当前模式会用到的边;`→` 单向、`↔` 双向,线型与图上一致。
25
+
26
+ ### 时间线:独立甘特
27
+
28
+ - 每行一个 task,横轴为相对今天的天数(`-8d … today … +4d`);
29
+ - 条长 = start/end,内部填充 = progress;
30
+ - 颜色 = status;橙色描边 = 关键路径;红色虚线 = 今天;
31
+ - 点击条/行可看任务详情。
32
+
33
+ ## 2. 边语义
34
+
35
+ | 关系 | 方向 | 线型 | 颜色 |
36
+ |---|---|---|---|
37
+ | `depends_on` | 单向 `→` | 实线 | 蓝 |
38
+ | `touches` | 单向 `→` | 虚线 `6 4` | 紫 |
39
+ | `claims` | 单向 `→` | 点线 `2 3` | 青 |
40
+ | `collaborates_with` | 双向 `↔` | 点线 `1 3` + 双箭头 | 灰(视觉示例,未进 M1 契约) |
29
41
 
30
- ## 3. 交互
42
+ ## 3. 为什么不再按状态列/行
31
43
 
32
- - 点击节点 → 右侧详情:kind/status/owner + 关联边列表;
33
- - 顶部过滤:all / doing / blocked / review(客户端过滤,不改数据);
34
- - 密度切换:comfortable / compact;
35
- - 刷新:MVP 静态导出;live 版用 `dia graph --json` 轮询(2s),不做 WebSocket;
36
- - 不做:编辑、拖拽改依赖、写 claim(只读)。
44
+ 状态列会把“关系”切碎,且 TODO/DONE、REVIEW/BLOCKED 混在一列没有语义。现在:
37
45
 
38
- ## 4. 与现有数据契约的映射
46
+ - 依赖顺序是主结构(拓扑层);
47
+ - 状态是属性(颜色/进度);
48
+ - 优先级是行分组(P0/P1/P2 各一行,最高在最上,虚线框圈组);
49
+ - 时间放在独立时间线,不挤在关系图里。
39
50
 
40
- | UI | `dia graph --json` |
51
+ ## 4. 与数据契约的映射
52
+
53
+ | UI | 数据 |
41
54
  |---|---|
42
- | 卡片 | `nodes[]`:task/session/path |
55
+ | task 卡片 | `nodes[kind=task]` + status/owner |
43
56
  | 依赖边 | `edges[kind=depends_on]` |
44
- | 触碰边 | `edges[kind=touches]` |
45
- | claim 边 | `edges[kind=claims]` |
46
- | 阻塞提示 | task 节点 `blocked_by[]`(派生,不是边) |
47
- | 环 | `cycles[]`,顶部红条提示 |
57
+ | touches / claims | `edges` 对应 kind |
58
+ | 进度 | 后续 `task.progress`(0-100) |
59
+ | 时间线 | 后续 `task.start/end`(相对/ISO) |
60
+ | 关键路径 | 后续由依赖图最长路径计算 |
48
61
 
49
- ## 5. 实现约束(重要)
62
+ ## 5. 实现约束
50
63
 
51
- 仓库的 `tests/static.test.ts` 禁止框架 import `node:http` / `fetch` / 网络模块。因此:
64
+ `tests/static.test.ts` 禁止框架 import `node:http` / `fetch` / 网络模块。因此:
52
65
 
53
- 1. **本稿只是静态 HTML**,不进入 `src/`,不启服务;
54
- 2. 若将来要做真实 Web:
55
- - 方案 A:`dia graph --export-html` 生成静态文件(推荐,不需要网络);
56
- - 方案 B:独立可选包 `@dia/graph-web`(明确例外 + 只绑 loopback + 单独评审),不改核心框架规则。
66
+ 1. 本稿是静态 HTML,不进入 `src/`;
67
+ 2. 真实实现优先 `dia graph --export-html` 生成静态文件;
68
+ 3. live Web 需独立可选包 + 单独评审(loopback only)。
57
69
 
58
70
  ## 6. 后续
59
71
 
60
- - 先确认视觉稿的列布局/信息密度/右侧 attention 位置;
61
- - 确认后先做 `--export-html` 静态导出(零网络、零 daemon),再评估 live 轮询。
72
+ - 先确认“依赖深度分层 + 简约/详细 + 时间线”结构;
73
+ - 确认后做 `--export-html`(零网络),再评估 live 轮询。
74
+
75
+ ## 7. URL 路由
76
+
77
+ 静态稿支持 hash 路由(`file://` 直接可用):
78
+
79
+ ```
80
+ #/graph
81
+ #/graph?mode=simple
82
+ #/graph?mode=detail
83
+ #/timeline
84
+ ```
85
+
86
+ - `view` 也可用 query 形式:`?view=timeline&mode=detail`;
87
+ - 切换 tab / 简约详细会同步更新 hash,便于分享/回链;
88
+ - 真正的独立 path(`/graph`、`/timeline`)需要部署层 rewrite 到同一 HTML,或导出时生成
89
+ `graph.html` / `timeline.html`;静态文件本身不能凭空提供两个 path。
90
+
91
+ ## 8. 颜色语义(v0.4)
92
+
93
+ 原则:**颜色只表达警告/异常,正常状态去色**。
94
+
95
+ | 颜色 | 用途 | 不再用于 |
96
+ |---|---|---|
97
+ | 红 | blocked、P0、异常/失败 | 普通任务状态 |
98
+ | 橙 | review、claim 提醒、P1 | 完成态 |
99
+ | 蓝 | 只有“进行中”的进度条 | done/accepted/todo |
100
+ | 灰 | todo / done / accepted / P2 / 中性文本 | 警告/异常 |
101
+
102
+ - 优先级:P0 红实心;P1 橙描边;P2 灰描边(不抢眼);
103
+ - 状态文本:只有 blocked / review 带颜色,其余中性;
104
+ - 进度条:doing 蓝色条纹动画;blocked 红色暂停/置灰;review 橙色;done/accepted 灰色静态;
105
+ - 时间线同一套语义;关键路径只用橙色描边。
106
+
107
+ ## 9. 任务抽屉与介入(v0.5)
108
+
109
+ 关系图里的卡片**全部是 task**,不再叫“节点详情”。
110
+
111
+ - 点击 task 卡片 → 右侧抽屉「任务详情」,两个 tab:
112
+ - **任务**:status / priority / owner / progress / deps / 关联;
113
+ - **介入**:通道切换 + 目标会话 + 输入框 + 发送(设计稿只演示)。
114
+ - 「需要人」每行带快速操作:
115
+ - 阻塞 → `介入` / `查看`
116
+ - claim → `联系持有者`
117
+ - 待评审 → `开始评审`
118
+ - 介入通道明确区分:
119
+ 1. **TUI 会话**:选择 opencode / dsh 会话,输入后注入;
120
+ 2. **钉钉消息链**:选择群聊/私聊,输入后走钉钉;
121
+ 3. **dia msg**:选择 session id,走 dia 消息邮箱。
122
+ - 同一任务可以在抽屉里切换通道,方便“会话里没人就发钉钉 / 需要留痕就 dia msg”。
123
+ - 深链:`#/graph?task=t-4&tab=intervene` 可直接打开任务抽屉。
124
+
125
+ ## 10. 规模样例(100 tasks / 30 天)
126
+
127
+ mockup 数据改为 **100 个任务、时间跨度 30 天**,用来验证真实规模下的布局:
128
+
129
+ - 关系图有两种布局:**优先级视图**(P0/P1/P2/P3 泳道)和**依赖视图**(按依赖深度从左到右);
130
+ 两种视图**都不折叠任务**(没有 `+N 个` 聚合,任何卡片都能点开),靠时间窗 + 搜索 + 聚焦控制规模;
131
+ - mock 数据补齐跨优先级依赖,确保两种视图都能看到跨层关系;依赖边只连接当前可见任务;
132
+ - 卡片垂直基准分两种模式:**详细** = 进度条中心线(关系虚线/箭头也锚在进度条中心,同一行连线与进度条同高);
133
+ **简约** = 卡片盒整体垂直居中(连线锚在卡片盒中心);两种模式都和泳道中心线/左侧级别标签对齐;
134
+ - 卡片偏移量按实际渲染测量(含字体加载后重排),不写死魔法数;泳道顶部/右侧空白按实际内容收紧。
135
+ - 时间线:按当前时间窗过滤后纵向滚动;轴显示绝对日期(`MM-DD`)并保留相对天数
136
+ (`-7d / 今天 / +Nd`)。
137
+ - 泳道标签在**外层固定 rail**,不参与横向滚动;只随纵向 `scrollTop` 同步位置;
138
+ - 泳道标签为全宽色带 + 虚线分隔,P0/P1/P2/P3 由数据动态生成。
139
+ - 依赖视图按实测卡片高度逐列堆叠,`详细` 模式下卡片不会互相重叠;依赖视图没有泳道,左侧不留级别标签遮罩/占位;
140
+ - 关系视图参数:`#/graph?layout=dep`(依赖视图)/ 默认优先级视图;
141
+ - 时间窗参数(两个视图共用):`#/graph?w=7|14|30|0`(0 = 全部);时间线 `#/timeline?w=7`;
142
+ 显式区间:`#/timeline?from=2026-10-22&to=2026-11-21`。
143
+
144
+ ## 10.1 规模控制:时间窗 + 搜索 + 聚焦
145
+
146
+ 无折叠后,用三种手段控制一次看多少:
147
+
148
+ - **时间窗(默认近 7 天,按开始时间,结束时间不限)**:只显示 `start >= 今天 - N` 的任务,
149
+ 长任务即使结束在很久以后也照常显示;时间线轴自动从 `-N` 延伸到最晚的 `end`;
150
+ - **搜索即选择(token 输入框)**:聚焦就出下拉(列出当前时间窗全部任务,按优先级/开始时间排序),
151
+ 输入 ID / 名称 / owner 实时过滤;选中项**回显成 tag**(`t-003 ×`)留在输入框里,可多选;
152
+ `↑↓` 选择、`Enter` 加入、`Esc` 收起、`Backspace` 删除最后一个 tag、点 `×` 删除单个;
153
+ 已选 tag 用蓝底高亮,下拉里带 `✓`;选择状态写进 URL(`sel=t-003,t-006`,向后兼容旧的 `focus=`);
154
+ - **邻域聚焦(类 nx graph focus)**:选中任务后只保留这些任务的依赖邻域(多个取并集),
155
+ 深度可切 `1 层 / 2 层 / 全链路`,`清空` 回全量;双击卡片等同于点选该任务;
156
+ - 被过滤掉的任务不计入概览统计,标题显示 `显示 N / 总数`。
157
+
158
+ ## 11. 时间线滚动与缩放
159
+
160
+ - 左侧任务列是**独立固定 pane**(不依赖 sticky),横向滚动时始终可见;任务名顶部对齐;
161
+ - 时间区右上角显示「← 左右滚动 →」提示,避免看不出可滚动;
162
+ - 右侧时间轴在 `tl-scroll` 内横向滚动,表头与内容同步;
163
+ - `zoom` = 每天像素宽度(默认 28,范围 **14–84**,即 50%–300%),控制条 `- / 100% / +`,到边界禁用;
164
+ - URL:`#/timeline?w=7&zoom=40`;
165
+ - 任务纵向滚动,时间窗默认近 7 天开始(结束不限),绝对日期 + 相对天数同时显示。
@@ -0,0 +1,138 @@
1
+ # 固定席位 + 上下文亲和(M2 设计)
2
+
3
+ > 状态:设计定稿(2026-11-06,未实现)|依赖:`graph-prio-dag.md`(eff/rank)、`graph-ui-m1.md`(面板)
4
+ > 红线:**只读建议;不写 tasks 账本、不常驻守护、不抢占、不自动 force 接管**。
5
+
6
+ ## 1. 目标 / 非目标
7
+
8
+ **目标**:席位(并发会话)数量固定时,回答「每个席位现在接哪件」——同时考虑价值(rank)、约束(依赖/claim)与**断链成本**(上下文)。
9
+
10
+ **非目标**:自动领取/自动开工;跨机席位发现;任务级分布式锁;把 rank 变成门禁。
11
+
12
+ ## 2. 名词与事实源
13
+
14
+ | 名词 | 定义 | 事实源 |
15
+ |---|---|---|
16
+ | seat | 一个在线会话(opencode/dsh 进程) | `~/.dia/collab/sessions/*.json` 的 `last_seen` |
17
+ | presence | `now - last_seen <= TTL`(默认 30m,`--presence-ttl`) | 同上 |
18
+ | claim | **路径**租约(谁在改哪个文件) | `~/.dia/collab/claims.jsonl`(TTL 默认 4h) |
19
+ | owner | 任务声明的负责人(不发租约) | `tasks/ledger.jsonl` 的 add/plan |
20
+ | busy | 席位有 `doing` 任务 | 任务 status |
21
+ | 亲和 | 任务与该席位近期工作面的重叠度 | 任务 touches + 席位最近任务 |
22
+
23
+ **分工写死**:`rank` 只回答「先做什么」;`claim` 只回答「谁能改哪条路径」;`owner` 只回答「谁负责」,三者不得互相替代。
24
+
25
+ ## 3. 席位状态机(全派生,零写入)
26
+
27
+ ```
28
+ online & 无 doing online & doing(todo/doing)
29
+ ┌──────── idle ◄────────┐ ┌──────── busy
30
+ │ │ │
31
+ │ (接新任务即 busy) └────────┤
32
+ └──────── holding ◄──────┘ doing 但 status∈{review,blocked}(释放执行位,保留上下文)
33
+ │
34
+ last_seen 超 TTL
35
+ ▼
36
+ offline(不再分配;其 doing 任务标 stale,需人确认)
37
+ ```
38
+
39
+ - `holding` 仍可接新任务(执行位空闲),但提示「上下文占用 N 个」;N>3 建议先 handoff;
40
+ - `offline` 的 `doing` 任务**回候选集**但标 `reason:'seat_offline'`,不自动改 status/owner;
41
+ - 无自动回收路径:busy→idle 只在任务状态变化时派生,进程退出即 offline(靠 TTL)。
42
+
43
+ ## 4. 亲和度(affinity)
44
+
45
+ ```
46
+ aff(t, seat) = 3 * ownerMatch # seat.session == t.owner
47
+ + 2 * splitGroup # 与席位最近任务共享 split_from 或同一父
48
+ + 2 * jaccard # touches 的 Jaccard,[0,1],0/0 = 0
49
+ + 1 * moduleOverlap # 路径前 2 段前缀相同
50
+ cost(t, seat) = 1 - aff/8 # ∈ [0,1],越小越省
51
+ ```
52
+
53
+ - `seatTouches` = 该席位最近 H=5 个任务(doing/review/done)的 touches 并集;
54
+ - 分量量纲固定:owner/split/module 为 0/1,jaccard 归一化到 [0,1];`aff` 最大 8;
55
+ - **算例**(必须进单测):
56
+
57
+ | 场景 | aff | cost | 结论 |
58
+ |---|---|---|---|
59
+ | 同 owner(3) | 3 | 0.625 | 可接,但不算强亲和 |
60
+ | 同 split 组(2)+ jaccard 0.5(1) | 3 | 0.625 | 同上 |
61
+ | 同 owner + 同模块(4) | 4 | 0.5 | 达到「续做」阈值 |
62
+ | 完全同 touches(jaccard=1,2) | 2 | 0.75 | 单靠路径重叠不续做 |
63
+ | owner+split+jaccard1+module(8) | 8 | 0 | 强续做 |
64
+
65
+ > 阈值:**续做** `cost ≤ 0.5`;**换人** 当 `cost > 0.5` 且存在另一席位 `cost 低 ≥ 0.25`。单靠「路径重叠」不足以免换人——避免把不相关任务硬塞给同一席位。
66
+
67
+ ## 5. 分配算法(rank 优先,亲和定席位)
68
+
69
+ ```
70
+ 候选 = startable ∧ rank≠null ∧ 无「路径 claim 冲突」
71
+ 对候选按 rank 升序:
72
+ 在空闲/持有席位中,选 (cost, seat.id) 最小者
73
+ 若该席位 busy → 跳过(不抢占)
74
+ 输出 assignments;未被分配的进入 unassigned,带原因
75
+ ```
76
+
77
+ - 复杂度 `O(N·S)`(N 候选、S 席位),500×3 < 1ms;
78
+ - 确定性 tiebreak:`(cost, declared prio index, task.id, seat.id)`;
79
+ - **不做**「全局最优分配」:席位小、账本变动频繁,贪心 + 可解释优先;
80
+ - 显式反例(写入文档,防止误读):rank 1 但亲和极差的任务仍会被先分配——这是「价值优先」的代价;若需要「上下文优先」模式,M3 再加 `--by=affinity`。
81
+
82
+ ## 6. CLI 契约
83
+
84
+ ```bash
85
+ dia graph --next [n] --seats 2 --presence-ttl 30m --now 2026-11-06T10:00:00 --json
86
+ ```
87
+
88
+ ```json
89
+ {
90
+ "schema": 1,
91
+ "policy": "seats-v1",
92
+ "seats": [{"seat":"ses_a","state":"idle","presence_at":"2026-11-06T09:58:12Z","focus":null}],
93
+ "assignments": [{"seat":"ses_a","task":"t-3c1f","rank":1,"eff":"P0","cost":0.25,"affinity":{"owner":1,"split":0,"jaccard":1,"module":0},"why":"同 owner + 路径重叠 1.0"}],
94
+ "unassigned": [{"task":"t-9","rank":4,"reason":"no_seat|seat_offline|claim_conflict"}],
95
+ "conflicts": [{"task":"t-9","kind":"claim_overlap","detail":"src/x.ts 被 ses_b claim"}],
96
+ "metrics": {"switches": 1, "handoff_rate": 0.0, "idle_slots": 0}
97
+ }
98
+ ```
99
+
100
+ - **不含墙钟**:只出 `presence_at`(ISO);`--now` 仅用于计算 state/交接判断,不落输出;
101
+ - 同一 `--now` + 同一账本 → 字节相等;打乱任务顺序结果不变;
102
+ - 退出码:0 正常(含无任务)、2 参数错、3 依赖环(排序不可用)。
103
+
104
+ ## 7. 链式续做与 handoff
105
+
106
+ 默认**续做**(cost ≤ 0.5 且席位非 busy)。**必须打断**的四条:
107
+ 1. 出现更高 `rank` 且其它席位都接不住的任务(价值让位);
108
+ 2. 下一环 touches 与席位工作面**无重叠**(模块切换,cost > 0.5);
109
+ 3. 当前任务进入 review/blocked(执行位空出,可换手);
110
+ 4. 上下文将满(启发式:本会话完成任务数 ≥K 或已有 1 次 handoff 建议)。
111
+
112
+ handoff 内容固定五段:目标状态 / 已完成 / 未完成 / 触碰文件 / 下一步命令;通道用 `dia msg send --kind handoff`,交接后**人工重新 claim**,不自动转移。
113
+
114
+ ## 8. 指标(只读派生)
115
+
116
+ - `switches`:同一任务换席位的次数;
117
+ - `handoff_rate`:有 handoff 消息的任务 / 被接手任务;
118
+ - `idle_slots`:在线但无任务的席位数;
119
+ - 采样窗口由调用方传 `--since`,不落账本;用于评估「固定席位是否够用」。
120
+
121
+ ## 9. 边界
122
+
123
+ - **claim 与 presence 冲突**:claim 4h 有效但会话 30m 离线 → 标 `conflicts: claim_held_offline`,任务进 unassigned 且**不自动 force**(人工 `dia claim --force` 或等 TTL);
124
+ - 席位掉线:`offline`,其任务回候选并标 stale;
125
+ - S > 任务数:多余席位 idle,不为填满降低门槛;
126
+ - S=0:纯建议模式,全量 unassigned,exit 0;
127
+ - 跨机:只读本机 `~/.dia`,其它机器会话视为离线;
128
+ - 空 touches:jaccard=0,不除零;
129
+ - 环内/终态任务不进候选。
130
+
131
+ ## 10. 验收
132
+
133
+ - 打乱任务顺序、固定 `--now` 两次输出字节一致;
134
+ - busy 席位不被二次分配;holding 可接新任务;
135
+ - affinity 五个算例硬断言(含 0/0);
136
+ - claim 冲突进 conflicts 且不抢占;
137
+ - `--seats 1` 与 `graph --next` 单项一致(同一排序源);
138
+ - e2e:两会话(`DIA_SESSION_ID`)跑 `whoami` + doing → 各得一任务;杀一个会话超 TTL 后任务回落。
package/docs/index.md CHANGED
@@ -17,7 +17,9 @@ Agent 入口:[`llms.txt`](https://github.com/minonl/dia/blob/main/llms.txt)(
17
17
  | [用户入口:人在哪进,agent 叫什么](entrypoints.md) | 一句话:入口仍是 opencode/dsh(和 yan 一样),但 agent 名是 dabo,不是 dia;dia 是控制面命令,不是人格。 |
18
18
  | [进化:dia 的第一性定位](evolution-process.md) | 本文不是功能清单,是过程定义。 |
19
19
  | [宿主控制面:黑盒边界与内部 seam(2026-10)](host-control-surface.md) | 问题:把 opencode(含 v2)与 dsh 当"黑盒宿主"时,我们能在哪些边界上控制它们? |
20
+ | [安装防呆:脚本安装不许抢走 npm 全局安装](install-guard-design.md) | 日期:2026-11-06 | 状态:已实现(D0.65) | 触发:同一台 Linux 机器 3 次出现 ~/.local/bin/dia 被指回脚本安装 |
20
21
  | [Kit:内容包安装(persona 的第二个半边)](kit-install.md) | 前置:docs/persona-install.md(persona = 人格,kit = 内容)。 |
22
+ | [Kit 层:把「经验」从 persona 里抽出来(设计 v0.1)](kit-layer.md) | 2026-11-06 用户定调:dws-hub 作为 channel 层重构,同时把 git / node 这类通用工程经验抽成 kit 层, |
21
23
  | [模型基准与 Sidecar 监控(A+B 一起)](model-baseline-sidecar.md) | A:首次使用先跑基准,用数据决定配置。 |
22
24
  | [模型行为调控(客户端侧)](model-behavior.md) | 边界(用户定):只使用各平台提供的通用模型;不做本地构建/微调;不依赖多数模型不支持的推理与输入输出控制参数; |
23
25
  | [dia monorepo:框架模块的归属](monorepo.md) | 结论:dia-job 回 dia monorepo,作为 packages/job;kb 作为 packages/kb。 |
@@ -92,5 +94,10 @@ Agent 入口:[`llms.txt`](https://github.com/minonl/dia/blob/main/llms.txt)(
92
94
 
93
95
  | 文档 | 一句话 |
94
96
  |---|---|
95
- | [dia graph Web 视觉设计稿(v0.1)](design/graph-web.md) | 静态 mockup:graph-web-mockup.html(直接用浏览器打开,无网络/无依赖) |
97
+ | [优先级 × 依赖:任务编排语义(M1)](design/graph-prio-dag.md) | 状态:已实现(2026-11-06)|代码:src/plan.ts(派生纯函数)、src/task.ts(split)、src/graph.ts(契约) |
98
+ | [关系图界面:派生量上屏(M1 UI)](design/graph-ui-m1.md) | 状态:设计定稿(2026-11-06)|实现:src/graph-html/(待接)|上游:graph-prio-dag.md |
99
+ | [dia graph Web(M1)设计:真实数据 → 单文件静态导出](design/graph-web-m1.md) | 状态:M1 设计(2026-11-06)|前置:graph-web.md(视觉稿 v0.3)、graph-web-mockup.html(交互原型) |
100
+ | [graph live 服务(M2 设计,可选包)](design/graph-web-m2.md) | 状态:设计定稿(2026-11-06,未实现)|前置:graph-web-m1.md(静态导出)、graph-ui-m1.md、seat-scheduling-m2.md |
101
+ | [dia graph Web 视觉设计稿(v0.3)](design/graph-web.md) | 落地状态(2026-11-06):M1 已实现 dia graph --export-html(真实账本 → 单文件静态 HTML,无网络); |
102
+ | [固定席位 + 上下文亲和(M2 设计)](design/seat-scheduling-m2.md) | 状态:设计定稿(2026-11-06,未实现)|依赖:graph-prio-dag.md(eff/rank)、graph-ui-m1.md(面板) |
96
103
 
@@ -0,0 +1,148 @@
1
+ # 安装防呆:脚本安装不许抢走 npm 全局安装
2
+
3
+ > 日期:2026-11-06 | 状态:**已实现(D0.65)** | 触发:同一台 Linux 机器 3 次出现 `~/.local/bin/dia` 被指回脚本安装
4
+ > 上游:三名独立子会话的 grill(架构/契约、红队、兼容迁移),结论一致 | 判据:`bash tests/install-guard.test.sh`
5
+
6
+ ---
7
+
8
+ ## 1. 现象与根因
9
+
10
+ **现象**:`~/.local/bin/dia` 从 npm 包 bin 被改成 `~/.local/share/dia/bin/dia.js`,随后:
11
+
12
+ - `paths.installDir()` 按 `import.meta.url` 反推安装根 → 落在脚本安装目录
13
+ - 内置能力包 `kits/dia-msg` 去旧目录找 → `missing-kit` / `E_KIT_EMPTY`
14
+ - `dia doctor` 红、`dia update` 走脚本模式(自己 `git pull && bun run build && bash install.sh`,再抢一次)
15
+
16
+ **根因**:`install.sh` 第 58 行 **无条件** `ln -sfn "$DIA_INSTALL_DIR/bin/dia.js" "$DIA_BIN_DIR/dia"`。
17
+ 两种安装共存本身不是错误(npm 可以装在别的 prefix),错误是**后来者覆盖了正在生效的那条链接**。
18
+
19
+ ```mermaid
20
+ graph LR
21
+ A[install.sh 运行] --> B{目标 bin 现在指向谁}
22
+ B -->|npm 包 bin| C[拒绝 exit 3<br/>不动任何文件]
23
+ B -->|脚本安装 / 不存在| D[正常安装]
24
+ D --> E[打印实际生效的 dia + version]
25
+ E --> F{生效路径 == 目标?}
26
+ F -->|否| G[warn:不在 PATH 或 PATH 顺序靠后]
27
+ ```
28
+
29
+ ## 2. 被否掉的初版方案
30
+
31
+ 初版设想是「检测到 npm 全局包存在就拒绝」。grill 三方一致否掉,理由:
32
+
33
+ | 问题 | 证据 |
34
+ |---|---|
35
+ | 两种安装可以合法共存 | 本机 `npm prefix -g` 有 `@minonl/dia`,同时 `~/.local/bin/dia` 是脚本安装,两者并存且脚本版更新 |
36
+ | npm 包可能是**旧残留** | npm 0.0.13 + 脚本 0.0.15 时,拒绝安装等于**永久锁死升级** |
37
+ | 多 Node 前缀 | fnm/nvm 切版本后 `npm ls -g` 结果就变,检测不稳定 |
38
+ | 依赖 npm 命令 | 无 npm 的容器/离线场景会误伤;且 `npm ls` 有 0.5s 启动成本 |
39
+
40
+ ## 3. 决定(三条)
41
+
42
+ **D1 判据 = 抢链接,不是「包存在」**
43
+
44
+ ```
45
+ pkgOwned(p):三种形态任一命中即算包管理器全局安装
46
+ A 软链/拷贝:逐级解析符号链接后,真实路径匹配 (/|\\)node_modules(/|\\)@minonl(/|\\)dia
47
+ B shim 脚本:目标文件不是软链,内容含 @minonl/dia 或 node_modules/(@minonl/)?dia/
48
+ C 二进制 shim:所在目录是已知包管理器全局 bin(~/.volta/bin、PNPM_HOME 等)
49
+ ```
50
+
51
+ 只在这些情况拒绝(且该路径正是 `command -v dia` 会命中的那条):
52
+
53
+ 1. `$DIA_BIN_DIR/dia` 的 realpath 落在 `node_modules/@minonl/dia`(npm / bun 全局安装);
54
+ 2. bin 是 **shim 脚本/拷贝**且内容带包路径(pnpm / yarn-berry);
55
+ 3. bin 所在目录属于已知包管理器全局 bin(volta 这类不含包路径的同名 shim)。
56
+
57
+ 包存在但 bin 未被占 → **只 warn 一行,继续安装**。
58
+
59
+ > 红队回灌(2026-11-06,实现后):`--home` 独占**不算**沙箱(链接仍写默认 bin,守卫照旧生效);
60
+ > 链接落位失败必须退 1(不再谎报装好);`--migrate` 改为**只搬数据**(不装、不改 bin)。
61
+
62
+ > 为什么要有 B/C:pnpm / yarn-berry / volta 的全局 bin 是 **shim 脚本或同名二进制**,不是指向
63
+ > `node_modules/@minonl/dia` 的软链。只认软链时守卫会把这些安装判成「脚本安装」并静默覆盖,
64
+ > 事故形态与 npm 完全一致(本仓库 packageManager 就是 pnpm)。
65
+
66
+ **D2 退出码 3 + 显式逃生舱**
67
+
68
+ | 码 | 含义 |
69
+ |---|---|
70
+ | 0 | 装好(含「已警告但仍安装」) |
71
+ | 1 | 前置缺失(缺 `dist/cli.js` / `bin/dia.js`) |
72
+ | 2 | usage |
73
+ | **3** | **守卫拒绝:未改动任何文件**(可安全重试,重试必然同样结果) |
74
+
75
+ 放行条件(任一命中即跳过守卫):
76
+
77
+ - `DIA_ALLOW_SCRIPT_INSTALL=1`(人类显式意图)
78
+ - `--dry-run`(只演练)
79
+ - **生效 bin 目录被重定向**:`--bin DIR` / `DIA_BIN_DIR=DIR`,且不等于默认 `~/.local/bin`
80
+
81
+ > **只改安装目录不算沙箱**(`--home` / `--install-dir` / `DIA_INSTALL_DIR` / `DIA_HOME_INSTALL`):
82
+ > 链接仍写在默认 `~/.local/bin/dia`,照样会抢走生效安装,守卫必须照旧生效(exit 3)。
83
+ > 想要「不碰生效路径」的沙箱,必须同时指定 `--bin`。
84
+
85
+ **D3 `dia update` 不硬失败**
86
+
87
+ 脚本模式下**保持现状**(打印升级指引、exit 0),不升级为错误:否则「脚本安装的机器」会彻底无法升级,且 CI 里 `update` 测试会红。只加一条:若检测到 npm 包与当前安装不一致,多打印一行 `dia doctor` 提示。
88
+
89
+ ## 4. 配套
90
+
91
+ **D4 安装后自检**:`install.sh` 结束时打印
92
+
93
+ ```
94
+ dia installed: <install dir> → <bin dir>/dia
95
+ effective: <realpath of $(command -v dia)> (<version>)
96
+ ```
97
+
98
+ 生效路径 != 目标时 warn(这正是「npm 装到不在 PATH 的 prefix」的可见化)。
99
+
100
+ **D5 `dia doctor` 新增 `install_shadow`(fail)**
101
+
102
+ 判定:`installDir()` 不是包管理器安装 + `command -v dia` 解析到包管理器安装(或反过来)→ 报不一致,附修复命令。
103
+ 包管理器安装的 bin 是 shim(pnpm/yarn)时按脚本内容识别,不只看 realpath:
104
+
105
+ ```bash
106
+ ln -sfn "$(npm prefix -g)/bin/dia" ~/.local/bin/dia
107
+ ```
108
+
109
+ **D6 原子写**(低成本顺带):`cp` 落 `<file>.tmp` 再 `mv -f`;`ln -sfn` 改「先建临时链接再 rename」,避免并发会话看到半截文件/短暂无命令。
110
+
111
+ ## 5. 失败模式(红队保留项)
112
+
113
+ | 场景 | 处理 |
114
+ |---|---|
115
+ | npm 包在别的 Node 前缀 | 不检测 npm 列表,只判真实路径 → 天然不受影响 |
116
+ | pnpm/yarn/bun/volta 全局装 | 软链看 realpath(`node_modules/@minonl/dia`);shim 看内容(`@minonl/dia`);volta 二进制 shim 看全局 bin 目录归属 |
117
+ | 只改安装目录当沙箱 | 不算沙箱:链接仍写默认 bin,守卫照旧 exit 3;沙箱必须重定向 `--bin`/`DIA_BIN_DIR` |
118
+ | `readlink -f` 在 macOS 不存在 | 用 `cd "$(dirname x)" && pwd -P` 逐级解析,两边同构 |
119
+ | 本机测试被守卫误拦 | 测试全部用临时 `DIA_INSTALL_DIR` + 临时 `DIA_BIN_DIR`(= 沙箱放行),不探测真机环境;只给安装目录的用例应得 rc=3(`tests/install-guard-strict.test.sh` ①) |
120
+ | 上层脚本把 exit 3 当致命 | 文案给出三条出路(显式放行 / 回 npm / 换 `--bin` 沙箱);`docs/` 里所有 `bash install.sh` 用法补注「默认路径已被包管理器占用时会拒绝」 |
121
+ | 老版本 dia 没有防呆 | `doctor install_shadow` 负责发现,修复命令一条 |
122
+
123
+ ## 6. 判据(可执行)
124
+
125
+ ```bash
126
+ # 1) 目标 bin 被包管理器占用(**默认路径**)→ 拒绝,且链接字节不变
127
+ # 自动化等价物:tests/install-guard.test.sh ①(临时 HOME + 临时 PATH + 伪 npm 包)
128
+ # 1b) shim 形态(pnpm/yarn/volta)→ 同样拒绝;只改安装目录(--home 等)→ 同样拒绝
129
+ # 自动化等价物:tests/install-guard-strict.test.sh ①②
130
+ # 2) 真沙箱(**bin 目录**被重定向)→ 放行
131
+ DIA_INSTALL_DIR=/tmp/x DIA_BIN_DIR=/tmp/xbin bash install.sh; test $? -eq 0
132
+ # 3) npm 包存在但 bin 未被占 → warn 但安装(install-guard.test.sh ③)
133
+ # 4) doctor:真实路径与 installDir 不一致 → install_shadow fail
134
+ dia doctor | grep -q install_shadow
135
+ # 5) npm 安装的机器上:installDir 含 node_modules/@minonl,且 kits/dia-msg 可解析
136
+ dia doctor --paths | grep -q 'node_modules/@minonl'
137
+ ```
138
+
139
+ ## 7. 不做的
140
+
141
+ - **不做 `dia install repair`**:新增命令面大于收益;doctor 给一行可复制命令即可。
142
+ - **不改 `dia update` 退出码语义**:`--check` 的 0/1 语义保持不变。
143
+ - **不引入 npm 依赖检测**(`npm ls -g` / `npm prefix -g`):确定性优先。
144
+
145
+ ## 8. 文档同步
146
+
147
+ `README.md`(安装章节)、`docs/cli.md`(`doctor` 检查项)、`docs/kit-install.md`(install.sh 分发说明)、
148
+ `docs/status.md`(新增条目)、`docs/index.md` + `llms.txt`(新文档索引,改完跑 `npm run docs:index`)。
@@ -30,6 +30,13 @@
30
30
 
31
31
  结论:**persona 装完是"哑"的**;kit 就是把这些补上,且要能 `verify` / `remove`。
32
32
 
33
+ > **install.sh 分发守卫(2026-11-06)**:`~/.local/bin/dia` 当前属于包管理器全局安装
34
+ > (npm/bun 的 `node_modules/@minonl/dia` 软链、pnpm/yarn 的 shim 脚本、volta 的 bin)且正是 `command -v dia`
35
+ > 命中的那条时,`install.sh` **exit 3 且不改动任何文件**(避免脚本安装抢链接后 kit 去旧目录找、
36
+ > `missing-kit` / `E_KIT_EMPTY`)。逃生舱:`DIA_ALLOW_SCRIPT_INSTALL=1`、`--dry-run`,
37
+ > 或**把生效 bin 目录重定向**(`--bin DIR` / `DIA_BIN_DIR=DIR`)。注意:`--home` / `DIA_INSTALL_DIR`
38
+ > 只改安装目录,链接仍写默认 `~/.local/bin`,**不算沙箱**(守卫照旧生效)。包存在但 bin 未被占 → 只 warn 一行。
39
+
33
40
  ---
34
41
 
35
42
  ## 2. 清单:`dia.json`(kind=kit)
@@ -0,0 +1,78 @@
1
+ # Kit 层:把「经验」从 persona 里抽出来(设计 v0.1)
2
+
3
+ > 2026-11-06 用户定调:dws-hub 作为 **channel 层**重构,同时把 git / node 这类**通用工程经验**抽成 **kit 层**,
4
+ > 让 yan-agent 更薄、更专业,dabo 等其它 persona 直接复用。本文定 kit 的边界、清单、检索、治理与迁移顺序。
5
+
6
+ ## 0. 三层模型
7
+
8
+ | 层 | 回答的问题 | 例子 | 归属 |
9
+ |---|---|---|---|
10
+ | **persona** | 你是谁、怎么干活(身份/语气/工作流编排) | yan、dabo | 各自 persona 仓 |
11
+ | **kit** | 你会什么(可复用的经验、工具、知识页) | `kit/git`、`kit/node`、`kit/compose-delivery` | kit 源仓 / registry |
12
+ | **channel** | 消息怎么进出 | dws-hub(钉钉 channel+) | dws-hub 仓 |
13
+
14
+ 约束:
15
+
16
+ - kit **不带人格**、不写人设与语气;只放「换个 agent/换个人也用得上」的内容。
17
+ - persona 只保留:身份、规则(must/must-not)、流程编排、**对 kit 的引用**。
18
+ - channel 只管传输与协议,不承载业务经验。
19
+
20
+ ## 1. 双标签:一份内容、多领域可检索
21
+
22
+ 同一条经验经常跨领域(本次实例:hook manager 共存 = git × node)。做法:
23
+
24
+ - kit 清单与知识页 front-matter 都支持 `tags: string[]`;
25
+ - 检索按 tag 命中**同一份内容**,不复制两套;
26
+ - tag 命名:`git` / `node` / `pnpm` / `hooks` / `worktree` / `channel` / `delivery` …(扁平,不做层级)。
27
+
28
+ 示例:
29
+
30
+ ```json
31
+ { "id": "git", "version": "0.1.0", "tags": ["git", "worktree", "hooks"], "knowledge": ["knowledge/**"] }
32
+ ```
33
+
34
+ ## 2. 清单(`dia.json`,kind=kit)增量
35
+
36
+ 在现有字段(files / skills / bin / dirs / commands / knowledge / data_root / requires)上新增:
37
+
38
+ | 字段 | 说明 |
39
+ |---|---|
40
+ | `tags: string[]` | 检索标签(双/多标签) |
41
+ | `provides: string[]` | 提供的知识主题(供 persona 引用与 doctor 校验,如 `git.hooks`) |
42
+ | `requires: { "<kit>": "<semver>" }` | 依赖其它 kit(如 `dingtalk` requires `node`) |
43
+
44
+ ## 3. 检索:kb 必须同时搜 persona KB 与已装 kit
45
+
46
+ 现状:`dia kb` 只索引 persona 自带 knowledge。抽 kit 后若不同时索引 kit,会出现「装了解析不到」的假空。
47
+
48
+ 要求:
49
+
50
+ - `dia kb query --tag git "hook"` → 命中 persona KB + 所有已装 kit 的 knowledge;
51
+ - 结果标注来源(`persona:yan` / `kit:git@0.1.0`),便于追踪;
52
+ - 0 命中时显式告警(沿用现规则,别当「没有知识」)。
53
+
54
+ ## 4. 治理(doctor)
55
+
56
+ | 检查 | 级别 |
57
+ |---|---|
58
+ | kit 已安装且版本满足 persona 的 `requires` | FAIL |
59
+ | kit 内容哈希与 lock 一致(drift) | FAIL(可 `dia kit verify` 修复) |
60
+ | kit 声明的 bin/skills/files 落点仍在 | FAIL |
61
+ | tag 索引与实际知识页 front-matter 一致 | WARN |
62
+ | 同一 tag 下存在重复内容(hash 相同) | WARN(提示合并) |
63
+
64
+ ## 5. 迁移顺序(小步,不搬家式重写)
65
+
66
+ 1. **P0**:本设计 + `dia kit` 支持 `tags`(本 PR)
67
+ 2. **P1(pilot)**:抽 `kit/git` 与 `kit/node`
68
+ - `kit/git`:worktree 纪律、adopt/doctor/gate、守卫模型、对外无逃生口原则
69
+ - `kit/node`:pnpm 11 `allowBuilds`、lockfile 最小变更、frozen-lockfile、hook manager(husky→simple-git-hooks)
70
+ - 交叉页(hook manager 共存)挂在 `kit/git`,`tags` 双写 `[git, node, hooks]`
71
+ 3. **P2**:yan-agent 变薄 —— git/node 页面移入 kit,persona 只留身份 + 工作流 + 索引;`install.sh` 改 `dia kit add <src>`
72
+ 4. **P3**:dabo 装同一套 kit,验证「换 persona 不换经验」;doctor 校验 kit 版本约束
73
+
74
+ ## 6. 非目标
75
+
76
+ - 不做 kit 市场/评分;kit 来源先支持 git ref 与本地路径。
77
+ - 不把 persona 规则(must/must-not)塞进 kit —— 规则仍归 persona,kit 只提供「怎么做」与「踩过什么坑」。
78
+ - channel 层不因 kit 化而承担知识分发。
@@ -9,7 +9,7 @@
9
9
  # 1) 备份(可选但建议)
10
10
  mv ~/.local/share/dia ~/.local/share/dia.bak-$(date +%Y%m%d)
11
11
 
12
- # 2) 用新安装器迁(自动 mv 旧根 → ~/.dia 并装新版)
12
+ # 2) 用新安装器迁数据(只 mv 旧数据根 → ~/.dia;不装、不改 bin 链接)
13
13
  cd ~/repos/github/dia
14
14
  bun run build
15
15
  bash install.sh --migrate
@@ -52,9 +52,10 @@ tar czf ~/backups/dia-job-$TS.tgz -C ~ .local/share/dia-job
52
52
  crontab -l > ~/backups/crontab-$TS.txt
53
53
  # 把所有 `dia-job:` 行注释掉(记下原样,后面恢复)
54
54
 
55
- # 1) 装新版 + 迁数据(只搬数据子目录,不动安装物)
55
+ # 1) 只迁数据(2026-11-06 起 --migrate 不再顺带安装/改 bin 链接,避免抢走 npm 安装)
56
56
  # 源码来自 mac 构建产物:tar 传 bin/dist/packages/docs/install.sh/package.json
57
57
  bash install.sh --migrate
58
+ # 装新版走 npm(推荐):npm i -g @minonl/dia@latest
58
59
 
59
60
  # 2) cache(install.sh 不搬)
60
61
  mkdir -p ~/.dia/cache && [ -d ~/.cache/dia/index ] && mv ~/.cache/dia/index ~/.dia/cache/