@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,317 +1,317 @@
|
|
|
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 contractError(reason) {
|
|
9
|
-
throw new TypeError(`schedulability verifier v2契約違反: ${reason}`);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function isPlainObject(value) {
|
|
13
|
-
return value !== null
|
|
14
|
-
&& typeof value === 'object'
|
|
15
|
-
&& !Array.isArray(value)
|
|
16
|
-
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function hasOnlyKeys(value, keys) {
|
|
20
|
-
if (!isPlainObject(value)) return false;
|
|
21
|
-
const found = Object.keys(value).sort();
|
|
22
|
-
const wanted = [...keys].sort();
|
|
23
|
-
return found.length === wanted.length
|
|
24
|
-
&& found.every((key, index) => key === wanted[index]);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function validIdentifier(value) {
|
|
28
|
-
return typeof value === 'string' && IDENTIFIER.test(value);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function validReason(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 textOrder(left, right) {
|
|
39
|
-
return left < right ? -1 : left > right ? 1 : 0;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function requireArray(value, label) {
|
|
43
|
-
if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype) {
|
|
44
|
-
contractError(`${label}がarrayではない`);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function verifierOptions(options) {
|
|
49
|
-
if (!hasOnlyKeys(options, []) && !hasOnlyKeys(options, ['maxSearchStates'])) {
|
|
50
|
-
contractError('options shapeが不正');
|
|
51
|
-
}
|
|
52
|
-
const limit = options.maxSearchStates ?? DEFAULT_MAX_SEARCH_STATES;
|
|
53
|
-
if (!Number.isSafeInteger(limit) || limit < 0) {
|
|
54
|
-
contractError('maxSearchStatesがnon-negative safe integerではない');
|
|
55
|
-
}
|
|
56
|
-
return { limit };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function requireDag(todoIds, precedences) {
|
|
60
|
-
const incoming = new Map(todoIds.map((todoId) => [todoId, 0]));
|
|
61
|
-
const nextByTodo = new Map(todoIds.map((todoId) => [todoId, new Set()]));
|
|
62
|
-
for (const edge of precedences) {
|
|
63
|
-
const next = nextByTodo.get(edge.from);
|
|
64
|
-
if (!next.has(edge.to)) {
|
|
65
|
-
next.add(edge.to);
|
|
66
|
-
incoming.set(edge.to, incoming.get(edge.to) + 1);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
const queue = todoIds.filter((todoId) => incoming.get(todoId) === 0);
|
|
70
|
-
let removed = 0;
|
|
71
|
-
for (let cursor = 0; cursor < queue.length; cursor += 1) {
|
|
72
|
-
const todoId = queue[cursor];
|
|
73
|
-
removed += 1;
|
|
74
|
-
for (const next of nextByTodo.get(todoId)) {
|
|
75
|
-
incoming.set(next, incoming.get(next) - 1);
|
|
76
|
-
if (incoming.get(next) === 0) queue.push(next);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
if (removed !== todoIds.length) contractError('precedencesにcycleがある');
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function verifierGraph(value) {
|
|
83
|
-
if (!hasOnlyKeys(value, [
|
|
84
|
-
'schema_version',
|
|
85
|
-
'todos',
|
|
86
|
-
'conflicts',
|
|
87
|
-
'precedences',
|
|
88
|
-
'unknowns',
|
|
89
|
-
'capacity',
|
|
90
|
-
]) || value.schema_version !== GRAPH_SCHEMA) {
|
|
91
|
-
contractError('normalized graph shapeまたはschema_versionが不正');
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
requireArray(value.todos, 'todos');
|
|
95
|
-
if (value.todos.length < 1 || value.todos.length > MAX_INPUT_TODOS
|
|
96
|
-
|| !value.todos.every(validIdentifier)
|
|
97
|
-
|| new Set(value.todos).size !== value.todos.length) {
|
|
98
|
-
contractError('todosがbounded unique identifier arrayではない');
|
|
99
|
-
}
|
|
100
|
-
const todoIds = [...value.todos].sort(textOrder);
|
|
101
|
-
const knownTodos = new Set(todoIds);
|
|
102
|
-
if (!Number.isSafeInteger(value.capacity)
|
|
103
|
-
|| value.capacity < 1
|
|
104
|
-
|| value.capacity > MAX_INPUT_TODOS) {
|
|
105
|
-
contractError('capacityがbounded positive integerではない');
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
requireArray(value.conflicts, 'conflicts');
|
|
109
|
-
const seenConflicts = new Set();
|
|
110
|
-
const conflicts = value.conflicts.map((item) => {
|
|
111
|
-
if (!hasOnlyKeys(item, ['todo_ids', 'resource_id'])) {
|
|
112
|
-
contractError('conflict shapeが不正');
|
|
113
|
-
}
|
|
114
|
-
requireArray(item.todo_ids, 'conflict.todo_ids');
|
|
115
|
-
if (item.todo_ids.length !== 2
|
|
116
|
-
|| !item.todo_ids.every((todoId) => knownTodos.has(todoId))
|
|
117
|
-
|| item.todo_ids[0] === item.todo_ids[1]
|
|
118
|
-
|| !validIdentifier(item.resource_id)) {
|
|
119
|
-
contractError('conflict relationが不正');
|
|
120
|
-
}
|
|
121
|
-
const pair = [...item.todo_ids].sort(textOrder);
|
|
122
|
-
const key = `${pair[0]}\u0000${pair[1]}\u0000${item.resource_id}`;
|
|
123
|
-
if (seenConflicts.has(key)) contractError('duplicate conflictがある');
|
|
124
|
-
seenConflicts.add(key);
|
|
125
|
-
return { left: pair[0], right: pair[1] };
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
requireArray(value.precedences, 'precedences');
|
|
129
|
-
const seenPrecedences = new Set();
|
|
130
|
-
const precedences = value.precedences.map((item) => {
|
|
131
|
-
if (!hasOnlyKeys(item, ['from_todo_id', 'to_todo_id', 'reason'])
|
|
132
|
-
|| !knownTodos.has(item.from_todo_id)
|
|
133
|
-
|| !knownTodos.has(item.to_todo_id)
|
|
134
|
-
|| item.from_todo_id === item.to_todo_id
|
|
135
|
-
|| !validReason(item.reason)) {
|
|
136
|
-
contractError('precedence relationが不正');
|
|
137
|
-
}
|
|
138
|
-
const key = `${item.from_todo_id}\u0000${item.to_todo_id}\u0000${item.reason}`;
|
|
139
|
-
if (seenPrecedences.has(key)) contractError('duplicate precedenceがある');
|
|
140
|
-
seenPrecedences.add(key);
|
|
141
|
-
return { from: item.from_todo_id, to: item.to_todo_id };
|
|
142
|
-
});
|
|
143
|
-
requireDag(todoIds, precedences);
|
|
144
|
-
|
|
145
|
-
requireArray(value.unknowns, 'unknowns');
|
|
146
|
-
const seenUnknowns = new Set();
|
|
147
|
-
const unknowns = value.unknowns.map((item) => {
|
|
148
|
-
if (!hasOnlyKeys(item, ['todo_id', 'kind', 'reason'])
|
|
149
|
-
|| !knownTodos.has(item.todo_id)
|
|
150
|
-
|| !validIdentifier(item.kind)
|
|
151
|
-
|| !validReason(item.reason)) {
|
|
152
|
-
contractError('unknown relationが不正');
|
|
153
|
-
}
|
|
154
|
-
const key = `${item.todo_id}\u0000${item.kind}\u0000${item.reason}`;
|
|
155
|
-
if (seenUnknowns.has(key)) contractError('duplicate unknownがある');
|
|
156
|
-
seenUnknowns.add(key);
|
|
157
|
-
return { todoId: item.todo_id };
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
return { todoIds, knownTodos, conflicts, precedences, unknowns, capacity: value.capacity };
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function rejected(code, extra = {}) {
|
|
164
|
-
return { outcome: 'rejected', code, ...extra };
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function inspectPlan(value, graph) {
|
|
168
|
-
if (!hasOnlyKeys(value, ['schema_version', 'waves', 'minimum_feasible_waves'])
|
|
169
|
-
|| value.schema_version !== PLAN_SCHEMA
|
|
170
|
-
|| !Number.isSafeInteger(value.minimum_feasible_waves)
|
|
171
|
-
|| value.minimum_feasible_waves < 1) {
|
|
172
|
-
return { result: rejected('PLAN_SCHEMA_INVALID') };
|
|
173
|
-
}
|
|
174
|
-
if (!Array.isArray(value.waves)
|
|
175
|
-
|| Object.getPrototypeOf(value.waves) !== Array.prototype
|
|
176
|
-
|| value.waves.length < 1
|
|
177
|
-
|| value.waves.length > graph.todoIds.length) {
|
|
178
|
-
return { result: rejected('PLAN_SCHEMA_INVALID') };
|
|
179
|
-
}
|
|
180
|
-
if (value.minimum_feasible_waves !== value.waves.length) {
|
|
181
|
-
return { result: rejected('MINIMUM_CLAIM_MISMATCH') };
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const waveByTodo = new Map();
|
|
185
|
-
for (let waveIndex = 0; waveIndex < value.waves.length; waveIndex += 1) {
|
|
186
|
-
const wave = value.waves[waveIndex];
|
|
187
|
-
if (!hasOnlyKeys(wave, ['todo_ids'])
|
|
188
|
-
|| !Array.isArray(wave.todo_ids)
|
|
189
|
-
|| Object.getPrototypeOf(wave.todo_ids) !== Array.prototype
|
|
190
|
-
|| wave.todo_ids.length < 1
|
|
191
|
-
|| !wave.todo_ids.every(validIdentifier)) {
|
|
192
|
-
return { result: rejected('PLAN_SCHEMA_INVALID') };
|
|
193
|
-
}
|
|
194
|
-
if (wave.todo_ids.length > graph.capacity) {
|
|
195
|
-
return { result: rejected('CAPACITY_EXCEEDED') };
|
|
196
|
-
}
|
|
197
|
-
for (const todoId of wave.todo_ids) {
|
|
198
|
-
if (!graph.knownTodos.has(todoId)) return { result: rejected('TODO_UNKNOWN') };
|
|
199
|
-
if (waveByTodo.has(todoId)) return { result: rejected('TODO_DUPLICATED') };
|
|
200
|
-
waveByTodo.set(todoId, waveIndex);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
if (waveByTodo.size !== graph.todoIds.length) {
|
|
204
|
-
return { result: rejected('TODO_MISSING') };
|
|
205
|
-
}
|
|
206
|
-
for (const conflict of graph.conflicts) {
|
|
207
|
-
if (waveByTodo.get(conflict.left) === waveByTodo.get(conflict.right)) {
|
|
208
|
-
return { result: rejected('CONFLICT_COLOCATED') };
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
for (const precedence of graph.precedences) {
|
|
212
|
-
if (waveByTodo.get(precedence.from) >= waveByTodo.get(precedence.to)) {
|
|
213
|
-
return { result: rejected('PRECEDENCE_VIOLATION') };
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
return { waveCount: value.waves.length };
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function assignmentSatisfies(graph, assignment, positionByTodo) {
|
|
220
|
-
const occupancy = new Map();
|
|
221
|
-
for (const waveIndex of assignment) {
|
|
222
|
-
const count = (occupancy.get(waveIndex) ?? 0) + 1;
|
|
223
|
-
if (count > graph.capacity) return false;
|
|
224
|
-
occupancy.set(waveIndex, count);
|
|
225
|
-
}
|
|
226
|
-
for (const edge of graph.conflicts) {
|
|
227
|
-
if (assignment[positionByTodo.get(edge.left)]
|
|
228
|
-
=== assignment[positionByTodo.get(edge.right)]) {
|
|
229
|
-
return false;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
for (const edge of graph.precedences) {
|
|
233
|
-
if (assignment[positionByTodo.get(edge.from)]
|
|
234
|
-
>= assignment[positionByTodo.get(edge.to)]) {
|
|
235
|
-
return false;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
return true;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function shorterSchedule(graph, upperExclusive, budget) {
|
|
242
|
-
const assignment = new Array(graph.todoIds.length).fill(0);
|
|
243
|
-
const positionByTodo = new Map(graph.todoIds.map((todoId, index) => [todoId, index]));
|
|
244
|
-
|
|
245
|
-
for (let waveCount = 1; waveCount < upperExclusive; waveCount += 1) {
|
|
246
|
-
const occupancy = new Array(waveCount).fill(0);
|
|
247
|
-
let answer = false;
|
|
248
|
-
let ranOut = false;
|
|
249
|
-
|
|
250
|
-
const enumerate = (todoIndex, occupiedWaves) => {
|
|
251
|
-
if (answer || ranOut) return;
|
|
252
|
-
if (todoIndex === graph.todoIds.length) {
|
|
253
|
-
if (occupiedWaves !== waveCount) return;
|
|
254
|
-
if (budget.examined >= budget.limit) {
|
|
255
|
-
ranOut = true;
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
budget.examined += 1;
|
|
259
|
-
answer = assignmentSatisfies(graph, assignment, positionByTodo);
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const unassignedAfter = graph.todoIds.length - todoIndex - 1;
|
|
264
|
-
for (let candidateWave = 0; candidateWave < waveCount; candidateWave += 1) {
|
|
265
|
-
const opensWave = occupancy[candidateWave] === 0;
|
|
266
|
-
const nextOccupied = occupiedWaves + (opensWave ? 1 : 0);
|
|
267
|
-
if (waveCount - nextOccupied > unassignedAfter) continue;
|
|
268
|
-
assignment[todoIndex] = candidateWave;
|
|
269
|
-
occupancy[candidateWave] += 1;
|
|
270
|
-
enumerate(todoIndex + 1, nextOccupied);
|
|
271
|
-
occupancy[candidateWave] -= 1;
|
|
272
|
-
if (answer || ranOut) return;
|
|
273
|
-
}
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
enumerate(0, 0);
|
|
277
|
-
if (ranOut) return { state: 'exhausted' };
|
|
278
|
-
if (answer) return { state: 'found', waveCount };
|
|
279
|
-
}
|
|
280
|
-
return { state: 'none' };
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Producerを呼ばず、保存graphからplanのfeasibilityとminimumを再計算する。
|
|
285
|
-
* @param {unknown} normalizedGraph
|
|
286
|
-
* @param {unknown} plan
|
|
287
|
-
* @param {unknown} options
|
|
288
|
-
* @returns {object}
|
|
289
|
-
*/
|
|
290
|
-
export function verifySchedulabilityPlanV2(normalizedGraph, plan, options = {}) {
|
|
291
|
-
const graph = verifierGraph(normalizedGraph);
|
|
292
|
-
const { limit } = verifierOptions(options);
|
|
293
|
-
|
|
294
|
-
if (graph.todoIds.length > MAX_TODOS) {
|
|
295
|
-
return { outcome: 'unsupported', code: 'NODE_LIMIT_EXCEEDED' };
|
|
296
|
-
}
|
|
297
|
-
if (graph.unknowns.length > 0) {
|
|
298
|
-
return { outcome: 'unknown', code: 'BOUNDARY_UNKNOWN' };
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const inspected = inspectPlan(plan, graph);
|
|
302
|
-
if (inspected.result !== undefined) return inspected.result;
|
|
303
|
-
|
|
304
|
-
const shorter = shorterSchedule(graph, inspected.waveCount, { limit, examined: 0 });
|
|
305
|
-
if (shorter.state === 'exhausted') {
|
|
306
|
-
return { outcome: 'unsupported', code: 'SEARCH_BUDGET_EXHAUSTED' };
|
|
307
|
-
}
|
|
308
|
-
if (shorter.state === 'found') {
|
|
309
|
-
return rejected('NON_MINIMUM_SCHEDULE', {
|
|
310
|
-
minimum_feasible_waves: shorter.waveCount,
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
return {
|
|
314
|
-
outcome: 'verified',
|
|
315
|
-
minimum_feasible_waves: inspected.waveCount,
|
|
316
|
-
};
|
|
317
|
-
}
|
|
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 contractError(reason) {
|
|
9
|
+
throw new TypeError(`schedulability verifier v2契約違反: ${reason}`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isPlainObject(value) {
|
|
13
|
+
return value !== null
|
|
14
|
+
&& typeof value === 'object'
|
|
15
|
+
&& !Array.isArray(value)
|
|
16
|
+
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function hasOnlyKeys(value, keys) {
|
|
20
|
+
if (!isPlainObject(value)) return false;
|
|
21
|
+
const found = Object.keys(value).sort();
|
|
22
|
+
const wanted = [...keys].sort();
|
|
23
|
+
return found.length === wanted.length
|
|
24
|
+
&& found.every((key, index) => key === wanted[index]);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function validIdentifier(value) {
|
|
28
|
+
return typeof value === 'string' && IDENTIFIER.test(value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function validReason(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 textOrder(left, right) {
|
|
39
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function requireArray(value, label) {
|
|
43
|
+
if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype) {
|
|
44
|
+
contractError(`${label}がarrayではない`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function verifierOptions(options) {
|
|
49
|
+
if (!hasOnlyKeys(options, []) && !hasOnlyKeys(options, ['maxSearchStates'])) {
|
|
50
|
+
contractError('options shapeが不正');
|
|
51
|
+
}
|
|
52
|
+
const limit = options.maxSearchStates ?? DEFAULT_MAX_SEARCH_STATES;
|
|
53
|
+
if (!Number.isSafeInteger(limit) || limit < 0) {
|
|
54
|
+
contractError('maxSearchStatesがnon-negative safe integerではない');
|
|
55
|
+
}
|
|
56
|
+
return { limit };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function requireDag(todoIds, precedences) {
|
|
60
|
+
const incoming = new Map(todoIds.map((todoId) => [todoId, 0]));
|
|
61
|
+
const nextByTodo = new Map(todoIds.map((todoId) => [todoId, new Set()]));
|
|
62
|
+
for (const edge of precedences) {
|
|
63
|
+
const next = nextByTodo.get(edge.from);
|
|
64
|
+
if (!next.has(edge.to)) {
|
|
65
|
+
next.add(edge.to);
|
|
66
|
+
incoming.set(edge.to, incoming.get(edge.to) + 1);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const queue = todoIds.filter((todoId) => incoming.get(todoId) === 0);
|
|
70
|
+
let removed = 0;
|
|
71
|
+
for (let cursor = 0; cursor < queue.length; cursor += 1) {
|
|
72
|
+
const todoId = queue[cursor];
|
|
73
|
+
removed += 1;
|
|
74
|
+
for (const next of nextByTodo.get(todoId)) {
|
|
75
|
+
incoming.set(next, incoming.get(next) - 1);
|
|
76
|
+
if (incoming.get(next) === 0) queue.push(next);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (removed !== todoIds.length) contractError('precedencesにcycleがある');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function verifierGraph(value) {
|
|
83
|
+
if (!hasOnlyKeys(value, [
|
|
84
|
+
'schema_version',
|
|
85
|
+
'todos',
|
|
86
|
+
'conflicts',
|
|
87
|
+
'precedences',
|
|
88
|
+
'unknowns',
|
|
89
|
+
'capacity',
|
|
90
|
+
]) || value.schema_version !== GRAPH_SCHEMA) {
|
|
91
|
+
contractError('normalized graph shapeまたはschema_versionが不正');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
requireArray(value.todos, 'todos');
|
|
95
|
+
if (value.todos.length < 1 || value.todos.length > MAX_INPUT_TODOS
|
|
96
|
+
|| !value.todos.every(validIdentifier)
|
|
97
|
+
|| new Set(value.todos).size !== value.todos.length) {
|
|
98
|
+
contractError('todosがbounded unique identifier arrayではない');
|
|
99
|
+
}
|
|
100
|
+
const todoIds = [...value.todos].sort(textOrder);
|
|
101
|
+
const knownTodos = new Set(todoIds);
|
|
102
|
+
if (!Number.isSafeInteger(value.capacity)
|
|
103
|
+
|| value.capacity < 1
|
|
104
|
+
|| value.capacity > MAX_INPUT_TODOS) {
|
|
105
|
+
contractError('capacityがbounded positive integerではない');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
requireArray(value.conflicts, 'conflicts');
|
|
109
|
+
const seenConflicts = new Set();
|
|
110
|
+
const conflicts = value.conflicts.map((item) => {
|
|
111
|
+
if (!hasOnlyKeys(item, ['todo_ids', 'resource_id'])) {
|
|
112
|
+
contractError('conflict shapeが不正');
|
|
113
|
+
}
|
|
114
|
+
requireArray(item.todo_ids, 'conflict.todo_ids');
|
|
115
|
+
if (item.todo_ids.length !== 2
|
|
116
|
+
|| !item.todo_ids.every((todoId) => knownTodos.has(todoId))
|
|
117
|
+
|| item.todo_ids[0] === item.todo_ids[1]
|
|
118
|
+
|| !validIdentifier(item.resource_id)) {
|
|
119
|
+
contractError('conflict relationが不正');
|
|
120
|
+
}
|
|
121
|
+
const pair = [...item.todo_ids].sort(textOrder);
|
|
122
|
+
const key = `${pair[0]}\u0000${pair[1]}\u0000${item.resource_id}`;
|
|
123
|
+
if (seenConflicts.has(key)) contractError('duplicate conflictがある');
|
|
124
|
+
seenConflicts.add(key);
|
|
125
|
+
return { left: pair[0], right: pair[1] };
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
requireArray(value.precedences, 'precedences');
|
|
129
|
+
const seenPrecedences = new Set();
|
|
130
|
+
const precedences = value.precedences.map((item) => {
|
|
131
|
+
if (!hasOnlyKeys(item, ['from_todo_id', 'to_todo_id', 'reason'])
|
|
132
|
+
|| !knownTodos.has(item.from_todo_id)
|
|
133
|
+
|| !knownTodos.has(item.to_todo_id)
|
|
134
|
+
|| item.from_todo_id === item.to_todo_id
|
|
135
|
+
|| !validReason(item.reason)) {
|
|
136
|
+
contractError('precedence relationが不正');
|
|
137
|
+
}
|
|
138
|
+
const key = `${item.from_todo_id}\u0000${item.to_todo_id}\u0000${item.reason}`;
|
|
139
|
+
if (seenPrecedences.has(key)) contractError('duplicate precedenceがある');
|
|
140
|
+
seenPrecedences.add(key);
|
|
141
|
+
return { from: item.from_todo_id, to: item.to_todo_id };
|
|
142
|
+
});
|
|
143
|
+
requireDag(todoIds, precedences);
|
|
144
|
+
|
|
145
|
+
requireArray(value.unknowns, 'unknowns');
|
|
146
|
+
const seenUnknowns = new Set();
|
|
147
|
+
const unknowns = value.unknowns.map((item) => {
|
|
148
|
+
if (!hasOnlyKeys(item, ['todo_id', 'kind', 'reason'])
|
|
149
|
+
|| !knownTodos.has(item.todo_id)
|
|
150
|
+
|| !validIdentifier(item.kind)
|
|
151
|
+
|| !validReason(item.reason)) {
|
|
152
|
+
contractError('unknown relationが不正');
|
|
153
|
+
}
|
|
154
|
+
const key = `${item.todo_id}\u0000${item.kind}\u0000${item.reason}`;
|
|
155
|
+
if (seenUnknowns.has(key)) contractError('duplicate unknownがある');
|
|
156
|
+
seenUnknowns.add(key);
|
|
157
|
+
return { todoId: item.todo_id };
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return { todoIds, knownTodos, conflicts, precedences, unknowns, capacity: value.capacity };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function rejected(code, extra = {}) {
|
|
164
|
+
return { outcome: 'rejected', code, ...extra };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function inspectPlan(value, graph) {
|
|
168
|
+
if (!hasOnlyKeys(value, ['schema_version', 'waves', 'minimum_feasible_waves'])
|
|
169
|
+
|| value.schema_version !== PLAN_SCHEMA
|
|
170
|
+
|| !Number.isSafeInteger(value.minimum_feasible_waves)
|
|
171
|
+
|| value.minimum_feasible_waves < 1) {
|
|
172
|
+
return { result: rejected('PLAN_SCHEMA_INVALID') };
|
|
173
|
+
}
|
|
174
|
+
if (!Array.isArray(value.waves)
|
|
175
|
+
|| Object.getPrototypeOf(value.waves) !== Array.prototype
|
|
176
|
+
|| value.waves.length < 1
|
|
177
|
+
|| value.waves.length > graph.todoIds.length) {
|
|
178
|
+
return { result: rejected('PLAN_SCHEMA_INVALID') };
|
|
179
|
+
}
|
|
180
|
+
if (value.minimum_feasible_waves !== value.waves.length) {
|
|
181
|
+
return { result: rejected('MINIMUM_CLAIM_MISMATCH') };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const waveByTodo = new Map();
|
|
185
|
+
for (let waveIndex = 0; waveIndex < value.waves.length; waveIndex += 1) {
|
|
186
|
+
const wave = value.waves[waveIndex];
|
|
187
|
+
if (!hasOnlyKeys(wave, ['todo_ids'])
|
|
188
|
+
|| !Array.isArray(wave.todo_ids)
|
|
189
|
+
|| Object.getPrototypeOf(wave.todo_ids) !== Array.prototype
|
|
190
|
+
|| wave.todo_ids.length < 1
|
|
191
|
+
|| !wave.todo_ids.every(validIdentifier)) {
|
|
192
|
+
return { result: rejected('PLAN_SCHEMA_INVALID') };
|
|
193
|
+
}
|
|
194
|
+
if (wave.todo_ids.length > graph.capacity) {
|
|
195
|
+
return { result: rejected('CAPACITY_EXCEEDED') };
|
|
196
|
+
}
|
|
197
|
+
for (const todoId of wave.todo_ids) {
|
|
198
|
+
if (!graph.knownTodos.has(todoId)) return { result: rejected('TODO_UNKNOWN') };
|
|
199
|
+
if (waveByTodo.has(todoId)) return { result: rejected('TODO_DUPLICATED') };
|
|
200
|
+
waveByTodo.set(todoId, waveIndex);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (waveByTodo.size !== graph.todoIds.length) {
|
|
204
|
+
return { result: rejected('TODO_MISSING') };
|
|
205
|
+
}
|
|
206
|
+
for (const conflict of graph.conflicts) {
|
|
207
|
+
if (waveByTodo.get(conflict.left) === waveByTodo.get(conflict.right)) {
|
|
208
|
+
return { result: rejected('CONFLICT_COLOCATED') };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
for (const precedence of graph.precedences) {
|
|
212
|
+
if (waveByTodo.get(precedence.from) >= waveByTodo.get(precedence.to)) {
|
|
213
|
+
return { result: rejected('PRECEDENCE_VIOLATION') };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return { waveCount: value.waves.length };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function assignmentSatisfies(graph, assignment, positionByTodo) {
|
|
220
|
+
const occupancy = new Map();
|
|
221
|
+
for (const waveIndex of assignment) {
|
|
222
|
+
const count = (occupancy.get(waveIndex) ?? 0) + 1;
|
|
223
|
+
if (count > graph.capacity) return false;
|
|
224
|
+
occupancy.set(waveIndex, count);
|
|
225
|
+
}
|
|
226
|
+
for (const edge of graph.conflicts) {
|
|
227
|
+
if (assignment[positionByTodo.get(edge.left)]
|
|
228
|
+
=== assignment[positionByTodo.get(edge.right)]) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
for (const edge of graph.precedences) {
|
|
233
|
+
if (assignment[positionByTodo.get(edge.from)]
|
|
234
|
+
>= assignment[positionByTodo.get(edge.to)]) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function shorterSchedule(graph, upperExclusive, budget) {
|
|
242
|
+
const assignment = new Array(graph.todoIds.length).fill(0);
|
|
243
|
+
const positionByTodo = new Map(graph.todoIds.map((todoId, index) => [todoId, index]));
|
|
244
|
+
|
|
245
|
+
for (let waveCount = 1; waveCount < upperExclusive; waveCount += 1) {
|
|
246
|
+
const occupancy = new Array(waveCount).fill(0);
|
|
247
|
+
let answer = false;
|
|
248
|
+
let ranOut = false;
|
|
249
|
+
|
|
250
|
+
const enumerate = (todoIndex, occupiedWaves) => {
|
|
251
|
+
if (answer || ranOut) return;
|
|
252
|
+
if (todoIndex === graph.todoIds.length) {
|
|
253
|
+
if (occupiedWaves !== waveCount) return;
|
|
254
|
+
if (budget.examined >= budget.limit) {
|
|
255
|
+
ranOut = true;
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
budget.examined += 1;
|
|
259
|
+
answer = assignmentSatisfies(graph, assignment, positionByTodo);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const unassignedAfter = graph.todoIds.length - todoIndex - 1;
|
|
264
|
+
for (let candidateWave = 0; candidateWave < waveCount; candidateWave += 1) {
|
|
265
|
+
const opensWave = occupancy[candidateWave] === 0;
|
|
266
|
+
const nextOccupied = occupiedWaves + (opensWave ? 1 : 0);
|
|
267
|
+
if (waveCount - nextOccupied > unassignedAfter) continue;
|
|
268
|
+
assignment[todoIndex] = candidateWave;
|
|
269
|
+
occupancy[candidateWave] += 1;
|
|
270
|
+
enumerate(todoIndex + 1, nextOccupied);
|
|
271
|
+
occupancy[candidateWave] -= 1;
|
|
272
|
+
if (answer || ranOut) return;
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
enumerate(0, 0);
|
|
277
|
+
if (ranOut) return { state: 'exhausted' };
|
|
278
|
+
if (answer) return { state: 'found', waveCount };
|
|
279
|
+
}
|
|
280
|
+
return { state: 'none' };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Producerを呼ばず、保存graphからplanのfeasibilityとminimumを再計算する。
|
|
285
|
+
* @param {unknown} normalizedGraph
|
|
286
|
+
* @param {unknown} plan
|
|
287
|
+
* @param {unknown} options
|
|
288
|
+
* @returns {object}
|
|
289
|
+
*/
|
|
290
|
+
export function verifySchedulabilityPlanV2(normalizedGraph, plan, options = {}) {
|
|
291
|
+
const graph = verifierGraph(normalizedGraph);
|
|
292
|
+
const { limit } = verifierOptions(options);
|
|
293
|
+
|
|
294
|
+
if (graph.todoIds.length > MAX_TODOS) {
|
|
295
|
+
return { outcome: 'unsupported', code: 'NODE_LIMIT_EXCEEDED' };
|
|
296
|
+
}
|
|
297
|
+
if (graph.unknowns.length > 0) {
|
|
298
|
+
return { outcome: 'unknown', code: 'BOUNDARY_UNKNOWN' };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const inspected = inspectPlan(plan, graph);
|
|
302
|
+
if (inspected.result !== undefined) return inspected.result;
|
|
303
|
+
|
|
304
|
+
const shorter = shorterSchedule(graph, inspected.waveCount, { limit, examined: 0 });
|
|
305
|
+
if (shorter.state === 'exhausted') {
|
|
306
|
+
return { outcome: 'unsupported', code: 'SEARCH_BUDGET_EXHAUSTED' };
|
|
307
|
+
}
|
|
308
|
+
if (shorter.state === 'found') {
|
|
309
|
+
return rejected('NON_MINIMUM_SCHEDULE', {
|
|
310
|
+
minimum_feasible_waves: shorter.waveCount,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
outcome: 'verified',
|
|
315
|
+
minimum_feasible_waves: inspected.waveCount,
|
|
316
|
+
};
|
|
317
|
+
}
|