@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/dist/cli.js CHANGED
@@ -2136,11 +2136,42 @@ function install(from, opts = {}) {
2136
2136
  }, null, 1));
2137
2137
  return { id: m.id, version: m.version, dir: target, root: kitRoot, rendered, hash, changed: true };
2138
2138
  }
2139
+ function installed() {
2140
+ const root = join10(dataDir2(), "kits");
2141
+ if (!existsSync8(root))
2142
+ return [];
2143
+ const out = [];
2144
+ for (const e of readdirSync3(root, { withFileTypes: true })) {
2145
+ if (!e.isDirectory())
2146
+ continue;
2147
+ const versions = readdirSync3(join10(root, e.name)).filter((v) => statSync5(join10(root, e.name, v)).isDirectory()).sort();
2148
+ const version = versions[versions.length - 1];
2149
+ if (!version)
2150
+ continue;
2151
+ const dir = join10(root, e.name, version);
2152
+ let tags = [];
2153
+ try {
2154
+ tags = readKitManifest(dir).tags ?? [];
2155
+ } catch {}
2156
+ out.push({ id: e.name, version, dir, tags });
2157
+ }
2158
+ return out;
2159
+ }
2139
2160
  function list() {
2140
2161
  const root = join10(dataDir2(), "kits");
2141
2162
  if (!existsSync8(root))
2142
2163
  return [];
2143
- return readdirSync3(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => ({ id: e.name, versions: readdirSync3(join10(root, e.name)).filter((v) => statSync5(join10(root, e.name, v)).isDirectory()) }));
2164
+ return readdirSync3(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => {
2165
+ const versions = readdirSync3(join10(root, e.name)).filter((v) => statSync5(join10(root, e.name, v)).isDirectory()).sort();
2166
+ const latest = versions[versions.length - 1];
2167
+ let tags = [];
2168
+ if (latest) {
2169
+ try {
2170
+ tags = readKitManifest(join10(root, e.name, latest)).tags ?? [];
2171
+ } catch {}
2172
+ }
2173
+ return { id: e.name, versions, tags };
2174
+ });
2144
2175
  }
2145
2176
  function readLock(id, version) {
2146
2177
  const idDir = join10(dataDir2(), "kits", id);
@@ -4573,6 +4604,30 @@ import { appendFileSync as appendFileSync6, existsSync as existsSync15, mkdirSyn
4573
4604
  import { join as join17 } from "node:path";
4574
4605
  import { createHash as createHash9 } from "node:crypto";
4575
4606
  var DATA5 = () => diaHome();
4607
+ var PRIO_RE = /^P\d{1,2}$/;
4608
+ var DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
4609
+ function normPrio(v) {
4610
+ const s = String(v).trim().toUpperCase();
4611
+ if (!PRIO_RE.test(s))
4612
+ throw new Error(`E_TASK: prio 必须是 P0–P99(收到 ${v})`);
4613
+ return s;
4614
+ }
4615
+ function normDay(v) {
4616
+ const s = String(v).trim();
4617
+ if (DAY_RE.test(s)) {
4618
+ const [y, mo, d] = s.split("-").map(Number);
4619
+ const dt = new Date(y, mo - 1, d);
4620
+ if (dt.getFullYear() === y && dt.getMonth() === mo - 1 && dt.getDate() === d)
4621
+ return s;
4622
+ }
4623
+ throw new Error(`E_TASK: 日期必须是有效 YYYY-MM-DD(收到 ${v})`);
4624
+ }
4625
+ function normProgress(v) {
4626
+ const n = Number(v);
4627
+ if (!Number.isInteger(n) || n < 0 || n > 100)
4628
+ throw new Error(`E_TASK: progress 必须是 0–100 的整数(收到 ${v})`);
4629
+ return n;
4630
+ }
4576
4631
  function file2() {
4577
4632
  return join17(DATA5(), "tasks", "ledger.jsonl");
4578
4633
  }
@@ -4603,10 +4658,39 @@ function fold2() {
4603
4658
  deps: uniq(e.deps || []),
4604
4659
  touches: uniq(e.touches || []),
4605
4660
  created_at: e.ts,
4606
- updated_at: e.ts
4661
+ updated_at: e.ts,
4662
+ ...readPlan(e)
4607
4663
  });
4608
4664
  continue;
4609
4665
  }
4666
+ if (e.event === "split" && e.parent && Array.isArray(e.children)) {
4667
+ const parent = map.get(e.parent);
4668
+ if (parent) {
4669
+ for (const c of e.children) {
4670
+ if (!c || !c.id || map.has(c.id))
4671
+ continue;
4672
+ map.set(c.id, {
4673
+ id: c.id,
4674
+ title: c.title || c.id,
4675
+ role: c.role || "builder",
4676
+ judge: c.judge || "",
4677
+ status: "todo",
4678
+ evidence: "",
4679
+ deps: [],
4680
+ touches: uniq(c.touches || []),
4681
+ created_at: e.ts,
4682
+ updated_at: e.ts,
4683
+ split_from: e.parent,
4684
+ ...c.judge ? { judge_inherited: true } : {},
4685
+ ...e.slice && e.slice === c.id ? { slice: true } : {}
4686
+ });
4687
+ }
4688
+ const ids = e.children.map((c) => c.id).filter((id) => map.has(id));
4689
+ parent.deps = uniq([...parent.deps, ...ids]);
4690
+ parent.split_children = uniq([...parent.split_children || [], ...ids]);
4691
+ }
4692
+ continue;
4693
+ }
4610
4694
  const t = map.get(e.id);
4611
4695
  if (!t)
4612
4696
  continue;
@@ -4614,9 +4698,16 @@ function fold2() {
4614
4698
  const remove = new Set([...e.remove_deps || [], ...e.remove_touches || []]);
4615
4699
  t.deps = uniq([...t.deps, ...e.deps || []]).filter((x) => !remove.has(x));
4616
4700
  t.touches = uniq([...t.touches, ...e.touches || []]).filter((x) => !remove.has(x));
4701
+ } else if (e.event === "plan") {
4702
+ Object.assign(t, readPlan(e));
4617
4703
  } else {
4618
- if (e.status)
4704
+ if (e.status) {
4619
4705
  t.status = e.status;
4706
+ if (e.status === "done" || e.status === "accepted")
4707
+ t.closed_at = t.closed_at || e.ts;
4708
+ else
4709
+ t.closed_at = undefined;
4710
+ }
4620
4711
  if (e.evidence !== undefined)
4621
4712
  t.evidence = e.evidence;
4622
4713
  if (e.closure_reason !== undefined)
@@ -4643,10 +4734,87 @@ function add(title, opts = {}) {
4643
4734
  judge: opts.judge || "",
4644
4735
  owner: opts.owner,
4645
4736
  deps: uniq(opts.deps || []),
4646
- touches: uniq(opts.touches || [])
4737
+ touches: uniq(opts.touches || []),
4738
+ ...planFields(opts)
4647
4739
  });
4648
4740
  return fold2().get(id);
4649
4741
  }
4742
+ function planFields(o) {
4743
+ const out = {};
4744
+ if (o.prio !== undefined)
4745
+ out.prio = normPrio(o.prio);
4746
+ if (o.start !== undefined)
4747
+ out.start = normDay(o.start);
4748
+ if (o.due !== undefined)
4749
+ out.due = normDay(o.due);
4750
+ if (o.progress !== undefined)
4751
+ out.progress = normProgress(o.progress);
4752
+ return out;
4753
+ }
4754
+ function readPlan(e) {
4755
+ const out = {};
4756
+ try {
4757
+ if (e.prio !== undefined && PRIO_RE.test(String(e.prio).toUpperCase()))
4758
+ out.prio = String(e.prio).toUpperCase();
4759
+ } catch {}
4760
+ try {
4761
+ if (e.start !== undefined && DAY_RE.test(String(e.start)))
4762
+ out.start = String(e.start);
4763
+ } catch {}
4764
+ try {
4765
+ if (e.due !== undefined && DAY_RE.test(String(e.due)))
4766
+ out.due = String(e.due);
4767
+ } catch {}
4768
+ try {
4769
+ const n = Number(e.progress);
4770
+ if (e.progress !== undefined && Number.isInteger(n) && n >= 0 && n <= 100)
4771
+ out.progress = n;
4772
+ } catch {}
4773
+ return out;
4774
+ }
4775
+ function plan(id, patch) {
4776
+ const cur = get2(id);
4777
+ const fields = planFields(patch);
4778
+ if (!Object.keys(fields).length)
4779
+ throw new Error("E_TASK: plan 至少需要一个字段(--prio/--start/--due/--progress)");
4780
+ const start = fields.start ?? cur.start, due = fields.due ?? cur.due;
4781
+ if (start && due && start > due)
4782
+ throw new Error("E_TASK: start 不能晚于 due");
4783
+ append3({ event: "plan", id, ...fields });
4784
+ return get2(id);
4785
+ }
4786
+ var SATISFIED = ["done", "accepted", "dropped"];
4787
+ function isSatisfied(t) {
4788
+ return !!t && SATISFIED.includes(t.status);
4789
+ }
4790
+ function isTerminal2(t) {
4791
+ return !!t && ["done", "accepted", "dropped", "rejected"].includes(t.status);
4792
+ }
4793
+ function split(parentId, titles, opts = {}) {
4794
+ const parent = get2(parentId);
4795
+ if (isTerminal2(parent))
4796
+ throw new Error("E_TASK: 终态任务不能拆分");
4797
+ if (parent.status === "doing" && !opts.force)
4798
+ throw new Error("E_TASK: 进行中的任务拆分需 --force(拆分后会先等子任务)");
4799
+ const names = titles.map((x) => x.trim()).filter(Boolean);
4800
+ if (names.length < 2)
4801
+ throw new Error("E_TASK: 至少拆成 2 个切片");
4802
+ if (new Set(names).size !== names.length)
4803
+ throw new Error("E_TASK: 切片标题重复");
4804
+ if (opts.slice && !names.includes(opts.slice))
4805
+ throw new Error("E_TASK: --slice 必须是某个切片标题");
4806
+ const judge = opts.judge ?? parent.judge;
4807
+ const children = names.map((title) => ({
4808
+ id: "t-" + createHash9("sha1").update(title + Date.now() + Math.random()).digest("hex").slice(0, 8),
4809
+ title,
4810
+ role: opts.role || parent.role,
4811
+ judge,
4812
+ touches: uniq(opts.touches ?? parent.touches)
4813
+ }));
4814
+ const sliceId = opts.slice ? children[names.indexOf(opts.slice)].id : undefined;
4815
+ append3({ event: "split", parent: parentId, children, ...sliceId ? { slice: sliceId } : {} });
4816
+ return get2(parentId);
4817
+ }
4650
4818
  function list4() {
4651
4819
  return [...fold2().values()];
4652
4820
  }
@@ -4688,11 +4856,258 @@ function status3(id, status, evidence, opts = {}) {
4688
4856
  return get2(id);
4689
4857
  }
4690
4858
 
