@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,303 +1,303 @@
|
|
|
1
|
-
const GRAPH_SCHEMA = 'lattice.normalized_boundary_graph.v2';
|
|
2
|
-
const PLAN_SCHEMA = 'lattice.plan_graph.v2';
|
|
3
|
-
const MAX_TODOS = 8;
|
|
4
|
-
const MAX_INPUT_TODOS = 256;
|
|
5
|
-
const DEFAULT_MAX_SEARCH_STATES = 2_000_000;
|
|
6
|
-
const IDENTIFIER = /^[0-9A-Za-z](?:[0-9A-Za-z._-]{0,127})$/;
|
|
7
|
-
|
|
8
|
-
function fail(reason) {
|
|
9
|
-
throw new TypeError(`schedulability compiler v2契約違反: ${reason}`);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function plainRecord(value) {
|
|
13
|
-
return value !== null
|
|
14
|
-
&& typeof value === 'object'
|
|
15
|
-
&& !Array.isArray(value)
|
|
16
|
-
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function exactRecord(value, keys) {
|
|
20
|
-
if (!plainRecord(value)) return false;
|
|
21
|
-
const actual = Object.keys(value).sort();
|
|
22
|
-
const expected = [...keys].sort();
|
|
23
|
-
return actual.length === expected.length
|
|
24
|
-
&& actual.every((key, index) => key === expected[index]);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function identifier(value) {
|
|
28
|
-
return typeof value === 'string' && IDENTIFIER.test(value);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function boundedText(value) {
|
|
32
|
-
return typeof value === 'string'
|
|
33
|
-
&& value.length > 0
|
|
34
|
-
&& value === value.trim()
|
|
35
|
-
&& Buffer.byteLength(value, 'utf8') <= 4_096;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function compareText(left, right) {
|
|
39
|
-
if (left < right) return -1;
|
|
40
|
-
if (left > right) return 1;
|
|
41
|
-
return 0;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function assertArray(value, label) {
|
|
45
|
-
if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype) {
|
|
46
|
-
fail(`${label}がarrayではない`);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function normalizeOptions(options) {
|
|
51
|
-
if (!exactRecord(options, []) && !exactRecord(options, ['maxSearchStates'])) {
|
|
52
|
-
fail('options shapeが不正');
|
|
53
|
-
}
|
|
54
|
-
const maxSearchStates = options.maxSearchStates ?? DEFAULT_MAX_SEARCH_STATES;
|
|
55
|
-
if (!Number.isSafeInteger(maxSearchStates) || maxSearchStates < 0) {
|
|
56
|
-
fail('maxSearchStatesがnon-negative safe integerではない');
|
|
57
|
-
}
|
|
58
|
-
return { maxSearchStates };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function assertAcyclic(todos, precedences) {
|
|
62
|
-
const indegree = new Map(todos.map((todoId) => [todoId, 0]));
|
|
63
|
-
const outgoing = new Map(todos.map((todoId) => [todoId, new Set()]));
|
|
64
|
-
for (const { from_todo_id: from, to_todo_id: to } of precedences) {
|
|
65
|
-
if (!outgoing.get(from).has(to)) {
|
|
66
|
-
outgoing.get(from).add(to);
|
|
67
|
-
indegree.set(to, indegree.get(to) + 1);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const ready = todos.filter((todoId) => indegree.get(todoId) === 0);
|
|
72
|
-
let visited = 0;
|
|
73
|
-
while (ready.length > 0) {
|
|
74
|
-
const todoId = ready.shift();
|
|
75
|
-
visited += 1;
|
|
76
|
-
for (const next of outgoing.get(todoId)) {
|
|
77
|
-
const remaining = indegree.get(next) - 1;
|
|
78
|
-
indegree.set(next, remaining);
|
|
79
|
-
if (remaining === 0) ready.push(next);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
if (visited !== todos.length) fail('precedencesにcycleがある');
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function normalizeGraph(value) {
|
|
86
|
-
if (!exactRecord(value, [
|
|
87
|
-
'schema_version',
|
|
88
|
-
'todos',
|
|
89
|
-
'conflicts',
|
|
90
|
-
'precedences',
|
|
91
|
-
'unknowns',
|
|
92
|
-
'capacity',
|
|
93
|
-
]) || value.schema_version !== GRAPH_SCHEMA) {
|
|
94
|
-
fail('normalized graph shapeまたはschema_versionが不正');
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
assertArray(value.todos, 'todos');
|
|
98
|
-
if (value.todos.length < 1 || value.todos.length > MAX_INPUT_TODOS
|
|
99
|
-
|| !value.todos.every(identifier)
|
|
100
|
-
|| new Set(value.todos).size !== value.todos.length) {
|
|
101
|
-
fail('todosがbounded unique identifier arrayではない');
|
|
102
|
-
}
|
|
103
|
-
const todos = [...value.todos].sort(compareText);
|
|
104
|
-
const todoSet = new Set(todos);
|
|
105
|
-
|
|
106
|
-
if (!Number.isSafeInteger(value.capacity)
|
|
107
|
-
|| value.capacity < 1
|
|
108
|
-
|| value.capacity > MAX_INPUT_TODOS) {
|
|
109
|
-
fail('capacityがbounded positive integerではない');
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
assertArray(value.conflicts, 'conflicts');
|
|
113
|
-
const conflictKeys = new Set();
|
|
114
|
-
const conflicts = value.conflicts.map((entry) => {
|
|
115
|
-
if (!exactRecord(entry, ['todo_ids', 'resource_id'])) {
|
|
116
|
-
fail('conflict shapeが不正');
|
|
117
|
-
}
|
|
118
|
-
assertArray(entry.todo_ids, 'conflict.todo_ids');
|
|
119
|
-
if (entry.todo_ids.length !== 2
|
|
120
|
-
|| !entry.todo_ids.every((todoId) => todoSet.has(todoId))
|
|
121
|
-
|| entry.todo_ids[0] === entry.todo_ids[1]
|
|
122
|
-
|| !identifier(entry.resource_id)) {
|
|
123
|
-
fail('conflict relationが不正');
|
|
124
|
-
}
|
|
125
|
-
const todoIds = [...entry.todo_ids].sort(compareText);
|
|
126
|
-
const key = `${todoIds[0]}\u0000${todoIds[1]}\u0000${entry.resource_id}`;
|
|
127
|
-
if (conflictKeys.has(key)) fail('duplicate conflictがある');
|
|
128
|
-
conflictKeys.add(key);
|
|
129
|
-
return { todo_ids: todoIds, resource_id: entry.resource_id };
|
|
130
|
-
}).sort((left, right) => (
|
|
131
|
-
compareText(left.todo_ids[0], right.todo_ids[0])
|
|
132
|
-
|| compareText(left.todo_ids[1], right.todo_ids[1])
|
|
133
|
-
|| compareText(left.resource_id, right.resource_id)
|
|
134
|
-
));
|
|
135
|
-
|
|
136
|
-
assertArray(value.precedences, 'precedences');
|
|
137
|
-
const precedenceKeys = new Set();
|
|
138
|
-
const precedences = value.precedences.map((entry) => {
|
|
139
|
-
if (!exactRecord(entry, ['from_todo_id', 'to_todo_id', 'reason'])
|
|
140
|
-
|| !todoSet.has(entry.from_todo_id)
|
|
141
|
-
|| !todoSet.has(entry.to_todo_id)
|
|
142
|
-
|| entry.from_todo_id === entry.to_todo_id
|
|
143
|
-
|| !boundedText(entry.reason)) {
|
|
144
|
-
fail('precedence relationが不正');
|
|
145
|
-
}
|
|
146
|
-
const key = `${entry.from_todo_id}\u0000${entry.to_todo_id}\u0000${entry.reason}`;
|
|
147
|
-
if (precedenceKeys.has(key)) fail('duplicate precedenceがある');
|
|
148
|
-
precedenceKeys.add(key);
|
|
149
|
-
return {
|
|
150
|
-
from_todo_id: entry.from_todo_id,
|
|
151
|
-
to_todo_id: entry.to_todo_id,
|
|
152
|
-
reason: entry.reason,
|
|
153
|
-
};
|
|
154
|
-
}).sort((left, right) => (
|
|
155
|
-
compareText(left.from_todo_id, right.from_todo_id)
|
|
156
|
-
|| compareText(left.to_todo_id, right.to_todo_id)
|
|
157
|
-
|| compareText(left.reason, right.reason)
|
|
158
|
-
));
|
|
159
|
-
assertAcyclic(todos, precedences);
|
|
160
|
-
|
|
161
|
-
assertArray(value.unknowns, 'unknowns');
|
|
162
|
-
const unknownKeys = new Set();
|
|
163
|
-
const unknowns = value.unknowns.map((entry) => {
|
|
164
|
-
if (!exactRecord(entry, ['todo_id', 'kind', 'reason'])
|
|
165
|
-
|| !todoSet.has(entry.todo_id)
|
|
166
|
-
|| !identifier(entry.kind)
|
|
167
|
-
|| !boundedText(entry.reason)) {
|
|
168
|
-
fail('unknown relationが不正');
|
|
169
|
-
}
|
|
170
|
-
const key = `${entry.todo_id}\u0000${entry.kind}\u0000${entry.reason}`;
|
|
171
|
-
if (unknownKeys.has(key)) fail('duplicate unknownがある');
|
|
172
|
-
unknownKeys.add(key);
|
|
173
|
-
return { todo_id: entry.todo_id, kind: entry.kind, reason: entry.reason };
|
|
174
|
-
}).sort((left, right) => (
|
|
175
|
-
compareText(left.todo_id, right.todo_id)
|
|
176
|
-
|| compareText(left.kind, right.kind)
|
|
177
|
-
|| compareText(left.reason, right.reason)
|
|
178
|
-
));
|
|
179
|
-
|
|
180
|
-
return { todos, conflicts, precedences, unknowns, capacity: value.capacity };
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function feasibleAssignment(graph, waveByIndex, indexByTodo) {
|
|
184
|
-
const counts = new Array(graph.todos.length).fill(0);
|
|
185
|
-
for (const waveIndex of waveByIndex) {
|
|
186
|
-
counts[waveIndex] += 1;
|
|
187
|
-
if (counts[waveIndex] > graph.capacity) return false;
|
|
188
|
-
}
|
|
189
|
-
for (const conflict of graph.conflicts) {
|
|
190
|
-
if (waveByIndex[indexByTodo.get(conflict.todo_ids[0])]
|
|
191
|
-
=== waveByIndex[indexByTodo.get(conflict.todo_ids[1])]) {
|
|
192
|
-
return false;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
for (const precedence of graph.precedences) {
|
|
196
|
-
if (waveByIndex[indexByTodo.get(precedence.from_todo_id)]
|
|
197
|
-
>= waveByIndex[indexByTodo.get(precedence.to_todo_id)]) {
|
|
198
|
-
return false;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
return true;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function searchMinimum(graph, maxSearchStates) {
|
|
205
|
-
const assignment = new Array(graph.todos.length).fill(0);
|
|
206
|
-
const indexByTodo = new Map(graph.todos.map((todoId, index) => [todoId, index]));
|
|
207
|
-
let examined = 0;
|
|
208
|
-
|
|
209
|
-
for (let waveCount = 1; waveCount <= graph.todos.length; waveCount += 1) {
|
|
210
|
-
const used = new Array(waveCount).fill(0);
|
|
211
|
-
let solution = null;
|
|
212
|
-
let exhausted = false;
|
|
213
|
-
|
|
214
|
-
function visit(todoIndex, usedCount) {
|
|
215
|
-
if (solution !== null || exhausted) return;
|
|
216
|
-
if (todoIndex === graph.todos.length) {
|
|
217
|
-
if (usedCount !== waveCount) return;
|
|
218
|
-
if (examined >= maxSearchStates) {
|
|
219
|
-
exhausted = true;
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
examined += 1;
|
|
223
|
-
if (feasibleAssignment(graph, assignment, indexByTodo)) {
|
|
224
|
-
solution = [...assignment];
|
|
225
|
-
}
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const remainingAfter = graph.todos.length - todoIndex - 1;
|
|
230
|
-
for (let waveIndex = 0; waveIndex < waveCount; waveIndex += 1) {
|
|
231
|
-
const isNew = used[waveIndex] === 0;
|
|
232
|
-
const nextUsedCount = usedCount + (isNew ? 1 : 0);
|
|
233
|
-
if (waveCount - nextUsedCount > remainingAfter) continue;
|
|
234
|
-
assignment[todoIndex] = waveIndex;
|
|
235
|
-
used[waveIndex] += 1;
|
|
236
|
-
visit(todoIndex + 1, nextUsedCount);
|
|
237
|
-
used[waveIndex] -= 1;
|
|
238
|
-
if (solution !== null || exhausted) return;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
visit(0, 0);
|
|
243
|
-
if (exhausted) return { outcome: 'exhausted' };
|
|
244
|
-
if (solution !== null) return { outcome: 'found', assignment: solution, waveCount };
|
|
245
|
-
}
|
|
246
|
-
fail('acyclic bounded graphにfeasible scheduleがない');
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function pairwiseVerdicts(graph) {
|
|
250
|
-
return [
|
|
251
|
-
...graph.conflicts.map((conflict) => ({
|
|
252
|
-
type: 'conflict',
|
|
253
|
-
todo_ids: [...conflict.todo_ids],
|
|
254
|
-
resource_id: conflict.resource_id,
|
|
255
|
-
})),
|
|
256
|
-
...graph.precedences.map((precedence) => ({
|
|
257
|
-
type: 'precedence',
|
|
258
|
-
from_todo_id: precedence.from_todo_id,
|
|
259
|
-
to_todo_id: precedence.to_todo_id,
|
|
260
|
-
reason: precedence.reason,
|
|
261
|
-
})),
|
|
262
|
-
];
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Candidate非依存のbounded graphをcanonical minimum-wave planへcompileする。
|
|
267
|
-
* @param {unknown} normalizedGraph
|
|
268
|
-
* @param {unknown} options
|
|
269
|
-
* @returns {object}
|
|
270
|
-
*/
|
|
271
|
-
export function compileSchedulabilityGraphV2(normalizedGraph, options = {}) {
|
|
272
|
-
const graph = normalizeGraph(normalizedGraph);
|
|
273
|
-
const { maxSearchStates } = normalizeOptions(options);
|
|
274
|
-
|
|
275
|
-
if (graph.todos.length > MAX_TODOS) {
|
|
276
|
-
return { outcome: 'unsupported', code: 'NODE_LIMIT_EXCEEDED' };
|
|
277
|
-
}
|
|
278
|
-
if (graph.unknowns.length > 0) {
|
|
279
|
-
return { outcome: 'unknown', code: 'BOUNDARY_UNKNOWN', unknowns: graph.unknowns };
|
|
280
|
-
}
|
|
281
|
-
if (maxSearchStates === 0) {
|
|
282
|
-
return { outcome: 'unsupported', code: 'SEARCH_BUDGET_EXHAUSTED' };
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const searched = searchMinimum(graph, maxSearchStates);
|
|
286
|
-
if (searched.outcome === 'exhausted') {
|
|
287
|
-
return { outcome: 'unsupported', code: 'SEARCH_BUDGET_EXHAUSTED' };
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
const waves = Array.from({ length: searched.waveCount }, () => ({ todo_ids: [] }));
|
|
291
|
-
searched.assignment.forEach((waveIndex, todoIndex) => {
|
|
292
|
-
waves[waveIndex].todo_ids.push(graph.todos[todoIndex]);
|
|
293
|
-
});
|
|
294
|
-
return {
|
|
295
|
-
outcome: 'compiled',
|
|
296
|
-
pairwise_verdicts: pairwiseVerdicts(graph),
|
|
297
|
-
plan: {
|
|
298
|
-
schema_version: PLAN_SCHEMA,
|
|
299
|
-
waves,
|
|
300
|
-
minimum_feasible_waves: searched.waveCount,
|
|
301
|
-
},
|
|
302
|
-
};
|
|
303
|
-
}
|
|
1
|
+
const GRAPH_SCHEMA = 'lattice.normalized_boundary_graph.v2';
|
|
2
|
+
const PLAN_SCHEMA = 'lattice.plan_graph.v2';
|
|
3
|
+
const MAX_TODOS = 8;
|
|
4
|
+
const MAX_INPUT_TODOS = 256;
|
|
5
|
+
const DEFAULT_MAX_SEARCH_STATES = 2_000_000;
|
|
6
|
+
const IDENTIFIER = /^[0-9A-Za-z](?:[0-9A-Za-z._-]{0,127})$/;
|
|
7
|
+
|
|
8
|
+
function fail(reason) {
|
|
9
|
+
throw new TypeError(`schedulability compiler v2契約違反: ${reason}`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function plainRecord(value) {
|
|
13
|
+
return value !== null
|
|
14
|
+
&& typeof value === 'object'
|
|
15
|
+
&& !Array.isArray(value)
|
|
16
|
+
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function exactRecord(value, keys) {
|
|
20
|
+
if (!plainRecord(value)) return false;
|
|
21
|
+
const actual = Object.keys(value).sort();
|
|
22
|
+
const expected = [...keys].sort();
|
|
23
|
+
return actual.length === expected.length
|
|
24
|
+
&& actual.every((key, index) => key === expected[index]);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function identifier(value) {
|
|
28
|
+
return typeof value === 'string' && IDENTIFIER.test(value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function boundedText(value) {
|
|
32
|
+
return typeof value === 'string'
|
|
33
|
+
&& value.length > 0
|
|
34
|
+
&& value === value.trim()
|
|
35
|
+
&& Buffer.byteLength(value, 'utf8') <= 4_096;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function compareText(left, right) {
|
|
39
|
+
if (left < right) return -1;
|
|
40
|
+
if (left > right) return 1;
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function assertArray(value, label) {
|
|
45
|
+
if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype) {
|
|
46
|
+
fail(`${label}がarrayではない`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeOptions(options) {
|
|
51
|
+
if (!exactRecord(options, []) && !exactRecord(options, ['maxSearchStates'])) {
|
|
52
|
+
fail('options shapeが不正');
|
|
53
|
+
}
|
|
54
|
+
const maxSearchStates = options.maxSearchStates ?? DEFAULT_MAX_SEARCH_STATES;
|
|
55
|
+
if (!Number.isSafeInteger(maxSearchStates) || maxSearchStates < 0) {
|
|
56
|
+
fail('maxSearchStatesがnon-negative safe integerではない');
|
|
57
|
+
}
|
|
58
|
+
return { maxSearchStates };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function assertAcyclic(todos, precedences) {
|
|
62
|
+
const indegree = new Map(todos.map((todoId) => [todoId, 0]));
|
|
63
|
+
const outgoing = new Map(todos.map((todoId) => [todoId, new Set()]));
|
|
64
|
+
for (const { from_todo_id: from, to_todo_id: to } of precedences) {
|
|
65
|
+
if (!outgoing.get(from).has(to)) {
|
|
66
|
+
outgoing.get(from).add(to);
|
|
67
|
+
indegree.set(to, indegree.get(to) + 1);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const ready = todos.filter((todoId) => indegree.get(todoId) === 0);
|
|
72
|
+
let visited = 0;
|
|
73
|
+
while (ready.length > 0) {
|
|
74
|
+
const todoId = ready.shift();
|
|
75
|
+
visited += 1;
|
|
76
|
+
for (const next of outgoing.get(todoId)) {
|
|
77
|
+
const remaining = indegree.get(next) - 1;
|
|
78
|
+
indegree.set(next, remaining);
|
|
79
|
+
if (remaining === 0) ready.push(next);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (visited !== todos.length) fail('precedencesにcycleがある');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function normalizeGraph(value) {
|
|
86
|
+
if (!exactRecord(value, [
|
|
87
|
+
'schema_version',
|
|
88
|
+
'todos',
|
|
89
|
+
'conflicts',
|
|
90
|
+
'precedences',
|
|
91
|
+
'unknowns',
|
|
92
|
+
'capacity',
|
|
93
|
+
]) || value.schema_version !== GRAPH_SCHEMA) {
|
|
94
|
+
fail('normalized graph shapeまたはschema_versionが不正');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
assertArray(value.todos, 'todos');
|
|
98
|
+
if (value.todos.length < 1 || value.todos.length > MAX_INPUT_TODOS
|
|
99
|
+
|| !value.todos.every(identifier)
|
|
100
|
+
|| new Set(value.todos).size !== value.todos.length) {
|
|
101
|
+
fail('todosがbounded unique identifier arrayではない');
|
|
102
|
+
}
|
|
103
|
+
const todos = [...value.todos].sort(compareText);
|
|
104
|
+
const todoSet = new Set(todos);
|
|
105
|
+
|
|
106
|
+
if (!Number.isSafeInteger(value.capacity)
|
|
107
|
+
|| value.capacity < 1
|
|
108
|
+
|| value.capacity > MAX_INPUT_TODOS) {
|
|
109
|
+
fail('capacityがbounded positive integerではない');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
assertArray(value.conflicts, 'conflicts');
|
|
113
|
+
const conflictKeys = new Set();
|
|
114
|
+
const conflicts = value.conflicts.map((entry) => {
|
|
115
|
+
if (!exactRecord(entry, ['todo_ids', 'resource_id'])) {
|
|
116
|
+
fail('conflict shapeが不正');
|
|
117
|
+
}
|
|
118
|
+
assertArray(entry.todo_ids, 'conflict.todo_ids');
|
|
119
|
+
if (entry.todo_ids.length !== 2
|
|
120
|
+
|| !entry.todo_ids.every((todoId) => todoSet.has(todoId))
|
|
121
|
+
|| entry.todo_ids[0] === entry.todo_ids[1]
|
|
122
|
+
|| !identifier(entry.resource_id)) {
|
|
123
|
+
fail('conflict relationが不正');
|
|
124
|
+
}
|
|
125
|
+
const todoIds = [...entry.todo_ids].sort(compareText);
|
|
126
|
+
const key = `${todoIds[0]}\u0000${todoIds[1]}\u0000${entry.resource_id}`;
|
|
127
|
+
if (conflictKeys.has(key)) fail('duplicate conflictがある');
|
|
128
|
+
conflictKeys.add(key);
|
|
129
|
+
return { todo_ids: todoIds, resource_id: entry.resource_id };
|
|
130
|
+
}).sort((left, right) => (
|
|
131
|
+
compareText(left.todo_ids[0], right.todo_ids[0])
|
|
132
|
+
|| compareText(left.todo_ids[1], right.todo_ids[1])
|
|
133
|
+
|| compareText(left.resource_id, right.resource_id)
|
|
134
|
+
));
|
|
135
|
+
|
|
136
|
+
assertArray(value.precedences, 'precedences');
|
|
137
|
+
const precedenceKeys = new Set();
|
|
138
|
+
const precedences = value.precedences.map((entry) => {
|
|
139
|
+
if (!exactRecord(entry, ['from_todo_id', 'to_todo_id', 'reason'])
|
|
140
|
+
|| !todoSet.has(entry.from_todo_id)
|
|
141
|
+
|| !todoSet.has(entry.to_todo_id)
|
|
142
|
+
|| entry.from_todo_id === entry.to_todo_id
|
|
143
|
+
|| !boundedText(entry.reason)) {
|
|
144
|
+
fail('precedence relationが不正');
|
|
145
|
+
}
|
|
146
|
+
const key = `${entry.from_todo_id}\u0000${entry.to_todo_id}\u0000${entry.reason}`;
|
|
147
|
+
if (precedenceKeys.has(key)) fail('duplicate precedenceがある');
|
|
148
|
+
precedenceKeys.add(key);
|
|
149
|
+
return {
|
|
150
|
+
from_todo_id: entry.from_todo_id,
|
|
151
|
+
to_todo_id: entry.to_todo_id,
|
|
152
|
+
reason: entry.reason,
|
|
153
|
+
};
|
|
154
|
+
}).sort((left, right) => (
|
|
155
|
+
compareText(left.from_todo_id, right.from_todo_id)
|
|
156
|
+
|| compareText(left.to_todo_id, right.to_todo_id)
|
|
157
|
+
|| compareText(left.reason, right.reason)
|
|
158
|
+
));
|
|
159
|
+
assertAcyclic(todos, precedences);
|
|
160
|
+
|
|
161
|
+
assertArray(value.unknowns, 'unknowns');
|
|
162
|
+
const unknownKeys = new Set();
|
|
163
|
+
const unknowns = value.unknowns.map((entry) => {
|
|
164
|
+
if (!exactRecord(entry, ['todo_id', 'kind', 'reason'])
|
|
165
|
+
|| !todoSet.has(entry.todo_id)
|
|
166
|
+
|| !identifier(entry.kind)
|
|
167
|
+
|| !boundedText(entry.reason)) {
|
|
168
|
+
fail('unknown relationが不正');
|
|
169
|
+
}
|
|
170
|
+
const key = `${entry.todo_id}\u0000${entry.kind}\u0000${entry.reason}`;
|
|
171
|
+
if (unknownKeys.has(key)) fail('duplicate unknownがある');
|
|
172
|
+
unknownKeys.add(key);
|
|
173
|
+
return { todo_id: entry.todo_id, kind: entry.kind, reason: entry.reason };
|
|
174
|
+
}).sort((left, right) => (
|
|
175
|
+
compareText(left.todo_id, right.todo_id)
|
|
176
|
+
|| compareText(left.kind, right.kind)
|
|
177
|
+
|| compareText(left.reason, right.reason)
|
|
178
|
+
));
|
|
179
|
+
|
|
180
|
+
return { todos, conflicts, precedences, unknowns, capacity: value.capacity };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function feasibleAssignment(graph, waveByIndex, indexByTodo) {
|
|
184
|
+
const counts = new Array(graph.todos.length).fill(0);
|
|
185
|
+
for (const waveIndex of waveByIndex) {
|
|
186
|
+
counts[waveIndex] += 1;
|
|
187
|
+
if (counts[waveIndex] > graph.capacity) return false;
|
|
188
|
+
}
|
|
189
|
+
for (const conflict of graph.conflicts) {
|
|
190
|
+
if (waveByIndex[indexByTodo.get(conflict.todo_ids[0])]
|
|
191
|
+
=== waveByIndex[indexByTodo.get(conflict.todo_ids[1])]) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
for (const precedence of graph.precedences) {
|
|
196
|
+
if (waveByIndex[indexByTodo.get(precedence.from_todo_id)]
|
|
197
|
+
>= waveByIndex[indexByTodo.get(precedence.to_todo_id)]) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function searchMinimum(graph, maxSearchStates) {
|
|
205
|
+
const assignment = new Array(graph.todos.length).fill(0);
|
|
206
|
+
const indexByTodo = new Map(graph.todos.map((todoId, index) => [todoId, index]));
|
|
207
|
+
let examined = 0;
|
|
208
|
+
|
|
209
|
+
for (let waveCount = 1; waveCount <= graph.todos.length; waveCount += 1) {
|
|
210
|
+
const used = new Array(waveCount).fill(0);
|
|
211
|
+
let solution = null;
|
|
212
|
+
let exhausted = false;
|
|
213
|
+
|
|
214
|
+
function visit(todoIndex, usedCount) {
|
|
215
|
+
if (solution !== null || exhausted) return;
|
|
216
|
+
if (todoIndex === graph.todos.length) {
|
|
217
|
+
if (usedCount !== waveCount) return;
|
|
218
|
+
if (examined >= maxSearchStates) {
|
|
219
|
+
exhausted = true;
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
examined += 1;
|
|
223
|
+
if (feasibleAssignment(graph, assignment, indexByTodo)) {
|
|
224
|
+
solution = [...assignment];
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const remainingAfter = graph.todos.length - todoIndex - 1;
|
|
230
|
+
for (let waveIndex = 0; waveIndex < waveCount; waveIndex += 1) {
|
|
231
|
+
const isNew = used[waveIndex] === 0;
|
|
232
|
+
const nextUsedCount = usedCount + (isNew ? 1 : 0);
|
|
233
|
+
if (waveCount - nextUsedCount > remainingAfter) continue;
|
|
234
|
+
assignment[todoIndex] = waveIndex;
|
|
235
|
+
used[waveIndex] += 1;
|
|
236
|
+
visit(todoIndex + 1, nextUsedCount);
|
|
237
|
+
used[waveIndex] -= 1;
|
|
238
|
+
if (solution !== null || exhausted) return;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
visit(0, 0);
|
|
243
|
+
if (exhausted) return { outcome: 'exhausted' };
|
|
244
|
+
if (solution !== null) return { outcome: 'found', assignment: solution, waveCount };
|
|
245
|
+
}
|
|
246
|
+
fail('acyclic bounded graphにfeasible scheduleがない');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function pairwiseVerdicts(graph) {
|
|
250
|
+
return [
|
|
251
|
+
...graph.conflicts.map((conflict) => ({
|
|
252
|
+
type: 'conflict',
|
|
253
|
+
todo_ids: [...conflict.todo_ids],
|
|
254
|
+
resource_id: conflict.resource_id,
|
|
255
|
+
})),
|
|
256
|
+
...graph.precedences.map((precedence) => ({
|
|
257
|
+
type: 'precedence',
|
|
258
|
+
from_todo_id: precedence.from_todo_id,
|
|
259
|
+
to_todo_id: precedence.to_todo_id,
|
|
260
|
+
reason: precedence.reason,
|
|
261
|
+
})),
|
|
262
|
+
];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Candidate非依存のbounded graphをcanonical minimum-wave planへcompileする。
|
|
267
|
+
* @param {unknown} normalizedGraph
|
|
268
|
+
* @param {unknown} options
|
|
269
|
+
* @returns {object}
|
|
270
|
+
*/
|
|
271
|
+
export function compileSchedulabilityGraphV2(normalizedGraph, options = {}) {
|
|
272
|
+
const graph = normalizeGraph(normalizedGraph);
|
|
273
|
+
const { maxSearchStates } = normalizeOptions(options);
|
|
274
|
+
|
|
275
|
+
if (graph.todos.length > MAX_TODOS) {
|
|
276
|
+
return { outcome: 'unsupported', code: 'NODE_LIMIT_EXCEEDED' };
|
|
277
|
+
}
|
|
278
|
+
if (graph.unknowns.length > 0) {
|
|
279
|
+
return { outcome: 'unknown', code: 'BOUNDARY_UNKNOWN', unknowns: graph.unknowns };
|
|
280
|
+
}
|
|
281
|
+
if (maxSearchStates === 0) {
|
|
282
|
+
return { outcome: 'unsupported', code: 'SEARCH_BUDGET_EXHAUSTED' };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const searched = searchMinimum(graph, maxSearchStates);
|
|
286
|
+
if (searched.outcome === 'exhausted') {
|
|
287
|
+
return { outcome: 'unsupported', code: 'SEARCH_BUDGET_EXHAUSTED' };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const waves = Array.from({ length: searched.waveCount }, () => ({ todo_ids: [] }));
|
|
291
|
+
searched.assignment.forEach((waveIndex, todoIndex) => {
|
|
292
|
+
waves[waveIndex].todo_ids.push(graph.todos[todoIndex]);
|
|
293
|
+
});
|
|
294
|
+
return {
|
|
295
|
+
outcome: 'compiled',
|
|
296
|
+
pairwise_verdicts: pairwiseVerdicts(graph),
|
|
297
|
+
plan: {
|
|
298
|
+
schema_version: PLAN_SCHEMA,
|
|
299
|
+
waves,
|
|
300
|
+
minimum_feasible_waves: searched.waveCount,
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
}
|