@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,190 +1,190 @@
|
|
|
1
|
-
import { TodoStoreError } from './todo-store.mjs';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* 依存グラフの「直列度」を判定する既定閾値。
|
|
5
|
-
*
|
|
6
|
-
* `serialization_ratio = critical_path_length / task_count` がこれを超えると、
|
|
7
|
-
* 依存連鎖が全task数の半分を超えて連なっている=大半のtaskが並列候補ではなく
|
|
8
|
-
* 一本の鎖に押し込まれていることを意味する。`todo start`側が既に持つ
|
|
9
|
-
* `all_ready_parallel_by_default`方針をplan作成時点まで前倒しする出発点として
|
|
10
|
-
* 0.5を採る。実測(parent-child-repair: task 26, critical path約20,
|
|
11
|
-
* ratio≈0.77)を確実に超える一方、緩やかな分岐を持つ通常のplanまでは拾わない
|
|
12
|
-
* 水準として選んだ。
|
|
13
|
-
*/
|
|
14
|
-
export const DISPATCH_SHAPE_SERIALIZATION_THRESHOLD = 0.5;
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* 閾値判定の対象にする最小task数。
|
|
18
|
-
* 3〜5 task程度の一直線planを毎回突き返しても再考の余地がなく
|
|
19
|
-
* (並列化する意味のある規模でない)機構がノイズになるだけなので、
|
|
20
|
-
* 6 task未満は常に素通りさせる。
|
|
21
|
-
*/
|
|
22
|
-
export const DISPATCH_SHAPE_MIN_TASK_COUNT_FOR_GATE = 6;
|
|
23
|
-
|
|
24
|
-
export const DISPATCH_SHAPE_SERIALIZATION_REVIEWED_FLAG = '--serialization-reviewed';
|
|
25
|
-
|
|
26
|
-
function compareText(left, right) {
|
|
27
|
-
return left < right ? -1 : left > right ? 1 : 0;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* task_id集合と(既にこの集合の内側だけへ絞り込まれた)依存辺から、
|
|
32
|
-
* dispatch形状を計算する。
|
|
33
|
-
*
|
|
34
|
-
* 呼び出し側の責務: `edges`は「このtask集合の内側だけ」の task_id 対で渡すこと
|
|
35
|
-
* (cross-plan/既存planへの依存や、joinの`after→before`展開は呼び出し側で
|
|
36
|
-
* 行い、範囲外の参照はここへ渡さない)。範囲外の参照が混入した場合は
|
|
37
|
-
* 呼び出し側の実装誤りとして typed error で止める。
|
|
38
|
-
*
|
|
39
|
-
* 循環検出は専用のロジックを別途書き足すのではなく、最長path計算
|
|
40
|
-
* (Kahn法によるtopological order)が全nodeを消化できないことの自然な帰結
|
|
41
|
-
* として行う。この関数はplan作成/migrateがstoreへ書き込む前(拒否時に
|
|
42
|
-
* 何も書かない設計)に呼ばれるため、store側の`validateMergedGraph`による
|
|
43
|
-
* cycle拒否より前に走る——結果として、循環を含む入力はここで先に
|
|
44
|
-
* `DISPATCH_SHAPE_INVALID`として止まる(従来store書込み時に出ていた
|
|
45
|
-
* `STORE_INCONSISTENT`/`merged_cycle`より手前で検出されるようになるという、
|
|
46
|
-
* 観測可能だが意図した違いがある)。
|
|
47
|
-
*/
|
|
48
|
-
export function computeTodoDispatchShape({ taskIds, edges }) {
|
|
49
|
-
if (!Array.isArray(taskIds) || taskIds.length === 0
|
|
50
|
-
|| !taskIds.every((id) => typeof id === 'string' && id.length > 0)) {
|
|
51
|
-
throw new TodoStoreError('DISPATCH_SHAPE_INVALID', 'dispatch_shape_task_ids_invalid');
|
|
52
|
-
}
|
|
53
|
-
const idSet = new Set(taskIds);
|
|
54
|
-
if (idSet.size !== taskIds.length) {
|
|
55
|
-
throw new TodoStoreError('DISPATCH_SHAPE_INVALID', 'dispatch_shape_task_ids_duplicate');
|
|
56
|
-
}
|
|
57
|
-
if (!Array.isArray(edges) || edges.some((edge) => edge === null || typeof edge !== 'object'
|
|
58
|
-
|| typeof edge.from !== 'string' || typeof edge.to !== 'string'
|
|
59
|
-
|| !idSet.has(edge.from) || !idSet.has(edge.to))) {
|
|
60
|
-
throw new TodoStoreError('DISPATCH_SHAPE_INVALID', 'dispatch_shape_edge_out_of_scope');
|
|
61
|
-
}
|
|
62
|
-
if (edges.some((edge) => edge.from === edge.to)) {
|
|
63
|
-
throw new TodoStoreError('DISPATCH_SHAPE_INVALID', 'dispatch_shape_self_edge');
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const successors = new Map([...idSet].map((id) => [id, []]));
|
|
67
|
-
const indegree = new Map([...idSet].map((id) => [id, 0]));
|
|
68
|
-
const edgeKeys = new Set();
|
|
69
|
-
for (const { from, to } of edges) {
|
|
70
|
-
const key = `${from}\0${to}`;
|
|
71
|
-
if (edgeKeys.has(key)) continue; // hard_dependenciesとjoin由来で同じ辺が重複しても一度だけ数える
|
|
72
|
-
edgeKeys.add(key);
|
|
73
|
-
successors.get(from).push(to);
|
|
74
|
-
indegree.set(to, indegree.get(to) + 1);
|
|
75
|
-
}
|
|
76
|
-
for (const list of successors.values()) list.sort(compareText);
|
|
77
|
-
|
|
78
|
-
// Kahn法によるtopological order。dist[node] = nodeで終わる最長path長(辺数)は、
|
|
79
|
-
// 「nodeを、その全先行taskが処理済みになった時点で処理する」という不変条件から
|
|
80
|
-
// 標準的なlongest-path-in-DAG漸化式(dist[v] = max(dist[v], dist[u]+1))として導かれる。
|
|
81
|
-
const dist = new Map([...idSet].map((id) => [id, 0]));
|
|
82
|
-
const predecessor = new Map();
|
|
83
|
-
const queue = [...idSet].filter((id) => indegree.get(id) === 0).sort(compareText);
|
|
84
|
-
const order = [];
|
|
85
|
-
while (queue.length > 0) {
|
|
86
|
-
queue.sort(compareText);
|
|
87
|
-
const node = queue.shift();
|
|
88
|
-
order.push(node);
|
|
89
|
-
for (const successor of successors.get(node)) {
|
|
90
|
-
if (dist.get(node) + 1 > dist.get(successor)) {
|
|
91
|
-
dist.set(successor, dist.get(node) + 1);
|
|
92
|
-
predecessor.set(successor, node);
|
|
93
|
-
}
|
|
94
|
-
indegree.set(successor, indegree.get(successor) - 1);
|
|
95
|
-
if (indegree.get(successor) === 0) queue.push(successor);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
if (order.length !== idSet.size) {
|
|
99
|
-
throw new TodoStoreError('DISPATCH_SHAPE_INVALID', 'dispatch_shape_dependency_cycle');
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const taskCount = idSet.size;
|
|
103
|
-
const maxDist = Math.max(...order.map((id) => dist.get(id)));
|
|
104
|
-
const criticalPathLength = maxDist + 1;
|
|
105
|
-
const widthByDist = new Map();
|
|
106
|
-
for (const id of idSet) widthByDist.set(dist.get(id), (widthByDist.get(dist.get(id)) ?? 0) + 1);
|
|
107
|
-
const maxFrontierWidth = Math.max(...widthByDist.values());
|
|
108
|
-
|
|
109
|
-
// critical path(人向けヒント)の復元: distが最大のnodeから、決定的に選んだpredecessorを
|
|
110
|
-
// 遡って根まで辿る。表示専用でありdigest対象ではないため、決定性は再現性のためだけに要る。
|
|
111
|
-
const deepest = [...idSet].filter((id) => dist.get(id) === maxDist).sort(compareText)[0];
|
|
112
|
-
const criticalPathTaskIds = [];
|
|
113
|
-
for (let cursor = deepest; cursor !== undefined; cursor = predecessor.get(cursor)) {
|
|
114
|
-
criticalPathTaskIds.push(cursor);
|
|
115
|
-
}
|
|
116
|
-
criticalPathTaskIds.reverse();
|
|
117
|
-
|
|
118
|
-
return {
|
|
119
|
-
task_count: taskCount,
|
|
120
|
-
critical_path_length: criticalPathLength,
|
|
121
|
-
max_frontier_width: maxFrontierWidth,
|
|
122
|
-
// canonical digest(todoSelfDigest→digestTodoArtifact)はsafe integer以外の数値を
|
|
123
|
-
// TypeErrorで拒否する(todo-contracts.mjsのcanonicalPart)。dispatch_shapeは
|
|
124
|
-
// plan create/migrateの結果にそのまま埋め込まれ digest対象になるため、比率は
|
|
125
|
-
// 固定小数のstringで持つ(判定側はNumber()で復元する)。
|
|
126
|
-
serialization_ratio: (criticalPathLength / taskCount).toFixed(4),
|
|
127
|
-
critical_path_task_ids: criticalPathTaskIds,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** dispatch_shapeが、再考なしで直列のまま通してよい規模・度合いかを判定する。 */
|
|
132
|
-
export function isTodoDispatchShapeSerializationExcessive(shape) {
|
|
133
|
-
return shape.task_count >= DISPATCH_SHAPE_MIN_TASK_COUNT_FOR_GATE
|
|
134
|
-
&& Number(shape.serialization_ratio) > DISPATCH_SHAPE_SERIALIZATION_THRESHOLD;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* dispatch_shapeが直列に寄りすぎているplanを、再考なしでは通さない。
|
|
139
|
-
*
|
|
140
|
-
* `todo start`の`PARALLEL_DISPATCH_RECONSIDER`と同じ二段階(一度突き返し、
|
|
141
|
-
* 再考を経たflagが無ければ通さない)をplan作成時点へ前倒しする。ここで
|
|
142
|
-
* 拒否する場合、呼び出し側はまだstoreへ何も書いていないこと(初期化/追加を
|
|
143
|
-
* この呼び出しより後で行うこと)。
|
|
144
|
-
*/
|
|
145
|
-
export function assertTodoDispatchShapeReviewed({ shape, reviewed }) {
|
|
146
|
-
if (!isTodoDispatchShapeSerializationExcessive(shape) || reviewed) return;
|
|
147
|
-
throw new TodoStoreError('PARALLEL_DISPATCH_RECONSIDER', 'plan_shape_too_serial', undefined, {
|
|
148
|
-
task_count: shape.task_count,
|
|
149
|
-
critical_path_length: shape.critical_path_length,
|
|
150
|
-
max_frontier_width: shape.max_frontier_width,
|
|
151
|
-
serialization_ratio: shape.serialization_ratio,
|
|
152
|
-
critical_path_task_ids: shape.critical_path_task_ids,
|
|
153
|
-
default_policy: 'all_ready_parallel_by_default',
|
|
154
|
-
serialization_reviewed_flag: DISPATCH_SHAPE_SERIALIZATION_REVIEWED_FLAG,
|
|
155
|
-
guidance: `並列を検討しなさい。task ${shape.task_count}件のうちcritical pathが`
|
|
156
|
-
+ `${shape.critical_path_length}段(serialization_ratio ${shape.serialization_ratio})で、`
|
|
157
|
-
+ '大半のtaskが並列候補ではなく一本の依存鎖に押し込まれている。'
|
|
158
|
-
+ 'critical_path_task_idsに沿った依存のうち、実際には干渉しない組を'
|
|
159
|
-
+ 'hard_dependencies/joinsから外せないか見直す'
|
|
160
|
-
+ '(実行主体が足りないなら増やす。増やせないことは直列化の理由にならない)。'
|
|
161
|
-
+ `検討した上でなお直列でよいなら ${DISPATCH_SHAPE_SERIALIZATION_REVIEWED_FLAG} を付けて再実行する。`,
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* plan/extractionの`tasks`・`hard_dependencies`・`joins`(nodeRef形式)から、
|
|
167
|
-
* 「このproject_id/plan_keyの内側だけ」に絞ったdispatch形状を計算する高レベル入口。
|
|
168
|
-
*
|
|
169
|
-
* cross-plan参照やdanglingな参照はここで検証しない(既存のstore書込み経路が
|
|
170
|
-
* 別途検証する)。単に形状計算の対象から外すだけであり、それらの妥当性判断は
|
|
171
|
-
* 呼び出し側の後続処理(実際のstore書込み)に委ねる。
|
|
172
|
-
*/
|
|
173
|
-
export function computeTodoDispatchShapeForPlan({
|
|
174
|
-
projectId, planKey, taskIds, hardDependencies, joins,
|
|
175
|
-
}) {
|
|
176
|
-
const idSet = new Set(taskIds);
|
|
177
|
-
const isLocal = (ref) => ref?.project_id === projectId && ref?.plan_key === planKey
|
|
178
|
-
&& idSet.has(ref?.task_id);
|
|
179
|
-
const edges = [];
|
|
180
|
-
for (const edge of hardDependencies ?? []) {
|
|
181
|
-
if (isLocal(edge.from) && isLocal(edge.to)) edges.push({ from: edge.from.task_id, to: edge.to.task_id });
|
|
182
|
-
}
|
|
183
|
-
for (const join of joins ?? []) {
|
|
184
|
-
if (!isLocal(join.before)) continue;
|
|
185
|
-
for (const after of join.after) {
|
|
186
|
-
if (isLocal(after)) edges.push({ from: after.task_id, to: join.before.task_id });
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
return computeTodoDispatchShape({ taskIds, edges });
|
|
190
|
-
}
|
|
1
|
+
import { TodoStoreError } from './todo-store.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 依存グラフの「直列度」を判定する既定閾値。
|
|
5
|
+
*
|
|
6
|
+
* `serialization_ratio = critical_path_length / task_count` がこれを超えると、
|
|
7
|
+
* 依存連鎖が全task数の半分を超えて連なっている=大半のtaskが並列候補ではなく
|
|
8
|
+
* 一本の鎖に押し込まれていることを意味する。`todo start`側が既に持つ
|
|
9
|
+
* `all_ready_parallel_by_default`方針をplan作成時点まで前倒しする出発点として
|
|
10
|
+
* 0.5を採る。実測(parent-child-repair: task 26, critical path約20,
|
|
11
|
+
* ratio≈0.77)を確実に超える一方、緩やかな分岐を持つ通常のplanまでは拾わない
|
|
12
|
+
* 水準として選んだ。
|
|
13
|
+
*/
|
|
14
|
+
export const DISPATCH_SHAPE_SERIALIZATION_THRESHOLD = 0.5;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 閾値判定の対象にする最小task数。
|
|
18
|
+
* 3〜5 task程度の一直線planを毎回突き返しても再考の余地がなく
|
|
19
|
+
* (並列化する意味のある規模でない)機構がノイズになるだけなので、
|
|
20
|
+
* 6 task未満は常に素通りさせる。
|
|
21
|
+
*/
|
|
22
|
+
export const DISPATCH_SHAPE_MIN_TASK_COUNT_FOR_GATE = 6;
|
|
23
|
+
|
|
24
|
+
export const DISPATCH_SHAPE_SERIALIZATION_REVIEWED_FLAG = '--serialization-reviewed';
|
|
25
|
+
|
|
26
|
+
function compareText(left, right) {
|
|
27
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* task_id集合と(既にこの集合の内側だけへ絞り込まれた)依存辺から、
|
|
32
|
+
* dispatch形状を計算する。
|
|
33
|
+
*
|
|
34
|
+
* 呼び出し側の責務: `edges`は「このtask集合の内側だけ」の task_id 対で渡すこと
|
|
35
|
+
* (cross-plan/既存planへの依存や、joinの`after→before`展開は呼び出し側で
|
|
36
|
+
* 行い、範囲外の参照はここへ渡さない)。範囲外の参照が混入した場合は
|
|
37
|
+
* 呼び出し側の実装誤りとして typed error で止める。
|
|
38
|
+
*
|
|
39
|
+
* 循環検出は専用のロジックを別途書き足すのではなく、最長path計算
|
|
40
|
+
* (Kahn法によるtopological order)が全nodeを消化できないことの自然な帰結
|
|
41
|
+
* として行う。この関数はplan作成/migrateがstoreへ書き込む前(拒否時に
|
|
42
|
+
* 何も書かない設計)に呼ばれるため、store側の`validateMergedGraph`による
|
|
43
|
+
* cycle拒否より前に走る——結果として、循環を含む入力はここで先に
|
|
44
|
+
* `DISPATCH_SHAPE_INVALID`として止まる(従来store書込み時に出ていた
|
|
45
|
+
* `STORE_INCONSISTENT`/`merged_cycle`より手前で検出されるようになるという、
|
|
46
|
+
* 観測可能だが意図した違いがある)。
|
|
47
|
+
*/
|
|
48
|
+
export function computeTodoDispatchShape({ taskIds, edges }) {
|
|
49
|
+
if (!Array.isArray(taskIds) || taskIds.length === 0
|
|
50
|
+
|| !taskIds.every((id) => typeof id === 'string' && id.length > 0)) {
|
|
51
|
+
throw new TodoStoreError('DISPATCH_SHAPE_INVALID', 'dispatch_shape_task_ids_invalid');
|
|
52
|
+
}
|
|
53
|
+
const idSet = new Set(taskIds);
|
|
54
|
+
if (idSet.size !== taskIds.length) {
|
|
55
|
+
throw new TodoStoreError('DISPATCH_SHAPE_INVALID', 'dispatch_shape_task_ids_duplicate');
|
|
56
|
+
}
|
|
57
|
+
if (!Array.isArray(edges) || edges.some((edge) => edge === null || typeof edge !== 'object'
|
|
58
|
+
|| typeof edge.from !== 'string' || typeof edge.to !== 'string'
|
|
59
|
+
|| !idSet.has(edge.from) || !idSet.has(edge.to))) {
|
|
60
|
+
throw new TodoStoreError('DISPATCH_SHAPE_INVALID', 'dispatch_shape_edge_out_of_scope');
|
|
61
|
+
}
|
|
62
|
+
if (edges.some((edge) => edge.from === edge.to)) {
|
|
63
|
+
throw new TodoStoreError('DISPATCH_SHAPE_INVALID', 'dispatch_shape_self_edge');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const successors = new Map([...idSet].map((id) => [id, []]));
|
|
67
|
+
const indegree = new Map([...idSet].map((id) => [id, 0]));
|
|
68
|
+
const edgeKeys = new Set();
|
|
69
|
+
for (const { from, to } of edges) {
|
|
70
|
+
const key = `${from}\0${to}`;
|
|
71
|
+
if (edgeKeys.has(key)) continue; // hard_dependenciesとjoin由来で同じ辺が重複しても一度だけ数える
|
|
72
|
+
edgeKeys.add(key);
|
|
73
|
+
successors.get(from).push(to);
|
|
74
|
+
indegree.set(to, indegree.get(to) + 1);
|
|
75
|
+
}
|
|
76
|
+
for (const list of successors.values()) list.sort(compareText);
|
|
77
|
+
|
|
78
|
+
// Kahn法によるtopological order。dist[node] = nodeで終わる最長path長(辺数)は、
|
|
79
|
+
// 「nodeを、その全先行taskが処理済みになった時点で処理する」という不変条件から
|
|
80
|
+
// 標準的なlongest-path-in-DAG漸化式(dist[v] = max(dist[v], dist[u]+1))として導かれる。
|
|
81
|
+
const dist = new Map([...idSet].map((id) => [id, 0]));
|
|
82
|
+
const predecessor = new Map();
|
|
83
|
+
const queue = [...idSet].filter((id) => indegree.get(id) === 0).sort(compareText);
|
|
84
|
+
const order = [];
|
|
85
|
+
while (queue.length > 0) {
|
|
86
|
+
queue.sort(compareText);
|
|
87
|
+
const node = queue.shift();
|
|
88
|
+
order.push(node);
|
|
89
|
+
for (const successor of successors.get(node)) {
|
|
90
|
+
if (dist.get(node) + 1 > dist.get(successor)) {
|
|
91
|
+
dist.set(successor, dist.get(node) + 1);
|
|
92
|
+
predecessor.set(successor, node);
|
|
93
|
+
}
|
|
94
|
+
indegree.set(successor, indegree.get(successor) - 1);
|
|
95
|
+
if (indegree.get(successor) === 0) queue.push(successor);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (order.length !== idSet.size) {
|
|
99
|
+
throw new TodoStoreError('DISPATCH_SHAPE_INVALID', 'dispatch_shape_dependency_cycle');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const taskCount = idSet.size;
|
|
103
|
+
const maxDist = Math.max(...order.map((id) => dist.get(id)));
|
|
104
|
+
const criticalPathLength = maxDist + 1;
|
|
105
|
+
const widthByDist = new Map();
|
|
106
|
+
for (const id of idSet) widthByDist.set(dist.get(id), (widthByDist.get(dist.get(id)) ?? 0) + 1);
|
|
107
|
+
const maxFrontierWidth = Math.max(...widthByDist.values());
|
|
108
|
+
|
|
109
|
+
// critical path(人向けヒント)の復元: distが最大のnodeから、決定的に選んだpredecessorを
|
|
110
|
+
// 遡って根まで辿る。表示専用でありdigest対象ではないため、決定性は再現性のためだけに要る。
|
|
111
|
+
const deepest = [...idSet].filter((id) => dist.get(id) === maxDist).sort(compareText)[0];
|
|
112
|
+
const criticalPathTaskIds = [];
|
|
113
|
+
for (let cursor = deepest; cursor !== undefined; cursor = predecessor.get(cursor)) {
|
|
114
|
+
criticalPathTaskIds.push(cursor);
|
|
115
|
+
}
|
|
116
|
+
criticalPathTaskIds.reverse();
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
task_count: taskCount,
|
|
120
|
+
critical_path_length: criticalPathLength,
|
|
121
|
+
max_frontier_width: maxFrontierWidth,
|
|
122
|
+
// canonical digest(todoSelfDigest→digestTodoArtifact)はsafe integer以外の数値を
|
|
123
|
+
// TypeErrorで拒否する(todo-contracts.mjsのcanonicalPart)。dispatch_shapeは
|
|
124
|
+
// plan create/migrateの結果にそのまま埋め込まれ digest対象になるため、比率は
|
|
125
|
+
// 固定小数のstringで持つ(判定側はNumber()で復元する)。
|
|
126
|
+
serialization_ratio: (criticalPathLength / taskCount).toFixed(4),
|
|
127
|
+
critical_path_task_ids: criticalPathTaskIds,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** dispatch_shapeが、再考なしで直列のまま通してよい規模・度合いかを判定する。 */
|
|
132
|
+
export function isTodoDispatchShapeSerializationExcessive(shape) {
|
|
133
|
+
return shape.task_count >= DISPATCH_SHAPE_MIN_TASK_COUNT_FOR_GATE
|
|
134
|
+
&& Number(shape.serialization_ratio) > DISPATCH_SHAPE_SERIALIZATION_THRESHOLD;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* dispatch_shapeが直列に寄りすぎているplanを、再考なしでは通さない。
|
|
139
|
+
*
|
|
140
|
+
* `todo start`の`PARALLEL_DISPATCH_RECONSIDER`と同じ二段階(一度突き返し、
|
|
141
|
+
* 再考を経たflagが無ければ通さない)をplan作成時点へ前倒しする。ここで
|
|
142
|
+
* 拒否する場合、呼び出し側はまだstoreへ何も書いていないこと(初期化/追加を
|
|
143
|
+
* この呼び出しより後で行うこと)。
|
|
144
|
+
*/
|
|
145
|
+
export function assertTodoDispatchShapeReviewed({ shape, reviewed }) {
|
|
146
|
+
if (!isTodoDispatchShapeSerializationExcessive(shape) || reviewed) return;
|
|
147
|
+
throw new TodoStoreError('PARALLEL_DISPATCH_RECONSIDER', 'plan_shape_too_serial', undefined, {
|
|
148
|
+
task_count: shape.task_count,
|
|
149
|
+
critical_path_length: shape.critical_path_length,
|
|
150
|
+
max_frontier_width: shape.max_frontier_width,
|
|
151
|
+
serialization_ratio: shape.serialization_ratio,
|
|
152
|
+
critical_path_task_ids: shape.critical_path_task_ids,
|
|
153
|
+
default_policy: 'all_ready_parallel_by_default',
|
|
154
|
+
serialization_reviewed_flag: DISPATCH_SHAPE_SERIALIZATION_REVIEWED_FLAG,
|
|
155
|
+
guidance: `並列を検討しなさい。task ${shape.task_count}件のうちcritical pathが`
|
|
156
|
+
+ `${shape.critical_path_length}段(serialization_ratio ${shape.serialization_ratio})で、`
|
|
157
|
+
+ '大半のtaskが並列候補ではなく一本の依存鎖に押し込まれている。'
|
|
158
|
+
+ 'critical_path_task_idsに沿った依存のうち、実際には干渉しない組を'
|
|
159
|
+
+ 'hard_dependencies/joinsから外せないか見直す'
|
|
160
|
+
+ '(実行主体が足りないなら増やす。増やせないことは直列化の理由にならない)。'
|
|
161
|
+
+ `検討した上でなお直列でよいなら ${DISPATCH_SHAPE_SERIALIZATION_REVIEWED_FLAG} を付けて再実行する。`,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* plan/extractionの`tasks`・`hard_dependencies`・`joins`(nodeRef形式)から、
|
|
167
|
+
* 「このproject_id/plan_keyの内側だけ」に絞ったdispatch形状を計算する高レベル入口。
|
|
168
|
+
*
|
|
169
|
+
* cross-plan参照やdanglingな参照はここで検証しない(既存のstore書込み経路が
|
|
170
|
+
* 別途検証する)。単に形状計算の対象から外すだけであり、それらの妥当性判断は
|
|
171
|
+
* 呼び出し側の後続処理(実際のstore書込み)に委ねる。
|
|
172
|
+
*/
|
|
173
|
+
export function computeTodoDispatchShapeForPlan({
|
|
174
|
+
projectId, planKey, taskIds, hardDependencies, joins,
|
|
175
|
+
}) {
|
|
176
|
+
const idSet = new Set(taskIds);
|
|
177
|
+
const isLocal = (ref) => ref?.project_id === projectId && ref?.plan_key === planKey
|
|
178
|
+
&& idSet.has(ref?.task_id);
|
|
179
|
+
const edges = [];
|
|
180
|
+
for (const edge of hardDependencies ?? []) {
|
|
181
|
+
if (isLocal(edge.from) && isLocal(edge.to)) edges.push({ from: edge.from.task_id, to: edge.to.task_id });
|
|
182
|
+
}
|
|
183
|
+
for (const join of joins ?? []) {
|
|
184
|
+
if (!isLocal(join.before)) continue;
|
|
185
|
+
for (const after of join.after) {
|
|
186
|
+
if (isLocal(after)) edges.push({ from: after.task_id, to: join.before.task_id });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return computeTodoDispatchShape({ taskIds, edges });
|
|
190
|
+
}
|