@minonl/dia 0.0.15 → 0.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/cli.js +1589 -31
- package/docs/cli.md +3 -2
- package/docs/design/graph-prio-dag.md +85 -0
- package/docs/design/graph-ui-m1.md +106 -0
- package/docs/design/graph-web-m1.md +113 -0
- package/docs/design/graph-web-m2.md +92 -0
- package/docs/design/graph-web-mockup.html +49 -25
- package/docs/design/graph-web.md +9 -4
- package/docs/design/seat-scheduling-m2.md +138 -0
- package/docs/index.md +8 -1
- package/docs/install-guard-design.md +148 -0
- package/docs/kit-install.md +7 -0
- package/docs/kit-layer.md +78 -0
- package/docs/migrations/data-root-2026-11.md +3 -2
- package/docs/status.md +2 -0
- package/kits/git/dia.json +10 -0
- package/kits/git/knowledge/hooks.md +30 -0
- package/kits/git/knowledge/worktree.md +34 -0
- package/kits/node/dia.json +10 -0
- package/kits/node/knowledge/hook-managers.md +17 -0
- package/kits/node/knowledge/pnpm.md +34 -0
- package/package.json +1 -1
- package/packages/wt/bin/dia-wt +15 -0
- package/packages/wt/tests/adopt.test.sh +8 -0
package/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) =>
|
|
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
|
|
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)
|
|
4735
|
-
|
|
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:
|
|
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=>({'&':'&','<':'<','>':'>','"':'"'}[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 <title> --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
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
7646
|
-
resolve15(
|
|
7647
|
-
resolve15(
|
|
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(
|
|
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 =
|
|
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(
|
|
8339
|
-
if (
|
|
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(
|
|
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(
|
|
10046
|
+
item.bytes = Buffer.byteLength(getDoc(rowRoot, rowPath), "utf8");
|
|
8646
10047
|
if (full)
|
|
8647
|
-
item.text = getDoc(
|
|
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
|
|
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 =
|
|
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
|
}
|