@quolu/lattice 0.52.2 → 0.52.4
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/package.json +1 -1
- package/src/artifact-contracts-v2.mjs +325 -325
- package/src/artifact-contracts.mjs +895 -895
- package/src/boundary-compiler.mjs +712 -712
- package/src/boundary-observation-compiler-v2.mjs +344 -344
- package/src/bounded-seam.mjs +230 -230
- package/src/bridge-address.mjs +107 -107
- package/src/bridge-cli.mjs +297 -297
- package/src/bridge-config.mjs +346 -346
- package/src/bridge-daemon.mjs +378 -378
- package/src/bridge-hub-protocol.mjs +198 -0
- package/src/bridge-launch-agent.mjs +323 -323
- package/src/bridge-registrar.mjs +102 -102
- package/src/bridge-server.mjs +376 -376
- package/src/cli-help.mjs +308 -308
- package/src/cli-stdio.mjs +40 -40
- package/src/control-compiler.mjs +532 -532
- package/src/dag-chain.mjs +261 -261
- package/src/factory-diagnostics.mjs +188 -188
- package/src/git-process.mjs +72 -0
- package/src/hash-chain.mjs +76 -76
- package/src/hooks-cli.mjs +1057 -1057
- package/src/isolation-runner.mjs +409 -409
- package/src/node-version-guard.mjs +44 -44
- package/src/project-cli.mjs +699 -697
- package/src/project-identity.mjs +130 -130
- package/src/rc1-black-box-oracle.mjs +906 -906
- package/src/rc1-comparison.mjs +154 -154
- package/src/rc1-evidence-bundle.mjs +456 -456
- package/src/rc1-v4-campaign.mjs +708 -708
- package/src/rc1-v4-transform.mjs +467 -467
- package/src/rc1-v5-artifact-set.mjs +855 -855
- package/src/rc1-v5-behavior-evidence.mjs +594 -594
- package/src/rc1-v5-campaign.mjs +797 -797
- package/src/rc1-v5-transform.mjs +421 -421
- package/src/rc1-v6-artifact-set.mjs +807 -807
- package/src/rc1-v6-behavior-evidence.mjs +273 -273
- package/src/rc1-v6-campaign.mjs +623 -623
- package/src/rc1-v6-causal-binding.mjs +473 -473
- package/src/rc1-v6-measurement.mjs +313 -313
- package/src/rc2-artifact-set.mjs +1584 -1584
- package/src/rc2-campaign.mjs +1506 -1506
- package/src/rc2-delivery-policy-front-end.mjs +1079 -1079
- package/src/rc2-delivery-policy-oracle.mjs +134 -134
- package/src/rc2-delivery-policy-transform.mjs +1127 -1127
- package/src/rc2-rc1-transfer-front-end.mjs +511 -511
- package/src/rc3-actual-dogfood.mjs +652 -652
- package/src/rc3-dogfood-scaffold.mjs +315 -315
- package/src/rc3-scripted-campaign.mjs +1383 -1383
- package/src/rc4-stage1-dogfood.mjs +673 -673
- package/src/runtime-adapter-registry.mjs +524 -524
- package/src/runtime-cli.mjs +4588 -4588
- package/src/runtime-contracts.mjs +824 -824
- package/src/runtime-control-store.mjs +604 -604
- package/src/runtime-controller-protocol.mjs +587 -587
- package/src/runtime-decision-verifier.mjs +701 -701
- package/src/runtime-diff-observer.mjs +361 -361
- package/src/runtime-direct-os-observer.mjs +301 -301
- package/src/runtime-driver-state.mjs +166 -166
- package/src/runtime-engine.mjs +779 -779
- package/src/runtime-errors.mjs +356 -356
- package/src/runtime-event-store.mjs +189 -189
- package/src/runtime-front-end.mjs +925 -925
- package/src/runtime-gate-store.mjs +481 -481
- package/src/runtime-hold-recompile.mjs +916 -916
- package/src/runtime-io-sentinel.mjs +391 -391
- package/src/runtime-lifecycle-lock.mjs +294 -294
- package/src/runtime-managed-supervisor.mjs +1490 -1490
- package/src/runtime-multi-epoch-store.mjs +838 -838
- package/src/runtime-projection.mjs +269 -269
- package/src/runtime-pull-intake.mjs +1192 -1192
- package/src/runtime-scripted-adapter-controller.mjs +1160 -1160
- package/src/runtime-scripted-executor.mjs +163 -163
- package/src/runtime-scripted-worktree.mjs +104 -104
- package/src/runtime-seam-resolve.mjs +428 -428
- package/src/runtime-seam-treatment.mjs +173 -173
- package/src/runtime-socket-owner.mjs +125 -125
- package/src/runtime-work-order-contracts.mjs +91 -91
- package/src/runtime-work-order-controller.mjs +1171 -1171
- package/src/runtime-worktree-executor.mjs +199 -199
- package/src/schedulability-compiler-v2.mjs +303 -303
- package/src/schedulability-verifier-v2.mjs +317 -317
- package/src/seam-apply.mjs +549 -549
- package/src/seam-commit-shared.mjs +22 -22
- package/src/seam-commit-transform.mjs +81 -81
- package/src/seam-commit.mjs +18 -18
- package/src/seam-cost.mjs +322 -322
- package/src/seam-derivation.mjs +188 -188
- package/src/seam-gate.mjs +146 -146
- package/src/seam-proposal-contracts.mjs +446 -446
- package/src/seam-proposal-queries.mjs +521 -521
- package/src/seam-proposal.mjs +2011 -2011
- package/src/seam-ref.mjs +33 -33
- package/src/seam-rewrite.mjs +286 -286
- package/src/seam-transform.mjs +554 -554
- package/src/seam-verification.mjs +260 -260
- package/src/sensor-adapter.mjs +432 -432
- package/src/sensor-cli.mjs +139 -139
- package/src/sensor-diff.mjs +661 -661
- package/src/sensor-node-runtime.mjs +53 -53
- package/src/sensor-runtime.mjs +52 -52
- package/src/timestamp-contract.mjs +8 -8
- package/src/todo-audit-pending.mjs +91 -91
- package/src/todo-chain.mjs +178 -178
- package/src/todo-cli.mjs +3143 -3141
- package/src/todo-contracts.mjs +728 -728
- package/src/todo-dashboard-registry.mjs +573 -573
- package/src/todo-dispatch-shape.mjs +190 -190
- package/src/todo-gantt-html-independence.mjs +239 -239
- package/src/todo-gantt-html-shared.mjs +226 -226
- package/src/todo-gantt-html-style.mjs +131 -131
- package/src/todo-gantt-html.mjs +248 -248
- package/src/todo-gantt-layout.mjs +974 -974
- package/src/todo-gantt-live.mjs +361 -361
- package/src/todo-gantt-nested.mjs +263 -263
- package/src/todo-gantt-presentation.mjs +217 -217
- package/src/todo-gantt-scope.mjs +123 -123
- package/src/todo-gantt-svg.mjs +353 -353
- package/src/todo-independence-contracts.mjs +595 -595
- package/src/todo-independence-guidance.mjs +322 -322
- package/src/todo-independence.mjs +640 -640
- package/src/todo-markdown-renderer.mjs +260 -260
- package/src/todo-migration.mjs +448 -448
- package/src/todo-narrative-anchor.mjs +130 -130
- package/src/todo-note-store.mjs +629 -629
- package/src/todo-parallel-candidates.mjs +114 -114
- package/src/todo-revision.mjs +995 -995
- package/src/todo-split.mjs +472 -472
- package/src/todo-status.mjs +690 -690
- package/src/todo-store-git-transaction.mjs +418 -418
- package/src/todo-store.mjs +4360 -4322
- package/src/treatment-compiler.mjs +728 -728
- package/src/treatment-runner.mjs +656 -656
- package/src/witness-scaffold.mjs +180 -180
|
@@ -1,263 +1,263 @@
|
|
|
1
|
-
import { projectTodoChainV1 } from './todo-chain.mjs';
|
|
2
|
-
import { projectTodoCrossPlanDependencies } from './todo-store.mjs';
|
|
3
|
-
|
|
4
|
-
const ROOT = Symbol('todo-gantt-root');
|
|
5
|
-
|
|
6
|
-
function refKey(ref) {
|
|
7
|
-
return JSON.stringify([ref.project_id, ref.plan_key, ref.task_id]);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function compareText(left, right) {
|
|
11
|
-
return left < right ? -1 : left > right ? 1 : 0;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function fail(message, detail = null) {
|
|
15
|
-
const error = new Error(message);
|
|
16
|
-
error.code = 'TODO_LAYOUT_INVALID_HIERARCHY';
|
|
17
|
-
error.detail = detail;
|
|
18
|
-
throw error;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function taskRef(member, taskId) {
|
|
22
|
-
return {
|
|
23
|
-
project_id: member.plan.project_id,
|
|
24
|
-
plan_key: member.plan.plan_key,
|
|
25
|
-
task_id: taskId,
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function normalizeHierarchy(readModel) {
|
|
30
|
-
const tasks = new Map();
|
|
31
|
-
const parentByKey = new Map();
|
|
32
|
-
const childrenByParent = new Map([[ROOT, []]]);
|
|
33
|
-
let hasHierarchy = false;
|
|
34
|
-
|
|
35
|
-
for (const member of readModel.members) {
|
|
36
|
-
const ids = new Set(member.plan.tasks.map(({ task_id: taskId }) => taskId));
|
|
37
|
-
for (const task of member.plan.tasks) {
|
|
38
|
-
const ref = taskRef(member, task.task_id);
|
|
39
|
-
const key = refKey(ref);
|
|
40
|
-
const parentId = task.parent_task_id ?? null;
|
|
41
|
-
if (parentId !== null && (typeof parentId !== 'string' || !ids.has(parentId))) {
|
|
42
|
-
fail(`parent_task_id must reference a task in the same plan: ${task.task_id}`, {
|
|
43
|
-
ref, parent_task_id: parentId,
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
const parentKey = parentId === null ? ROOT : refKey(taskRef(member, parentId));
|
|
47
|
-
tasks.set(key, { ref, task, member });
|
|
48
|
-
parentByKey.set(key, parentKey);
|
|
49
|
-
if (!childrenByParent.has(parentKey)) childrenByParent.set(parentKey, []);
|
|
50
|
-
childrenByParent.get(parentKey).push(key);
|
|
51
|
-
if (!childrenByParent.has(key)) childrenByParent.set(key, []);
|
|
52
|
-
hasHierarchy ||= parentId !== null;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
if (!hasHierarchy) return null;
|
|
56
|
-
|
|
57
|
-
for (const key of tasks.keys()) {
|
|
58
|
-
const seen = new Set([key]);
|
|
59
|
-
let cursor = parentByKey.get(key);
|
|
60
|
-
while (cursor !== ROOT) {
|
|
61
|
-
if (seen.has(cursor)) {
|
|
62
|
-
fail('parent_task_id hierarchy contains a cycle', {
|
|
63
|
-
task: tasks.get(key).ref,
|
|
64
|
-
parent: tasks.get(cursor)?.ref ?? null,
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
seen.add(cursor);
|
|
68
|
-
cursor = parentByKey.get(cursor);
|
|
69
|
-
if (cursor === undefined) fail('parent_task_id hierarchy is disconnected');
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
for (const children of childrenByParent.values()) children.sort(compareText);
|
|
73
|
-
return { tasks, parentByKey, childrenByParent };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function branchUnder(hierarchy, containerKey, taskKey) {
|
|
77
|
-
let cursor = taskKey;
|
|
78
|
-
while (cursor !== ROOT) {
|
|
79
|
-
const parent = hierarchy.parentByKey.get(cursor);
|
|
80
|
-
if (parent === containerKey) return cursor;
|
|
81
|
-
cursor = parent;
|
|
82
|
-
}
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function projectRef(hierarchy, containerKey, value) {
|
|
87
|
-
if (value === null || typeof value !== 'object') return null;
|
|
88
|
-
const branch = branchUnder(hierarchy, containerKey, refKey(value));
|
|
89
|
-
return branch === null ? null : { ...hierarchy.tasks.get(branch).ref };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function uniqueRefs(refs) {
|
|
93
|
-
const byKey = new Map(refs.map((ref) => [refKey(ref), ref]));
|
|
94
|
-
return [...byKey.values()].sort((left, right) => compareText(refKey(left), refKey(right)));
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function projectMember(member, hierarchy, containerKey, selectedKeys) {
|
|
98
|
-
const tasks = member.plan.tasks.filter((task) => selectedKeys.has(refKey(taskRef(member, task.task_id))));
|
|
99
|
-
if (tasks.length === 0) return null;
|
|
100
|
-
|
|
101
|
-
const hardDependencies = [];
|
|
102
|
-
const seenHard = new Set();
|
|
103
|
-
for (const edge of member.plan.hard_dependencies) {
|
|
104
|
-
const from = projectRef(hierarchy, containerKey, edge.from);
|
|
105
|
-
const to = projectRef(hierarchy, containerKey, edge.to);
|
|
106
|
-
if (from === null || to === null || !selectedKeys.has(refKey(from))
|
|
107
|
-
|| !selectedKeys.has(refKey(to)) || refKey(from) === refKey(to)) continue;
|
|
108
|
-
const key = JSON.stringify([refKey(from), refKey(to)]);
|
|
109
|
-
if (seenHard.has(key)) continue;
|
|
110
|
-
seenHard.add(key);
|
|
111
|
-
hardDependencies.push({ from, to });
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const joins = [];
|
|
115
|
-
for (const join of member.plan.joins) {
|
|
116
|
-
const before = projectRef(hierarchy, containerKey, join.before);
|
|
117
|
-
if (before === null || !selectedKeys.has(refKey(before))) continue;
|
|
118
|
-
const after = uniqueRefs(join.after.map((ref) => projectRef(hierarchy, containerKey, ref))
|
|
119
|
-
.filter((ref) => ref !== null && selectedKeys.has(refKey(ref))
|
|
120
|
-
&& refKey(ref) !== refKey(before)));
|
|
121
|
-
if (after.length > 0) joins.push({ ...join, after, before });
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const phaseAcceptDependencies = [];
|
|
125
|
-
const seenPhaseAccept = new Set();
|
|
126
|
-
for (const dependency of member.plan.phase_accept_dependencies ?? []) {
|
|
127
|
-
const to = projectRef(hierarchy, containerKey, dependency.to);
|
|
128
|
-
if (to === null || !selectedKeys.has(refKey(to))) continue;
|
|
129
|
-
const key = JSON.stringify([dependency.from, refKey(to)]);
|
|
130
|
-
if (seenPhaseAccept.has(key)) continue;
|
|
131
|
-
seenPhaseAccept.add(key);
|
|
132
|
-
phaseAcceptDependencies.push({ ...dependency, to });
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// plan-scopedのcross-plan edgeも階層levelのtaskへ射影する。子task同士の接続を
|
|
136
|
-
// root levelで捨てると、nested表示だけ依存線とwaveが消えるため、hard edgeと同じく
|
|
137
|
-
// 各endpointをそのlevel直下のbranchへ束縛し直す。
|
|
138
|
-
const planScopedEvents = (member.plan_scoped?.events ?? []).flatMap((event) => {
|
|
139
|
-
if (event.kind !== 'cross_plan_dependency') return [event];
|
|
140
|
-
const from = projectRef(hierarchy, containerKey, event.payload.from);
|
|
141
|
-
const to = projectRef(hierarchy, containerKey, event.payload.to);
|
|
142
|
-
if (from === null || to === null || !selectedKeys.has(refKey(from))
|
|
143
|
-
|| !selectedKeys.has(refKey(to)) || refKey(from) === refKey(to)) return [];
|
|
144
|
-
return [{ ...event, payload: { ...event.payload, from, to } }];
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
return {
|
|
148
|
-
...member,
|
|
149
|
-
plan: {
|
|
150
|
-
...member.plan,
|
|
151
|
-
tasks,
|
|
152
|
-
hard_dependencies: hardDependencies,
|
|
153
|
-
joins,
|
|
154
|
-
phase_accept_dependencies: phaseAcceptDependencies,
|
|
155
|
-
},
|
|
156
|
-
...(member.plan_scoped === undefined ? {} : {
|
|
157
|
-
plan_scoped: { ...member.plan_scoped, events: planScopedEvents },
|
|
158
|
-
}),
|
|
159
|
-
tasks: member.tasks.filter(({ task_id: taskId }) => tasks.some((task) => task.task_id === taskId)),
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function filterIndependence(independence, selectedKeys) {
|
|
164
|
-
if (!Array.isArray(independence)) return independence;
|
|
165
|
-
const selectedByPlan = new Map();
|
|
166
|
-
for (const key of selectedKeys) {
|
|
167
|
-
const [projectId, planKey, taskId] = JSON.parse(key);
|
|
168
|
-
selectedByPlan.set(JSON.stringify([projectId, planKey]),
|
|
169
|
-
new Set([...(selectedByPlan.get(JSON.stringify([projectId, planKey])) ?? []), taskId]));
|
|
170
|
-
}
|
|
171
|
-
return independence.flatMap((projection) => {
|
|
172
|
-
const selected = selectedByPlan.get(JSON.stringify([projection.project_id, projection.plan_key]));
|
|
173
|
-
if (selected === undefined) return [];
|
|
174
|
-
const frontier = projection.frontier;
|
|
175
|
-
return [{
|
|
176
|
-
...projection,
|
|
177
|
-
frontier: {
|
|
178
|
-
...frontier,
|
|
179
|
-
parallel_groups: (frontier.parallel_groups ?? []).map((group) => ({
|
|
180
|
-
...group, task_ids: group.task_ids.filter((taskId) => selected.has(taskId)),
|
|
181
|
-
})).filter((group) => group.task_ids.length > 0),
|
|
182
|
-
serialize_pairs: (frontier.serialize_pairs ?? [])
|
|
183
|
-
.filter((pair) => pair.task_ids.every((taskId) => selected.has(taskId))),
|
|
184
|
-
conflicts_with_active: (frontier.conflicts_with_active ?? []).filter((entry) =>
|
|
185
|
-
selected.has(entry.ready_task_id) && selected.has(entry.active_task_id)),
|
|
186
|
-
unknown: (frontier.unknown ?? []).filter((entry) => selected.has(entry.task_id)),
|
|
187
|
-
},
|
|
188
|
-
}];
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function topologyOf(readModel) {
|
|
193
|
-
const crossPlanDependencies = projectTodoCrossPlanDependencies(readModel.members);
|
|
194
|
-
return {
|
|
195
|
-
nodes: readModel.members.flatMap((member) => member.plan.tasks
|
|
196
|
-
.map(({ task_id: taskId }) => taskRef(member, taskId))),
|
|
197
|
-
hard_edges: [
|
|
198
|
-
...readModel.members.flatMap((member) => member.plan.hard_dependencies),
|
|
199
|
-
...crossPlanDependencies.map(({ from, to }) => ({ from, to })),
|
|
200
|
-
],
|
|
201
|
-
joins: readModel.members.flatMap((member) => member.plan.joins),
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function buildLevel(readModel, hierarchy, containerKey, options, layoutFlat, includesSubtree) {
|
|
206
|
-
const selected = (hierarchy.childrenByParent.get(containerKey) ?? []).filter(includesSubtree);
|
|
207
|
-
const selectedKeys = new Set(selected);
|
|
208
|
-
const members = readModel.members.map((member) =>
|
|
209
|
-
projectMember(member, hierarchy, containerKey, selectedKeys)).filter(Boolean);
|
|
210
|
-
const projectedRead = { ...readModel, members };
|
|
211
|
-
const levelOptions = {
|
|
212
|
-
...options,
|
|
213
|
-
// live foldingは元のfull graphで一度だけ行い、下で選んだtask集合へ反映済み。
|
|
214
|
-
// 縮約levelでもう一度foldすると、可視な子を持つdone親containerが消える。
|
|
215
|
-
scope: 'all',
|
|
216
|
-
independence: filterIndependence(options.independence ?? null, selectedKeys),
|
|
217
|
-
seamProposals: containerKey === ROOT ? options.seamProposals ?? null : null,
|
|
218
|
-
};
|
|
219
|
-
const layout = layoutFlat(projectedRead, projectTodoChainV1(topologyOf(projectedRead)), levelOptions);
|
|
220
|
-
const children = selected.flatMap((parentKey) => {
|
|
221
|
-
if ((hierarchy.childrenByParent.get(parentKey) ?? []).length === 0) return [];
|
|
222
|
-
const childLevel = buildLevel(
|
|
223
|
-
readModel, hierarchy, parentKey, options, layoutFlat, includesSubtree,
|
|
224
|
-
);
|
|
225
|
-
if (childLevel.layout.nodes.length === 0 && childLevel.children.length === 0) return [];
|
|
226
|
-
return [{
|
|
227
|
-
parent_ref: { ...hierarchy.tasks.get(parentKey).ref },
|
|
228
|
-
level: childLevel,
|
|
229
|
-
}];
|
|
230
|
-
});
|
|
231
|
-
return { layout, children };
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function levelMetrics(level, depth = 1) {
|
|
235
|
-
let taskCount = level.layout.nodes.length;
|
|
236
|
-
let visibleNodeCount = level.layout.metrics.visible_node_count;
|
|
237
|
-
let visibleEdgeCount = level.layout.metrics.visible_edge_count;
|
|
238
|
-
let maximumDepth = depth;
|
|
239
|
-
for (const child of level.children) {
|
|
240
|
-
const nested = levelMetrics(child.level, depth + 1);
|
|
241
|
-
taskCount += nested.taskCount;
|
|
242
|
-
visibleNodeCount += nested.visibleNodeCount;
|
|
243
|
-
visibleEdgeCount += nested.visibleEdgeCount;
|
|
244
|
-
maximumDepth = Math.max(maximumDepth, nested.maximumDepth);
|
|
245
|
-
}
|
|
246
|
-
return { taskCount, visibleNodeCount, visibleEdgeCount, maximumDepth };
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
export function buildTodoGanttHierarchy(readModel, options, layoutFlat, visibleTaskKeys = null) {
|
|
250
|
-
const hierarchy = normalizeHierarchy(readModel);
|
|
251
|
-
if (hierarchy === null) return null;
|
|
252
|
-
const memo = new Map();
|
|
253
|
-
const includesSubtree = (key) => {
|
|
254
|
-
if (visibleTaskKeys === null) return true;
|
|
255
|
-
if (memo.has(key)) return memo.get(key);
|
|
256
|
-
const included = visibleTaskKeys.has(key)
|
|
257
|
-
|| (hierarchy.childrenByParent.get(key) ?? []).some(includesSubtree);
|
|
258
|
-
memo.set(key, included);
|
|
259
|
-
return included;
|
|
260
|
-
};
|
|
261
|
-
const root = buildLevel(readModel, hierarchy, ROOT, options, layoutFlat, includesSubtree);
|
|
262
|
-
return { root, metrics: levelMetrics(root) };
|
|
263
|
-
}
|
|
1
|
+
import { projectTodoChainV1 } from './todo-chain.mjs';
|
|
2
|
+
import { projectTodoCrossPlanDependencies } from './todo-store.mjs';
|
|
3
|
+
|
|
4
|
+
const ROOT = Symbol('todo-gantt-root');
|
|
5
|
+
|
|
6
|
+
function refKey(ref) {
|
|
7
|
+
return JSON.stringify([ref.project_id, ref.plan_key, ref.task_id]);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function compareText(left, right) {
|
|
11
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function fail(message, detail = null) {
|
|
15
|
+
const error = new Error(message);
|
|
16
|
+
error.code = 'TODO_LAYOUT_INVALID_HIERARCHY';
|
|
17
|
+
error.detail = detail;
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function taskRef(member, taskId) {
|
|
22
|
+
return {
|
|
23
|
+
project_id: member.plan.project_id,
|
|
24
|
+
plan_key: member.plan.plan_key,
|
|
25
|
+
task_id: taskId,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeHierarchy(readModel) {
|
|
30
|
+
const tasks = new Map();
|
|
31
|
+
const parentByKey = new Map();
|
|
32
|
+
const childrenByParent = new Map([[ROOT, []]]);
|
|
33
|
+
let hasHierarchy = false;
|
|
34
|
+
|
|
35
|
+
for (const member of readModel.members) {
|
|
36
|
+
const ids = new Set(member.plan.tasks.map(({ task_id: taskId }) => taskId));
|
|
37
|
+
for (const task of member.plan.tasks) {
|
|
38
|
+
const ref = taskRef(member, task.task_id);
|
|
39
|
+
const key = refKey(ref);
|
|
40
|
+
const parentId = task.parent_task_id ?? null;
|
|
41
|
+
if (parentId !== null && (typeof parentId !== 'string' || !ids.has(parentId))) {
|
|
42
|
+
fail(`parent_task_id must reference a task in the same plan: ${task.task_id}`, {
|
|
43
|
+
ref, parent_task_id: parentId,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
const parentKey = parentId === null ? ROOT : refKey(taskRef(member, parentId));
|
|
47
|
+
tasks.set(key, { ref, task, member });
|
|
48
|
+
parentByKey.set(key, parentKey);
|
|
49
|
+
if (!childrenByParent.has(parentKey)) childrenByParent.set(parentKey, []);
|
|
50
|
+
childrenByParent.get(parentKey).push(key);
|
|
51
|
+
if (!childrenByParent.has(key)) childrenByParent.set(key, []);
|
|
52
|
+
hasHierarchy ||= parentId !== null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (!hasHierarchy) return null;
|
|
56
|
+
|
|
57
|
+
for (const key of tasks.keys()) {
|
|
58
|
+
const seen = new Set([key]);
|
|
59
|
+
let cursor = parentByKey.get(key);
|
|
60
|
+
while (cursor !== ROOT) {
|
|
61
|
+
if (seen.has(cursor)) {
|
|
62
|
+
fail('parent_task_id hierarchy contains a cycle', {
|
|
63
|
+
task: tasks.get(key).ref,
|
|
64
|
+
parent: tasks.get(cursor)?.ref ?? null,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
seen.add(cursor);
|
|
68
|
+
cursor = parentByKey.get(cursor);
|
|
69
|
+
if (cursor === undefined) fail('parent_task_id hierarchy is disconnected');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
for (const children of childrenByParent.values()) children.sort(compareText);
|
|
73
|
+
return { tasks, parentByKey, childrenByParent };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function branchUnder(hierarchy, containerKey, taskKey) {
|
|
77
|
+
let cursor = taskKey;
|
|
78
|
+
while (cursor !== ROOT) {
|
|
79
|
+
const parent = hierarchy.parentByKey.get(cursor);
|
|
80
|
+
if (parent === containerKey) return cursor;
|
|
81
|
+
cursor = parent;
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function projectRef(hierarchy, containerKey, value) {
|
|
87
|
+
if (value === null || typeof value !== 'object') return null;
|
|
88
|
+
const branch = branchUnder(hierarchy, containerKey, refKey(value));
|
|
89
|
+
return branch === null ? null : { ...hierarchy.tasks.get(branch).ref };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function uniqueRefs(refs) {
|
|
93
|
+
const byKey = new Map(refs.map((ref) => [refKey(ref), ref]));
|
|
94
|
+
return [...byKey.values()].sort((left, right) => compareText(refKey(left), refKey(right)));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function projectMember(member, hierarchy, containerKey, selectedKeys) {
|
|
98
|
+
const tasks = member.plan.tasks.filter((task) => selectedKeys.has(refKey(taskRef(member, task.task_id))));
|
|
99
|
+
if (tasks.length === 0) return null;
|
|
100
|
+
|
|
101
|
+
const hardDependencies = [];
|
|
102
|
+
const seenHard = new Set();
|
|
103
|
+
for (const edge of member.plan.hard_dependencies) {
|
|
104
|
+
const from = projectRef(hierarchy, containerKey, edge.from);
|
|
105
|
+
const to = projectRef(hierarchy, containerKey, edge.to);
|
|
106
|
+
if (from === null || to === null || !selectedKeys.has(refKey(from))
|
|
107
|
+
|| !selectedKeys.has(refKey(to)) || refKey(from) === refKey(to)) continue;
|
|
108
|
+
const key = JSON.stringify([refKey(from), refKey(to)]);
|
|
109
|
+
if (seenHard.has(key)) continue;
|
|
110
|
+
seenHard.add(key);
|
|
111
|
+
hardDependencies.push({ from, to });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const joins = [];
|
|
115
|
+
for (const join of member.plan.joins) {
|
|
116
|
+
const before = projectRef(hierarchy, containerKey, join.before);
|
|
117
|
+
if (before === null || !selectedKeys.has(refKey(before))) continue;
|
|
118
|
+
const after = uniqueRefs(join.after.map((ref) => projectRef(hierarchy, containerKey, ref))
|
|
119
|
+
.filter((ref) => ref !== null && selectedKeys.has(refKey(ref))
|
|
120
|
+
&& refKey(ref) !== refKey(before)));
|
|
121
|
+
if (after.length > 0) joins.push({ ...join, after, before });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const phaseAcceptDependencies = [];
|
|
125
|
+
const seenPhaseAccept = new Set();
|
|
126
|
+
for (const dependency of member.plan.phase_accept_dependencies ?? []) {
|
|
127
|
+
const to = projectRef(hierarchy, containerKey, dependency.to);
|
|
128
|
+
if (to === null || !selectedKeys.has(refKey(to))) continue;
|
|
129
|
+
const key = JSON.stringify([dependency.from, refKey(to)]);
|
|
130
|
+
if (seenPhaseAccept.has(key)) continue;
|
|
131
|
+
seenPhaseAccept.add(key);
|
|
132
|
+
phaseAcceptDependencies.push({ ...dependency, to });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// plan-scopedのcross-plan edgeも階層levelのtaskへ射影する。子task同士の接続を
|
|
136
|
+
// root levelで捨てると、nested表示だけ依存線とwaveが消えるため、hard edgeと同じく
|
|
137
|
+
// 各endpointをそのlevel直下のbranchへ束縛し直す。
|
|
138
|
+
const planScopedEvents = (member.plan_scoped?.events ?? []).flatMap((event) => {
|
|
139
|
+
if (event.kind !== 'cross_plan_dependency') return [event];
|
|
140
|
+
const from = projectRef(hierarchy, containerKey, event.payload.from);
|
|
141
|
+
const to = projectRef(hierarchy, containerKey, event.payload.to);
|
|
142
|
+
if (from === null || to === null || !selectedKeys.has(refKey(from))
|
|
143
|
+
|| !selectedKeys.has(refKey(to)) || refKey(from) === refKey(to)) return [];
|
|
144
|
+
return [{ ...event, payload: { ...event.payload, from, to } }];
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
...member,
|
|
149
|
+
plan: {
|
|
150
|
+
...member.plan,
|
|
151
|
+
tasks,
|
|
152
|
+
hard_dependencies: hardDependencies,
|
|
153
|
+
joins,
|
|
154
|
+
phase_accept_dependencies: phaseAcceptDependencies,
|
|
155
|
+
},
|
|
156
|
+
...(member.plan_scoped === undefined ? {} : {
|
|
157
|
+
plan_scoped: { ...member.plan_scoped, events: planScopedEvents },
|
|
158
|
+
}),
|
|
159
|
+
tasks: member.tasks.filter(({ task_id: taskId }) => tasks.some((task) => task.task_id === taskId)),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function filterIndependence(independence, selectedKeys) {
|
|
164
|
+
if (!Array.isArray(independence)) return independence;
|
|
165
|
+
const selectedByPlan = new Map();
|
|
166
|
+
for (const key of selectedKeys) {
|
|
167
|
+
const [projectId, planKey, taskId] = JSON.parse(key);
|
|
168
|
+
selectedByPlan.set(JSON.stringify([projectId, planKey]),
|
|
169
|
+
new Set([...(selectedByPlan.get(JSON.stringify([projectId, planKey])) ?? []), taskId]));
|
|
170
|
+
}
|
|
171
|
+
return independence.flatMap((projection) => {
|
|
172
|
+
const selected = selectedByPlan.get(JSON.stringify([projection.project_id, projection.plan_key]));
|
|
173
|
+
if (selected === undefined) return [];
|
|
174
|
+
const frontier = projection.frontier;
|
|
175
|
+
return [{
|
|
176
|
+
...projection,
|
|
177
|
+
frontier: {
|
|
178
|
+
...frontier,
|
|
179
|
+
parallel_groups: (frontier.parallel_groups ?? []).map((group) => ({
|
|
180
|
+
...group, task_ids: group.task_ids.filter((taskId) => selected.has(taskId)),
|
|
181
|
+
})).filter((group) => group.task_ids.length > 0),
|
|
182
|
+
serialize_pairs: (frontier.serialize_pairs ?? [])
|
|
183
|
+
.filter((pair) => pair.task_ids.every((taskId) => selected.has(taskId))),
|
|
184
|
+
conflicts_with_active: (frontier.conflicts_with_active ?? []).filter((entry) =>
|
|
185
|
+
selected.has(entry.ready_task_id) && selected.has(entry.active_task_id)),
|
|
186
|
+
unknown: (frontier.unknown ?? []).filter((entry) => selected.has(entry.task_id)),
|
|
187
|
+
},
|
|
188
|
+
}];
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function topologyOf(readModel) {
|
|
193
|
+
const crossPlanDependencies = projectTodoCrossPlanDependencies(readModel.members);
|
|
194
|
+
return {
|
|
195
|
+
nodes: readModel.members.flatMap((member) => member.plan.tasks
|
|
196
|
+
.map(({ task_id: taskId }) => taskRef(member, taskId))),
|
|
197
|
+
hard_edges: [
|
|
198
|
+
...readModel.members.flatMap((member) => member.plan.hard_dependencies),
|
|
199
|
+
...crossPlanDependencies.map(({ from, to }) => ({ from, to })),
|
|
200
|
+
],
|
|
201
|
+
joins: readModel.members.flatMap((member) => member.plan.joins),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function buildLevel(readModel, hierarchy, containerKey, options, layoutFlat, includesSubtree) {
|
|
206
|
+
const selected = (hierarchy.childrenByParent.get(containerKey) ?? []).filter(includesSubtree);
|
|
207
|
+
const selectedKeys = new Set(selected);
|
|
208
|
+
const members = readModel.members.map((member) =>
|
|
209
|
+
projectMember(member, hierarchy, containerKey, selectedKeys)).filter(Boolean);
|
|
210
|
+
const projectedRead = { ...readModel, members };
|
|
211
|
+
const levelOptions = {
|
|
212
|
+
...options,
|
|
213
|
+
// live foldingは元のfull graphで一度だけ行い、下で選んだtask集合へ反映済み。
|
|
214
|
+
// 縮約levelでもう一度foldすると、可視な子を持つdone親containerが消える。
|
|
215
|
+
scope: 'all',
|
|
216
|
+
independence: filterIndependence(options.independence ?? null, selectedKeys),
|
|
217
|
+
seamProposals: containerKey === ROOT ? options.seamProposals ?? null : null,
|
|
218
|
+
};
|
|
219
|
+
const layout = layoutFlat(projectedRead, projectTodoChainV1(topologyOf(projectedRead)), levelOptions);
|
|
220
|
+
const children = selected.flatMap((parentKey) => {
|
|
221
|
+
if ((hierarchy.childrenByParent.get(parentKey) ?? []).length === 0) return [];
|
|
222
|
+
const childLevel = buildLevel(
|
|
223
|
+
readModel, hierarchy, parentKey, options, layoutFlat, includesSubtree,
|
|
224
|
+
);
|
|
225
|
+
if (childLevel.layout.nodes.length === 0 && childLevel.children.length === 0) return [];
|
|
226
|
+
return [{
|
|
227
|
+
parent_ref: { ...hierarchy.tasks.get(parentKey).ref },
|
|
228
|
+
level: childLevel,
|
|
229
|
+
}];
|
|
230
|
+
});
|
|
231
|
+
return { layout, children };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function levelMetrics(level, depth = 1) {
|
|
235
|
+
let taskCount = level.layout.nodes.length;
|
|
236
|
+
let visibleNodeCount = level.layout.metrics.visible_node_count;
|
|
237
|
+
let visibleEdgeCount = level.layout.metrics.visible_edge_count;
|
|
238
|
+
let maximumDepth = depth;
|
|
239
|
+
for (const child of level.children) {
|
|
240
|
+
const nested = levelMetrics(child.level, depth + 1);
|
|
241
|
+
taskCount += nested.taskCount;
|
|
242
|
+
visibleNodeCount += nested.visibleNodeCount;
|
|
243
|
+
visibleEdgeCount += nested.visibleEdgeCount;
|
|
244
|
+
maximumDepth = Math.max(maximumDepth, nested.maximumDepth);
|
|
245
|
+
}
|
|
246
|
+
return { taskCount, visibleNodeCount, visibleEdgeCount, maximumDepth };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function buildTodoGanttHierarchy(readModel, options, layoutFlat, visibleTaskKeys = null) {
|
|
250
|
+
const hierarchy = normalizeHierarchy(readModel);
|
|
251
|
+
if (hierarchy === null) return null;
|
|
252
|
+
const memo = new Map();
|
|
253
|
+
const includesSubtree = (key) => {
|
|
254
|
+
if (visibleTaskKeys === null) return true;
|
|
255
|
+
if (memo.has(key)) return memo.get(key);
|
|
256
|
+
const included = visibleTaskKeys.has(key)
|
|
257
|
+
|| (hierarchy.childrenByParent.get(key) ?? []).some(includesSubtree);
|
|
258
|
+
memo.set(key, included);
|
|
259
|
+
return included;
|
|
260
|
+
};
|
|
261
|
+
const root = buildLevel(readModel, hierarchy, ROOT, options, layoutFlat, includesSubtree);
|
|
262
|
+
return { root, metrics: levelMetrics(root) };
|
|
263
|
+
}
|