4859
+ // src/plan.ts
4860
+ var NO_PRIO = "P99";
4861
+ var TERMINAL3 = new Set(["done", "accepted", "dropped", "rejected"]);
4862
+ var SATISFIED2 = new Set(["done", "accepted", "dropped"]);
4863
+ var PROPAGATE = new Set(["P0", "P1"]);
4864
+ var isTerminal3 = (t) => TERMINAL3.has(t.status);
4865
+ var isSatisfied2 = (t) => !!t && SATISFIED2.has(t.status);
4866
+ function prioIndex(p) {
4867
+ const m = /^P(\d{1,2})$/.exec(String(p ?? "").toUpperCase());
4868
+ return m ? Number(m[1]) : 99;
4869
+ }
4870
+ function prioName(idx) {
4871
+ return idx >= 99 ? NO_PRIO : `P${idx}`;
4872
+ }
4873
+ function graphOf(tasks) {
4874
+ const byId = new Map(tasks.map((t) => [t.id, t]));
4875
+ const dependents = new Map;
4876
+ for (const t of tasks)
4877
+ for (const d of t.deps)
4878
+ if (byId.has(d))
4879
+ (dependents.get(d) ?? dependents.set(d, []).get(d)).push(t.id);
4880
+ return { byId, dependents };
4881
+ }
4882
+ function findCycles(tasks) {
4883
+ const byId = new Map(tasks.map((t) => [t.id, t]));
4884
+ const state = new Map;
4885
+ const stack = [];
4886
+ const cycles = [];
4887
+ const seen = new Set;
4888
+ const visit = (id) => {
4889
+ const st = state.get(id) || 0;
4890
+ if (st === 1) {
4891
+ const i = stack.indexOf(id);
4892
+ const cyc = i >= 0 ? stack.slice(i) : [id];
4893
+ const key = [...cyc].sort().join(",");
4894
+ if (!seen.has(key)) {
4895
+ seen.add(key);
4896
+ cycles.push(cyc);
4897
+ }
4898
+ return;
4899
+ }
4900
+ if (st === 2)
4901
+ return;
4902
+ state.set(id, 1);
4903
+ stack.push(id);
4904
+ for (const dep of byId.get(id)?.deps || [])
4905
+ if (byId.has(dep))
4906
+ visit(dep);
4907
+ stack.pop();
4908
+ state.set(id, 2);
4909
+ };
4910
+ for (const t of tasks)
4911
+ visit(t.id);
4912
+ return cycles.sort((a, b) => a.join(",").localeCompare(b.join(",")));
4913
+ }
4914
+ function computeEff(tasks, dependents) {
4915
+ const byId = new Map(tasks.map((t) => [t.id, t]));
4916
+ const eff = new Map;
4917
+ for (const t of tasks)
4918
+ eff.set(t.id, { idx: prioIndex(t.prio) });
4919
+ for (const src of tasks) {
4920
+ const idx = prioIndex(src.prio);
4921
+ if (isTerminal3(src) || !PROPAGATE.has(prioName(idx)))
4922
+ continue;
4923
+ const seen = new Map([[src.id, 0]]);
4924
+ const queue = [src.id];
4925
+ while (queue.length) {
4926
+ const cur = queue.shift();
4927
+ const dist = seen.get(cur);
4928
+ for (const up of byId.get(cur)?.deps || []) {
4929
+ if (!byId.has(up) || seen.has(up))
4930
+ continue;
4931
+ const upTask = byId.get(up);
4932
+ if (isTerminal3(upTask))
4933
+ continue;
4934
+ seen.set(up, dist + 1);
4935
+ queue.push(up);
4936
+ }
4937
+ }
4938
+ for (const [id, distance] of seen) {
4939
+ if (id === src.id)
4940
+ continue;
4941
+ const cur = eff.get(id);
4942
+ const cand = { id: src.id, declared: prioName(idx), distance };
4943
+ if (idx < cur.idx)
4944
+ eff.set(id, { idx, src: cand });
4945
+ else if (idx === cur.idx && cur.src) {
4946
+ const better = cand.distance < cur.src.distance || cand.distance === cur.src.distance && cand.id < cur.src.id;
4947
+ if (better)
4948
+ eff.set(id, { idx, src: cand });
4949
+ }
4950
+ }
4951
+ }
4952
+ return eff;
4953
+ }
4954
+ function longestDepth(tasks, byId, cyclic) {
4955
+ const memo = new Map;
4956
+ const visit = (id, stack) => {
4957
+ if (memo.has(id))
4958
+ return memo.get(id);
4959
+ if (stack.has(id))
4960
+ return 0;
4961
+ const t = byId.get(id);
4962
+ if (!t)
4963
+ return 0;
4964
+ stack.add(id);
4965
+ let d = 0;
4966
+ for (const dep of t.deps)
4967
+ if (byId.has(dep) && !cyclic.has(dep))
4968
+ d = Math.max(d, visit(dep, stack) + 1);
4969
+ stack.delete(id);
4970
+ memo.set(id, d);
4971
+ return d;
4972
+ };
4973
+ for (const t of tasks)
4974
+ visit(t.id, new Set);
4975
+ return memo;
4976
+ }
4977
+ function transitiveDependents(tasks, dependents) {
4978
+ const out = new Map;
4979
+ for (const t of tasks) {
4980
+ const seen = new Set;
4981
+ const queue = [...dependents.get(t.id) || []];
4982
+ while (queue.length) {
4983
+ const cur = queue.shift();
4984
+ if (seen.has(cur))
4985
+ continue;
4986
+ seen.add(cur);
4987
+ queue.push(...dependents.get(cur) || []);
4988
+ }
4989
+ out.set(t.id, seen);
4990
+ }
4991
+ return out;
4992
+ }
4993
+ function derive(tasks) {
4994
+ const { byId, dependents } = graphOf(tasks);
4995
+ const cycles = findCycles(tasks);
4996
+ const cyclic = new Set(cycles.flat());
4997
+ const eff = computeEff(tasks, dependents);
4998
+ const depth = longestDepth(tasks, byId, cyclic);
4999
+ const down = transitiveDependents(tasks, dependents);
5000
+ const critical = new Set;
5001
+ for (const t of tasks) {
5002
+ const idx = prioIndex(t.prio);
5003
+ if (isTerminal3(t) || !PROPAGATE.has(prioName(idx)))
5004
+ continue;
5005
+ const seen = new Set;
5006
+ const queue = [t.id];
5007
+ while (queue.length) {
5008
+ const cur = queue.shift();
5009
+ for (const dep of byId.get(cur)?.deps || []) {
5010
+ if (seen.has(dep) || !byId.has(dep))
5011
+ continue;
5012
+ seen.add(dep);
5013
+ critical.add(dep);
5014
+ queue.push(dep);
5015
+ }
5016
+ }
5017
+ }
5018
+ const byIdOut = new Map;
5019
+ for (const t of tasks) {
5020
+ const deps = t.deps;
5021
+ const dangling = deps.filter((d) => !byId.has(d));
5022
+ const unsatisfied = deps.filter((d) => byId.has(d) ? !isSatisfied2(byId.get(d)) : true);
5023
+ const released = deps.filter((d) => byId.get(d)?.status === "dropped");
5024
+ const hasCycle = cyclic.has(t.id);
5025
+ const ready = !isTerminal3(t) && !hasCycle && unsatisfied.length === 0;
5026
+ const e = eff.get(t.id);
5027
+ byIdOut.set(t.id, {
5028
+ id: t.id,
5029
+ eff_prio: prioName(e.idx),
5030
+ prio_inherited: e.idx < prioIndex(t.prio),
5031
+ ...e.src && e.idx < prioIndex(t.prio) ? { prio_source: e.src } : {},
5032
+ critical: critical.has(t.id),
5033
+ ready,
5034
+ startable: ready && t.status === "todo",
5035
+ blocked_by: unsatisfied,
5036
+ released_by: released,
5037
+ dangling_deps: dangling,
5038
+ depth: depth.get(t.id) ?? 0,
5039
+ unblocks: down.get(t.id)?.size ?? 0,
5040
+ rank: null,
5041
+ ...t.split_from ? { split_from: t.split_from } : {},
5042
+ ...t.split_children?.length ? { split_children: t.split_children } : {},
5043
+ ...t.slice ? { slice: true } : {}
5044
+ });
5045
+ }
5046
+ const nodes = tasks.filter((t) => !isTerminal3(t) && !cyclic.has(t.id));
5047
+ const indeg = new Map;
5048
+ for (const t of nodes)
5049
+ indeg.set(t.id, t.deps.filter((d) => nodes.some((n) => n.id === d)).length);
5050
+ const readyQueue = nodes.filter((t) => (indeg.get(t.id) ?? 0) === 0);
5051
+ const key = (t) => {
5052
+ const d = byIdOut.get(t.id);
5053
+ return [
5054
+ prioIndex(d.eff_prio),
5055
+ t.due ?? "9999-12-31",
5056
+ d.critical ? 0 : 1,
5057
+ d.slice ? 0 : 1,
5058
+ -d.unblocks,
5059
+ d.depth,
5060
+ t.created_at,
5061
+ t.id
5062
+ ];
5063
+ };
5064
+ const cmp = (a, b) => {
5065
+ const ka = key(a), kb = key(b);
5066
+ for (let i = 0;i < ka.length; i++) {
5067
+ if (ka[i] < kb[i])
5068
+ return -1;
5069
+ if (ka[i] > kb[i])
5070
+ return 1;
5071
+ }
5072
+ return 0;
5073
+ };
5074
+ let rank = 0;
5075
+ const pending = [...readyQueue].sort(cmp);
5076
+ while (pending.length) {
5077
+ const t = pending.shift();
5078
+ byIdOut.get(t.id).rank = ++rank;
5079
+ for (const d of dependents.get(t.id) || []) {
5080
+ const n = indeg.get(d);
5081
+ if (n === undefined)
5082
+ continue;
5083
+ indeg.set(d, n - 1);
5084
+ if (n - 1 === 0)
5085
+ pending.push(byId.get(d));
5086
+ }
5087
+ pending.sort(cmp);
5088
+ }
5089
+ const lint = [];
5090
+ for (const t of tasks) {
5091
+ const d = byIdOut.get(t.id);
5092
+ for (const x of d.dangling_deps)
5093
+ lint.push({ kind: "dangling_dep", id: t.id, detail: `依赖不存在:${x}` });
5094
+ for (const x of d.released_by)
5095
+ lint.push({ kind: "released_by_drop", id: t.id, detail: `依赖已 dropped:${x}(前提失效,确认是否还需要做)` });
5096
+ if (PROPAGATE.has(t.prio ?? "") && !isTerminal3(t) && d.depth >= 3) {
5097
+ lint.push({ kind: "long_chain", id: t.id, detail: `依赖链 ${d.depth} 跳,考虑拆分` });
5098
+ }
5099
+ }
5100
+ for (const c of cycles)
5101
+ lint.push({ kind: "cycle", id: c[0], detail: `环:${c.join(" → ")}` });
5102
+ return { byId: byIdOut, cycles, lint, ranking: { policy: "kahn-v1" } };
5103
+ }
5104
+
4691
5105
  // src/graph.ts
5106
+ var DERIVED_PROGRESS = { todo: 0, doing: 50, review: 80, done: 100, accepted: 100, blocked: 0, dropped: 0, rejected: 0, escalated: 50, awaiting_acceptance: 90 };
4692
5107
  var taskNode = (id) => `task:${id}`;
4693
5108
  var sessionNode = (id) => `session:${id}`;
4694
5109
  var pathNode = (p) => `path:${p}`;
