@minonl/dia 0.0.15 → 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.
- package/README.md +6 -0
- package/dist/cli.js +1589 -31
- package/docs/cli.md +3 -2
- package/docs/design/graph-prio-dag.md +85 -0
- package/docs/design/graph-ui-m1.md +106 -0
- package/docs/design/graph-web-m1.md +113 -0
- package/docs/design/graph-web-m2.md +92 -0
- package/docs/design/graph-web-mockup.html +49 -25
- package/docs/design/graph-web.md +9 -4
- package/docs/design/seat-scheduling-m2.md +138 -0
- package/docs/index.md +8 -1
- package/docs/install-guard-design.md +148 -0
- package/docs/kit-install.md +7 -0
- package/docs/kit-layer.md +78 -0
- package/docs/migrations/data-root-2026-11.md +3 -2
- package/docs/status.md +2 -0
- package/kits/git/dia.json +10 -0
- package/kits/git/knowledge/hooks.md +30 -0
- package/kits/git/knowledge/worktree.md +34 -0
- package/kits/node/dia.json +10 -0
- package/kits/node/knowledge/hook-managers.md +17 -0
- package/kits/node/knowledge/pnpm.md +34 -0
- package/package.json +1 -1
- package/packages/wt/bin/dia-wt +15 -0
- package/packages/wt/tests/adopt.test.sh +8 -0
package/docs/cli.md
CHANGED
|
@@ -99,12 +99,13 @@ dia persona remove dabo
|
|
|
99
99
|
| `env list / check / run` | **已实现** | 白名单注入、不回显值、`--redact`、密钥不进 argv;清单由 persona 声明(`src/env.ts` + `tests/env.test.ts`) |
|
|
100
100
|
| `update [--check]` | **已实现** | 查 npm `dist-tags`;`--check` 有新版 exit 1、领先/最新 exit 0;**按安装方式分流**:npm 全局包 → `npm i -g`;脚本安装 → 提示 `git pull && bun run build && bash install.sh`(避免装到不在 PATH 的 npm prefix) |
|
|
101
101
|
| `deps list / record` | **已实现** | `list` = 登记 + 实测派生视图(required 缺失 exit 2);`record` = 只追加一行(`--id/--kind/--version` 必填);`src/deps.ts` + `tests/deps*.test.ts` |
|
|
102
|
-
| `doctor` | **已实现** | 组合自检:persona verify / **persona.render(宿主产物只读漂移,warn + 修复命令)** / default_agent / kit(**只查该 persona 引用的**,孤儿 kit 不拖红)/ knowledge(**索引新鲜度三态**,过期给重建命令)/ dsh preset / **deps 登记校验** / **platform 版本(读 `docs/platforms/registry.json`)** / wt
|
|
102
|
+
| `doctor` | **已实现** | 组合自检:persona verify / **persona.render(宿主产物只读漂移,warn + 修复命令)** / default_agent / kit(**只查该 persona 引用的**,孤儿 kit 不拖红)/ knowledge(**索引新鲜度三态**,过期给重建命令)/ dsh preset / **deps 登记校验** / **platform 版本(读 `docs/platforms/registry.json`)** / wt / **install_shadow(脚本安装 vs 包管理器全局安装互相遮挡:双向一致判定,不一致 fail + 一行修复命令;shim 形态按内容识别)**;exit 0/1/2。不指定 persona 且装了多个 → **逐个自检**(条目加 `<id>.` 前缀),不再整体 `E_SCOPE` |
|
|
103
103
|
| `wt …` | **已实现(需安装包)** | 转发 `packages/wt/bin/dia-wt`(create/adopt/doctor/gate/list/status/check/clean/prune-merged;详见 [worktree.md](worktree.md));未装时 `E_WT`(exit 33),`install.sh` 会分发 |
|
|
104
104
|
| `kb index/search/query/get/lint/path/review` | **已实现** | persona 作用域下索引过期自动重建;显式 `--root` 保持「缺索引即报错」 |
|
|
105
105
|
| `kb <域命令>` | **已实现(委派)** | 未命中内置 → persona `commands` → 委派;argv 透传、exit code 原样 |
|
|
106
106
|
| `<persona-cmd>` / `cmd <name>` | **已实现** | 一级委派(内置优先);`dia cmd` 只查外部命令,可绕开同名内置 |
|
|
107
|
-
| `task add/list/status/done` | **已实现** | done 需 judge + evidence |
|
|
107
|
+
| `task add/list/link/plan/split/status/done` | **已实现** | done 需 judge + evidence;`plan` 写计划态(prio/start/due/progress);`split` 父 depends on 子(单条原子事件,judge 继承) |
|
|
108
|
+
| `graph [--json] [--task T] [--tui …] [--export-html] [--next/--explain/--lint]` | **已实现** | 图 JSON(schema 1 + capabilities);节点带 prio/start/due/progress + 派生 `eff_prio/ready/rank/unblocks/critical` 等;`--export-html` 单文件 HTML;`--next [n]` / `--explain <id>` / `--lint` 只读编排(Kahn-v1,见 [graph-prio-dag.md](design/graph-prio-dag.md)) |
|
|
108
109
|
| `team run --mock / --acp / --dsh`、`team gate/accept/reject` | **已实现** | 状态机 + 门禁;成功/失败路径均跑通 |
|
|
109
110
|
| `job …` | **已实现** | 转发 `packages/job`(launchd/cron/schtasks) |
|
|
110
111
|
| `patch …` | **未实现** | 当前 `unknown command`;设计见 `docs/patching.md` |
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# 优先级 × 依赖:任务编排语义(M1)
|
|
2
|
+
|
|
3
|
+
> 状态:已实现(2026-11-06)|代码:`src/plan.ts`(派生纯函数)、`src/task.ts`(split)、`src/graph.ts`(契约)
|
|
4
|
+
> 相关:`graph-web-m1.md`(导出)、`graph-ui-m1.md`(界面派生量)、`seat-scheduling-m2.md`(席位)、`msg-collab-m1-design.md`(任务账本)
|
|
5
|
+
|
|
6
|
+
## 1. 三个概念,别混
|
|
7
|
+
|
|
8
|
+
| 概念 | 回答 | 谁定 | 可否违反 |
|
|
9
|
+
|---|---|---|---|
|
|
10
|
+
| `prio`(声明优先级) | 这件事本身多重要 | 人 | 可随时改(`plan` 事件留痕) |
|
|
11
|
+
| `deps`(依赖) | 谁必须先完成 | 事实/拆分结果 | **不可违反**(硬约束) |
|
|
12
|
+
| `rank`(执行顺序) | 现在先做哪件 | 由前两者派生 | 建议,不写账本 |
|
|
13
|
+
|
|
14
|
+
结论:**被依赖 ≠ 优先级更高;优先级高 ≠ 先执行**。高优先级任务被卡住时,上游会获得「有效优先级」,否则 P0 永远开不了工。
|
|
15
|
+
|
|
16
|
+
## 2. 有效优先级(eff)
|
|
17
|
+
|
|
18
|
+
```mermaid
|
|
19
|
+
flowchart LR
|
|
20
|
+
C["C: P0(声明)"] --> B["B: P2(声明)"]
|
|
21
|
+
B --> A["A: P3(声明)"]
|
|
22
|
+
C -. "eff P0 传播" .-> B
|
|
23
|
+
C -. "eff P0 传播(2 跳)" .-> A
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- 只从**声明 P0/P1** 的非终态任务向**全部祖先**传播;
|
|
27
|
+
- **不衰减、不累加**(fan-in 取最小/最高;跳数只展示,不参与计算);
|
|
28
|
+
- 终态(done/accepted/dropped/rejected)下游不产生传播;
|
|
29
|
+
- 输出 `eff_prio` + `prio_inherited` + `prio_source{id,declared,distance}`,声明值不变。
|
|
30
|
+
|
|
31
|
+
## 3. 就绪与排序
|
|
32
|
+
|
|
33
|
+
- `satisfied = done | accepted | dropped`(`dropped` 释放下游,但产出 `released_by` 提示「前提失效」);
|
|
34
|
+
- `ready` = 非终态、不在环里、所有 deps 已 satisfied 且无 dangling;
|
|
35
|
+
- `startable` = `ready && status === "todo"`(`doing` 是已开工,`blocked` 是停着,都不进 `--next` 候选);
|
|
36
|
+
- `rank`:**Kahn + 优先队列**(不是先分层再排),key 升序:
|
|
37
|
+
`eff → due(无 due 排末尾)→ critical → slice → -unblocks → depth → created_at → id`;
|
|
38
|
+
- 终态与环内任务 `rank = null`;`--next` 默认取前 5,doing 置顶;
|
|
39
|
+
- **rank 不含「今天」**,保证 `graph --json` 两次运行字节相等;到期紧迫度只在导出时展示。
|
|
40
|
+
|
|
41
|
+
## 4. 关键路径与拆分
|
|
42
|
+
|
|
43
|
+
- `critical` = 非终态声明 P0/P1 的所有祖先(图里橙色高亮);
|
|
44
|
+
- `split`:父 depends on 子(父 = 集成收口),**单条原子 `split` 事件**落账;
|
|
45
|
+
- 子继承父 `judge`(`judge_inherited`),否则 done 门禁会卡死每个切片;
|
|
46
|
+
- `slice` 标记最小可用切片,仅做同级 tiebreak;
|
|
47
|
+
- 组成关系只做元数据(`split_from/split_children`),**绝不作为图边**参与环/排序;
|
|
48
|
+
- 子任务的 eff 由父的声明优先级自然传播得到,不复制 prio(避免 P0 灌水)。
|
|
49
|
+
|
|
50
|
+
## 5. 检查(advisory,exit 0;有环 exit 3)
|
|
51
|
+
|
|
52
|
+
| kind | 含义 | 处置 |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| `dangling_dep` | 依赖的 id 不存在 | 补 `link` 或删依赖;不视为就绪 |
|
|
55
|
+
| `released_by_drop` | 依赖已 dropped | 确认下游是否还需要做 |
|
|
56
|
+
| `long_chain` | 声明 P0/P1 的依赖链 ≥3 跳 | 提示拆分(真正的拆分触发点) |
|
|
57
|
+
| `cycle` | 依赖环 | 必须修(exit 3) |
|
|
58
|
+
|
|
59
|
+
## 6. 命令
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
dia task split <id> --into "最小可用,完整实现" [--slice 最小可用] [--force]
|
|
63
|
+
dia graph --next [n] [--json] # 按 rank 的开工建议(doing 置顶)
|
|
64
|
+
dia graph --explain <id> [--json] # 声明/有效/来源/被谁卡/解锁谁/rank
|
|
65
|
+
dia graph --lint [--json] # 编排检查(advisory)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`graph --json` 新增节点字段:`eff_prio` / `prio_inherited` / `prio_source` / `critical` / `ready` /
|
|
69
|
+
`startable` / `released_by` / `dangling_deps` / `depth` / `unblocks` / `rank` / `split_from` /
|
|
70
|
+
`split_children` / `slice`;顶层 `ranking.policy = "kahn-v1"`,`capabilities` 追加 `eff-prio/ready/rank/split`。
|
|
71
|
+
|
|
72
|
+
## 7. 边界
|
|
73
|
+
|
|
74
|
+
- **席位(并发数)不在本模型里**:rank 是单席位理想序;多席位 + 上下文亲和见 [seat-scheduling-m2.md](seat-scheduling-m2.md)(M2,未实现);
|
|
75
|
+
- `claim` 仍是唯一并发信号;eff/rank 只回答「先做什么」,不回答「谁能做」;
|
|
76
|
+
- 派生量全部只读,不写回账本,不进任何门禁。
|
|
77
|
+
|
|
78
|
+
## 8. 反例(自测覆盖)
|
|
79
|
+
|
|
80
|
+
- 链式:A(P3)←B(P2)←C(P0) → A/B eff P0,rank A<B<C;
|
|
81
|
+
- 终态:C 已完成 → 不传播,B 保持 P2;
|
|
82
|
+
- fan-in:一个 P3 被两个 P1 依赖 → eff P1;
|
|
83
|
+
- 悬空:依赖不存在 → not ready + lint;
|
|
84
|
+
- 环:rank null,`--next/--lint` exit 3;
|
|
85
|
+
- P10 vs P2:数字比较(`P2` 在前),无 prio 视为 `P99`。
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# 关系图界面:派生量上屏(M1 UI)
|
|
2
|
+
|
|
3
|
+
> 状态:设计定稿(2026-11-06)|实现:`src/graph-html/*`(待接)|上游:`graph-prio-dag.md`
|
|
4
|
+
> 原则:**排序只在 TS 算一次**(`src/plan.ts`),前端只渲染;导出件仍无网络。
|
|
5
|
+
|
|
6
|
+
## 1. 目标 / 非目标
|
|
7
|
+
|
|
8
|
+
**目标**:把 `eff /ready/rank/critical/blocked/slice` 变成可读、可验证的视觉与交互;让「该不该提优先级、先做哪件」在图上直接确认。
|
|
9
|
+
|
|
10
|
+
**非目标**:不做席位分配(见 `seat-scheduling-m2.md`);不做实时刷新(见 `graph-web-m2.md`);不在前端重算排序。
|
|
11
|
+
|
|
12
|
+
## 2. 数据契约(导出 payload v1.1,additive)
|
|
13
|
+
|
|
14
|
+
| 字段 | 类型 | 说明 |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| `eff` | `P0`–`P?` | 有效优先级;`P?` = 未排期(内部 99) |
|
|
17
|
+
| `inherited` | bool | `eff` 严格高于声明值 |
|
|
18
|
+
| `prio_source` | `{id,declared,distance}` | 继承来源(min eff → min distance → min id) |
|
|
19
|
+
| `declared` | `P0`–`P?` | 声明优先级 |
|
|
20
|
+
| `rank` | int \| null | Kahn-v1 全局序号;终态/环内为 null |
|
|
21
|
+
| `rank_key` | `(string\|number)[]` | 8 元组 `[eff,due,critical,slice,-unblocks,depth,created_at,id]`,由 `plan.rankKey()` 生成 |
|
|
22
|
+
| `rank_reason` | string | 首个分胜负的键(如 `eff: P0 < P2`) |
|
|
23
|
+
| `critical` | bool | 非终态声明 P0/P1 的祖先(关键闭包) |
|
|
24
|
+
| `ready` / `startable` | bool | 结构就绪 / 可开工(todo 且 ready) |
|
|
25
|
+
| `blocked_by` / `released_by` / `dangling` | string[] | 未满足依赖 / dropped 释放 / 不存在的依赖 |
|
|
26
|
+
| `due_offset` / `created_offset` | int \| null | 相对 `today` 的天数(前端不做日期算术) |
|
|
27
|
+
| `unblocks` / `depth` | int | 传递下游数 / 最长依赖链 |
|
|
28
|
+
| `slice` / `split_from` / `split_children` | — | 拆分元数据(不是图边) |
|
|
29
|
+
| `next` | `{policy,n,items[]}` | 与 `dia graph --next n` 同源同序 |
|
|
30
|
+
|
|
31
|
+
`schema` 仍为 1;`capabilities` 追加 `eff-lane`、`next-panel`、`rank-factors`。
|
|
32
|
+
**白名单**:不导出 evidence/closure_reason/blocked_reason;`--redact` 时 owner → 短 hash、touches 只留文件名。
|
|
33
|
+
|
|
34
|
+
## 3. 泳道口径
|
|
35
|
+
|
|
36
|
+
- 默认 **按 `eff`** 分行;工具栏 `声明 / 有效` 切换,URL `pl=eff|declared`(非默认才写);
|
|
37
|
+
- `laneOf(t) = pl==='declared' ? declared : eff`;泳道按 `prioIndex` 升序,`P?` 恒在最后;
|
|
38
|
+
- 切到「声明」时,被继承抬升的卡片显示 `↑P0`,让人看见「它其实更急」;
|
|
39
|
+
- **迁移说明**:默认从 declared 改 eff 属可见变更,导出件页头标注「泳道=有效优先级」,并更新既有截图基线。
|
|
40
|
+
|
|
41
|
+
## 4. 卡片徽标(多槽,不是单槽竞争)
|
|
42
|
+
|
|
43
|
+
| 槽 | 内容 | 规则 |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| 描边 | `critical` | 紫色描边(不与 P1 橙冲突);环内任务改用红色虚线描边 |
|
|
46
|
+
| 角标 | `#rank` / `#—` | 常驻;终态 `#✓`;环内 `#⚠`;过滤视图标「全局」 |
|
|
47
|
+
| 徽标行 | `↑P0`、`🔒2`、`slice` | 最多 2 枚 + `+N` 溢出;`🔒n` = 未满足依赖数(dangling 计入,tooltip 标 missing) |
|
|
48
|
+
| 状态 | `⏸ 人工 blocked` | 与依赖阻塞分开:`status==='blocked'` 但 deps 全满足时只显示这个 |
|
|
49
|
+
|
|
50
|
+
`tooltip` 固定模板(可 DOM 断言):
|
|
51
|
+
- `有效 P0 ← P0@t-3c1f(1 跳)· 声明 P3`
|
|
52
|
+
- `等 2 个前置:t-a(todo)、t-b(missing)`
|
|
53
|
+
- `解锁 3 个下游 · 关键闭包`
|
|
54
|
+
|
|
55
|
+
点击 `↑P0`:清空 `sel`、必要时切到「全部时间」、滚动居中并打开来源任务抽屉;来源被截断/时间窗隐藏时只 toast「来源不在当前视图」,不静默改视图。
|
|
56
|
+
|
|
57
|
+
## 5. 右侧「下一步」面板
|
|
58
|
+
|
|
59
|
+
- 与 `dia graph --next 5` **同源**(共用 `buildNext()`),顺序 `doing(rank 升序)→ startable(rank 升序)`;
|
|
60
|
+
- 每条显示 `#rank 标题 · eff · 理由(解锁 N / 关键闭包 / 继承来源)`;
|
|
61
|
+
- doing 超过 5 个 → 前 5 + 「另有 N 个进行中」;环内 doing 单列「异常」区;
|
|
62
|
+
- 空态:区分「账本为空」与「时间窗内无任务」(后者给「放宽到全部时间」)。
|
|
63
|
+
|
|
64
|
+
## 6. 任务抽屉「四问」
|
|
65
|
+
|
|
66
|
+
1. **优先级**:声明 / 有效 / 来源链(几跳、原始声明);
|
|
67
|
+
2. **等谁**:`blocked_by` 明细(各自 status;missing 标红);`released_by` 单独提示「前提已失效」;
|
|
68
|
+
3. **解锁谁**:直接 dependents 列表 + 传递计数 `unblocks`;
|
|
69
|
+
4. **为什么这个序**:`rank_key` 8 项逐项取值 + `rank_reason`(首个分胜负项)。
|
|
70
|
+
|
|
71
|
+
命令页保持只读(复制 `dia task plan/claim/msg`),不假装能发送。
|
|
72
|
+
|
|
73
|
+
## 7. 关键闭包(原“关键路径”)
|
|
74
|
+
|
|
75
|
+
- `critical` = 非终态声明 P0/P1 的**祖先闭包**(不是最长路,菱形会多分支)——文案统一写「关键闭包」,避免过度承诺;
|
|
76
|
+
- 节点:紫色描边;边:`depends_on && (from.critical || 声明 P0/P1)` 且 `to.critical`,描边同色;
|
|
77
|
+
- 与选中/阻塞叠加:描边只占一层,选中用外发光,阻塞用左侧锁条。
|
|
78
|
+
|
|
79
|
+
## 8. 截断与规模
|
|
80
|
+
|
|
81
|
+
| 可见卡片 | 行为 |
|
|
82
|
+
|---|---|
|
|
83
|
+
| ≤ 120 | 全功能(详细模式可用) |
|
|
84
|
+
| 121–200 | 强制简约;顶部黄条「已进入大图模式」 |
|
|
85
|
+
| 201–500 | 仅 `layout=dep` 且禁用「详细」;`--all` 才能突破 |
|
|
86
|
+
| > 500 | 拒绝渲染(exit 2),提示缩小时间窗或 `--limit` |
|
|
87
|
+
|
|
88
|
+
- 截断顺序:rank 非空升序 → 终态按 `closed_at` 降序保留前 20 → 声明优先级 → id;
|
|
89
|
+
- **deps 与 edges 一律按 `shownIds` 过滤**,卡片标 `deps_truncated`,避免「有依赖没卡片/没边」;
|
|
90
|
+
- `truncated={shown,total,hidden_ids≤20,policy:'rank-first'}`;`--limit N` 显式覆盖阈值但记录警告。
|
|
91
|
+
|
|
92
|
+
## 9. URL 与深链
|
|
93
|
+
|
|
94
|
+
`#/graph?layout=priority|dep&pl=eff|declared&w=7|14|30|0&sel=a,b&task=t-x&tab=task|intervene`
|
|
95
|
+
- 互斥:改 `w` 清 `from/to`;`layout=dep` 时 `pl` 保留但不生效;
|
|
96
|
+
- `go()`/`toggleSel()` 重建 hash 必须保留未知键(`pl`);
|
|
97
|
+
- `from/to` 偏移以 payload 的 `today` 为基准(删除模板里 2026-11-06 常量)。
|
|
98
|
+
|
|
99
|
+
## 10. 验收
|
|
100
|
+
|
|
101
|
+
- DOM:继承卡片有 `↑P0` 且 tooltip 含来源 id/跳数;`pl=eff` 与 `pl=declared` 泳道标签不同;
|
|
102
|
+
- 一致性:`payload.next.items` 与 `dia graph --next 5 --json` 逐项相等;
|
|
103
|
+
- 抽屉:四问非空,`rank_key` 8 项;`blocked_by` 含 missing 标注;
|
|
104
|
+
- 降级:环 / 悬空 / 全终态 / 空账本 / 时间窗空 五态不抛错、各有文案;
|
|
105
|
+
- 确定性:同一 `--now` 两次导出字节一致;无 `fetch(`/外链;
|
|
106
|
+
- 截图 5 张:有效泳道、声明泳道、下一步面板、抽屉四问、时间线(关键闭包+开放端)。
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# dia graph Web(M1)设计:真实数据 → 单文件静态导出
|
|
2
|
+
|
|
3
|
+
> 状态:M1 设计(2026-11-06)|前置:`graph-web.md`(视觉稿 v0.3)、`graph-web-mockup.html`(交互原型)
|
|
4
|
+
> 交付形态拍板:**先 A 后 B** —— M1 只做 `dia graph --export-html`;M2 再做 loopback 实时服务(见 [graph-web-m2.md](graph-web-m2.md));界面派生量见 [graph-ui-m1.md](graph-ui-m1.md)。
|
|
5
|
+
|
|
6
|
+
## 1. 目标 / 非目标
|
|
7
|
+
|
|
8
|
+
**目标**
|
|
9
|
+
|
|
10
|
+
- `dia graph --export-html [path]` 把真实账本渲染成**单文件 HTML**(CSS/JS/数据全内联),`file://` 打开即用;
|
|
11
|
+
- 视觉与交互沿用 mockup:优先级泳道 / 依赖视图、时间线、搜索 token、邻域聚焦、任务抽屉;
|
|
12
|
+
- 数据缺口显式降级,不伪造:缺字段有兜底并标注;
|
|
13
|
+
- 全程无网络:无端口、无外链、无 `fetch`。
|
|
14
|
+
|
|
15
|
+
**非目标(M1 不做)**
|
|
16
|
+
|
|
17
|
+
- 实时刷新、介入消息真正发送(M2 live 服务);
|
|
18
|
+
- 服务端过滤/鉴权、多人共享;
|
|
19
|
+
- 把 mockup 的演示数据(t-3/t-4、claims=3、live data)带进导出件。
|
|
20
|
+
|
|
21
|
+
## 2. 流水线
|
|
22
|
+
|
|
23
|
+
```mermaid
|
|
24
|
+
flowchart LR
|
|
25
|
+
L[账本 ledger.jsonl] --> F[task.fold<br/>add/link/plan/status]
|
|
26
|
+
F --> T[Task[]<br/>prio/start/due/progress/closed_at]
|
|
27
|
+
T --> G[graph.build<br/>schema 1 + capabilities]
|
|
28
|
+
G --> R[graph-html.render<br/>白名单 + JSON 转义]
|
|
29
|
+
R --> H[单文件 HTML<br/>file:// 打开]
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- 模板以 **TS 字符串常量**进 `src/graph-html/template.ts`,被 `bun build` 打进 `dist/cli.js`(`install.sh` 只拷 `dist`,不得运行时读文件);
|
|
33
|
+
- 渲染函数 `src/graph-html/render.ts` 纯函数:`render(graph, {now, redact}) -> string`,无 IO、无时钟(时间由调用方注入)。
|
|
34
|
+
|
|
35
|
+
## 3. 数据模型(Task 扩展)
|
|
36
|
+
|
|
37
|
+
| 字段 | 类型 | 语义 | 缺失时 |
|
|
38
|
+
|---|---|---|---|
|
|
39
|
+
| `prio` | `P0`–`P9` | 计划优先级(与 status 正交) | 归入 `P?` 末行泳道 |
|
|
40
|
+
| `start` | `YYYY-MM-DD` | **计划**开始(本地日) | 回填 `created_at` 的本地日,卡片标「按创建」 |
|
|
41
|
+
| `due` | `YYYY-MM-DD` | **计划**结束/截止 | 未完成 → 时间线画开放端;已完成 → 用 `closed_at` |
|
|
42
|
+
| `progress` | `0–100` | 人工进度 | 按 status 派生(todo 0 / doing 50 / review 80 / done 100 / blocked 0),标「估算」 |
|
|
43
|
+
| `closed_at` | ISO ts(派生) | 进入 done/accepted 的时间 | 旧账本:无(开放端) |
|
|
44
|
+
|
|
45
|
+
**账本事件**:新增 `{"event":"plan","id",prio?,start?,due?,progress?}`,只覆盖出现的字段;
|
|
46
|
+
`fold` 对 `plan` 单独分支(不得落入 status 分支,避免 plan 编辑伪造 `updated_at` 的语义);
|
|
47
|
+
`status` 事件在进入 `done|accepted` 时写 `closed_at`,回到进行态时清除。
|
|
48
|
+
|
|
49
|
+
**旧账本兼容**:additive,无需迁移;无字段走 §3 兜底,UI 不得因缺 `prio/progress` 抛错。
|
|
50
|
+
|
|
51
|
+
## 4. Graph 契约(schema 1,additive)
|
|
52
|
+
|
|
53
|
+
`GraphNode` 增加(全部可选):`prio`、`start`、`due`、`progress`、`progress_derived`、`created_at`、`updated_at`、`closed_at`、`deps`、`touches`。
|
|
54
|
+
`Graph` 顶层增加 `capabilities: ["prio","timeline","progress"]`。
|
|
55
|
+
`schema` 仍为 `1`;输出必须**确定性**(不写生成时间、不依赖 locale、稳定排序),满足现有「两次 `--json` 字节相等」测试。
|
|
56
|
+
补 `tests/fixtures/graph.v1.json` 作为 additive 回归网。
|
|
57
|
+
|
|
58
|
+
## 5. 渲染与安全
|
|
59
|
+
|
|
60
|
+
- 数据注入:`<script type="application/json" id="dia-graph">`,序列化时 `JSON.stringify(x).replace(/</g,'\\u003c')`;
|
|
61
|
+
- 模板内 JS 只读该 JSON,不拼接未转义的 label/owner;`progress` 一律 `Number()` 钳位;
|
|
62
|
+
- `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; img-src data:">`;
|
|
63
|
+
- **字段白名单**:只导出 id/title/status/prio/start/due/progress/deps/touches/owner;**永不导出** `evidence` / `closure_reason` / `blocked_reason`;
|
|
64
|
+
- `--redact`:owner 变短 hash、touches 只保留文件名;
|
|
65
|
+
- 默认输出 `~/.dia/export/graph-<YYYYMMDD-HHmm>.html`,权限 `0600`,stdout 只打印路径;`--out <path>` 可覆盖(拒绝覆盖已存在文件,除非 `--force`)。
|
|
66
|
+
|
|
67
|
+
## 6. 交互(沿用 mockup,去掉演示态)
|
|
68
|
+
|
|
69
|
+
- 两种布局:优先级泳道(P 数字升序,`P?` 最后)/ 依赖视图(深度从左到右);
|
|
70
|
+
- **不折叠**;默认时间窗「近 7 天开始(按 start,缺失=始终可见),结束不限」;可选 14/30/全部;
|
|
71
|
+
- 搜索:选中回显 tag(`sel` 多选),`↑↓/Enter/Esc/Backspace/×`;
|
|
72
|
+
- 聚焦:邻域 1/2/全链路;**sel 与时间窗取交集**(避免 URL 表达矛盾;空交集给出提示);
|
|
73
|
+
- 时间线:左侧固定列、缩放 14–84px/天、开放端用渐隐箭头;日期刻度按可用宽度动态取步长;
|
|
74
|
+
- 抽屉:只读详情;原「介入」区改成**复制命令**(`dia msg send …` / `dia task plan …`),不假装能发送。
|
|
75
|
+
|
|
76
|
+
## 7. 异常与边界
|
|
77
|
+
|
|
78
|
+
- 空账本 → 空态页(说明如何 `dia task add`),不渲染空泳道;
|
|
79
|
+
- 依赖环 → 顶部警示条,环内任务正常渲染;
|
|
80
|
+
- 超大账本(>500)→ 渲染前 N 条 + 顶部「已截断 N 条,用 `--all` 渲染全部」;时间线行高固定、后续再虚拟化;
|
|
81
|
+
- 时区:所有日粒度换算用本地日(手拼 `YYYY-MM-DD`),禁止 `toISOString()` 参与日粒度比较;
|
|
82
|
+
- `from/to` 深链与 `w` 互斥:改 `w` 时清空 `from/to`。
|
|
83
|
+
|
|
84
|
+
## 8. 测试 / 验收
|
|
85
|
+
|
|
86
|
+
- 单测:plan 往返、plan 不覆盖未提供字段、旧账本可读、graph 确定性、fixture 兼容、HTML 注入转义(`</script>`)、无外链/无 `fetch(`/无 `node:http`、空账本、截断、红action;
|
|
87
|
+
- 视觉:Edge headless 截图 4 张(优先级/依赖/详情/时间线)人工验收;
|
|
88
|
+
- judge:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
bun test tests && bash tests/smoke.sh && \
|
|
92
|
+
T=$(mktemp -d) && DIA_ROOT=$T bun packages/cli/src/index.ts graph --export-html --out $T/g.html --now 2026-11-06T10:00:00Z >/dev/null && \
|
|
93
|
+
grep -q '__DIA_GRAPH__\|id="dia-graph"' $T/g.html && ! grep -Eq 'fetch\(|node:http|src="http|@import' $T/g.html
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## 9. 发布顺序(每步可独立 MR)
|
|
97
|
+
|
|
98
|
+
1. **M1a 数据**:`task.ts`(字段 + plan + closed_at)+ CLI `dia task plan` + 单测;
|
|
99
|
+
2. **M1b 契约**:`graph.ts` 增字段 + `capabilities` + fixture + TUI 兼容(未知字段忽略);
|
|
100
|
+
3. **M1c 渲染**:`src/graph-html/{template,render}.ts` + 测试;
|
|
101
|
+
4. **M1d 入口**:`--export-html/--out/--redact/--now/--all` + smoke + 文档;
|
|
102
|
+
5. M2 live 服务另立设计(`packages/graph-web`,loopback + token + 只读 API)。
|
|
103
|
+
|
|
104
|
+
## 10. 风险
|
|
105
|
+
|
|
106
|
+
| 风险 | 缓解 |
|
|
107
|
+
|---|---|
|
|
108
|
+
| 模板塞进 TS 后难维护 | mockup 保留为设计源;模板只做数据换源;结构 marker 测试 |
|
|
109
|
+
| 缺 start 的旧账本被「近 7 天」隐藏 | 缺失 = 始终可见(§3) |
|
|
110
|
+
| 派生 progress 被当真实进度 | `progress_derived` + 斜纹 + 「估算」标注 |
|
|
111
|
+
| 导出件含内部信息 | 白名单 + 0600 + 文档警示;`--redact` |
|
|
112
|
+
| 不折叠 → 1000 任务画布 23 万 px | 默认截断 + 提示;M2 再做虚拟化 |
|
|
113
|
+
| 双击/单击手势冲突 | 单击=抽屉,长按/⌘ 点击=选中;或抽屉改「详情」按钮(M1c 定稿前实测) |
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# graph live 服务(M2 设计,可选包)
|
|
2
|
+
|
|
3
|
+
> 状态:设计定稿(2026-11-06,未实现)|前置:`graph-web-m1.md`(静态导出)、`graph-ui-m1.md`、`seat-scheduling-m2.md`
|
|
4
|
+
> 定位:本机**只读**实时预览;不写账本、不做多人共享、不暴露公网。
|
|
5
|
+
|
|
6
|
+
## 1. 目标 / 非目标
|
|
7
|
+
|
|
8
|
+
**目标**:把 M1 的静态快照变成「自动刷新」的只读页面,复用同一套 payload 与 UI。
|
|
9
|
+
|
|
10
|
+
**非目标**:多人协作/鉴权体系;写操作(介入、plan、claim);守护进程/开机自启;公网/局域网访问。
|
|
11
|
+
|
|
12
|
+
## 2. 包边界(为什么不在 `src/`)
|
|
13
|
+
|
|
14
|
+
- `tests/static.test.ts` 的意图是「框架核心零网络」;`src/**` 永远不出现 `node:http`;
|
|
15
|
+
- 服务实现放 `packages/graph-web/`(与 `packages/job`、`packages/wt` 同构),独立 `bin/dia-graph-web`;
|
|
16
|
+
- `dia graph --serve` 只做 **spawn**(`child_process`),由 `src`/CLI 转发,不 import http——保持静态约束与测试不变;
|
|
17
|
+
- 未安装该包时,`--serve` 输出明确安装命令(`bash install.sh`),不静默失败。
|
|
18
|
+
|
|
19
|
+
## 3. 进程与生命周期
|
|
20
|
+
|
|
21
|
+
- 前台阻塞运行,`Ctrl-C` 退出;不自动重启、不写 pid 文件、不依赖 launchd/systemd;
|
|
22
|
+
- 一次运行 = 一个浏览器会话;重启即换 token 与端口;
|
|
23
|
+
- `--no-open`:只打印 URL;默认尝试 `open`/`xdg-open`/`start`,失败则仅打印提示。
|
|
24
|
+
|
|
25
|
+
## 4. 网络面与 token
|
|
26
|
+
|
|
27
|
+
- 只绑 `127.0.0.1`;默认 `--port 0`(内核分配),启动后从 `server.address()` 取真实端口;
|
|
28
|
+
- 拒绝 `0.0.0.0` 与 IPv6 通配;显式 `--port` 也只绑 loopback;
|
|
29
|
+
- **token**:32B 随机 base64url,进程内有效(**不是单次**),所有 `/api/*` 要求 `X-Dia-Token`,比较用 `timingSafeEqual`;
|
|
30
|
+
- 传递:URL fragment `#t=<token>` → 启动时读取 → `sessionStorage` → `history.replaceState` 立即抹掉;**不能**与路由 hash 抢位(先剥离 token 再交给 `#/graph`);
|
|
31
|
+
- 输出:仅 TTY 打印完整 URL;非 TTY 打码,并把完整 URL 写入 `~/.dia/export/serve-<pid>.url`(0600),打印路径;
|
|
32
|
+
- 401:页面显示「会话已失效,请回到终端重新打开」+ 可复制的重开命令。
|
|
33
|
+
|
|
34
|
+
## 5. API(只读)
|
|
35
|
+
|
|
36
|
+
| 方法 | 路径 | 返回 |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| GET | `/api/graph?rev=` | `{schema,rev,payload}`(与导出件同一 `buildPayload`) |
|
|
39
|
+
| GET | `/api/next?n=5&seats=S` | 复用 `buildNext` / 席位分配(`seats` 缺省 1) |
|
|
40
|
+
| GET | `/api/meta` | `{version, capabilities, seats_default, presence_ttl_s, readonly:true}` |
|
|
41
|
+
| GET | `/api/health` | `{ok:true}`(同样鉴权,不泄露版本细节) |
|
|
42
|
+
|
|
43
|
+
- 非 GET → 405;未知路径 → 404;错误 envelope `{error:{code,message}}`,不返回堆栈;
|
|
44
|
+
- 响应头:`Cache-Control: no-store`、`X-Content-Type-Options: nosniff`、`Cross-Origin-Resource-Policy: same-origin`;
|
|
45
|
+
- `rev = sha1(ledger mtimeMs + size + tasks 数 + claims 数)` 前 12 位;**不含** `generated_at/today/presence_at`;
|
|
46
|
+
- 带 `If-None-Match` 或 `?rev=` 相同 → `304`(不重传 payload)。
|
|
47
|
+
|
|
48
|
+
## 6. 刷新
|
|
49
|
+
|
|
50
|
+
- 默认 **2s 轮询** `/api/graph?rev=<当前>`;`304` 只更新「已检查」时间;页面隐藏时暂停(`visibilitychange`);
|
|
51
|
+
- 失败:保留上一帧 + 顶部「离线,最后更新 HH:mm:ss」;连续失败退避 2→4→8s(上限 30s);
|
|
52
|
+
- SSE 推迟到 M2.1:单机只读场景收益小于长连接复杂度;
|
|
53
|
+
- `generated_at` 用**服务端**时间,前端不猜;快照语义与导出件一致。
|
|
54
|
+
|
|
55
|
+
## 7. 安全
|
|
56
|
+
|
|
57
|
+
- `Host` 必须精确等于 `127.0.0.1:<port>`(或 `localhost:<port>`),否则 421(防 DNS rebinding);
|
|
58
|
+
- `Origin` 存在时必须同源;`Sec-Fetch-Site` 非 `same-origin`/`none` 拒绝;
|
|
59
|
+
- **不发 CORS 头**;`OPTIONS` 一律 405;
|
|
60
|
+
- CSP:`default-src 'none'; connect-src 'self'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; base-uri 'none'; frame-ancestors 'none'`(内联脚本是当前模板现状,迁 nonce 列入 M2.1);
|
|
61
|
+
- 日志:只记 `{ts, method, path, status}`,不记 token/query/owner/路径;错误不回显 `req.url`;
|
|
62
|
+
- 文档警示:带 token 的 URL 仍会进浏览器历史/终端 scrollback/截图,别外发。
|
|
63
|
+
|
|
64
|
+
## 8. 分发与安装
|
|
65
|
+
|
|
66
|
+
- `packages/graph-web/{package.json,src/,bin/}`;根 `files` 增该目录;`install.sh` 用 `[ -d packages/graph-web ]` 保护拷贝(旧树不报错);
|
|
67
|
+
- `dia doctor` 增一项「graph-web 可选包」,缺包只 warn + 修复命令;
|
|
68
|
+
- 兼容:`dia graph --export-html` 始终可用,作为服务不可用时的兜底。
|
|
69
|
+
|
|
70
|
+
## 9. 失败回退
|
|
71
|
+
|
|
72
|
+
| 情况 | 行为 |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `--port` 被占 | 不自动换端口;`E_PORT` + 提示去掉 `--port` 用随机端口 |
|
|
75
|
+
| 浏览器打不开 | 打印 URL 与 `--no-open` 提示 |
|
|
76
|
+
| 无 bun/node | `E_RUNTIME` + 安装命令(node ≥ engines 下限) |
|
|
77
|
+
| 账本不可读/坏行 | 坏行按 fold 容忍;整文件不可读 → 503 + 页面错误条,不白屏 |
|
|
78
|
+
| 模板缺失 | `E_TEMPLATE`(打包把模板文本内联,理论不发生) |
|
|
79
|
+
| 账本被删/重建 | `rev` 变化触发全量重渲染,不复用旧 id |
|
|
80
|
+
|
|
81
|
+
## 10. 测试与发布顺序
|
|
82
|
+
|
|
83
|
+
1. **M2a 骨架**:包 + bin + 只绑 loopback + `/api/health`(单测:Host/端口/token 401);`
|
|
84
|
+
2. **M2b API**:`/api/graph` 与导出件同源对比、`rev/304`;
|
|
85
|
+
3. **M2c 安全**:Host/Origin/CSP/日志负例;静态测试仍全绿;
|
|
86
|
+
4. **M2d 分发**:install.sh 旧树/新树、doctor、`--serve` 未装提示。
|
|
87
|
+
|
|
88
|
+
判据:`bun test tests`(含新包测试)+ `bash tests/smoke.sh` + 手工 `curl` 负例 + Edge headless 一帧。
|
|
89
|
+
|
|
90
|
+
## 11. 待定(实现前确认)
|
|
91
|
+
|
|
92
|
+
- SSE 是否 M2.1;写接口是否永远不做(当前结论:只读 + 复制命令)。
|
|
@@ -32,14 +32,18 @@
|
|
|
32
32
|
.seg button.on{background:var(--blue);color:#fff}
|
|
33
33
|
.seg button:disabled{opacity:.35;cursor:not-allowed}
|
|
34
34
|
.win-select{border:1px solid var(--line);background:var(--bg);color:var(--ink);border-radius:8px;padding:6px 8px;font:inherit;font-size:12px}
|
|
35
|
-
.search{position:relative}
|
|
36
|
-
.search input{border:
|
|
37
|
-
.
|
|
35
|
+
.search{position:relative;display:flex;align-items:center;gap:4px;flex-wrap:wrap;border:1px solid var(--line);background:var(--bg);border-radius:8px;padding:4px 8px;min-width:240px;max-width:420px;cursor:text}
|
|
36
|
+
.search input{border:0;background:transparent;color:var(--ink);padding:2px 0;font:inherit;font-size:12px;flex:1;min-width:80px;outline:none}
|
|
37
|
+
.token{display:inline-flex;align-items:center;gap:4px;background:color-mix(in srgb,var(--blue) 22%,transparent);color:var(--blue);border-radius:999px;padding:2px 4px 2px 8px;font-size:11px;white-space:nowrap}
|
|
38
|
+
.token button{border:0;background:transparent;color:inherit;cursor:pointer;font:inherit;font-size:12px;line-height:1;padding:0 2px}
|
|
39
|
+
.search .q-list{position:absolute;left:0;top:calc(100% + 4px);z-index:20;width:300px;max-height:250px;overflow:auto;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:var(--shadow);display:none}
|
|
38
40
|
.search .q-list.open{display:block}
|
|
39
41
|
.q-item{padding:6px 10px;font-size:12px;cursor:pointer;display:flex;justify-content:space-between;gap:10px}
|
|
40
42
|
.q-item:hover,.q-item.active{background:color-mix(in srgb,var(--blue) 18%,transparent)}
|
|
41
43
|
.q-item .k{color:var(--muted)}
|
|
42
44
|
.q-item .q-prio{font-style:normal;font-size:10px;color:var(--muted);margin-left:6px}
|
|
45
|
+
.q-item.sel{color:var(--blue)}
|
|
46
|
+
.q-item.sel .k{color:var(--blue)}
|
|
43
47
|
.q-head{padding:6px 10px;font-size:10px;color:var(--muted);border-bottom:1px solid var(--line)}
|
|
44
48
|
.q-empty{padding:10px;font-size:12px;color:var(--muted)}
|
|
45
49
|
.node.hit{outline:2px solid var(--blue);outline-offset:1px}
|
|
@@ -181,11 +185,12 @@
|
|
|
181
185
|
<button class="on" data-m="simple">简约</button>
|
|
182
186
|
<button data-m="detail">详细</button>
|
|
183
187
|
</div>
|
|
184
|
-
<div class="search">
|
|
185
|
-
<
|
|
188
|
+
<div class="search" id="search-box">
|
|
189
|
+
<span id="tokens" style="display:contents"></span>
|
|
190
|
+
<input id="q" type="search" placeholder="搜索任务,选中后变成 tag" autocomplete="off">
|
|
186
191
|
<div class="q-list" id="q-list"></div>
|
|
187
192
|
</div>
|
|
188
|
-
<span class="focus-chip" id="focus-chip"><
|
|
193
|
+
<span class="focus-chip" id="focus-chip"><select id="focus-fd" class="fd"><option value="1">1 层</option><option value="2" selected>2 层</option><option value="all">全链路</option></select><button id="focus-exit">清空</button></span>
|
|
189
194
|
</div>
|
|
190
195
|
<div id="time-tools" style="display:none;gap:8px;align-items:center">
|
|
191
196
|
<div class="seg" id="zoom">
|
|
@@ -281,7 +286,7 @@ for (let i=0;i<100;i++){
|
|
|
281
286
|
const startD = -15 + ((i*7)%24);
|
|
282
287
|
const endD = Math.min(15, startD + 1 + ((i*3)%8));
|
|
283
288
|
const id = 't-'+String(i+1).padStart(3,'0');
|
|
284
|
-
const deps = i
|
|
289
|
+
const back=(i%5)+1, deps = i-back>=0 ? ['t-'+String(i-back).padStart(3,'0')] : [];
|
|
285
290
|
tasks.push({id, label:modules[i%modules.length]+' '+String(i+1).padStart(3,'0'), status, prio, progress, start:startD, end:endD, owner:i%3===0?null:(i%2?'ses_a':'ses_b'), deps});
|
|
286
291
|
}
|
|
287
292
|
const pad=n=>'t-'+String(n+1).padStart(3,'0');
|
|
@@ -292,7 +297,7 @@ for(let i=0;i<20;i++) taskEdges.push({from:tasks[i*5].id,to:paths[i%paths.length
|
|
|
292
297
|
for(let i=0;i<12;i++) taskEdges.push({from:i%2?'ses_a':'ses_b',to:paths[i%paths.length],kind:'claims'});
|
|
293
298
|
taskEdges.push({from:'t-001',to:'t-002',kind:'collaborates_with'});
|
|
294
299
|
|
|
295
|
-
const state={view:'graph', layout:'priority', mode:'simple', range:'30d', zoom:28, window:7, q:'',
|
|
300
|
+
const state={view:'graph', layout:'priority', mode:'simple', range:'30d', zoom:28, window:7, q:'', sel:[], fd:2, from:null, to:null};
|
|
296
301
|
const canvas=document.getElementById('canvas'), layer=document.getElementById('canvas-scroll'), rail=document.getElementById('lane-rail'), svg=document.getElementById('edges');
|
|
297
302
|
const esc=s=>String(s??'').replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
|
|
298
303
|
const stIcon={doing:'▶',blocked:'⏸',review:'◐',done:'✓',accepted:'✓',todo:'○'};
|
|
@@ -313,7 +318,11 @@ function focusSet(id,depth){
|
|
|
313
318
|
return out;
|
|
314
319
|
}
|
|
315
320
|
function visibleTasks(){
|
|
316
|
-
if(state.
|
|
321
|
+
if(state.sel.length){
|
|
322
|
+
const keep=new Set();
|
|
323
|
+
state.sel.forEach(id=>focusSet(id,state.fd).forEach(x=>keep.add(x)));
|
|
324
|
+
return tasks.filter(t=>keep.has(t.id));
|
|
325
|
+
}
|
|
317
326
|
return tasks.filter(t=>state.window<=0 || t.start>=-state.window);
|
|
318
327
|
}
|
|
319
328
|
function searchMatches(){
|
|
@@ -332,7 +341,7 @@ function syncStats(){
|
|
|
332
341
|
function updateHint(){
|
|
333
342
|
const h=document.getElementById('hint'); if(!h||state.view!=='graph') return;
|
|
334
343
|
const n=visibleTasks().length;
|
|
335
|
-
if(state.
|
|
344
|
+
if(state.sel.length){
|
|
336
345
|
h.textContent='显示 '+n+' 个任务 · '+(state.fd==='all'?'全链路':state.fd+' 层');
|
|
337
346
|
return;
|
|
338
347
|
}
|
|
@@ -403,7 +412,7 @@ function card(t,x,y,showPrio=false){
|
|
|
403
412
|
(state.mode==='detail'?'<div class="meta" style="margin-top:5px">'+(t.owner?'@'+esc(t.owner):'unassigned')+' · dep '+t.deps.length+'</div>'+rel:'');
|
|
404
413
|
el.dataset.id=t.id;
|
|
405
414
|
el.onclick=()=>{ clearTimeout(clickTimer); clickTimer=setTimeout(()=>openDrawer(t.id,'task'),180); };
|
|
406
|
-
el.ondblclick=e=>{ e.preventDefault(); clearTimeout(clickTimer);
|
|
415
|
+
el.ondblclick=e=>{ e.preventDefault(); clearTimeout(clickTimer); toggleSel(t.id); };
|
|
407
416
|
return el;
|
|
408
417
|
}
|
|
409
418
|
function edgePath(a,b){
|
|
@@ -609,16 +618,19 @@ function renderSearchList(ms){
|
|
|
609
618
|
const box=document.getElementById('q-list'); if(!box) return;
|
|
610
619
|
const q=(state.q||'').trim();
|
|
611
620
|
const browse=!q;
|
|
612
|
-
const pool=browse?
|
|
621
|
+
const pool=browse?tasks.filter(t=>state.window<=0||t.start>=-state.window).slice().sort((a,b)=>a.prio.localeCompare(b.prio)||a.start-b.start):ms;
|
|
613
622
|
if(!pool||!pool.length){
|
|
614
623
|
box.innerHTML='<div class="q-empty">'+(browse?'当前时间窗内没有任务':'没有匹配的任务')+'</div>';
|
|
615
624
|
box.classList.add('open'); return;
|
|
616
625
|
}
|
|
617
|
-
const head=browse?('<div class="q-head"
|
|
618
|
-
:('<div class="q-head">匹配 '+pool.length+' 个 · ↑↓ 选择,Enter
|
|
619
|
-
box.innerHTML=head+pool.slice(0,20).map(t=>
|
|
626
|
+
const head=browse?('<div class="q-head">'+pool.length+' 个任务 · ↑↓ 选择,Enter 加入</div>')
|
|
627
|
+
:('<div class="q-head">匹配 '+pool.length+' 个 · ↑↓ 选择,Enter 加入</div>');
|
|
628
|
+
box.innerHTML=head+pool.slice(0,20).map(t=>{
|
|
629
|
+
const on=state.sel.includes(t.id);
|
|
630
|
+
return '<div class="q-item'+(on?' sel':'')+'" data-id="'+esc(t.id)+'"><span>'+esc(t.label)+'<i class="q-prio">'+esc(t.prio)+'</i></span><span class="k">'+(on?'✓ ':'')+esc(t.id)+'</span></div>';
|
|
631
|
+
}).join('');
|
|
620
632
|
box.classList.add('open');
|
|
621
|
-
box.querySelectorAll('.q-item').forEach(el=>el.onclick=()
|
|
633
|
+
box.querySelectorAll('.q-item').forEach(el=>el.onclick=e=>{ e.stopPropagation(); toggleSel(el.dataset.id); });
|
|
622
634
|
}
|
|
623
635
|
function searchListItems(){
|
|
624
636
|
const box=document.getElementById('q-list');
|
|
@@ -632,11 +644,18 @@ function moveSearchCursor(delta){
|
|
|
632
644
|
items.forEach((el,i)=>el.classList.toggle('active',i===idx));
|
|
633
645
|
items[idx].scrollIntoView({block:'nearest'});
|
|
634
646
|
}
|
|
635
|
-
function
|
|
636
|
-
|
|
647
|
+
function toggleSel(id){
|
|
648
|
+
const i=state.sel.indexOf(id);
|
|
649
|
+
if(i>=0) state.sel.splice(i,1); else state.sel.push(id);
|
|
650
|
+
state.fd=state.fd||2; state.q='';
|
|
637
651
|
const qi=document.getElementById('q'); if(qi) qi.value='';
|
|
638
652
|
go(state.view,state.mode,state.range,state.zoom);
|
|
639
653
|
}
|
|
654
|
+
function renderTokens(){
|
|
655
|
+
const box=document.getElementById('tokens'); if(!box) return;
|
|
656
|
+
box.innerHTML=state.sel.map(id=>'<span class="token">'+esc(id)+'<button data-id="'+esc(id)+'" title="移除">×</button></span>').join('');
|
|
657
|
+
box.querySelectorAll('button').forEach(b=>b.onclick=e=>{ e.stopPropagation(); toggleSel(b.dataset.id); });
|
|
658
|
+
}
|
|
640
659
|
function readRoute(){
|
|
641
660
|
const raw=location.hash.replace(/^#\/?/,''); const [path,qs]=raw.split('?');
|
|
642
661
|
const params=new URLSearchParams(qs||location.search);
|
|
@@ -650,7 +669,7 @@ function readRoute(){
|
|
|
650
669
|
const win=winRaw!==null?Math.max(0,parseInt(winRaw,10)||0):(rangeRaw?Math.max(0,parseInt(rangeRaw,10)||0):7);
|
|
651
670
|
const fdRaw=params.get('fd');
|
|
652
671
|
const fd=fdRaw==='all'?'all':Math.min(9,Math.max(1,parseInt(fdRaw||'2',10)||2));
|
|
653
|
-
return {view:(path==='timeline'||params.get('view')==='timeline')?'time':'graph', layout:params.get('layout')==='dep'?'dep':'priority', mode:params.get('mode')==='detail'?'detail':'simple', range, zoom, sx, window:win, q:params.get('q')||'',
|
|
672
|
+
return {view:(path==='timeline'||params.get('view')==='timeline')?'time':'graph', layout:params.get('layout')==='dep'?'dep':'priority', mode:params.get('mode')==='detail'?'detail':'simple', range, zoom, sx, window:win, q:params.get('q')||'', sel:(params.get('sel')||params.get('focus')||'').split(',').map(x=>x.trim()).filter(x=>x&&tasks.some(t=>t.id===x)), fd, from:toOff(fromRaw), to:toOff(toRaw), task:params.get('task'), tab:params.get('tab')==='intervene'?'intervene':'task'};
|
|
654
673
|
}
|
|
655
674
|
function routeString(view,mode,range,zoom,from,to){
|
|
656
675
|
const p=view==='time'?'timeline':'graph'; const qs=new URLSearchParams();
|
|
@@ -659,7 +678,7 @@ function routeString(view,mode,range,zoom,from,to){
|
|
|
659
678
|
if(view==='graph'){
|
|
660
679
|
if(state.window!==7) qs.set('w',String(state.window));
|
|
661
680
|
if(state.q&&state.q.trim()) qs.set('q',state.q.trim());
|
|
662
|
-
if(state.
|
|
681
|
+
if(state.sel.length){ qs.set('sel',state.sel.join(',')); if(state.fd!==2) qs.set('fd',String(state.fd)); }
|
|
663
682
|
}
|
|
664
683
|
if(view==='time'){
|
|
665
684
|
if(from!==null&&to!==null){ qs.set('from',isoDate(from)); qs.set('to',isoDate(to)); }
|
|
@@ -669,7 +688,7 @@ function routeString(view,mode,range,zoom,from,to){
|
|
|
669
688
|
return '#/'+p+(qs.toString()?'?'+qs.toString():'');
|
|
670
689
|
}
|
|
671
690
|
function applyRoute(){
|
|
672
|
-
const r=readRoute(); state.view=r.view; state.layout=r.layout; state.mode=r.mode; state.range=r.range; state.zoom=r.zoom; state.sx=r.sx||0; state.window=r.window; state.q=r.q; state.
|
|
691
|
+
const r=readRoute(); state.view=r.view; state.layout=r.layout; state.mode=r.mode; state.range=r.range; state.zoom=r.zoom; state.sx=r.sx||0; state.window=r.window; state.q=r.q; state.sel=r.sel; state.fd=r.fd; state.from=r.from; state.to=r.to;
|
|
673
692
|
document.querySelectorAll('#tabs button').forEach(x=>x.classList.toggle('on',x.dataset.v===r.view));
|
|
674
693
|
document.querySelectorAll('#mode button').forEach(x=>x.classList.toggle('on',x.dataset.m===r.mode));
|
|
675
694
|
document.querySelectorAll('#layout button').forEach(x=>x.classList.toggle('on',x.dataset.l===r.layout));
|
|
@@ -683,8 +702,10 @@ function applyRoute(){
|
|
|
683
702
|
const winSel=document.getElementById('win'); if(winSel) winSel.value=String(r.window);
|
|
684
703
|
const qIn=document.getElementById('q'); if(qIn && qIn.value!==r.q) qIn.value=r.q;
|
|
685
704
|
const chip=document.getElementById('focus-chip');
|
|
686
|
-
if(chip)
|
|
705
|
+
if(chip) chip.classList.toggle('on',r.sel.length>0);
|
|
706
|
+
if(chip) chip.title='按依赖邻域聚焦;清空即回全量';
|
|
687
707
|
const fdEl=document.getElementById('focus-fd'); if(fdEl) fdEl.value=String(r.fd);
|
|
708
|
+
renderTokens();
|
|
688
709
|
const rp=document.getElementById('route'); if(rp) rp.textContent=location.hash||routeString(r.view,r.mode,r.range,state.zoom,state.from,state.to);
|
|
689
710
|
document.title='dia graph · '+(r.view==='time'?'时间线':'关系图');
|
|
690
711
|
if(r.view==='time'){ drawTimeline(); const sc=document.getElementById('tl-scroll'), lb=document.getElementById('tl-labels-body'); sc.scrollTop=0; lb.style.transform='translateY(0px)'; if(state.sx) sc.scrollLeft=state.sx; } else drawGraph();
|
|
@@ -705,17 +726,20 @@ if(qInput){
|
|
|
705
726
|
else if(e.key==='Enter'){
|
|
706
727
|
const items=searchListItems();
|
|
707
728
|
const active=items.find(el=>el.classList.contains('active'))||items[0];
|
|
708
|
-
if(active)
|
|
729
|
+
if(active) toggleSel(active.dataset.id);
|
|
709
730
|
} else if(e.key==='Escape'){ state.q=''; qInput.value=''; applySearch(); qInput.blur(); }
|
|
731
|
+
else if(e.key==='Backspace' && !qInput.value && state.sel.length){ e.preventDefault(); toggleSel(state.sel[state.sel.length-1]); }
|
|
710
732
|
};
|
|
711
733
|
qInput.onfocus=()=>{ renderSearchList(searchMatches()); };
|
|
712
734
|
qInput.onclick=()=>{ renderSearchList(searchMatches()); };
|
|
713
|
-
|
|
735
|
+
const sbox=document.getElementById('search-box');
|
|
736
|
+
if(sbox) sbox.onclick=e=>{ if(e.target.closest('.q-item')||e.target.closest('.token')) return; qInput.focus(); };
|
|
737
|
+
qInput.onblur=()=>{ setTimeout(()=>{ const box=document.getElementById('q-list'); if(box && !document.getElementById('search-box').contains(document.activeElement)) box.classList.remove('open'); },160); };
|
|
714
738
|
}
|
|
715
739
|
const fdSel=document.getElementById('focus-fd');
|
|
716
740
|
if(fdSel) fdSel.onchange=()=>{ state.fd=fdSel.value==='all'?'all':Number(fdSel.value); go(state.view,state.mode,state.range,state.zoom); };
|
|
717
741
|
const fxBtn=document.getElementById('focus-exit');
|
|
718
|
-
if(fxBtn) fxBtn.onclick=()=>{ state.
|
|
742
|
+
if(fxBtn) fxBtn.onclick=()=>{ state.sel=[]; go(state.view,state.mode,state.range,state.zoom); };
|
|
719
743
|
document.querySelectorAll('#mode button').forEach(b=>b.onclick=()=>go(state.view,b.dataset.m,state.range,state.zoom));
|
|
720
744
|
document.querySelectorAll('#zoom button').forEach(b=>b.onclick=()=>{ const z=b.dataset.z==='+'?Math.min(84,state.zoom+8):b.dataset.z==='-'?Math.max(14,state.zoom-8):28; go('time',state.mode,state.range,z); });
|
|
721
745
|
layer.addEventListener('scroll',syncLaneLabels,{passive:true});
|