@minonl/dia 0.0.12 → 0.0.14

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/dist/cli.js CHANGED
@@ -1898,11 +1898,12 @@ function assertInside(base, p) {
1898
1898
  throw new Error(`E_MANIFEST: source escapes kit: ${p}`);
1899
1899
  return abs;
1900
1900
  }
1901
+ var TREE_SKIP = new Set([".git", "node_modules", ".data"]);
1901
1902
  function walkFiles(root) {
1902
1903
  const out = [];
1903
1904
  (function rec(dir) {
1904
1905
  for (const e of readdirSync3(dir, { withFileTypes: true })) {
1905
- if (e.name === ".git" || e.name === "node_modules" || e.name === "kit.lock")
1906
+ if (TREE_SKIP.has(e.name) || e.name === "kit.lock")
1906
1907
  continue;
1907
1908
  const p = join10(dir, e.name);
1908
1909
  if (e.isDirectory())
@@ -2108,7 +2109,7 @@ function install(from, opts = {}) {
2108
2109
  if (!alreadyInstalled || hashTree(src.dir) !== hashTree(target)) {
2109
2110
  rmSync3(target, { recursive: true, force: true });
2110
2111
  mkdirSync7(target, { recursive: true });
2111
- cpSync(src.dir, target, { recursive: true, dereference: true, filter: (p) => !p.split(sep4).includes(".git") && !p.split(sep4).includes("node_modules") });
2112
+ cpSync(src.dir, target, { recursive: true, dereference: true, filter: (p) => !p.split(sep4).some((s) => TREE_SKIP.has(s)) });
2112
2113
  }
2113
2114
  const rendered = renderContent(src.dir, target, m) ?? planned;
2114
2115
  const appliedConfig = apply(m.config ?? []);
@@ -2710,7 +2711,7 @@ function walkFiles2(root) {
2710
2711
  const out = [];
2711
2712
  (function rec(dir) {
2712
2713
  for (const e of readdirSync6(dir, { withFileTypes: true })) {
2713
- if (e.name === ".git" || e.name === "node_modules")
2714
+ if (TREE_SKIP.has(e.name))
2714
2715
  continue;
2715
2716
  const p = join13(dir, e.name);
2716
2717
  if (e.isDirectory())
@@ -2880,7 +2881,7 @@ function install2(from, opts = {}) {
2880
2881
  const staging = join13(idDir, `.staging-${process.pid}-${Date.now().toString(36)}`);
2881
2882
  rmSync4(staging, { recursive: true, force: true });
2882
2883
  mkdirSync10(staging, { recursive: true });
2883
- cpSync2(src.dir, staging, { recursive: true, dereference: true, filter: (p) => !p.split(sep5).includes(".git") && !p.split(sep5).includes("node_modules") });
2884
+ cpSync2(src.dir, staging, { recursive: true, dereference: true, filter: (p) => !p.split(sep5).some((s) => TREE_SKIP.has(s)) });
2884
2885
  const stagingHash = hashTree2(staging);
2885
2886
  let reuseTarget = false;
2886
2887
  if (existsSync11(target)) {
@@ -4775,12 +4776,29 @@ function build(opts = {}) {
4775
4776
  // src/graph-tui.ts
4776
4777
  var ORDER = ["doing", "blocked", "review", "todo", "awaiting_acceptance", "accepted", "done", "dropped", "rejected", "escalated"];
4777
4778
  var color = (s, code, on) => on ? `\x1B[${code}m${s}\x1B[0m` : s;
4779
+ var clip2 = (s, n) => {
4780
+ const one = String(s || "").replace(/\s+/g, " ").trim();
4781
+ return one.length > n ? `${one.slice(0, n - 1)}…` : one;
4782
+ };
4783
+ var shortId = (id) => id ? id.replace(/^ses_/, "").slice(-8) : "";
4778
4784
  function render2(g, opts = {}) {
4779
4785
  const on = opts.color ?? false;
4780
- const tasks = g.nodes.filter((n) => n.kind === "task");
4781
- const taskById = new Map(tasks.map((t) => [t.id, t]));
4786
+ const width = Math.max(32, opts.width ?? 96);
4787
+ const tasksAll = g.nodes.filter((n) => n.kind === "task");
4788
+ const tasks = tasksAll.filter((t) => (!opts.status || t.status === opts.status) && (!opts.owner || t.owner === opts.owner));
4789
+ const taskById = new Map(tasksAll.map((t) => [t.id, t]));
4782
4790
  const edgesFrom = (id, kind) => g.edges.filter((e) => e.from === id && e.kind === kind);
4783
4791
  const lines = [];
4792
+ const counts = new Map;
4793
+ for (const t of tasksAll)
4794
+ counts.set(t.status || "todo", (counts.get(t.status || "todo") || 0) + 1);
4795
+ const countText = [...ORDER.filter((s) => counts.has(s)), ...[...counts.keys()].filter((s) => !ORDER.includes(s))].map((s) => `${s}=${counts.get(s)}`).join(" ");
4796
+ const claims = g.edges.filter((e) => e.kind === "claims");
4797
+ const filter = [opts.status ? `status=${opts.status}` : "", opts.owner ? `owner=${opts.owner}` : ""].filter(Boolean).join(" ");
4798
+ lines.push(color(`dia graph tasks=${tasksAll.length} ${countText} claims=${claims.length}${filter ? ` filter: ${filter}` : ""}`, "1", on));
4799
+ if (opts.now)
4800
+ lines.push(color(`updated ${opts.now.toLocaleTimeString()}`, "2", on));
4801
+ lines.push("");
4784
4802
  const groups = new Map;
4785
4803
  for (const t of tasks) {
4786
4804
  const st = t.status || "todo";
@@ -4789,34 +4807,41 @@ function render2(g, opts = {}) {
4789
4807
  for (const st of [...ORDER.filter((s) => groups.has(s)), ...[...groups.keys()].filter((s) => !ORDER.includes(s))]) {
4790
4808
  lines.push(color(`[${st}]`, "1;36", on));
4791
4809
  for (const t of groups.get(st).sort((a, b) => a.label.localeCompare(b.label))) {
4792
- lines.push(` ${color(t.label, "1", on)} ${color(t.id.replace(/^task:/, ""), "2", on)}${t.owner ? ` owner=${t.owner}` : ""}`);
4810
+ const owner = t.owner ? ` @${shortId(t.owner)}` : "";
4811
+ const deps = edgesFrom(t.id, "depends_on").length;
4812
+ lines.push(` ${color("●", "32", on)} ${color(clip2(t.label, width - 24), "1", on)} ${color(shortId(t.id.replace(/^task:/, "")), "2", on)}${owner}${deps ? ` deps=${deps}` : ""}`);
4793
4813
  for (const e of edgesFrom(t.id, "depends_on")) {
4794
4814
  const dep = taskById.get(e.to);
4795
- lines.push(` ${color("↳ depends", "34", on)} ${dep?.label || e.to}${dep ? ` [${dep.status}]` : ""}`);
4815
+ lines.push(` ${color("↳ depends", "34", on)} ${clip2(dep?.label || e.to, width - 20)}${dep ? ` [${dep.status}]` : ""}`);
4796
4816
  }
4797
4817
  for (const e of edgesFrom(t.id, "touches"))
4798
- lines.push(` ${color("↳ touches", "35", on)} ${e.to.replace(/^path:/, "")}`);
4799
- if (t.blocked_by?.length)
4800
- lines.push(` ${color(`⚠ blocked_by ${t.blocked_by.join(", ")}`, "31", on)}`);
4818
+ lines.push(` ${color("↳ touches", "35", on)} ${clip2(e.to.replace(/^path:/, ""), width - 20)}`);
4819
+ if (t.blocked_by?.length) {
4820
+ const names = t.blocked_by.map((id) => taskById.get(`task:${id}`)?.label || id);
4821
+ lines.push(` ${color(`⚠ blocked_by ${names.map((x) => clip2(x, 40)).join(", ")}`, "31", on)}`);
4822
+ }
4801
4823
  }
4802
4824
  lines.push("");
4803
4825
  }
4804
4826
  if (!tasks.length)
4805
- lines.push("(no tasks)");
4806
- const claimEdges = g.edges.filter((e) => e.kind === "claims");
4807
- if (claimEdges.length) {
4827
+ lines.push(opts.status || opts.owner ? "(no matching tasks)" : "(no tasks)");
4828
+ if (claims.length) {
4808
4829
  lines.push(color("claims", "1;36", on));
4809
- for (const e of claimEdges)
4810
- lines.push(` ${e.from.replace(/^session:/, "")} → ${e.to.replace(/^path:/, "")}`);
4830
+ for (const e of claims) {
4831
+ const holder = e.from.replace(/^session:/, "");
4832
+ if (opts.owner && holder !== opts.owner)
4833
+ continue;
4834
+ lines.push(` ${shortId(holder)} → ${clip2(e.to.replace(/^path:/, ""), width - 16)}`);
4835
+ }
4811
4836
  lines.push("");
4812
4837
  }
4813
4838
  for (const c of g.cycles)
4814
- lines.push(color(`⚠ cycle: ${c.join(" -> ")}`, "31", on));
4839
+ lines.push(color(`⚠ cycle: ${c.map((x) => x.replace(/^task:/, "")).join(" -> ")}`, "31", on));
4815
4840
  return lines.join(`
4816
4841
  `);
4817
4842
  }
4818
4843
  function watch(build, opts = {}) {
4819
- const paint = () => process.stdout.write("\x1B[2J\x1B[H" + render2(build(), { color: opts.color ?? process.stdout.isTTY }) + `
4844
+ const paint = () => process.stdout.write("\x1B[2J\x1B[H" + render2(build(), { ...opts, now: new Date }) + `
4820
4845
  `);
4821
4846
  paint();
4822
4847
  const timer = setInterval(() => {
@@ -8564,7 +8589,7 @@ usage:
8564
8589
  dia agents list|register|unregister|discover
8565
8590
  dia msg deliver <agent> <id> [--transport tmux|acp]
8566
8591
  dia task add|list|link|status|done [--owner S] [--depends a,b] [--touches p1,p2]
8567
- dia graph [--json] [--task t-...] [--tui [--watch] [--no-color]]
8592
+ dia graph [--json] [--task t-...] [--tui [--watch] [--status S] [--owner S] [--width N] [--no-color]]
8568
8593
  dia claim <path> [--reason R] [--ttl 4h] [--session S] | dia claims [--all] [--mine] [--gc [--days 7]] | dia unclaim <path|id> [--force]
8569
8594
  dia team run <id> --mock | --acp|--dsh --workspace DIR
8570
8595
  dia team gate|accept|reject|next
@@ -8958,10 +8983,17 @@ async function taskCli(argv) {
8958
8983
  async function graphCli(argv) {
8959
8984
  const asJson = has(argv, "--json");
8960
8985
  if (has(argv, "--tui") || has(argv, "--watch")) {
8961
- const paint = (g) => out(render2(g, { color: !!process.stdout.isTTY && !has(argv, "--no-color") }));
8986
+ const width = Number(flag(argv, "--width") || 0);
8987
+ const tuiOpts = {
8988
+ color: !!process.stdout.isTTY && !has(argv, "--no-color"),
8989
+ status: flag(argv, "--status"),
8990
+ owner: flag(argv, "--owner"),
8991
+ width: width > 0 ? width : undefined
8992
+ };
8993
+ const paint = (g) => out(render2(g, tuiOpts));
8962
8994
  try {
8963
8995
  if (has(argv, "--watch"))
8964
- return await watch(() => build({ task: flag(argv, "--task") }), { color: !!process.stdout.isTTY && !has(argv, "--no-color") });
8996
+ return await watch(() => build({ task: flag(argv, "--task") }), tuiOpts);
8965
8997
  const g = build({ task: flag(argv, "--task") });
8966
8998
  paint(g);
8967
8999
  return g.cycles.length ? 3 : 0;
@@ -0,0 +1,220 @@
1
+ <!doctype html>
2
+ <html lang="zh">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <title>dia graph · 视觉设计稿</title>
7
+ <style>
8
+ :root{
9
+ --bg:#f6f7f9; --panel:#fff; --ink:#111827; --muted:#6b7280; --line:#e5e7eb;
10
+ --blue:#2563eb; --violet:#7c3aed; --cyan:#0891b2; --green:#16a34a; --red:#dc2626; --amber:#d97706;
11
+ --shadow:0 1px 2px rgba(17,24,39,.06),0 8px 24px rgba(17,24,39,.06);
12
+ }
13
+ @media (prefers-color-scheme: dark){
14
+ :root{ --bg:#0f1115; --panel:#171a21; --ink:#e5e7eb; --muted:#9ca3af; --line:#2a2f3a;
15
+ --shadow:0 1px 2px rgba(0,0,0,.4),0 8px 24px rgba(0,0,0,.25); }
16
+ }
17
+ *{box-sizing:border-box}
18
+ body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif}
19
+ header{display:flex;align-items:center;gap:16px;padding:16px 22px;border-bottom:1px solid var(--line);background:var(--panel)}
20
+ header h1{font-size:17px;margin:0;letter-spacing:.2px}
21
+ header .sub{color:var(--muted);font-size:12px}
22
+ header .spacer{flex:1}
23
+ .pill{border:1px solid var(--line);border-radius:999px;padding:4px 10px;font-size:12px;color:var(--muted);background:var(--bg)}
24
+ main{display:grid;grid-template-columns:minmax(0,1fr) 340px;gap:18px;padding:18px;max-width:1480px;margin:0 auto}
25
+ .panel{background:var(--panel);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow)}
26
+ .toolbar{display:flex;gap:8px;align-items:center;padding:12px 14px;border-bottom:1px solid var(--line);flex-wrap:wrap}
27
+ .seg{display:flex;border:1px solid var(--line);border-radius:9px;overflow:hidden}
28
+ .seg button{border:0;background:transparent;color:var(--muted);padding:6px 10px;font:inherit;font-size:12px;cursor:pointer}
29
+ .seg button.on{background:var(--blue);color:#fff}
30
+ .toolbar .hint{margin-left:auto;color:var(--muted);font-size:12px}
31
+ .canvas{position:relative;height:640px;overflow:auto;padding:10px}
32
+ svg.edges{position:absolute;inset:0;pointer-events:none}
33
+ svg.edges line{stroke:#94a3b8;stroke-width:1.2;opacity:.8}
34
+ svg.edges line.depends_on{stroke:var(--blue)}
35
+ svg.edges line.touches{stroke:var(--violet);stroke-dasharray:3 4}
36
+ svg.edges line.claims{stroke:var(--cyan);stroke-dasharray:5 4}
37
+ .node{position:absolute;width:206px;border:1px solid var(--line);border-left:4px solid var(--muted);border-radius:10px;background:var(--panel);padding:9px 11px;box-shadow:var(--shadow);cursor:pointer;transition:transform .12s ease,box-shadow .12s ease}
38
+ .node:hover{transform:translateY(-1px);box-shadow:0 10px 28px rgba(17,24,39,.12)}
39
+ .node.task.doing{border-left-color:var(--blue)}
40
+ .node.task.blocked{border-left-color:var(--red)}
41
+ .node.task.review{border-left-color:var(--amber)}
42
+ .node.task.done,.node.task.accepted{border-left-color:var(--green)}
43
+ .node.path{border-left-color:var(--violet)}
44
+ .node.session{border-left-color:var(--cyan)}
45
+ .node b{display:block;font-size:13px;line-height:1.35;margin-bottom:3px}
46
+ .node .meta{color:var(--muted);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
47
+ .badge{display:inline-block;font-size:10px;padding:1px 6px;border-radius:999px;border:1px solid var(--line);color:var(--muted);margin-top:4px}
48
+ .col-label{position:absolute;top:10px;font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
49
+ aside{display:flex;flex-direction:column;gap:14px}
50
+ .side-title{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;border-bottom:1px solid var(--line)}
51
+ .side-title h2{font-size:13px;margin:0}
52
+ .side-title span{font-size:11px;color:var(--muted)}
53
+ .list{padding:6px 14px 12px}
54
+ .item{display:flex;justify-content:space-between;gap:10px;padding:8px 0;border-bottom:1px dashed var(--line);font-size:12px}
55
+ .item:last-child{border-bottom:0}
56
+ .item .k{color:var(--muted)}
57
+ .dot{display:inline-block;width:7px;height:7px;border-radius:99px;background:var(--muted);margin-right:6px;vertical-align:1px}
58
+ .dot.blue{background:var(--blue)} .dot.red{background:var(--red)} .dot.green{background:var(--green)} .dot.amber{background:var(--amber)}
59
+ .legend{padding:10px 14px 14px;color:var(--muted);font-size:12px;display:grid;gap:6px}
60
+ .legend i{display:inline-block;width:18px;height:2px;margin-right:8px;vertical-align:3px}
61
+ .attention{padding:8px 14px 14px}
62
+ .attention .row{display:flex;gap:8px;align-items:flex-start;padding:8px 0;border-bottom:1px dashed var(--line);font-size:12px}
63
+ .attention .row:last-child{border-bottom:0}
64
+ .tag{font-size:10px;border-radius:6px;padding:1px 6px;background:rgba(220,38,38,.12);color:var(--red);white-space:nowrap}
65
+ .tag.amber{background:rgba(217,119,6,.12);color:var(--amber)}
66
+ .detail{padding:14px;display:none}
67
+ .detail.show{display:block}
68
+ .detail h3{font-size:14px;margin:0 0 6px}
69
+ .detail p{margin:0 0 8px;color:var(--muted);font-size:12px}
70
+ .kv{display:grid;grid-template-columns:80px 1fr;gap:4px 10px;font-size:12px}
71
+ .kv .k{color:var(--muted)}
72
+ footer{padding:0 22px 22px;color:var(--muted);font-size:11px}
73
+ @media (max-width:980px){ main{grid-template-columns:1fr} .canvas{height:560px} }
74
+ </style>
75
+ </head>
76
+ <body>
77
+ <header>
78
+ <h1>dia graph</h1>
79
+ <span class="sub">只读依赖图 · 视觉设计稿 v0.1</span>
80
+ <span class="pill">静态 mockup · 无网络</span>
81
+ <span class="spacer"></span>
82
+ <span class="pill">updated 18:42</span>
83
+ </header>
84
+
85
+ <main>
86
+ <section class="panel">
87
+ <div class="toolbar">
88
+ <div class="seg" id="filter">
89
+ <button class="on" data-f="all">全部</button>
90
+ <button data-f="doing">进行中</button>
91
+ <button data-f="blocked">阻塞</button>
92
+ <button data-f="review">待评审</button>
93
+ </div>
94
+ <div class="seg" id="density">
95
+ <button class="on" data-d="comfortable">舒适</button>
96
+ <button data-d="compact">紧凑</button>
97
+ </div>
98
+ <span class="hint">点击节点查看详情 · 边:依赖 / 触碰 / claim</span>
99
+ </div>
100
+ <div class="canvas" id="canvas">
101
+ <svg class="edges" id="edges" width="1200" height="640"></svg>
102
+ <div class="col-label" style="left:18px">sessions</div>
103
+ <div class="col-label" style="left:250px">todo</div>
104
+ <div class="col-label" style="left:470px">doing</div>
105
+ <div class="col-label" style="left:690px">review</div>
106
+ <div class="col-label" style="left:910px">paths</div>
107
+ </div>
108
+ </section>
109
+
110
+ <aside>
111
+ <div class="panel">
112
+ <div class="side-title"><h2>概览</h2><span>live data</span></div>
113
+ <div class="list">
114
+ <div class="item"><span class="k"><i class="dot blue"></i>任务</span><b>7</b></div>
115
+ <div class="item"><span class="k"><i class="dot red"></i>阻塞</span><b>1</b></div>
116
+ <div class="item"><span class="k"><i class="dot amber"></i>待评审</span><b>1</b></div>
117
+ <div class="item"><span class="k"><i class="dot green"></i>完成</span><b>2</b></div>
118
+ <div class="item"><span class="k">claims</span><b>3</b></div>
119
+ </div>
120
+ </div>
121
+
122
+ <div class="panel">
123
+ <div class="side-title"><h2>需要人</h2><span>attention</span></div>
124
+ <div class="attention">
125
+ <div class="row"><span class="tag">阻塞</span><div><b>install.sh 迁移</b><br><span class="k">等待数据根切换完成</span></div></div>
126
+ <div class="row"><span class="tag amber">claim</span><div><b>src/collab.ts</b><br><span class="k">session-a 持有中</span></div></div>
127
+ <div class="row"><span class="tag amber">评审</span><div><b>graph TUI</b><br><span class="k">等待独立 verifier</span></div></div>
128
+ </div>
129
+ </div>
130
+
131
+ <div class="panel">
132
+ <div class="side-title"><h2>图例</h2><span>legend</span></div>
133
+ <div class="legend">
134
+ <div><i style="background:var(--blue)"></i>depends_on(任务依赖)</div>
135
+ <div><i style="background:var(--violet)"></i>touches(触碰路径)</div>
136
+ <div><i style="background:var(--cyan)"></i>claims(会话声明)</div>
137
+ </div>
138
+ </div>
139
+
140
+ <div class="panel detail" id="detail">
141
+ <div class="side-title"><h2>节点详情</h2><button onclick="document.getElementById('detail').classList.remove('show')" style="border:0;background:transparent;color:var(--muted);cursor:pointer">关闭</button></div>
142
+ <div class="detail" style="display:block" id="detail-body"></div>
143
+ </div>
144
+ </aside>
145
+ </main>
146
+ <footer>视觉稿:布局 / 信息层级 / 边语义。真实实现需先解除「框架无网络模块」限制并单独评审。</footer>
147
+
148
+ <script>
149
+ const nodes = [
150
+ {id:'session:ses_a', kind:'session', label:'session-a', x:20, y:60},
151
+ {id:'session:ses_b', kind:'session', label:'session-b', x:20, y:200},
152
+ {id:'task:t-1', kind:'task', status:'todo', label:'数据根迁移', x:240, y:40, owner:'ses_a'},
153
+ {id:'task:t-2', kind:'task', status:'doing', label:'install.sh 拆分', x:460, y:40, owner:'ses_a'},
154
+ {id:'task:t-3', kind:'task', status:'blocked', label:'launchd 安装脚本', x:680, y:40, owner:'ses_b'},
155
+ {id:'task:t-4', kind:'task', status:'review', label:'graph TUI', x:680, y:190, owner:'ses_b'},
156
+ {id:'task:t-5', kind:'task', status:'done', label:'dia msg M0', x:240, y:190},
157
+ {id:'task:t-6', kind:'task', status:'accepted', label:'claim 机制', x:240, y:320},
158
+ {id:'task:t-7', kind:'task', status:'doing', label:'handoff + claim', x:460, y:320, owner:'ses_a'},
159
+ {id:'path:install.sh', kind:'path', label:'install.sh', x:900, y:40},
160
+ {id:'path:src/collab.ts', kind:'path', label:'src/collab.ts', x:900, y:180},
161
+ {id:'path:docs/msg.md', kind:'path', label:'docs/msg.md', x:900, y:320}
162
+ ];
163
+ const edges = [
164
+ {from:'task:t-2', to:'task:t-1', kind:'depends_on'},
165
+ {from:'task:t-3', to:'task:t-2', kind:'depends_on'},
166
+ {from:'task:t-7', to:'task:t-6', kind:'depends_on'},
167
+ {from:'task:t-2', to:'path:install.sh', kind:'touches'},
168
+ {from:'task:t-4', to:'path:src/collab.ts', kind:'touches'},
169
+ {from:'task:t-7', to:'path:src/collab.ts', kind:'touches'},
170
+ {from:'session:ses_a', to:'path:src/collab.ts', kind:'claims'},
171
+ {from:'session:ses_b', to:'path:install.sh', kind:'claims'}
172
+ ];
173
+ const canvas = document.getElementById('canvas');
174
+ const svg = document.getElementById('edges');
175
+ const byId = new Map(nodes.map(n=>[n.id,n]));
176
+ function card(n){
177
+ const el=document.createElement('div');
178
+ el.className='node '+n.kind+(n.status?' '+n.status:'');
179
+ el.style.left=n.x+'px'; el.style.top=n.y+'px'; el.dataset.id=n.id;
180
+ const meta = n.kind==='task' ? (n.owner?'@'+n.owner:'unassigned') : n.id.split(':')[0];
181
+ el.innerHTML='<b>'+esc(n.label)+'</b><div class="meta">'+esc(n.id)+'</div>'+
182
+ (n.status?'<span class="badge">'+esc(n.status)+'</span>':'')+
183
+ '<div class="meta" style="margin-top:4px">'+esc(meta)+'</div>';
184
+ el.onclick=()=>show(n);
185
+ return el;
186
+ }
187
+ function esc(s){return String(s??'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]))}
188
+ function draw(){
189
+ [...canvas.querySelectorAll('.node')].forEach(e=>e.remove());
190
+ nodes.forEach(n=>canvas.appendChild(card(n)));
191
+ const maxX=Math.max(...nodes.map(n=>n.x))+240, maxY=Math.max(...nodes.map(n=>n.y))+120;
192
+ svg.setAttribute('width',maxX); svg.setAttribute('height',maxY);
193
+ svg.innerHTML=edges.map(e=>{
194
+ const a=byId.get(e.from), b=byId.get(e.to); if(!a||!b) return '';
195
+ return '<line class="'+e.kind+'" x1="'+(a.x+103)+'" y1="'+(a.y+30)+'" x2="'+(b.x+103)+'" y2="'+(b.y+30)+'" />';
196
+ }).join('');
197
+ }
198
+ function show(n){
199
+ const d=document.getElementById('detail'), body=document.getElementById('detail-body');
200
+ const out=edges.filter(e=>e.from===n.id||e.to===n.id).map(e=>{
201
+ const other=e.from===n.id?e.to:e.from;
202
+ return '<div class="item"><span class="k">'+e.kind+'</span><span>'+esc(other)+'</span></div>';
203
+ }).join('') || '<p class="muted">无边</p>';
204
+ body.innerHTML='<h3>'+esc(n.label)+'</h3><p>'+esc(n.id)+'</p><div class="kv"><div class="k">kind</div><div>'+esc(n.kind)+'</div><div class="k">status</div><div>'+esc(n.status||'-')+'</div><div class="k">owner</div><div>'+esc(n.owner||'-')+'</div></div><h3 style="margin-top:14px">关联</h3>'+out;
205
+ d.classList.add('show');
206
+ }
207
+ document.querySelectorAll('#filter button').forEach(b=>b.onclick=()=>{
208
+ document.querySelectorAll('#filter button').forEach(x=>x.classList.remove('on')); b.classList.add('on');
209
+ const f=b.dataset.f;
210
+ nodes.forEach(n=>{ if(n.kind==='task'){ const el=document.querySelector('[data-id="'+n.id+'"]'); if(el) el.style.display=(f==='all'||n.status===f)?'':'none'; }});
211
+ });
212
+ document.querySelectorAll('#density button').forEach(b=>b.onclick=()=>{
213
+ document.querySelectorAll('#density button').forEach(x=>x.classList.remove('on')); b.classList.add('on');
214
+ const compact=b.dataset.d==='compact';
215
+ document.querySelectorAll('.node').forEach(el=>{el.style.padding=compact?'5px 8px':'9px 11px';el.style.width=compact?'180px':'206px';});
216
+ });
217
+ draw();
218
+ </script>
219
+ </body>
220
+ </html>
@@ -0,0 +1,61 @@
1
+ # dia graph Web 视觉设计稿(v0.1)
2
+
3
+ > 静态 mockup:[`graph-web-mockup.html`](graph-web-mockup.html)(直接用浏览器打开,无网络/无依赖)
4
+ > 数据契约:`dia graph --json`(schema 1);本稿只做视觉与信息层级,不是实现。
5
+
6
+ ## 1. 布局
7
+
8
+ ```
9
+ ┌ header: dia graph · 只读 · 更新时间 ──────────────────────────┐
10
+ ├ 工具栏:状态过滤 | 密度 | 提示 │
11
+ ├ 主画布(卡片 + SVG 边) ┌ 右侧 320px ────┐
12
+ │ sessions | todo | doing | review | paths │ 概览 / 需要人 │
13
+ │ │ 图例 / 节点详情 │
14
+ └ footer: 数据来源 / 约束 ───────────────────┴────────────────┘
15
+ ```
16
+
17
+ - 主画布按状态分列:session → todo → doing → review → path;边的方向从左到右。
18
+ - 右侧固定 320px:概览计数、attention(阻塞/claim/待评审)、图例、节点详情。
19
+ - 节点卡片颜色只表达 **kind + status**,不用于装饰。
20
+
21
+ ## 2. 信息层级
22
+
23
+ | 层 | 内容 | 设计 |
24
+ |---|---|---|
25
+ | 第一层 | task 标题 + status | 加粗 13px;状态色只出现在左边框和 badge |
26
+ | 第二层 | task id / owner | 11px muted;owner 显示短 id(`@a1b2c3d4`) |
27
+ | 第三层 | 依赖/触碰/claim | SVG 边 + 右侧详情;不在卡片里堆文字 |
28
+ | attention | 阻塞 / claim 冲突 / 待评审 | 右上角固定区,不藏在 hover 里 |
29
+
30
+ ## 3. 交互
31
+
32
+ - 点击节点 → 右侧详情:kind/status/owner + 关联边列表;
33
+ - 顶部过滤:all / doing / blocked / review(客户端过滤,不改数据);
34
+ - 密度切换:comfortable / compact;
35
+ - 刷新:MVP 静态导出;live 版用 `dia graph --json` 轮询(2s),不做 WebSocket;
36
+ - 不做:编辑、拖拽改依赖、写 claim(只读)。
37
+
38
+ ## 4. 与现有数据契约的映射
39
+
40
+ | UI | `dia graph --json` |
41
+ |---|---|
42
+ | 卡片 | `nodes[]`:task/session/path |
43
+ | 依赖边 | `edges[kind=depends_on]` |
44
+ | 触碰边 | `edges[kind=touches]` |
45
+ | claim 边 | `edges[kind=claims]` |
46
+ | 阻塞提示 | task 节点 `blocked_by[]`(派生,不是边) |
47
+ | 环 | `cycles[]`,顶部红条提示 |
48
+
49
+ ## 5. 实现约束(重要)
50
+
51
+ 仓库的 `tests/static.test.ts` 禁止框架 import `node:http` / `fetch` / 网络模块。因此:
52
+
53
+ 1. **本稿只是静态 HTML**,不进入 `src/`,不启服务;
54
+ 2. 若将来要做真实 Web:
55
+ - 方案 A:`dia graph --export-html` 生成静态文件(推荐,不需要网络);
56
+ - 方案 B:独立可选包 `@dia/graph-web`(明确例外 + 只绑 loopback + 单独评审),不改核心框架规则。
57
+
58
+ ## 6. 后续
59
+
60
+ - 先确认视觉稿的列布局/信息密度/右侧 attention 位置;
61
+ - 确认后先做 `--export-html` 静态导出(零网络、零 daemon),再评估 live 轮询。
package/docs/index.md CHANGED
@@ -88,3 +88,9 @@ Agent 入口:[`llms.txt`](https://github.com/minonl/dia/blob/main/llms.txt)(
88
88
  |---|---|
89
89
  | [框架启动方案(bootstrap)](history/framework-bootstrap.md) | 已归档(2026-11-03):本文的启动/分发方案(release tarball + agentctl + framework.lock)未采用; |
90
90
 
91
+ ## 其他
92
+
93
+ | 文档 | 一句话 |
94
+ |---|---|
95
+ | [dia graph Web 视觉设计稿(v0.1)](design/graph-web.md) | 静态 mockup:graph-web-mockup.html(直接用浏览器打开,无网络/无依赖) |
96
+
package/docs/status.md CHANGED
@@ -90,6 +90,7 @@
90
90
  | D0.21 | worktree 抽取 `packages/wt` | **完成** | 7 处 python→node;`dia wt`;4 套测试原样迁移全绿 |
91
91
  | D0.23 | doctor 平台版本自检 | **完成** | `platform:opencode` / `platform:dsh` 报版本(缺宿主 = warn,不判红);`tests/active-doctor.test.ts` |
92
92
  | D0.44 | 配置迁移走 LLM 任务 | **完成** | `docs/migrations/llm-migration.md`:升级只改声明;迁移=任务+判据+回滚;禁止代码里堆版本分支(安全敏感/原子场景例外) |
93
+ | D0.63 | 运行时数据 `.data/` 不进快照、不参与哈希 | **完成** | `TREE_SKIP`(kit/persona 共用):安装复制与 `hashTree`/verify 都跳过 `.data`,防运行时写入造成 verify 假漂移 |
93
94
  | D0.62 | persona 旧版技能软链视为受管 | **完成** | `linkManaged` 同时认 `~/.dia/kits/**` 与 `~/.dia/personas/**`;`persona update` 升级不再需要 `--force` |
94
95
  | D0.61 | kit 空内容拒绝 + doctor 只查引用 kit | **完成** | `kit add` 拒绝空清单/空树(E_MANIFEST/E_KIT_EMPTY);doctor 按 persona lock 的 kits 校验,孤儿不再拖红 |
95
96
  | D0.60 | persona --force 覆盖内联内容 / update --force | **完成** | `kit.renderContent` 支持 force(写盘前备份到 `backups/force-*`);`persona update --force`;版本冲突错误给可执行提示;`tests/kit.test.ts` |
package/docs/worktree.md CHANGED
@@ -11,27 +11,29 @@ dia wt adopt <repo> --apply # 写入配置 + 三个守卫钩子
11
11
  dia wt adopt --all --apply # 批量:仓根下所有主 checkout(跳过 *-worktree--*)
12
12
  ```
13
13
 
14
- 写入内容(幂等):
14
+ 写入内容(幂等,**不改仓内文件、不写全局配置**):
15
15
 
16
- | 项 | 位置 |
17
- |---|---|
18
- | 策略 | `<git-common-dir>/dia/config.json`(`defaultBranch` / `template` / `hooksPath` / `hooksPrevious` / `hooksChain`) |
19
- | 钩子 | `<git-common-dir>/dia/hooks/{pre-commit,pre-push,post-checkout}` |
20
- | `core.hooksPath` | 指向上面目录,**必须是绝对路径**(相对路径在 worktree 会静默失效) |
16
+ | 仓类型 | 做法 | 记录 |
17
+ |---|---|---|
18
+ | 普通仓 | 钩子写 `<git-common-dir>/dia/hooks/`,并把**仓库本地** `core.hooksPath` 指过去(绝对路径) | `hooksManager=local` |
19
+ | **simple-git-hooks 仓** | 不碰 `.git/hooks`、不改 `package.json`;守卫写进 `SIMPLE_GIT_HOOKS_RC` 指向的脚本(钩子会 source 它) | `hooksManager=simple-git-hooks` |
20
+ | 已有其它 hooksPath(husky / `.githooks`) | **不抢占**,只报告(建议改用 simple-git-hooks) | `hooksManager=external` |
21
+
22
+ > 判定:`package.json` 里有 `simple-git-hooks`(配置或 devDependency)→ RC 模式;否则看 `core.hooksPath` 是否已被别人占用。
21
23
 
22
24
  守卫行为:
23
25
 
24
26
  | 钩子 | 规则 |
25
27
  |---|---|
26
28
  | `pre-commit` | 主 checkout 上、分支属于 protected(默认分支 + main/master/develop/dev)→ 拒绝 |
27
- | `pre-push` | 主 checkout 上**任何 push**(含 tag / feature 分支)→ 拒绝;推 `main/master/develop/dev` → 拒绝 |
29
+ | `pre-push` | 主 checkout 上**任何 push**(含 tag / feature 分支)→ 拒绝;推 `main/master/develop/dev` → 拒绝;并对推送内容做敏感信息扫描 |
28
30
  | `post-checkout` | 主 checkout 切到非默认分支 → 黄色提示(不阻断) |
29
31
 
30
- **钩子链**:`hooksChain`(首次 adopt 前的 hooksPath,如 `.husky` / `.githooks`)→ `<common>/hooks` → 默认放行。
31
- 所以 husky / simple-git-hooks / lefthook 的钩子不会被吞。
32
+ **钩子链**:本地钩子先跑守卫,再执行 `<common>/hooks/<name>`(simple-git-hooks 等管理器生成处);`pre-push` 的 stdin 原样转发。
33
+ `SIMPLE_GIT_HOOKS_RC` 由 adopt 写入 `~/.zshenv`(带 `# dia-wt:` 标记),脚本内容固定为 `dia wt guard <hook>`。
32
34
 
33
- > ⚠️ `simple-git-hooks` 的 `postinstall` 会重写 `core.hooksPath` 下的钩子。install 后若守卫被覆盖:
34
- > `dia wt doctor --fix`(或重跑 `dia wt adopt --apply`)。
35
+ > ⚠️ husky / `.githooks` 仓:本地 `core.hooksPath` 会覆盖一切,守卫不生效(doctor WARN)。
36
+ > 迁移办法:把钩子迁到 simple-git-hooks(`postinstall: simple-git-hooks` + `simple-git-hooks` 配置),再 `dia wt adopt --apply`。
35
37
 
36
38
  ## 2. 巡检
37
39
 
@@ -53,6 +55,8 @@ dia wt doctor --all --fix # 无损修复:重装三个钩子 + 校正 hooksPa
53
55
  | D06 | 无 prunable / locked |
54
56
  | D07 | 同一分支不被多个 worktree 占用 |
55
57
  | D11 | 默认分支可解析(config > origin/HEAD(校验仍存在)> main > master) |
58
+ | D20 | 全局 `core.hooksPath` 不得指向 dia 旧钩子目录(防外溢;有则 FAIL + 清理命令) |
59
+ | D21 | 仓库本地 `core.hooksPath` 与模式一致:local=dia 目录;external=只 WARN |
56
60
 
57
61
  挂成日巡检(mac launchd / linux cron 由 `dia job` 统一):
58
62
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minonl/dia",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
4
4
  "type": "module",
5
5
  "description": "dia (嗲) — lightweight agent framework: knowledge index, audit, session ingest",
6
6
  "bin": {