4695
- function findCycles(tasks) {
5110
+ function findCycles2(tasks) {
4696
5111
  const byId = new Map(tasks.map((t) => [t.id, t]));
4697
5112
  const state = new Map;
4698
5113
  const stack = [];
@@ -4726,13 +5141,46 @@ function findCycles(tasks) {
4726
5141
  }
4727
5142
  function build(opts = {}) {
4728
5143
  const tasks = list4();
5144
+ const derived = derive(tasks);
4729
5145
  const active = list3();
4730
5146
  const nodes = new Map;
4731
5147
  const edges = [];
4732
5148
  const byId = new Map(tasks.map((t) => [t.id, t]));
4733
5149
  for (const t of tasks) {
4734
- const blocked = t.deps.filter((d) => byId.get(d)?.status !== "done");
4735
- nodes.set(taskNode(t.id), { id: taskNode(t.id), kind: "task", label: t.title, status: t.status, owner: t.owner, ...blocked.length ? { blocked_by: blocked } : {} });
5150
+ const blocked = t.deps.filter((d) => !isSatisfied(byId.get(d)));
5151
+ const d = derived.byId.get(t.id);
5152
+ nodes.set(taskNode(t.id), {
5153
+ id: taskNode(t.id),
5154
+ kind: "task",
5155
+ label: t.title,
5156
+ status: t.status,
5157
+ owner: t.owner,
5158
+ ...blocked.length ? { blocked_by: blocked } : {},
5159
+ ...t.prio ? { prio: t.prio } : {},
5160
+ ...t.start ? { start: t.start } : {},
5161
+ ...t.due ? { due: t.due } : {},
5162
+ progress: t.progress ?? (DERIVED_PROGRESS[t.status] ?? 0),
5163
+ progress_derived: t.progress === undefined,
5164
+ created_at: t.created_at,
5165
+ updated_at: t.updated_at,
5166
+ ...t.closed_at ? { closed_at: t.closed_at } : {},
5167
+ deps: t.deps,
5168
+ touches: t.touches,
5169
+ eff_prio: d.eff_prio,
5170
+ prio_inherited: d.prio_inherited,
5171
+ ...d.prio_source ? { prio_source: d.prio_source } : {},
5172
+ critical: d.critical,
5173
+ ready: d.ready,
5174
+ startable: d.startable,
5175
+ released_by: d.released_by,
5176
+ dangling_deps: d.dangling_deps,
5177
+ depth: d.depth,
5178
+ unblocks: d.unblocks,
5179
+ rank: d.rank,
5180
+ ...d.split_from ? { split_from: d.split_from } : {},
5181
+ ...d.split_children ? { split_children: d.split_children } : {},
5182
+ ...d.slice ? { slice: true } : {}
5183
+ });
4736
5184
  if (t.owner)
4737
5185
  nodes.set(sessionNode(t.owner), { id: sessionNode(t.owner), kind: "session", label: t.owner });
4738
5186
  for (const d of t.deps) {
@@ -4773,7 +5221,7 @@ function build(opts = {}) {
4773
5221
  }
4774
5222
  filteredNodes.sort((a, b) => a.id.localeCompare(b.id));
4775
5223
  filteredEdges.sort((a, b) => (a.from + "\x00" + a.to + "\x00" + a.kind).localeCompare(b.from + "\x00" + b.to + "\x00" + b.kind));
4776
- return { schema: 1, nodes: filteredNodes, edges: filteredEdges, cycles: findCycles(tasks) };
5224
+ return { schema: 1, capabilities: ["prio", "timeline", "progress", "eff-prio", "ready", "rank", "split"], ranking: derived.ranking, nodes: filteredNodes, edges: filteredEdges, cycles: findCycles2(tasks) };
4777
5225
  }
4778
5226
 
4779
5227
  // src/graph-tui.ts
@@ -4861,6 +5309,842 @@ function watch(build, opts = {}) {
4861
5309
  return new Promise(() => {});
4862
5310
  }
4863
5311
 
5312
+ // src/graph-html/template.html
5313
+ var template_default = `<!doctype html>
5314
+ <html lang="zh">
5315
+ <head>
5316
+ <meta charset="utf-8">
5317
+ <meta name="viewport" content="width=device-width,initial-scale=1">
5318
+ <title>dia graph · 快照</title>
5319
+ <style>
5320
+ :root{
5321
+ --bg:#f6f7f9; --panel:#fff; --ink:#111827; --muted:#6b7280; --line:#e5e7eb;
5322
+ --blue:#2563eb; --violet:#7c3aed; --cyan:#0891b2; --green:#16a34a; --red:#dc2626; --amber:#d97706;
5323
+ --shadow:0 1px 2px rgba(17,24,39,.06),0 8px 24px rgba(17,24,39,.06);
5324
+ }
5325
+ @media (prefers-color-scheme: dark){
5326
+ :root{ --bg:#0f1115; --panel:#171a21; --ink:#e5e7eb; --muted:#9ca3af; --line:#2a2f3a;
5327
+ --shadow:0 1px 2px rgba(0,0,0,.4),0 8px 24px rgba(0,0,0,.25); }
5328
+ }
5329
+ *{box-sizing:border-box}
5330
+ body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif}
5331
+ header{display:flex;align-items:center;gap:16px;padding:14px 22px;border-bottom:1px solid var(--line);background:var(--panel);flex-wrap:wrap}
5332
+ header h1{font-size:17px;margin:0;letter-spacing:.2px}
5333
+ header .sub{color:var(--muted);font-size:12px}
5334
+ header .spacer{flex:1}
5335
+ .tabs{display:flex;border:1px solid var(--line);border-radius:9px;overflow:hidden}
5336
+ .tabs button{border:0;background:transparent;color:var(--muted);padding:6px 12px;font:inherit;font-size:12px;cursor:pointer}
5337
+ .tabs button.on{background:var(--blue);color:#fff}
5338
+ .pill{border:1px solid var(--line);border-radius:999px;padding:4px 10px;font-size:12px;color:var(--muted);background:var(--bg)}
5339
+ main{display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:18px;padding:18px;max-width:1480px;margin:0 auto}
5340
+ .panel{background:var(--panel);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow)}
5341
+ .toolbar{display:flex;gap:8px;align-items:center;padding:12px 14px;border-bottom:1px solid var(--line);flex-wrap:wrap}
5342
+ .seg{display:flex;border:1px solid var(--line);border-radius:9px;overflow:hidden}
5343
+ .seg button{border:0;background:transparent;color:var(--muted);padding:6px 10px;font:inherit;font-size:12px;cursor:pointer}
5344
+ .seg button.on{background:var(--blue);color:#fff}
5345
+ .seg button:disabled{opacity:.35;cursor:not-allowed}
5346
+ .win-select{border:1px solid var(--line);background:var(--bg);color:var(--ink);border-radius:8px;padding:6px 8px;font:inherit;font-size:12px}
5347
+ .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}
5348
+ .search input{border:0;background:transparent;color:var(--ink);padding:2px 0;font:inherit;font-size:12px;flex:1;min-width:80px;outline:none}
5349
+ .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}
5350
+ .token button{border:0;background:transparent;color:inherit;cursor:pointer;font:inherit;font-size:12px;line-height:1;padding:0 2px}
5351
+ .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}
5352
+ .search .q-list.open{display:block}
5353
+ .q-item{padding:6px 10px;font-size:12px;cursor:pointer;display:flex;justify-content:space-between;gap:10px}
5354
+ .q-item:hover,.q-item.active{background:color-mix(in srgb,var(--blue) 18%,transparent)}
5355
+ .q-item .k{color:var(--muted)}
5356
+ .q-item .q-prio{font-style:normal;font-size:10px;color:var(--muted);margin-left:6px}
5357
+ .q-item.sel{color:var(--blue)}
5358
+ .q-item.sel .k{color:var(--blue)}
5359
+ .q-head{padding:6px 10px;font-size:10px;color:var(--muted);border-bottom:1px solid var(--line)}
5360
+ .q-empty{padding:10px;font-size:12px;color:var(--muted)}
5361
+ .node.hit{outline:2px solid var(--blue);outline-offset:1px}
5362
+ .node.dim{opacity:.22}
5363
+ .focus-chip{display:none;align-items:center;gap:4px;border:1px solid var(--blue);color:var(--blue);border-radius:999px;padding:3px 4px 3px 10px;font-size:11px}
5364
+ .focus-chip.on{display:inline-flex}
5365
+ .focus-chip button{border:0;background:transparent;color:var(--blue);cursor:pointer;font:inherit;font-size:11px;padding:0 6px}
5366
+ .focus-chip .fd{border:0;background:transparent;color:var(--blue);font:inherit;font-size:11px}
5367
+ .toolbar .hint{margin-left:auto;color:var(--muted);font-size:12px}
5368
+ .canvas{position:relative;height:620px;overflow:hidden}
5369
+ .canvas-scroll{position:absolute;inset:0;overflow:auto;padding:10px}
5370
+ .lane-rail{position:absolute;left:0;top:0;bottom:0;width:110px;z-index:6;pointer-events:none;background:linear-gradient(90deg,var(--panel) 86%,transparent)}
5371
+ svg.edges{position:absolute;inset:0;pointer-events:none;overflow:visible}
5372
+ .node{position:absolute;width:190px;border:1px solid var(--line);border-left:1px solid var(--line);border-radius:10px;background:var(--panel);padding:8px 10px;box-shadow:var(--shadow);cursor:pointer}
5373
+ .node.path{border-left-color:var(--violet)}
5374
+ .node.session{border-left-color:var(--cyan)}
5375
+ .node b{display:block;font-size:13px;line-height:1.3;margin-bottom:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
5376
+ .node .meta{color:var(--muted);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
5377
+ .progress{height:4px;border-radius:99px;background:var(--line);margin-top:6px;overflow:hidden}
5378
+ .progress i{display:block;height:100%;background:var(--blue);background-image:linear-gradient(45deg,rgba(255,255,255,.28) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.28) 50%,rgba(255,255,255,.28) 75%,transparent 75%);background-size:12px 12px;animation:flow 1s linear infinite}
5379
+ .progress i.blocked{background-color:var(--red);animation-play-state:paused;opacity:.65}
5380
+ .progress i.review{background-color:var(--amber)}
5381
+ .progress i.done,.progress i.accepted{background-color:var(--muted);background-image:none;animation:none}
5382
+ .progress i.todo{background-color:var(--muted);background-image:none;animation:none}
5383
+ @keyframes flow{to{background-position:12px 0}}
5384
+ .st{font-weight:500}
5385
+ .st.doing{color:var(--ink)} .st.blocked{color:var(--red)} .st.review{color:var(--amber)}
5386
+ .st.done,.st.accepted{color:var(--muted)} .st.todo{color:var(--muted)}
5387
+ .prio{float:right;font-size:10px;font-weight:600;border-radius:5px;padding:0 6px;color:#fff;margin-left:4px}
5388
+ .prio{background:transparent;border:1px solid var(--line)}
5389
+ .prio.p0{background:var(--red);border-color:var(--red);color:#fff}
5390
+ .prio.p1{border-color:var(--amber);color:var(--amber)}
5391
+ .prio.p2{border-color:var(--line);color:var(--muted)}
5392
+ .pgroup{position:absolute;left:0;pointer-events:none;border-bottom:1px dashed var(--line)}
5393
+ .plane-label{position:absolute;left:20px;width:78px;z-index:6;pointer-events:none;padding:2px 6px 2px 0;background:linear-gradient(90deg,var(--panel) 72%,transparent);transform:translateY(-50%);font-size:13px;font-weight:700;letter-spacing:.1em}
5394
+ .plane-label small{display:block;font-size:9px;font-weight:400;letter-spacing:0;opacity:.7;margin-top:2px}
5395
+ .legend-overlay{position:absolute;right:12px;bottom:12px;z-index:5;background:color-mix(in srgb,var(--panel) 92%,transparent);border:1px solid var(--line);border-radius:10px;padding:8px 10px;box-shadow:var(--shadow);font-size:11px;color:var(--muted);display:grid;gap:5px;min-width:190px}
5396
+ .legend-overlay .row{display:flex;align-items:center;gap:8px;white-space:nowrap}
5397
+ .legend-overlay .sample{width:38px;height:10px;flex:0 0 38px}
5398
+ .legend-overlay b{color:var(--ink);font-weight:600}
5399
+ .canvas.simple .legend-overlay .detail-row{display:none}
5400
+ aside{display:flex;flex-direction:column;gap:14px}
5401
+ .side-title{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;border-bottom:1px solid var(--line)}
5402
+ .side-title h2{font-size:13px;margin:0}
5403
+ .side-title span{font-size:11px;color:var(--muted)}
5404
+ .list{padding:6px 14px 12px}
5405
+ .item{display:flex;justify-content:space-between;gap:10px;padding:8px 0;border-bottom:1px dashed var(--line);font-size:12px}
5406
+ .item:last-child{border-bottom:0}
5407
+ .item .k{color:var(--muted)}
5408
+ .attention{padding:8px 14px 14px}
5409
+ .attention .row{display:flex;gap:8px;align-items:flex-start;padding:8px 0;border-bottom:1px dashed var(--line);font-size:12px}
5410
+ .attention .row:last-child{border-bottom:0}
5411
+ .tag{font-size:10px;border-radius:6px;padding:1px 6px;background:rgba(220,38,38,.12);color:var(--red);white-space:nowrap}
5412
+ .tag.amber{background:rgba(217,119,6,.12);color:var(--amber)}
5413
+ .attention .row{cursor:pointer}
5414
+ .quick{display:flex;gap:6px;margin-top:6px}
5415
+ .quick button,.btn{border:1px solid var(--line);background:var(--bg);color:var(--ink);border-radius:7px;padding:3px 8px;font:inherit;font-size:11px;cursor:pointer}
5416
+ .quick button.primary,.btn.primary{background:var(--blue);border-color:var(--blue);color:#fff}
5417
+ .backdrop{position:fixed;inset:0;background:rgba(0,0,0,.35);opacity:0;pointer-events:none;transition:opacity .18s;z-index:40}
5418
+ .backdrop.show{opacity:1;pointer-events:auto}
5419
+ .drawer{position:fixed;top:0;right:0;bottom:0;width:440px;max-width:92vw;background:var(--panel);border-left:1px solid var(--line);box-shadow:-12px 0 40px rgba(0,0,0,.25);transform:translateX(100%);transition:transform .2s ease;z-index:50;display:flex;flex-direction:column}
5420
+ .drawer.show{transform:none}
5421
+ .drawer-head{display:flex;align-items:flex-start;gap:10px;padding:14px 16px;border-bottom:1px solid var(--line)}
5422
+ .drawer-head h3{font-size:15px;margin:0 0 3px}
5423
+ .drawer-head .meta{color:var(--muted);font-size:11px}
5424
+ .drawer-head .close{margin-left:auto;border:0;background:transparent;color:var(--muted);font-size:18px;cursor:pointer}
5425
+ .drawer-tabs{display:flex;gap:6px;padding:10px 16px;border-bottom:1px solid var(--line)}
5426
+ .drawer-tabs button{border:1px solid var(--line);background:transparent;color:var(--muted);border-radius:7px;padding:4px 10px;font:inherit;font-size:12px;cursor:pointer}
5427
+ .drawer-tabs button.on{background:var(--blue);border-color:var(--blue);color:#fff}
5428
+ .drawer-body{padding:14px 16px 24px;overflow:auto}
5429
+ .drawer-body h4{font-size:12px;margin:14px 0 6px;color:var(--muted);letter-spacing:.05em;text-transform:uppercase}
5430
+ .kv{display:grid;grid-template-columns:70px 1fr;gap:4px 10px;font-size:12px}
5431
+ .kv .k{color:var(--muted)}
5432
+ .iv-seg{display:flex;border:1px solid var(--line);border-radius:9px;overflow:hidden;margin-bottom:12px}
5433
+ .iv-seg button{flex:1;border:0;background:transparent;color:var(--muted);padding:7px 8px;font:inherit;font-size:12px;cursor:pointer}
5434
+ .iv-seg button.on{background:var(--blue);color:#fff}
5435
+ .iv-row{display:grid;gap:6px;margin-bottom:10px}
5436
+ .iv-row label{font-size:11px;color:var(--muted)}
5437
+ .iv-row select,.iv-row textarea{width:100%;border:1px solid var(--line);border-radius:8px;background:var(--bg);color:var(--ink);padding:8px;font:inherit;font-size:12px}
5438
+ .iv-row textarea{min-height:96px;resize:vertical}
5439
+ .iv-status{color:var(--green);font-size:11px;margin-left:8px}
5440
+ .rel-list .item{display:flex;justify-content:space-between;gap:10px;padding:7px 0;border-bottom:1px dashed var(--line);font-size:12px}
5441
+ .rel-list .item:last-child{border-bottom:0}
5442
+ .rel-list .k{color:var(--muted)}
5443
+ .kv{display:grid;grid-template-columns:70px 1fr;gap:4px 10px;font-size:12px}
5444
+ .kv .k{color:var(--muted)}
5445
+ /* timeline */
5446
+ .timeline{padding:14px 16px 18px;overflow:hidden;position:relative}
5447
+ .tl-grid{display:grid;grid-template-columns:190px minmax(0,1fr);gap:8px;align-items:start}
5448
+ .tl-labels{overflow:hidden;position:relative}
5449
+ .tl-label-head{height:34px;display:flex;align-items:flex-end;font-size:11px;color:var(--muted);padding-bottom:6px}
5450
+ .tl-labels-body{will-change:transform}
5451
+ .tl-label-row{height:38px;box-sizing:border-box;padding:6px 8px 4px 0;border-top:1px dashed var(--line);font-size:12px;overflow:hidden}
5452
+ .tl-label-row b{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
5453
+ .tl-label-row span{color:var(--muted);font-size:10px}
5454
+ .tl-scroll{overflow:auto;max-height:620px;scrollbar-width:thin}
5455
+ .scroll-hint{position:absolute;right:18px;top:10px;z-index:6;background:var(--panel);border:1px solid var(--line);border-radius:999px;padding:3px 9px;font-size:10px;color:var(--muted);box-shadow:var(--shadow)}
5456
+ .tl-head{display:grid;grid-template-columns:var(--tlw,900px);gap:8px;align-items:end;color:var(--muted);font-size:11px;margin-bottom:8px;position:sticky;top:0;z-index:4;background:var(--panel)}
5457
+ .tl-axis{position:relative;height:34px;width:var(--tlw,900px)}
5458
+ .tl-axis span{position:absolute;transform:translateX(-50%);white-space:nowrap;line-height:1.2}
5459
+ .tl-axis span small{display:block;font-size:9px;opacity:.55}
5460
+ .tl-body{position:relative}
5461
+ .tl-row{display:grid;grid-template-columns:var(--tlw,900px);gap:8px;align-items:start;min-height:38px;border-top:1px dashed var(--line)}
5462
+ .tl-track{position:relative;height:28px;overflow:hidden;width:var(--tlw,900px);margin-top:4px}
5463
+ .tl-bar{position:absolute;top:7px;height:14px;border-radius:5px;background:var(--blue);opacity:.9;cursor:pointer}
5464
+ .tl-bar .fill{position:absolute;left:0;top:0;bottom:0;border-radius:5px;background:rgba(255,255,255,.45)}
5465
+ .tl-bar.blocked{background:var(--red)} .tl-bar.done,.tl-bar.accepted,.tl-bar.todo{background:var(--muted)}
5466
+ .tl-bar.review{background:var(--amber)}
5467
+ .tl-bar.critical{outline:2px solid var(--amber);outline-offset:2px}
5468
+ .tl-bar.open{border-right:2px dashed var(--muted);opacity:.8}
5469
+ .tl-today{position:absolute;top:0;bottom:0;pointer-events:none;z-index:2;border-left:1px dashed var(--red);color:var(--red);font-size:10px;padding-left:4px}
5470
+ .tl-caption{color:var(--muted);font-size:12px;padding:10px 0 2px}
5471
+ footer{padding:0 22px 22px;color:var(--muted);font-size:11px}
5472
+ @media (max-width:980px){ main{grid-template-columns:1fr} .canvas{height:560px} }
5473
+ </style>
5474
+ </head>
5475
+ <body>
5476
+ <header>
5477
+ <h1>dia graph</h1>
5478
+ <span class="sub">只读依赖图 + 时间线 · 静态快照</span>
5479
+ <div class="tabs" id="tabs">
5480
+ <button class="on" data-v="graph">关系图</button>
5481
+ <button data-v="time">时间线</button>
5482
+ </div>
5483
+ <span class="spacer"></span>
5484
+ <span class="pill" id="trunc-pill" style="display:none"></span>
5485
+ <span class="pill">静态快照 · 无网络</span>
5486
+ <span class="pill" id="route">#/graph</span>
5487
+ <span class="pill" id="updated">-</span>
5488
+ </header>
5489
+
5490
+ <main>
5491
+ <section class="panel">
5492
+ <div class="toolbar">
5493
+ <div id="graph-tools" style="display:flex;gap:8px;align-items:center">
5494
+ <div class="seg" id="layout">
5495
+ <button class="on" data-l="priority">优先级视图</button>
5496
+ <button data-l="dep">依赖视图</button>
5497
+ </div>
5498
+ <div class="seg" id="mode">
5499
+ <button class="on" data-m="simple">简约</button>
5500
+ <button data-m="detail">详细</button>
5501
+ </div>
5502
+ <div class="search" id="search-box">
5503
+ <span id="tokens" style="display:contents"></span>
5504
+ <input id="q" type="search" placeholder="搜索任务,选中后变成 tag" autocomplete="off">
5505
+ <div class="q-list" id="q-list"></div>
5506
+ </div>
5507
+ <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>
5508
+ </div>
5509
+ <div id="time-tools" style="display:none;gap:8px;align-items:center">
5510
+ <div class="seg" id="zoom">
5511
+ <button data-z="-">-</button>
5512
+ <button data-z="reset">100%</button>
5513
+ <button data-z="+">+</button>
5514
+ </div>
5515
+ </div>
5516
+ <select class="win-select" id="win" title="按任务开始时间过滤,结束时间不限(两个视图共用)">
5517
+ <option value="7">近 7 天</option>
5518
+ <option value="14">近 14 天</option>
5519
+ <option value="30">近 30 天</option>
5520
+ <option value="0">全部时间</option>
5521
+ </select>
5522
+ <span class="hint" id="hint">按优先级分行(P0 最高);行内从左到右 = 依赖顺序。简约只看任务和依赖,详细展开具体关联。</span>
5523
+ </div>
5524
+ <div id="view-graph">
5525
+ <div class="canvas" id="canvas">
5526
+ <div class="canvas-scroll" id="canvas-scroll">
5527
+ <svg class="edges" id="edges"></svg>
5528
+ </div>
5529
+ <div class="lane-rail" id="lane-rail"></div>
5530
+ <div class="legend-overlay" id="legend">
5531
+ <div class="row"><svg class="sample" viewBox="0 0 38 10"><defs><marker id="lg-dep" markerWidth="6" markerHeight="6" refX="5" refY="3" orient="auto"><path d="M0,0 L6,3 L0,6" fill="#2563eb"/></marker></defs><line x1="1" y1="5" x2="30" y2="5" stroke="#2563eb" stroke-width="1.6" marker-end="url(#lg-dep)"/></svg><span><b>→ depends_on</b> 单向</span></div>
5532
+ </div>
5533
+ </div>
5534
+ </div>
5535
+ <div id="view-time" style="display:none">
5536
+ <div class="timeline">
5537
+ <div class="scroll-hint">← 左右滚动 →</div>
5538
+ <div class="tl-grid">
5539
+ <div class="tl-labels">
5540
+ <div class="tl-label-head">任务</div>
5541
+ <div class="tl-labels-body" id="tl-labels-body"></div>
5542
+ </div>
5543
+ <div class="tl-scroll" id="tl-scroll">
5544
+ <div class="tl-head"><div class="tl-axis" id="tl-axis"></div></div>
5545
+ <div class="tl-body" id="tl-body"></div>
5546
+ </div>
5547
+ </div>
5548
+ <div class="tl-caption">红色虚线 = 今天;虚线右端 = 未录入 due 的开放任务;斜纹 = 进度按状态估算。</div>
5549
+ </div>
5550
+ </div>
5551
+ </section>
5552
+
5553
+ <aside>
5554
+ <div class="panel">
5555
+ <div class="side-title"><h2>概览</h2><span>snapshot</span></div>
5556
+ <div class="list">
5557
+ <div class="item"><span class="k"><i class="dot" style="background:var(--muted)"></i>任务</span><b id="stat-tasks">0</b></div>
5558
+ <div class="item"><span class="k"><i class="dot" style="background:var(--red)"></i>阻塞</span><b id="stat-blocked">0</b></div>
5559
+ <div class="item"><span class="k"><i class="dot" style="background:var(--amber)"></i>待评审</span><b id="stat-review">0</b></div>
5560
+ <div class="item"><span class="k"><i class="dot" style="background:var(--muted)"></i>完成</span><b id="stat-done">0</b></div>
5561
+ <div class="item"><span class="k">claims</span><b id="stat-claims">0</b></div>
5562
+ </div>
5563
+ </div>
5564
+ <div class="panel">
5565
+ <div class="side-title"><h2>需要人</h2><span>attention</span></div>
5566
+ <div class="attention" id="attention-list"></div>
5567
+ </div>
5568
+ </aside>
5569
+ </main>
5570
+ <div class="backdrop" id="backdrop" onclick="closeDrawer()"></div>
5571
+ <aside class="drawer" id="drawer" aria-hidden="true">
5572
+ <div class="drawer-head">
5573
+ <div><h3 id="drawer-title">任务详情</h3><div class="meta" id="drawer-sub"></div></div>
5574
+ <button class="close" onclick="closeDrawer()">×</button>
5575
+ </div>
5576
+ <div class="drawer-tabs">
5577
+ <button data-dtab="task" onclick="setDrawerTab('task')">任务</button>
5578
+ <button data-dtab="intervene" onclick="setDrawerTab('intervene')">命令</button>
5579
+ </div>
5580
+ <div class="drawer-body" id="drawer-body"></div>
5581
+ </aside>
5582
+ <footer>颜色只表达警告/异常:红=阻塞/P0,橙=待评审/P1;其余状态用中性灰 + 单一蓝色进度。关系图按依赖深度分层,时间线独立展示执行情况。</footer>
5583
+
5584
+ <script type="application/json" id="dia-graph">__DIA_PAYLOAD__</script>
5585
+ <script>
5586
+ const payload=(()=>{ try{ const el=document.getElementById('dia-graph'); return JSON.parse(el?el.textContent:'{}')||{}; }catch{ return {}; } })();
5587
+ const TODAY=payload.today||'1970-01-01';
5588
+ const STATUSES=new Set(['todo','doing','review','blocked','done','accepted','dropped','awaiting_acceptance','rejected','escalated']);
5589
+ const tasks=(payload.tasks||[]).map(t=>{
5590
+ const status=STATUSES.has(t.status)?t.status:'todo';
5591
+ const progress=Number.isFinite(t.progress)?Math.max(0,Math.min(100,Math.round(t.progress))):0;
5592
+ return {
5593
+ id:String(t.id), label:String(t.label||t.id), status,
5594
+ prio:String(t.prio||'P?'), progress, derived:!!t.derived,
5595
+ start:Number.isFinite(t.start)?t.start:null,
5596
+ end:Number.isFinite(t.end)?t.end:null,
5597
+ owner:t.owner?String(t.owner):null,
5598
+ deps:Array.isArray(t.deps)?t.deps.map(String):[],
5599
+ touches:Array.isArray(t.touches)?t.touches.map(String):[],
5600
+ };
5601
+ });
5602
+ const taskEdges=(payload.edges||[]).filter(e=>e&&e.kind==='depends_on').map(e=>({from:String(e.from),to:String(e.to),kind:'depends_on'}));
5603
+ const truncated=payload.truncated||null;
5604
+ const state={view:'graph', layout:'priority', mode:'simple', zoom:28, window:7, q:'', sel:[], fd:2, from:null, to:null};
5605
+ const canvas=document.getElementById('canvas'), layer=document.getElementById('canvas-scroll'), rail=document.getElementById('lane-rail'), svg=document.getElementById('edges');
5606
+ const esc=s=>String(s??'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
5607
+ const stIcon={doing:'▶',blocked:'⏸',review:'◐',done:'✓',accepted:'✓',todo:'○',dropped:'⨯',rejected:'✗',escalated:'▲',awaiting_acceptance:'…'};
5608
+ const stIconOf=s=>stIcon[s]||'·';
5609
+
5610
+ function focusSet(id,depth){
5611
+ const by=new Map(tasks.map(t=>[t.id,t]));
5612
+ const max=depth==='all'?Infinity:Math.max(1,Number(depth)||2);
5613
+ const out=new Set([id]); let frontier=[id];
5614
+ for(let step=0; step<max && frontier.length; step++){
5615
+ const next=[];
5616
+ frontier.forEach(cur=>{
5617
+ const t=by.get(cur); if(!t) return;
5618
+ (t.deps||[]).forEach(x=>{ if(by.has(x)&&!out.has(x)){ out.add(x); next.push(x); } });
5619
+ tasks.forEach(o=>{ if((o.deps||[]).includes(cur)&&!out.has(o.id)){ out.add(o.id); next.push(o.id); } });
5620
+ });
5621
+ frontier=next;
5622
+ }
5623
+ return out;
5624
+ }
5625
+ function visibleTasks(){
5626
+ const base=tasks.filter(t=>state.window<=0 || t.start===null || t.start>=-state.window);
5627
+ if(!state.sel.length) return base;
5628
+ const keep=new Set();
5629
+ state.sel.forEach(id=>focusSet(id,state.fd).forEach(x=>keep.add(x)));
5630
+ return base.filter(t=>keep.has(t.id));
5631
+ }
5632
+ function searchMatches(){
5633
+ const q=(state.q||'').trim().toLowerCase();
5634
+ if(!q) return null;
5635
+ return tasks.filter(t=>t.id.toLowerCase().includes(q)||t.label.toLowerCase().includes(q)||String(t.owner||'').toLowerCase().includes(q));
5636
+ }
5637
+ function syncStats(){
5638
+ const list=visibleTasks(), st=id=>document.getElementById(id);
5639
+ if(!st('stat-tasks')) return;
5640
+ st('stat-tasks').textContent = list.length===tasks.length ? String(tasks.length) : list.length+' / '+tasks.length;
5641
+ st('stat-blocked').textContent=String(list.filter(t=>t.status==='blocked').length);
5642
+ st('stat-review').textContent=String(list.filter(t=>t.status==='review').length);
5643
+ st('stat-done').textContent=String(list.filter(t=>t.status==='done'||t.status==='accepted').length);
5644
+ if(st('stat-claims')) st('stat-claims').textContent=String((payload.claims||[]).length);
5645
+ renderAttention(list);
5646
+ const tp=document.getElementById('trunc-pill');
5647
+ if(tp){ if(truncated&&truncated.shown<truncated.total){ tp.style.display=''; tp.textContent='已截断 '+truncated.shown+' / '+truncated.total+'(--all 全量)'; } else tp.style.display='none'; }
5648
+ }
5649
+ function renderAttention(list){
5650
+ const box=document.getElementById('attention-list'); if(!box) return;
5651
+ const rows=[];
5652
+ list.filter(t=>t.status==='blocked').slice(0,3).forEach(t=>rows.push({tag:'阻塞',cls:'',t,note:'blocked'}));
5653
+ list.filter(t=>t.status==='review').slice(0,3).forEach(t=>rows.push({tag:'评审',cls:' amber',t,note:'等待评审'}));
5654
+ box.innerHTML=rows.length?rows.map(r=>'<div class="row" data-task="'+esc(r.t.id)+'"><span class="tag'+r.cls+'">'+r.tag+'</span><div><b>'+esc(r.t.label)+'</b><br><span class="k">'+esc(r.t.id)+' · '+esc(r.note)+'</span><div class="quick"><button class="primary" onclick="event.stopPropagation();openDrawer(\\''+esc(r.t.id)+'\\',\\'intervene\\')">命令</button><button onclick="event.stopPropagation();openDrawer(\\''+esc(r.t.id)+'\\',\\'task\\')">查看</button></div></div></div>').join('')
5655
+ :'<p class="k" style="padding:8px 0">当前没有阻塞或待评审任务。</p>';
5656
+ box.querySelectorAll('.row[data-task]').forEach(el=>el.onclick=()=>openDrawer(el.dataset.task,'task'));
5657
+ }
5658
+ function renderEmpty(){
5659
+ if(tasks.length) return;
5660
+ const el=document.getElementById('canvas-scroll');
5661
+ if(el) el.innerHTML='<div style="padding:60px 20px;text-align:center;color:var(--muted)">账本里还没有任务。<br>先跑 <code>dia task add &lt;title&gt; --prio P1</code>,再重新导出。</div>';
5662
+ }
5663
+ function updateHint(){
5664
+ const h=document.getElementById('hint'); if(!h||state.view!=='graph') return;
5665
+ const n=visibleTasks().length;
5666
+ if(state.sel.length){
5667
+ h.textContent='显示 '+n+' 个任务 · '+(state.fd==='all'?'全链路':state.fd+' 层');
5668
+ return;
5669
+ }
5670
+ h.textContent='显示 '+n+' / '+tasks.length+' 个任务 · '+(state.layout==='dep'?'从左到右 = 依赖深度':'按优先级分行(P0 最高)');
5671
+ }
5672
+ function depths(){
5673
+ const list=visibleTasks();
5674
+ const by=new Map(list.map(t=>[t.id,t])), memo=new Map();
5675
+ const depth=t=>{
5676
+ if(!t) return 0;
5677
+ if(memo.has(t.id)) return memo.get(t.id);
5678
+ memo.set(t.id,0);
5679
+ const d=t.deps.length?Math.max(...t.deps.map(x=>{ const dt=by.get(x); return dt?depth(dt):-1; }))+1:0;
5680
+ memo.set(t.id,d); return d;
5681
+ };
5682
+ list.forEach(depth); return memo;
5683
+ }
5684
+ function levels(){
5685
+ return [...new Set(visibleTasks().map(t=>t.prio))].sort((a,b)=>{
5686
+ const na=parseInt(String(a).replace(/^P/i,''),10), nb=parseInt(String(b).replace(/^P/i,''),10);
5687
+ return (Number.isFinite(na)?na:99)-(Number.isFinite(nb)?nb:99)||String(a).localeCompare(String(b));
5688
+ });
5689
+ }
5690
+ function rowsFor(){
5691
+ const list=visibleTasks(), d=depths();
5692
+ const gap=state.mode==='detail'?230:145;
5693
+ const h=state.mode==='detail'?205:110;
5694
+ return levels().map((prio,i)=>{
5695
+ const rowTasks=list.filter(t=>t.prio===prio).sort((a,b)=>(d.get(a.id)-d.get(b.id))||a.label.localeCompare(b.label));
5696
+ return {prio,y:36+i*gap,h,tasks:rowTasks};
5697
+ }).filter(r=>r.tasks.length);
5698
+ }
5699
+ let cardMetrics={mid:52,h:58}, clickTimer=null;
5700
+ function measureCard(){
5701
+ const probe=card(tasks[0],-9999,-9999);
5702
+ probe.style.visibility='hidden';
5703
+ layer.appendChild(probe);
5704
+ const prog=probe.querySelector('.progress');
5705
+ const mid=prog?prog.offsetTop+prog.offsetHeight/2+probe.clientTop:44;
5706
+ const h=probe.offsetHeight||58;
5707
+ probe.remove();
5708
+ return {mid,h};
5709
+ }
5710
+ function bandCenter(r, bandH){ return r.y-12+bandH/2; }
5711
+ function cardTop(r){
5712
+ const bandH=state.mode==='detail'?230:145;
5713
+ const center=bandCenter(r,bandH);
5714
+ // 详细:进度条中心线对齐泳道中心;简约:卡片盒整体垂直居中
5715
+ return state.mode==='detail'?center-cardMetrics.mid:center-cardMetrics.h/2;
5716
+ }
5717
+ function edgeOffset(){ return state.mode==='detail'?cardMetrics.mid:cardMetrics.h/2; }
5718
+ function pos(){
5719
+ const rows=rowsFor(), p=new Map();
5720
+ rows.forEach(r=>r.tasks.forEach((t,i)=>p.set(t.id,{x:170+i*230,y:cardTop(r)})));
5721
+ return {p,rows};
5722
+ }
5723
+ function card(t,x,y,showPrio=false){
5724
+ const el=document.createElement('div'); el.className='node task '+t.status; el.style.left=x+'px'; el.style.top=y+'px';
5725
+ const rel=taskEdges.filter(e=>e.from===t.id||e.to===t.id).map(e=>{
5726
+ const outgoing=e.from===t.id, other=outgoing?e.to:e.from;
5727
+ const kind=e.kind==='depends_on'?(outgoing?'depends_on':'depended_by'):e.kind;
5728
+ return '<div class="meta" style="margin-top:2px">↳ '+esc(kind)+': '+esc(other)+'</div>';
5729
+ }).slice(0,4).join('');
5730
+ const badge=showPrio?'<span class="prio '+esc(t.prio.toLowerCase())+'">'+esc(t.prio)+'</span>':'';
5731
+ el.innerHTML='<b>'+esc(t.label)+badge+'</b>'+
5732
+ '<div class="meta">'+esc(t.id)+' · <span class="st '+esc(t.status)+'">'+stIconOf(t.status)+' '+esc(t.status)+'</span> · '+t.progress+'%</div>'+
5733
+ '<div class="progress"><i class="'+esc(t.status)+'" style="width:'+t.progress+'%"></i></div>'+
5734
+ (state.mode==='detail'?'<div class="meta" style="margin-top:5px">'+(t.owner?'@'+esc(t.owner):'unassigned')+' · dep '+t.deps.length+'</div>'+rel:'');
5735
+ el.dataset.id=t.id;
5736
+ el.onclick=()=>{ clearTimeout(clickTimer); clickTimer=setTimeout(()=>openDrawer(t.id,'task'),180); };
5737
+ el.ondblclick=e=>{ e.preventDefault(); clearTimeout(clickTimer); toggleSel(t.id); };
5738
+ return el;
5739
+ }
5740
+ function edgePath(a,b){
5741
+ const o=edgeOffset(), x1=a.x+190,y1=a.y+o,x2=b.x,y2=b.y+o;
5742
+ if(x2>=x1) return \`M\${x1} \${y1} H\${x1+Math.max(24,(x2-x1)/2)} V\${y2} H\${x2}\`;
5743
+ return \`M\${x1} \${y1} H\${x1-30} V\${y2} H\${x2}\`;
5744
+ }
5745
+ function drawGraph(){
5746
+ canvas.classList.toggle('simple', state.mode==='simple');
5747
+ if(state.layout==='dep'){ drawDepGraph(); return; }
5748
+ cardMetrics=measureCard();
5749
+ const {p,rows}=pos();
5750
+ [...layer.querySelectorAll('.node,.pgroup')].forEach(e=>e.remove());
5751
+ rail.style.display=''; rail.querySelectorAll('.plane-label').forEach(e=>e.remove());
5752
+ const maxX=Math.max(600, ...rows.map(r=>170+(r.tasks.length-1)*230+190+10));
5753
+ const maxY=rows.length?Math.max(...rows.map(r=>r.y+r.h))+40:600;
5754
+ const bandH=state.mode==='detail'?230:145;
5755
+ rows.forEach((r,i)=>{
5756
+ const g=document.createElement('div'); g.className='pgroup '+r.prio.toLowerCase();
5757
+ g.style.top=(r.y-12)+'px'; g.style.width=maxX+'px'; g.style.height=bandH+'px';
5758
+ const alpha=Math.max(0.02, 0.075-i*0.012);
5759
+ g.style.background='color-mix(in srgb, var(--blue) '+Math.round(alpha*100)+'%, transparent)';
5760
+ layer.appendChild(g);
5761
+ const label=document.createElement('div'); label.className='plane-label '+r.prio.toLowerCase();
5762
+ label.dataset.y=String(r.y-12+bandH/2); label.style.top=(r.y-12+bandH/2-layer.scrollTop)+'px';
5763
+ const meta=r.prio==='P0'?['var(--red)','紧急']:r.prio==='P1'?['var(--amber)','高']:r.prio==='P?'?['var(--muted)','未排期']:['var(--muted)','常规'];
5764
+ label.style.color=meta[0];
5765
+ label.innerHTML=esc(r.prio)+' · '+r.tasks.length+'<small>'+meta[1]+'</small>';
5766
+ rail.appendChild(label);
5767
+ r.tasks.forEach((t,j)=>layer.appendChild(card(t,170+j*230,cardTop(r))));
5768
+ });
5769
+ svg.setAttribute('width',maxX); svg.setAttribute('height',maxY);
5770
+ const defs=\`<defs>
5771
+ <marker id="ar-dep" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7" fill="#2563eb"/></marker>
5772
+ <marker id="ar-collab" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7" fill="#64748b"/></marker>
5773
+ </defs>\`;
5774
+ const lines=taskEdges.map(e=>{
5775
+ const a=p.get(e.from), b=p.get(e.to); if(!a||!b) return '';
5776
+ return \`<path d="\${edgePath(a,b)}" fill="none" stroke-width="1.6" stroke="#2563eb" marker-end="url(#ar-dep)"/>\`;
5777
+ }).join('');
5778
+ svg.innerHTML=defs+lines;
5779
+ document.getElementById('updated').textContent='快照 '+(payload.generated_at||'-');
5780
+ syncStats(); updateHint(); applySearch();
5781
+ }
5782
+ let drawerTask=null, drawerTab='task';
5783
+ function relations(t){
5784
+ return taskEdges.filter(e=>e.from===t.id||e.to===t.id).map(e=>{
5785
+ const outgoing=e.from===t.id, other=outgoing?e.to:e.from;
5786
+ const kind=e.kind==='depends_on'?(outgoing?'depends_on':'depended_by'):e.kind;
5787
+ return '<div class="item"><span class="k">'+esc(kind)+'</span><span>'+esc(other)+'</span></div>';
5788
+ }).join('') || '<p class="k">无边</p>';
5789
+ }
5790
+ function taskHtml(t){
5791
+ return '<div class="kv"><div class="k">status</div><div>'+esc(t.status)+'</div>'+
5792
+ '<div class="k">priority</div><div>'+esc(t.prio)+'</div>'+
5793
+ '<div class="k">owner</div><div>'+esc(t.owner||'-')+'</div>'+
5794
+ '<div class="k">start</div><div>'+(t.start===null?'-(按创建时间)':esc(isoDate(t.start)))+'</div>'+
5795
+ '<div class="k">due</div><div>'+(Number.isFinite(t.end)?esc(isoDate(t.end)):'开放')+'</div>'+
5796
+ '<div class="k">progress</div><div>'+t.progress+'%'+(t.derived?'(估算)':'')+'</div>'+
5797
+ '<div class="k">deps</div><div>'+esc(t.deps.join(', ')||'-')+'</div>'+
5798
+ '<div class="k">touches</div><div>'+esc(t.touches.join(', ')||'-')+'</div></div>'+
5799
+ '<h4>关联</h4><div class="rel-list">'+relations(t)+'</div>';
5800
+ }
5801
+ function commandHtml(t){
5802
+ const cmds=[
5803
+ ['dia task plan '+t.id+' --prio P1', '设置优先级'],
5804
+ ['dia task plan '+t.id+' --start '+isoDate(0)+' --due '+isoDate(7), '设置计划起止'],
5805
+ ['dia task plan '+t.id+' --progress 50', '更新进度'],
5806
+ ['dia msg send <session> "关于 '+t.id+': "', '给负责人发消息'],
5807
+ ['dia claim '+t.id, '声明正在处理'],
5808
+ ];
5809
+ return '<p class="k">本页是静态快照:命令只供复制,不会发送。</p>'+
5810
+ cmds.map(([c,l])=>'<div class="cmd"><code>'+esc(c)+'</code><button onclick="copyCmd(this)" data-cmd="'+esc(c)+'">复制</button><span class="k">'+esc(l)+'</span></div>').join('');
5811
+ }
5812
+ function copyCmd(btn){
5813
+ const text=btn.dataset.cmd||'';
5814
+ if(navigator.clipboard&&navigator.clipboard.writeText){ navigator.clipboard.writeText(text).then(()=>{ btn.textContent='已复制'; },()=>{ btn.textContent='复制失败'; }); }
5815
+ else { btn.textContent='请手动复制'; }
5816
+ }
5817
+ function openDrawer(id, tab){
5818
+ drawerTask=tasks.find(t=>t.id===id); if(!drawerTask) return;
5819
+ drawerTab=tab||'task';
5820
+ document.getElementById('drawer-title').textContent='任务详情';
5821
+ document.getElementById('drawer-sub').textContent=drawerTask.label+' · '+drawerTask.id+' · '+drawerTask.prio+' · '+drawerTask.status;
5822
+ document.getElementById('drawer').classList.add('show');
5823
+ document.getElementById('drawer').setAttribute('aria-hidden','false');
5824
+ document.getElementById('backdrop').classList.add('show');
5825
+ setDrawerTab(drawerTab);
5826
+ }
5827
+ function setDrawerTab(tab){
5828
+ drawerTab=tab;
5829
+ document.querySelectorAll('[data-dtab]').forEach(b=>b.classList.toggle('on',b.dataset.dtab===tab));
5830
+ const body=document.getElementById('drawer-body');
5831
+ body.innerHTML=tab==='intervene'?commandHtml(drawerTask):taskHtml(drawerTask);
5832
+ }
5833
+ function closeDrawer(){
5834
+ document.getElementById('drawer').classList.remove('show');
5835
+ document.getElementById('drawer').setAttribute('aria-hidden','true');
5836
+ document.getElementById('backdrop').classList.remove('show');
5837
+ }
5838
+ function todayUTC(){ const [y,m,d]=TODAY.split('-').map(Number); return Date.UTC(y,(m||1)-1,d||1); }
5839
+ function dayDate(d){
5840
+ const dt=new Date(todayUTC()+d*86400000);
5841
+ return ('0'+(dt.getUTCMonth()+1)).slice(-2)+'-'+('0'+dt.getUTCDate()).slice(-2);
5842
+ }
5843
+ function isoDate(d){ return new Date(todayUTC()+d*86400000).toISOString().slice(0,10); }
5844
+ function timelineWindow(){
5845
+ if(state.from!==null&&state.to!==null) return {from:state.from,to:state.to};
5846
+ const list=visibleTasks();
5847
+ const w=state.window>0?state.window:14;
5848
+ const starts=list.map(t=>t.start).filter(Number.isFinite);
5849
+ const ends=list.map(t=>t.end).filter(Number.isFinite);
5850
+ const from=Math.min(-w, starts.length?Math.min(...starts):-w);
5851
+ const to=Math.max(0, ends.length?Math.max(...ends):0)+1;
5852
+ return {from,to};
5853
+ }
5854
+ function drawTimeline(){
5855
+ const win=timelineWindow(), span=Math.max(1,win.to-win.from);
5856
+ const x=v=>((v-win.from)/span)*100;
5857
+ const chartW=Math.max(640, span*state.zoom);
5858
+ document.getElementById('view-time').style.setProperty('--tlw', chartW+'px');
5859
+ const axis=document.getElementById('tl-axis'); axis.innerHTML='';
5860
+ const step=span<=10?1:span<=18?2:3;
5861
+ for(let d=win.from;d<=win.to;d+=step){
5862
+ const el=document.createElement('span'); el.style.left=x(d)+'%';
5863
+ el.innerHTML=dayDate(d)+'<small>'+(d===0?'今天':(d>0?'+'+d+'d':d+'d'))+'</small>';
5864
+ if(d===win.from) el.style.transform='translateX(0)'; else if(d+step>win.to) el.style.transform='translateX(-100%)';
5865
+ if(d===0)el.style.color='var(--red)'; axis.appendChild(el);
5866
+ }
5867
+ const body=document.getElementById('tl-body'); body.innerHTML=''; const labelsBody=document.getElementById('tl-labels-body'); labelsBody.innerHTML='';
5868
+ visibleTasks().slice().sort((a,b)=>a.start-b.start).forEach(t=>{
5869
+ const row=document.createElement('div'); row.className='tl-row';
5870
+ row.innerHTML='<div class="tl-track"></div>';
5871
+ const label=document.createElement('div'); label.className='tl-label-row'; label.innerHTML='<b>'+esc(t.label)+'</b><span>'+esc(t.id)+' · '+esc(t.prio)+' · '+esc(t.status)+'</span>'; labelsBody.appendChild(label);
5872
+ const track=row.querySelector('.tl-track');
5873
+ const startOff=t.start===null?win.from:t.start;
5874
+ const endOff=Number.isFinite(t.end)?t.end:win.to;
5875
+ const bar=document.createElement('div'); bar.className='tl-bar '+t.status+(Number.isFinite(t.end)?'':' open');
5876
+ bar.style.left=x(startOff)+'%'; bar.style.width=Math.max(1.5,x(endOff)-x(startOff))+'%';
5877
+ bar.title=dayDate(startOff)+' → '+(Number.isFinite(t.end)?dayDate(t.end):'开放')+' ('+t.progress+'%'+(t.derived?' 估算':'')+')';
5878
+ bar.innerHTML='<div class="fill" style="width:'+t.progress+'%"></div>'; bar.onclick=()=>openDrawer(t.id,'task'); track.appendChild(bar);
5879
+ if(win.from<=0&&win.to>=0){ const line=document.createElement('div'); line.className='tl-today'; line.style.left=x(0)+'%'; line.style.top='0'; line.style.bottom='0'; line.textContent=''; track.appendChild(line); }
5880
+ body.appendChild(row);
5881
+ });
5882
+ syncStats();
5883
+ }
5884
+ function drawDepGraph(){
5885
+ cardMetrics=measureCard();
5886
+ const list=visibleTasks(), d=depths(), groups=new Map();
5887
+ list.forEach(t=>{ const k=d.get(t.id)||0; groups.set(k,[...(groups.get(k)||[]),t]); });
5888
+ const p=new Map(); const rows=[];
5889
+ const gap=14;
5890
+ [...groups.keys()].sort((a,b)=>a-b).forEach((depth,ci)=>{
5891
+ const all=groups.get(depth).sort((a,b)=>a.prio.localeCompare(b.prio)||a.label.localeCompare(b.label));
5892
+ rows.push({depth,all,x:30+ci*230});
5893
+ });
5894
+ [...layer.querySelectorAll('.node,.pgroup')].forEach(e=>e.remove());
5895
+ rail.innerHTML=''; rail.style.display='none';
5896
+ let maxY=0;
5897
+ // 先渲染卡片,再按实测高度逐列堆叠,避免详细模式卡片相互重叠
5898
+ rows.forEach(r=>{
5899
+ let y=60;
5900
+ r.all.forEach(t=>{
5901
+ const el=card(t,r.x,y,true);
5902
+ el.style.top=y+'px';
5903
+ layer.appendChild(el);
5904
+ p.set(t.id,{x:r.x,y});
5905
+ y+=el.offsetHeight+gap;
5906
+ });
5907
+ maxY=Math.max(maxY,y);
5908
+ });
5909
+ maxY=Math.max(600,maxY);
5910
+ const maxX=Math.max(600,...rows.map(r=>r.x+200+10));
5911
+ svg.setAttribute('width',maxX); svg.setAttribute('height',maxY+40);
5912
+ const defs=\`<defs>
5913
+ <marker id="ar-dep" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7" fill="#2563eb"/></marker>
5914
+ <marker id="ar-collab" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7" fill="#64748b"/></marker>
5915
+ </defs>\`;
5916
+ const lines=taskEdges.map(e=>{
5917
+ const a=p.get(e.from), b=p.get(e.to); if(!a||!b) return '';
5918
+ return \`<path d="\${edgePath(a,b)}" fill="none" stroke-width="1.6" stroke="#2563eb" marker-end="url(#ar-dep)"/>\`;
5919
+ }).join('');
5920
+ svg.innerHTML=defs+lines;
5921
+ document.getElementById('updated').textContent='updated '+new Date().toLocaleTimeString();
5922
+ syncStats(); updateHint(); applySearch();
5923
+ }
5924
+ function syncLaneLabels(){
5925
+ rail.querySelectorAll('.plane-label').forEach(el=>{
5926
+ el.style.top=(Number(el.dataset.y||0)-layer.scrollTop)+'px';
5927
+ });
5928
+ }
5929
+ function applySearch(){
5930
+ const ms=searchMatches();
5931
+ layer.querySelectorAll('.node.task').forEach(el=>{
5932
+ el.classList.remove('hit','dim');
5933
+ if(!ms) return;
5934
+ if(ms.some(t=>t.id===el.dataset.id)) el.classList.add('hit'); else el.classList.add('dim');
5935
+ });
5936
+ const box=document.getElementById('q-list');
5937
+ if(box && box.classList.contains('open')) renderSearchList(ms);
5938
+ }
5939
+ function renderSearchList(ms){
5940
+ const box=document.getElementById('q-list'); if(!box) return;
5941
+ const q=(state.q||'').trim();
5942
+ const browse=!q;
5943
+ 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;
5944
+ if(!pool||!pool.length){
5945
+ box.innerHTML='<div class="q-empty">'+(browse?'当前时间窗内没有任务':'没有匹配的任务')+'</div>';
5946
+ box.classList.add('open'); return;
5947
+ }
5948
+ const head=browse?('<div class="q-head">'+pool.length+' 个任务 · ↑↓ 选择,Enter 加入</div>')
5949
+ :('<div class="q-head">匹配 '+pool.length+' 个 · ↑↓ 选择,Enter 加入</div>');
5950
+ box.innerHTML=head+pool.slice(0,20).map(t=>{
5951
+ const on=state.sel.includes(t.id);
5952
+ 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>';
5953
+ }).join('');
5954
+ box.classList.add('open');
5955
+ box.querySelectorAll('.q-item').forEach(el=>el.onclick=e=>{ e.stopPropagation(); toggleSel(el.dataset.id); });
5956
+ }
5957
+ function searchListItems(){
5958
+ const box=document.getElementById('q-list');
5959
+ return box?[...box.querySelectorAll('.q-item')]:[];
5960
+ }
5961
+ function moveSearchCursor(delta){
5962
+ let items=searchListItems();
5963
+ if(!items.length){ renderSearchList(searchMatches()); items=searchListItems(); if(!items.length) return; }
5964
+ let idx=items.findIndex(el=>el.classList.contains('active'));
5965
+ idx=idx<0?0:(idx+delta+items.length)%items.length;
5966
+ items.forEach((el,i)=>el.classList.toggle('active',i===idx));
5967
+ items[idx].scrollIntoView({block:'nearest'});
5968
+ }
5969
+ function toggleSel(id){
5970
+ const i=state.sel.indexOf(id);
5971
+ if(i>=0) state.sel.splice(i,1); else state.sel.push(id);
5972
+ state.fd=state.fd||2; state.q='';
5973
+ const qi=document.getElementById('q'); if(qi) qi.value='';
5974
+ go(state.view,state.mode,state.range,state.zoom);
5975
+ }
5976
+ function renderTokens(){
5977
+ const box=document.getElementById('tokens'); if(!box) return;
5978
+ box.innerHTML=state.sel.map(id=>'<span class="token">'+esc(id)+'<button data-id="'+esc(id)+'" title="移除">×</button></span>').join('');
5979
+ box.querySelectorAll('button').forEach(b=>b.onclick=e=>{ e.stopPropagation(); toggleSel(b.dataset.id); });
5980
+ }
5981
+ function readRoute(){
5982
+ const raw=location.hash.replace(/^#\\/?/,''); const [path,qs]=raw.split('?');
5983
+ const params=new URLSearchParams(qs||location.search);
5984
+ const range=(params.get('range')||'30d');
5985
+ const zoom=Math.min(84,Math.max(14,parseInt(params.get('zoom')||'28',10)||28));
5986
+ const sx=Number(params.get('sx')||0);
5987
+ const fromRaw=params.get('from'), toRaw=params.get('to');
5988
+ const toOff=v=>v?Math.round((new Date(v+'T00:00:00').getTime()-new Date('2026-11-06T00:00:00').getTime())/86400000):null;
5989
+ const winRaw=params.get('w');
5990
+ const rangeRaw=params.get('range');
5991
+ const win=winRaw!==null?Math.max(0,parseInt(winRaw,10)||0):(rangeRaw?Math.max(0,parseInt(rangeRaw,10)||0):7);
5992
+ const fdRaw=params.get('fd');
5993
+ const fd=fdRaw==='all'?'all':Math.min(9,Math.max(1,parseInt(fdRaw||'2',10)||2));
5994
+ 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'};
5995
+ }
5996
+ function routeString(view,mode,range,zoom,from,to){
5997
+ const p=view==='time'?'timeline':'graph'; const qs=new URLSearchParams();
5998
+ if(view==='graph'&&mode==='detail') qs.set('mode','detail');
5999
+ if(view==='graph'&&state.layout==='dep') qs.set('layout','dep');
6000
+ if(view==='graph'){
6001
+ if(state.window!==7) qs.set('w',String(state.window));
6002
+ if(state.q&&state.q.trim()) qs.set('q',state.q.trim());
6003
+ if(state.sel.length){ qs.set('sel',state.sel.join(',')); if(state.fd!==2) qs.set('fd',String(state.fd)); }
6004
+ }
6005
+ if(view==='time'){
6006
+ if(from!==null&&to!==null){ qs.set('from',isoDate(from)); qs.set('to',isoDate(to)); }
6007
+ if(state.window!==7) qs.set('w',String(state.window));
6008
+ if(zoom&&zoom!==28) qs.set('zoom',String(zoom));
6009
+ }
6010
+ return '#/'+p+(qs.toString()?'?'+qs.toString():'');
6011
+ }
6012
+ function applyRoute(){
6013
+ const stampEl=document.getElementById('updated');
6014
+ if(stampEl) stampEl.textContent='快照 '+(payload.generated_at||'-');
6015
+ 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;
6016
+ document.querySelectorAll('#tabs button').forEach(x=>x.classList.toggle('on',x.dataset.v===r.view));
6017
+ document.querySelectorAll('#mode button').forEach(x=>x.classList.toggle('on',x.dataset.m===r.mode));
6018
+ document.querySelectorAll('#layout button').forEach(x=>x.classList.toggle('on',x.dataset.l===r.layout));
6019
+ document.getElementById('view-graph').style.display=r.view==='graph'?'':'none';
6020
+ document.getElementById('view-time').style.display=r.view==='time'?'':'none';
6021
+ document.getElementById('graph-tools').style.display=r.view==='graph'?'flex':'none';
6022
+ document.getElementById('time-tools').style.display=r.view==='time'?'flex':'none';
6023
+ const zbtn=document.querySelector('#zoom button[data-z="reset"]'); if(zbtn) zbtn.textContent=Math.round(r.zoom/28*100)+'%';
6024
+ const dec=document.querySelector('#zoom button[data-z="-"]'), inc=document.querySelector('#zoom button[data-z="+"]'); if(dec) dec.disabled=r.zoom<=14; if(inc) inc.disabled=r.zoom>=84;
6025
+ if(r.view!=='graph') document.getElementById('hint').textContent='时间线:左右滚动查看;-/+缩放;红色虚线 = 今天';
6026
+ const winSel=document.getElementById('win'); if(winSel) winSel.value=String(r.window);
6027
+ const qIn=document.getElementById('q'); if(qIn && qIn.value!==r.q) qIn.value=r.q;
6028
+ const chip=document.getElementById('focus-chip');
6029
+ if(chip) chip.classList.toggle('on',r.sel.length>0);
6030
+ if(chip) chip.title='按依赖邻域聚焦;清空即回全量';
6031
+ const fdEl=document.getElementById('focus-fd'); if(fdEl) fdEl.value=String(r.fd);
6032
+ renderTokens();
6033
+ const rp=document.getElementById('route'); if(rp) rp.textContent=location.hash||routeString(r.view,r.mode,r.range,state.zoom,state.from,state.to);
6034
+ document.title='dia graph · '+(r.view==='time'?'时间线':'关系图');
6035
+ 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();
6036
+ if(r.task) openDrawer(r.task,r.tab);
6037
+ syncLaneLabels();
6038
+ if(r.view==='graph') renderEmpty();
6039
+ }
6040
+ function go(view,mode,range,zoom){ const h=routeString(view,mode,range||state.range,zoom||state.zoom,state.from,state.to); if(location.hash===h) applyRoute(); else location.hash=h; }
6041
+ document.querySelectorAll('#tabs button').forEach(b=>b.onclick=()=>go(b.dataset.v==='time'?'time':'graph',state.mode,state.range));
6042
+ document.querySelectorAll('#layout button').forEach(b=>b.onclick=()=>{state.layout=b.dataset.l;go(state.view,state.mode,state.range,state.zoom);});
6043
+ const winSelEl=document.getElementById('win');
6044
+ if(winSelEl) winSelEl.onchange=()=>{ state.window=Number(winSelEl.value); go(state.view,state.mode,state.range,state.zoom); };
6045
+ const qInput=document.getElementById('q');
6046
+ if(qInput){
6047
+ qInput.oninput=()=>{ state.q=qInput.value; applySearch(); renderSearchList(searchMatches()); };
6048
+ qInput.onkeydown=e=>{
6049
+ if(e.key==='ArrowDown'){ e.preventDefault(); moveSearchCursor(1); }
6050
+ else if(e.key==='ArrowUp'){ e.preventDefault(); moveSearchCursor(-1); }
6051
+ else if(e.key==='Enter'){
6052
+ const items=searchListItems();
6053
+ const active=items.find(el=>el.classList.contains('active'))||items[0];
6054
+ if(active) toggleSel(active.dataset.id);
6055
+ } else if(e.key==='Escape'){ state.q=''; qInput.value=''; applySearch(); qInput.blur(); }
6056
+ else if(e.key==='Backspace' && !qInput.value && state.sel.length){ e.preventDefault(); toggleSel(state.sel[state.sel.length-1]); }
6057
+ };
6058
+ qInput.onfocus=()=>{ renderSearchList(searchMatches()); };
6059
+ qInput.onclick=()=>{ renderSearchList(searchMatches()); };
6060
+ const sbox=document.getElementById('search-box');
6061
+ if(sbox) sbox.onclick=e=>{ if(e.target.closest('.q-item')||e.target.closest('.token')) return; qInput.focus(); };
6062
+ qInput.onblur=()=>{ setTimeout(()=>{ const box=document.getElementById('q-list'); if(box && !document.getElementById('search-box').contains(document.activeElement)) box.classList.remove('open'); },160); };
6063
+ }
6064
+ const fdSel=document.getElementById('focus-fd');
6065
+ if(fdSel) fdSel.onchange=()=>{ state.fd=fdSel.value==='all'?'all':Number(fdSel.value); go(state.view,state.mode,state.range,state.zoom); };
6066
+ const fxBtn=document.getElementById('focus-exit');
6067
+ if(fxBtn) fxBtn.onclick=()=>{ state.sel=[]; go(state.view,state.mode,state.range,state.zoom); };
6068
+ document.querySelectorAll('#mode button').forEach(b=>b.onclick=()=>go(state.view,b.dataset.m,state.range,state.zoom));
6069
+ 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); });
6070
+ layer.addEventListener('scroll',syncLaneLabels,{passive:true});
6071
+ const tlScroller=document.getElementById('tl-scroll');
6072
+ tlScroller.addEventListener('scroll',()=>{ const lb=document.getElementById('tl-labels-body'); if(lb) lb.style.transform='translateY(-'+tlScroller.scrollTop+'px)'; },{passive:true});
6073
+ document.querySelectorAll('.attention .row[data-task]').forEach(r=>r.onclick=()=>openDrawer(r.dataset.task,'intervene'));
6074
+ window.addEventListener('hashchange',applyRoute);
6075
+ applyRoute();
6076
+ if(document.fonts&&document.fonts.ready){ document.fonts.ready.then(()=>{ if(state.view==='graph'){ if(state.layout==='dep') drawDepGraph(); else drawGraph(); } }); }
6077
+ </script>
6078
+ </body>
6079
+ </html>
6080
+ `;
6081
+
6082
+ // src/graph-html/render.ts
6083
+ var pad = (n) => String(n).padStart(2, "0");
6084
+ function localDay(d) {
6085
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
6086
+ }
6087
+ function dayDiff(from, to) {
6088
+ const a = from.split("-").map(Number), b = to.split("-").map(Number);
6089
+ return Math.round((Date.UTC(b[0], b[1] - 1, b[2]) - Date.UTC(a[0], a[1] - 1, a[2])) / 86400000);
6090
+ }
6091
+ function shortHash(s) {
6092
+ let h = 2166136261;
6093
+ for (let i = 0;i < s.length; i++) {
6094
+ h ^= s.charCodeAt(i);
6095
+ h = Math.imul(h, 16777619);
6096
+ }
6097
+ return (h >>> 0).toString(16).padStart(8, "0").slice(0, 8);
6098
+ }
6099
+ function safeJson(v) {
6100
+ return JSON.stringify(v).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
6101
+ }
6102
+ var bare = (id) => id.replace(/^task:/, "");
6103
+ function buildPayload(graph, opts = {}) {
6104
+ const now = opts.now ?? new Date;
6105
+ const today = localDay(now);
6106
+ const limit = opts.limit === undefined ? 500 : opts.limit;
6107
+ const redact = !!opts.redact;
6108
+ const taskNodes = graph.nodes.filter((n) => n.kind === "task");
6109
+ const known = new Set(taskNodes.map((n) => n.id));
6110
+ const ordered = [...taskNodes].sort((a, b) => String(a.prio ?? "P9").localeCompare(String(b.prio ?? "P9")) || String(a.start ?? a.created_at ?? "").localeCompare(String(b.start ?? b.created_at ?? "")) || a.id.localeCompare(b.id));
6111
+ const shown = limit > 0 ? ordered.slice(0, limit) : ordered;
6112
+ const tasks = shown.map((n) => {
6113
+ const startDay = n.start ?? (n.created_at ? localDay(new Date(n.created_at)) : null);
6114
+ const endDay = n.due ?? (n.closed_at ? localDay(new Date(n.closed_at)) : null);
6115
+ return {
6116
+ id: bare(n.id),
6117
+ label: n.label,
6118
+ status: n.status ?? "todo",
6119
+ prio: n.prio ?? null,
6120
+ start: startDay ? dayDiff(today, startDay) : null,
6121
+ end: endDay && startDay ? dayDiff(today, endDay) : null,
6122
+ progress: typeof n.progress === "number" ? n.progress : 0,
6123
+ derived: n.progress_derived !== false,
6124
+ owner: n.owner ? redact ? `owner-${shortHash(n.owner)}` : n.owner : null,
6125
+ deps: (n.deps ?? []).filter((d) => known.has(`task:${d}`)),
6126
+ touches: (n.touches ?? []).map((p) => redact ? p.split("/").pop() || p : p)
6127
+ };
6128
+ });
6129
+ const shownIds = new Set(tasks.map((t) => t.id));
6130
+ const edges = graph.edges.filter((e) => e.kind === "depends_on").map((e) => ({ from: bare(e.from), to: bare(e.to), kind: "depends_on" })).filter((e) => shownIds.has(e.from) && shownIds.has(e.to));
6131
+ return {
6132
+ schema: 1,
6133
+ today,
6134
+ generated_at: `${today} ${pad(now.getHours())}:${pad(now.getMinutes())}`,
6135
+ tasks,
6136
+ edges,
6137
+ cycles: graph.cycles.map((c) => c.map(bare)),
6138
+ claims: opts.claims ?? graph.edges.filter((e) => e.kind === "claims").length,
6139
+ truncated: limit > 0 && ordered.length > limit ? { shown: limit, total: ordered.length } : null,
6140
+ capabilities: graph.capabilities ?? []
6141
+ };
6142
+ }
6143
+ function renderHtml(graph, opts = {}) {
6144
+ const payload = buildPayload(graph, opts);
6145
+ return template_default.replace("__DIA_PAYLOAD__", safeJson(payload));
6146
+ }
6147
+
4864
6148
  // packages/cli/src/index.ts
4865
6149
  init_paths();
4866
6150
 
@@ -5285,9 +6569,9 @@ function scopeEnv(s, extra = {}) {
5285
6569
  }
5286
6570
 
5287
6571
  // src/doctor.ts
5288
- import { existsSync as existsSync22, readFileSync as readFileSync19 } from "node:fs";
6572
+ import { closeSync as closeSync3, existsSync as existsSync22, openSync as openSync3, readFileSync as readFileSync19, readSync, realpathSync as realpathSync5, statSync as statSync12 } from "node:fs";
5289
6573
  import { homedir as homedir12 } from "node:os";
5290
- import { join as join25, resolve as resolve13 } from "node:path";
6574
+ import { delimiter, dirname as dirname13, join as join25, resolve as resolve13 } from "node:path";
5291
6575
 
5292
6576
  // src/dsh.ts
5293
6577
  import { execFileSync as execFileSync7 } from "node:child_process";
@@ -5684,6 +6968,92 @@ function platformChecks() {
5684
6968
  function quoteArg(v) {
5685
6969
  return /^[\w.@%+=:,\/-]+$/.test(v) ? v : `'${v.replace(/'/g, "'\\''")}'`;
5686
6970
  }
6971
+ function npmOwnedPath(p) {
6972
+ return /(^|[\\/])node_modules[\\/]@minonl[\\/]dia([\\/]|$)/.test(p);
6973
+ }
6974
+ function pkgShimPath(p) {
6975
+ try {
6976
+ const st = statSync12(p);
6977
+ if (!st.isFile() || st.size > 1024 * 1024)
6978
+ return false;
6979
+ const fd = openSync3(p, "r");
6980
+ try {
6981
+ const buf = Buffer.alloc(8192);
6982
+ const n = readSync(fd, buf, 0, buf.length, 0);
6983
+ return /@minonl\/dia|node_modules\/(@minonl\/)?dia\//.test(buf.subarray(0, n).toString("utf8"));
6984
+ } finally {
6985
+ closeSync3(fd);
6986
+ }
6987
+ } catch {
6988
+ return false;
6989
+ }
6990
+ }
6991
+ function pkgBinDirOwned(p) {
6992
+ const d = realOrSelf(dirname13(p));
6993
+ const home = homedir12();
6994
+ const known = [
6995
+ join25(home, ".volta/bin"),
6996
+ join25(home, ".bun/bin"),
6997
+ join25(home, ".yarn/bin"),
6998
+ join25(home, ".pnpm-global/bin"),
6999
+ join25(home, ".local/share/pnpm"),
7000
+ join25(home, ".npm-global/bin"),
7001
+ process.env.PNPM_HOME,
7002
+ process.env.YARN_GLOBAL_FOLDER,
7003
+ process.env.VOLTA_HOME ? join25(process.env.VOLTA_HOME, "bin") : "",
7004
+ process.env.BUN_INSTALL ? join25(process.env.BUN_INSTALL, "bin") : ""
7005
+ ].filter(Boolean);
7006
+ return known.some((k) => realOrSelf(k) === d);
7007
+ }
7008
+ function pkgOwnedPath(p) {
7009
+ return npmOwnedPath(p) || pkgShimPath(p) || pkgBinDirOwned(p);
7010
+ }
7011
+ function realOrSelf(p) {
7012
+ try {
7013
+ return realpathSync5(p);
7014
+ } catch {
7015
+ return resolve13(p);
7016
+ }
7017
+ }
7018
+ function whichDia(pathEnv = process.env.PATH || "") {
7019
+ for (const dir of pathEnv.split(delimiter)) {
7020
+ if (!dir)
7021
+ continue;
7022
+ const cand = join25(dir, "dia");
7023
+ try {
7024
+ const st = statSync12(cand);
7025
+ if (!st.isFile())
7026
+ continue;
7027
+ if (process.platform !== "win32" && (st.mode & 73) === 0)
7028
+ continue;
7029
+ return cand;
7030
+ } catch {}
7031
+ }
7032
+ return null;
7033
+ }
7034
+ function installShadowCheck() {
7035
+ const name = "install_shadow";
7036
+ const install = installDir();
7037
+ const installNpm = npmOwnedPath(realOrSelf(install));
7038
+ if (!installNpm && !existsSync22(join25(install, "install.json"))) {
7039
+ return { name, status: "ok", detail: `源码运行(install=${install} 无 install.json,跳过一致性判定)` };
7040
+ }
7041
+ const bin = whichDia();
7042
+ if (!bin)
7043
+ return { name, status: "ok", detail: `PATH 中没有 dia(跳过一致性判定;install=${install})` };
7044
+ const binReal = realOrSelf(bin);
7045
+ const binNpm = pkgOwnedPath(binReal);
7046
+ const kind = (npm) => npm ? "包管理器全局安装" : "脚本安装";
7047
+ if (installNpm === binNpm) {
7048
+ return { name, status: "ok", detail: `一致(${kind(installNpm)}):install=${install};PATH dia=${binReal}` };
7049
+ }
7050
+ const fix = installNpm ? `ln -sfn "$(npm prefix -g)/bin/dia" ${quoteArg(bin)}` : `ln -sfn ${quoteArg(join25(install, "bin/dia.js"))} ${quoteArg(bin)}`;
7051
+ return {
7052
+ name,
7053
+ status: "fail",
7054
+ detail: `安装类型不一致:install=${install}(${kind(installNpm)})但 PATH dia=${binReal}(${kind(binNpm)});修复:${fix}`
7055
+ };
7056
+ }
5687
7057
  function describeStale(st) {
5688
7058
  const diff = st.added + st.removed + st.changed;
5689
7059
  const parts = [st.added ? `新增 ${st.added}` : "", st.removed ? `删除 ${st.removed}` : "", st.changed ? `修改 ${st.changed}` : ""].filter(Boolean);
@@ -5829,6 +7199,7 @@ async function run(opts = {}) {
5829
7199
  status: install === diaHome() ? "warn" : "ok",
5830
7200
  detail: `install=${install} data=${diaHome()}${install === diaHome() ? "(安装物与数据同根)" : ""}`
5831
7201
  });
7202
+ checks.push(installShadowCheck());
5832
7203
  const wt = join25(install, "packages/wt/bin/dia-wt");
5833
7204
  checks.push({ name: "wt", status: existsSync22(wt) ? "ok" : "warn", detail: existsSync22(wt) ? wt : "dia wt 未安装(dia install.sh)" });
5834
7205
  const jobRoot = jobRootResolved();
@@ -6181,9 +7552,9 @@ function askBroker(req, timeoutMs = 5000) {
6181
7552
 
6182
7553
  // packages/cli/src/index.ts
6183
7554
  import { execFileSync as execFileSync12, spawn as spawn3, spawnSync as spawnSync5 } from "node:child_process";
6184
- import { mkdirSync as mkdirSync20, openSync as openSync3 } from "node:fs";
7555
+ import { mkdirSync as mkdirSync20, openSync as openSync4 } from "node:fs";
6185
7556
  import { basename as basename10, join as join32 } from "node:path";
6186
- import { existsSync as existsSync29, readFileSync as readFileSync26 } from "node:fs";
7557
+ import { existsSync as existsSync29, readFileSync as readFileSync26, writeFileSync as writeFileSync18 } from "node:fs";
6187
7558
  import { fileURLToPath as fileURLToPath4 } from "node:url";
6188
7559
  import { resolve as resolvePath, sep as pathSep } from "node:path";
6189
7560
 
@@ -7188,13 +8559,13 @@ import {
7188
8559
  readdirSync as readdirSync13,
7189
8560
  readlinkSync as readlinkSync2,
7190
8561
  rmSync as rmSync9,
7191
- statSync as statSync12,
8562
+ statSync as statSync13,
7192
8563
  symlinkSync as symlinkSync3,
7193
8564
  unlinkSync as unlinkSync3,
7194
8565
  writeFileSync as writeFileSync17
7195
8566
  } from "node:fs";
7196
8567
  import { hostname as osHostname } from "node:os";
7197
- import { basename as basename9, dirname as dirname13, isAbsolute as isAbsolute4, join as join31, relative as relative6, resolve as resolve15, sep as sep8 } from "node:path";
8568
+ import { basename as basename9, dirname as dirname14, isAbsolute as isAbsolute4, join as join31, relative as relative6, resolve as resolve15, sep as sep8 } from "node:path";
7198
8569
  import { fileURLToPath as fileURLToPath3 } from "node:url";
7199
8570
  init_paths();
7200
8571
  var ID_RE2 = /^[a-z0-9][a-z0-9-]*$/;
@@ -7405,7 +8776,7 @@ function resolveEntry(cmd, baseDir) {
7405
8776
  }
7406
8777
  function isExecutableFile(p) {
7407
8778
  try {
7408
- const st = statSync12(p);
8779
+ const st = statSync13(p);
7409
8780
  if (!st.isFile())
7410
8781
  return false;
7411
8782
  return process.platform === "win32" ? true : (st.mode & 73) !== 0;
@@ -7642,9 +9013,9 @@ function diaCmd() {
7642
9013
  if (onPath)
7643
9014
  return "dia";
7644
9015
  const entries = [
7645
- resolve15(dirname13(fileURLToPath3(import.meta.url)), "..", "packages", "cli", "src", "index.ts"),
7646
- resolve15(dirname13(process.argv[1] ?? ""), "..", "src", "cli.ts"),
7647
- resolve15(dirname13(process.argv[1] ?? ""), "..", "dist", "cli.js")
9016
+ resolve15(dirname14(fileURLToPath3(import.meta.url)), "..", "packages", "cli", "src", "index.ts"),
9017
+ resolve15(dirname14(process.argv[1] ?? ""), "..", "src", "cli.ts"),
9018
+ resolve15(dirname14(process.argv[1] ?? ""), "..", "dist", "cli.js")
7648
9019
  ];
7649
9020
  const entry = entries.find((p) => existsSync28(p));
7650
9021
  return entry ? `${q(process.execPath)} ${q(entry)}` : "dia";
@@ -7733,7 +9104,7 @@ function readHealthLines(file) {
7733
9104
  return rows;
7734
9105
  }
7735
9106
  function appendHealthRecord(file, rec) {
7736
- mkdirSync19(dirname13(file), { recursive: true });
9107
+ mkdirSync19(dirname14(file), { recursive: true });
7737
9108
  appendFileSync9(file, JSON.stringify(rec) + `
7738
9109
  `);
7739
9110
  }
@@ -8114,7 +9485,7 @@ function collectManagedFiles(base, hc) {
8114
9485
  continue;
8115
9486
  let st;
8116
9487
  try {
8117
- st = statSync12(abs);
9488
+ st = statSync13(abs);
8118
9489
  } catch {
8119
9490
  continue;
8120
9491
  }
@@ -8335,8 +9706,8 @@ function copyTree(from, to) {
8335
9706
  function copyAdopted(from, to) {
8336
9707
  if (existsSync28(to))
8337
9708
  throw new Error(`E_CONFLICT: ${to} 已存在`);
8338
- mkdirSync19(dirname13(to), { recursive: true });
8339
- if (statSync12(from).isDirectory())
9709
+ mkdirSync19(dirname14(to), { recursive: true });
9710
+ if (statSync13(from).isDirectory())
8340
9711
  copyTree(from, to);
8341
9712
  else {
8342
9713
  mkdirSync19(to, { recursive: true });
@@ -8358,7 +9729,7 @@ function remove3(opts) {
8358
9729
  }
8359
9730
  if (lock.entry.kind === "repo" && lock.entry.link && isSymlink2(lock.entry.link)) {
8360
9731
  const rel = readlinkSync2(lock.entry.link);
8361
- if (rel.startsWith(lock.dir) || resolve15(dirname13(lock.entry.link), rel).startsWith(lock.dir)) {
9732
+ if (rel.startsWith(lock.dir) || resolve15(dirname14(lock.entry.link), rel).startsWith(lock.dir)) {
8362
9733
  unlinkSync3(lock.entry.link);
8363
9734
  removed.push(lock.entry.link);
8364
9735
  } else
@@ -8591,8 +9962,11 @@ usage:
8591
9962
  dia msg send|show|inbox|ack|delivered|deliver [--wait]|reply|needs-info|status|stuck|thread|escalate|agents|migrate
8592
9963
  dia agents list|register|unregister|discover
8593
9964
  dia msg deliver <agent> <id> [--transport tmux|acp]
8594
- dia task add|list|link|status|done [--owner S] [--depends a,b] [--touches p1,p2]
9965
+ dia task add|list|link|plan|split|status|done [--owner S] [--depends a,b] [--touches p1,p2] [--prio P1] [--start D] [--due D] [--progress 0-100]
9966
+ dia task split <id> --into "切片A,切片B" [--slice 切片A] [--role R] [--judge J] [--touches p] [--force]
8595
9967
  dia graph [--json] [--task t-...] [--tui [--watch] [--status S] [--owner S] [--width N] [--no-color]]
9968
+ dia graph --export-html [path] [--out path] [--now ISO] [--redact] [--all] [--limit N] [--force]
9969
+ dia graph --next [n] | --explain <id> | --lint # 只读编排建议(Kahn-v1)
8596
9970
  dia claim <path> [--reason R] [--ttl 4h] [--session S] | dia claims [--all] [--mine] [--gc [--days 7]] | dia unclaim <path|id> [--force]
8597
9971
  dia team run <id> --mock | --acp|--dsh --workspace DIR
8598
9972
  dia team gate|accept|reject|next
@@ -8636,15 +10010,42 @@ async function kb(argv) {
8636
10010
  throw e;
8637
10011
  }
8638
10012
  }
10013
+ const tagFilter = flag(args, "--tag");
10014
+ const rootsByPath = new Map;
10015
+ let kitHits = 0;
10016
+ for (const k of installed()) {
10017
+ if (tagFilter && !k.tags.includes(tagFilter))
10018
+ continue;
10019
+ let kr = [];
10020
+ try {
10021
+ kr = await search(k.dir, q, undefined, limit);
10022
+ } catch {
10023
+ try {
10024
+ await buildIndex(k.dir);
10025
+ kr = await search(k.dir, q, undefined, limit);
10026
+ } catch {
10027
+ kr = [];
10028
+ }
10029
+ }
10030
+ for (const h of kr) {
10031
+ rootsByPath.set(`${k.id}:${h.path}`, k.dir);
10032
+ rows.push({ ...h, path: `${k.id}:${h.path}`, source: `kit:${k.id}@${k.version}` });
10033
+ kitHits++;
10034
+ }
10035
+ }
10036
+ if (kitHits)
10037
+ rows.sort((a, b) => (b.score ?? 0) - (a.score ?? 0)).splice(limit);
8639
10038
  const full = sub === "query" && has(args, "--full");
8640
10039
  const bytes = sub === "query" && has(args, "--bytes");
8641
10040
  const enrich = rows.map((r) => {
8642
10041
  const item = { ...r };
10042
+ const rowRoot = rootsByPath.get(r.path) ?? root;
10043
+ const rowPath = r.source ? r.path.split(":").slice(1).join(":") : r.path;
8643
10044
  try {
8644
10045
  if (bytes)
8645
- item.bytes = Buffer.byteLength(getDoc(root, r.path), "utf8");
10046
+ item.bytes = Buffer.byteLength(getDoc(rowRoot, rowPath), "utf8");
8646
10047
  if (full)
8647
- item.text = getDoc(root, r.path);
10048
+ item.text = getDoc(rowRoot, rowPath);
8648
10049
  } catch {
8649
10050
  item.stale_index = true;
8650
10051
  item.text = null;
@@ -8656,7 +10057,7 @@ async function kb(argv) {
8656
10057
  out(JSON.stringify(enrich));
8657
10058
  else
8658
10059
  for (const r of enrich) {
8659
- out(`${r.path} ${r.title}
10060
+ out(`${r.source ? `[${r.source}] ` : ""}${r.path} ${r.title}
8660
10061
  ${r.snippet}`);
8661
10062
  if (r.bytes !== undefined)
8662
10063
  out(` bytes ${r.bytes}`);
@@ -8935,7 +10336,18 @@ async function taskCli(argv) {
8935
10336
  err("missing title");
8936
10337
  return 2;
8937
10338
  }
8938
- const t = add(title, { role: flag(args, "--role"), judge: flag(args, "--judge"), owner: flag(args, "--owner"), deps: csv("--depends"), touches: csv("--touches") });
10339
+ const prog = flag(args, "--progress");
10340
+ const t = add(title, {
10341
+ role: flag(args, "--role"),
10342
+ judge: flag(args, "--judge"),
10343
+ owner: flag(args, "--owner"),
10344
+ deps: csv("--depends"),
10345
+ touches: csv("--touches"),
10346
+ prio: flag(args, "--prio"),
10347
+ start: flag(args, "--start"),
10348
+ due: flag(args, "--due"),
10349
+ progress: prog === undefined ? undefined : normProgress(prog)
10350
+ });
8939
10351
  out(asJson ? JSON.stringify(t) : `${t.id} ${t.title}`);
8940
10352
  return 0;
8941
10353
  }
@@ -8955,6 +10367,45 @@ async function taskCli(argv) {
8955
10367
  out(asJson ? JSON.stringify(t) : `${t.id} deps=${t.deps.join(",") || "-"} touches=${t.touches.join(",") || "-"}`);
8956
10368
  return 0;
8957
10369
  }
10370
+ if (sub === "plan") {
10371
+ const id = positional(args)[0];
10372
+ if (!id) {
10373
+ err("usage: dia task plan <id> [--prio P1] [--start YYYY-MM-DD] [--due YYYY-MM-DD] [--progress 0-100]");
10374
+ return 2;
10375
+ }
10376
+ const prog = flag(args, "--progress");
10377
+ const t = plan(id, {
10378
+ prio: flag(args, "--prio"),
10379
+ start: flag(args, "--start"),
10380
+ due: flag(args, "--due"),
10381
+ progress: prog === undefined ? undefined : normProgress(prog)
10382
+ });
10383
+ out(asJson ? JSON.stringify(t) : `${t.id} prio=${t.prio || "-"} start=${t.start || "-"} due=${t.due || "-"} progress=${t.progress ?? "-"}`);
10384
+ return 0;
10385
+ }
10386
+ if (sub === "split") {
10387
+ const id = positional(args)[0];
10388
+ const into = flag(args, "--into");
10389
+ if (!id || !into) {
10390
+ err('usage: dia task split <id> --into "切片A,切片B" [--slice 切片A] [--force]');
10391
+ return 2;
10392
+ }
10393
+ const parent = split(id, into.split(","), {
10394
+ slice: flag(args, "--slice"),
10395
+ role: flag(args, "--role"),
10396
+ judge: flag(args, "--judge"),
10397
+ touches: csv("--touches"),
10398
+ force: has(args, "--force")
10399
+ });
10400
+ const kids = (parent.split_children || []).map((cid) => {
10401
+ const c = get2(cid);
10402
+ return `${c.id} ${c.title}${c.slice ? "\t[slice]" : ""}`;
10403
+ });
10404
+ out(asJson ? JSON.stringify(parent) : `${parent.id} children=${(parent.split_children || []).length}
10405
+ ${kids.join(`
10406
+ `)}`);
10407
+ return 0;
10408
+ }
8958
10409
  if (sub === "status") {
8959
10410
  const id = positional(args)[0];
8960
10411
  const st = positional(args)[1];
@@ -8980,11 +10431,118 @@ async function taskCli(argv) {
8980
10431
  err(e?.message || String(e));
8981
10432
  return 30;
8982
10433
  }
8983
- err("usage: dia task add|list|link|status|done");
10434
+ err("usage: dia task add|list|link|plan|split|status|done");
8984
10435
  return 2;
8985
10436
  }
8986
10437
  async function graphCli(argv) {
8987
10438
  const asJson = has(argv, "--json");
10439
+ if (has(argv, "--next") || has(argv, "--explain") || has(argv, "--lint")) {
10440
+ const tasks = list4();
10441
+ const dr = derive(tasks);
10442
+ if (has(argv, "--explain")) {
10443
+ const id = flag(argv, "--explain");
10444
+ if (!id || id.startsWith("--")) {
10445
+ err("usage: dia graph --explain <id>");
10446
+ return 2;
10447
+ }
10448
+ const t = tasks.find((x) => x.id === id);
10449
+ const d = dr.byId.get(id);
10450
+ if (!t || !d) {
10451
+ err(`E_GRAPH: 任务不存在 ${id}`);
10452
+ return 2;
10453
+ }
10454
+ const info = {
10455
+ id,
10456
+ title: t.title,
10457
+ status: t.status,
10458
+ declared: t.prio ?? null,
10459
+ eff: d.eff_prio,
10460
+ inherited: d.prio_inherited,
10461
+ source: d.prio_source ?? null,
10462
+ ready: d.ready,
10463
+ startable: d.startable,
10464
+ rank: d.rank,
10465
+ blocked_by: d.blocked_by,
10466
+ released_by: d.released_by,
10467
+ dangling: d.dangling_deps,
10468
+ unblocks: d.unblocks,
10469
+ critical: d.critical,
10470
+ slice: !!t.slice
10471
+ };
10472
+ out(asJson ? JSON.stringify(info) : [
10473
+ `${id} ${t.title}`,
10474
+ `status=${t.status} declared=${info.declared ?? "-"} eff=${info.eff}${d.prio_inherited ? ` (继承自 ${d.prio_source?.id} ${d.prio_source?.declared}, ${d.prio_source?.distance} 跳)` : ""}`,
10475
+ `ready=${d.ready} startable=${d.startable} rank=${d.rank ?? "-"} unblocks=${d.unblocks} critical=${d.critical}`,
10476
+ `blocked_by=${d.blocked_by.join(",") || "-"} released_by=${d.released_by.join(",") || "-"} dangling=${d.dangling_deps.join(",") || "-"}`
10477
+ ].join(`
10478
+ `));
10479
+ return 0;
10480
+ }
10481
+ if (has(argv, "--lint")) {
10482
+ const findings = dr.lint;
10483
+ out(asJson ? JSON.stringify({ findings, cycles: dr.cycles }) : findings.length ? findings.map((f) => `${f.kind} ${f.id} ${f.detail}`).join(`
10484
+ `) : "(clean)");
10485
+ return dr.cycles.length ? 3 : 0;
10486
+ }
10487
+ const raw = flag(argv, "--next");
10488
+ const n = raw && /^\d+$/.test(raw) ? Math.max(1, Number(raw)) : 5;
10489
+ const doing = tasks.filter((t) => t.status === "doing" && dr.byId.get(t.id).rank !== null).sort((a, b) => dr.byId.get(a.id).rank - dr.byId.get(b.id).rank);
10490
+ const startable = tasks.filter((t) => dr.byId.get(t.id).startable).sort((a, b) => dr.byId.get(a.id).rank - dr.byId.get(b.id).rank);
10491
+ const picked = [...doing, ...startable].slice(0, n);
10492
+ const items = picked.map((t) => {
10493
+ const d = dr.byId.get(t.id);
10494
+ return {
10495
+ id: t.id,
10496
+ title: t.title,
10497
+ status: t.status,
10498
+ rank: d.rank,
10499
+ declared: t.prio ?? null,
10500
+ eff: d.eff_prio,
10501
+ inherited: d.prio_inherited,
10502
+ ready: d.ready,
10503
+ critical: d.critical,
10504
+ unblocks: d.unblocks,
10505
+ blocked_by: d.blocked_by,
10506
+ why: `${t.status === "doing" ? "进行中" : "可开工"} · eff=${d.eff_prio}${d.prio_inherited ? `(←${d.prio_source?.id})` : ""} · 解锁${d.unblocks}${d.critical ? " · 关键路径" : ""}`
10507
+ };
10508
+ });
10509
+ out(asJson ? JSON.stringify({ policy: dr.ranking.policy, items }) : items.length ? items.map((x, i) => `#${i + 1} ${x.id} ${x.eff}${x.inherited ? "(继承)" : ""} ${x.title} ${x.why}`).join(`
10510
+ `) : "(没有可开工任务)");
10511
+ return dr.cycles.length ? 3 : 0;
10512
+ }
10513
+ if (has(argv, "--export-html") || has(argv, "--out")) {
10514
+ const nowRaw = flag(argv, "--now");
10515
+ const now = nowRaw ? new Date(nowRaw) : new Date;
10516
+ if (Number.isNaN(now.getTime())) {
10517
+ err(`E_GRAPH: --now 不是合法时间(收到 ${nowRaw})`);
10518
+ return 2;
10519
+ }
10520
+ const limitRaw = flag(argv, "--limit");
10521
+ const limit = has(argv, "--all") ? 0 : limitRaw === undefined ? 500 : Number(limitRaw);
10522
+ if (!Number.isInteger(limit) || limit < 0) {
10523
+ err("E_GRAPH: --limit 必须是 >=0 的整数");
10524
+ return 2;
10525
+ }
10526
+ const stamp = `${localDay(now).replace(/-/g, "")}-${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}`;
10527
+ const argPath = flag(argv, "--export-html");
10528
+ const positionalOut = argPath && !argPath.startsWith("-") ? argPath : "";
10529
+ const outPath = flag(argv, "--out") || positionalOut || join32(diaHome(), "export", `graph-${stamp}.html`);
10530
+ if (existsSync29(outPath) && !has(argv, "--force")) {
10531
+ err(`E_GRAPH: 已存在 ${outPath}(用 --force 覆盖)`);
10532
+ return 2;
10533
+ }
10534
+ try {
10535
+ const g = build({ task: flag(argv, "--task") });
10536
+ const html = renderHtml(g, { now, redact: has(argv, "--redact"), limit, claims: list3().length });
10537
+ mkdirSync20(join32(outPath, ".."), { recursive: true });
10538
+ writeFileSync18(outPath, html, { mode: 384 });
10539
+ out(outPath);
10540
+ return 0;
10541
+ } catch (e) {
10542
+ err(e?.message || String(e));
10543
+ return String(e?.message || e).startsWith("E_GRAPH") ? 2 : 31;
10544
+ }
10545
+ }
8988
10546
  if (has(argv, "--tui") || has(argv, "--watch")) {
8989
10547
  const width = Number(flag(argv, "--width") || 0);
8990
10548
  const tuiOpts = {
@@ -9089,7 +10647,7 @@ function spawnDetachedDelivery(args) {
9089
10647
  const dir = join32(root, agent);
9090
10648
  mkdirSync20(dir, { recursive: true });
9091
10649
  const log = join32(dir, `deliver-${id}.log`);
9092
- const fd = openSync3(log, "a");
10650
+ const fd = openSync4(log, "a");
9093
10651
  const bin = process.argv[1];
9094
10652
  const child = spawn3(process.execPath, [bin, "msg", "deliver", ...args, "--wait"], {
9095
10653
  detached: true,
@@ -9935,7 +11493,7 @@ async function kitCli(argv) {
9935
11493
  }
9936
11494
  if (sub === "list") {
9937
11495
  const rows = list();
9938
- out(asJson ? JSON.stringify(rows, null, 1) : rows.map((r) => `${r.id}: ${r.versions.join(", ")}`).join(`
11496
+ out(asJson ? JSON.stringify(rows, null, 1) : rows.map((r) => `${r.id}: ${r.versions.join(", ")}${r.tags?.length ? ` [${r.tags.join(", ")}]` : ""}`).join(`
9939
11497
  `) || "(none)");
9940
11498
  return 0;
9941
11499
  }