@quolu/lattice 0.52.3 → 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 +3 -3
- 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 +8 -8
- 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 +42 -21
- package/src/treatment-compiler.mjs +728 -728
- package/src/treatment-runner.mjs +656 -656
- package/src/witness-scaffold.mjs +180 -180
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quolu/lattice",
|
|
3
|
-
"version": "0.52.
|
|
3
|
+
"version": "0.52.4",
|
|
4
4
|
"description": "Schedulability compiler for multi-agent development: observe real code boundaries, refactor the conflicting seam, recompile the plan for parallel execution",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Quo / クオ at kitepon.dev",
|
|
@@ -1,325 +1,325 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { isDeepStrictEqual } from 'node:util';
|
|
3
|
-
|
|
4
|
-
import { compileBoundaryObservationV2 } from './boundary-observation-compiler-v2.mjs';
|
|
5
|
-
import { verifySchedulabilityPlanV2 } from './schedulability-verifier-v2.mjs';
|
|
6
|
-
|
|
7
|
-
const OBSERVATION_SCHEMA = 'lattice.boundary_observation_set.v2';
|
|
8
|
-
const BUNDLE_SCHEMA = 'lattice.normalized_boundary_bundle.v2';
|
|
9
|
-
const GRAPH_SCHEMA = 'lattice.normalized_boundary_graph.v2';
|
|
10
|
-
const VERDICT_SCHEMA = 'lattice.boundary_verdict.v2';
|
|
11
|
-
const MAX_TODOS = 256;
|
|
12
|
-
const IDENTIFIER = /^[0-9A-Za-z](?:[0-9A-Za-z._-]{0,127})$/;
|
|
13
|
-
const DIGEST = /^[0-9a-f]{64}$/;
|
|
14
|
-
|
|
15
|
-
function contractError(reason) {
|
|
16
|
-
throw new TypeError(`artifact contracts v2契約違反: ${reason}`);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function plainRecord(value) {
|
|
20
|
-
return value !== null
|
|
21
|
-
&& typeof value === 'object'
|
|
22
|
-
&& !Array.isArray(value)
|
|
23
|
-
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function exactRecord(value, keys) {
|
|
27
|
-
if (!plainRecord(value)) return false;
|
|
28
|
-
const actual = Object.keys(value).sort();
|
|
29
|
-
const expected = [...keys].sort();
|
|
30
|
-
return actual.length === expected.length
|
|
31
|
-
&& actual.every((key, index) => key === expected[index]);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function requireArray(value, label) {
|
|
35
|
-
if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype) {
|
|
36
|
-
contractError(`${label}がarrayではない`);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function identifier(value) {
|
|
41
|
-
return typeof value === 'string' && IDENTIFIER.test(value);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function boundedText(value) {
|
|
45
|
-
return typeof value === 'string'
|
|
46
|
-
&& value.length > 0
|
|
47
|
-
&& value === value.trim()
|
|
48
|
-
&& Buffer.byteLength(value, 'utf8') <= 4_096;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function compareText(left, right) {
|
|
52
|
-
if (left < right) return -1;
|
|
53
|
-
if (left > right) return 1;
|
|
54
|
-
return 0;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function sha256(value) {
|
|
58
|
-
return createHash('sha256').update(value).digest('hex');
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function conflictOrder(left, right) {
|
|
62
|
-
return compareText(left.todo_ids[0], right.todo_ids[0])
|
|
63
|
-
|| compareText(left.todo_ids[1], right.todo_ids[1])
|
|
64
|
-
|| compareText(left.resource_id, right.resource_id);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function precedenceOrder(left, right) {
|
|
68
|
-
return compareText(left.from_todo_id, right.from_todo_id)
|
|
69
|
-
|| compareText(left.to_todo_id, right.to_todo_id)
|
|
70
|
-
|| compareText(left.reason, right.reason);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function unknownOrder(left, right) {
|
|
74
|
-
return compareText(left.todo_id, right.todo_id)
|
|
75
|
-
|| compareText(left.kind, right.kind)
|
|
76
|
-
|| compareText(left.reason, right.reason);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function assertAcyclic(todos, precedences) {
|
|
80
|
-
const indegree = new Map(todos.map((todoId) => [todoId, 0]));
|
|
81
|
-
const outgoing = new Map(todos.map((todoId) => [todoId, new Set()]));
|
|
82
|
-
for (const precedence of precedences) {
|
|
83
|
-
const next = outgoing.get(precedence.from_todo_id);
|
|
84
|
-
if (!next.has(precedence.to_todo_id)) {
|
|
85
|
-
next.add(precedence.to_todo_id);
|
|
86
|
-
indegree.set(precedence.to_todo_id, indegree.get(precedence.to_todo_id) + 1);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
const ready = todos.filter((todoId) => indegree.get(todoId) === 0);
|
|
90
|
-
let visited = 0;
|
|
91
|
-
for (let cursor = 0; cursor < ready.length; cursor += 1) {
|
|
92
|
-
const todoId = ready[cursor];
|
|
93
|
-
visited += 1;
|
|
94
|
-
for (const next of outgoing.get(todoId)) {
|
|
95
|
-
indegree.set(next, indegree.get(next) - 1);
|
|
96
|
-
if (indegree.get(next) === 0) ready.push(next);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
if (visited !== todos.length) contractError('precedencesにcycleがある');
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function normalizeGraph(value) {
|
|
103
|
-
if (!exactRecord(value, [
|
|
104
|
-
'schema_version',
|
|
105
|
-
'todos',
|
|
106
|
-
'conflicts',
|
|
107
|
-
'precedences',
|
|
108
|
-
'unknowns',
|
|
109
|
-
'capacity',
|
|
110
|
-
]) || value.schema_version !== GRAPH_SCHEMA) {
|
|
111
|
-
contractError('normalized graph shapeまたはschema_versionが不正');
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
requireArray(value.todos, 'todos');
|
|
115
|
-
if (value.todos.length < 1
|
|
116
|
-
|| value.todos.length > MAX_TODOS
|
|
117
|
-
|| !value.todos.every(identifier)
|
|
118
|
-
|| new Set(value.todos).size !== value.todos.length) {
|
|
119
|
-
contractError('todosがbounded unique identifier arrayではない');
|
|
120
|
-
}
|
|
121
|
-
const todos = [...value.todos].sort(compareText);
|
|
122
|
-
const todoSet = new Set(todos);
|
|
123
|
-
if (!Number.isSafeInteger(value.capacity)
|
|
124
|
-
|| value.capacity < 1
|
|
125
|
-
|| value.capacity > MAX_TODOS) {
|
|
126
|
-
contractError('capacityがbounded positive integerではない');
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
requireArray(value.conflicts, 'conflicts');
|
|
130
|
-
const conflictKeys = new Set();
|
|
131
|
-
const conflicts = value.conflicts.map((entry) => {
|
|
132
|
-
if (!exactRecord(entry, ['todo_ids', 'resource_id'])) {
|
|
133
|
-
contractError('conflict shapeが不正');
|
|
134
|
-
}
|
|
135
|
-
requireArray(entry.todo_ids, 'conflict.todo_ids');
|
|
136
|
-
if (entry.todo_ids.length !== 2
|
|
137
|
-
|| entry.todo_ids.some((todoId) => !todoSet.has(todoId))
|
|
138
|
-
|| entry.todo_ids[0] === entry.todo_ids[1]
|
|
139
|
-
|| !identifier(entry.resource_id)) {
|
|
140
|
-
contractError('conflict relationが不正');
|
|
141
|
-
}
|
|
142
|
-
const todoIds = [...entry.todo_ids].sort(compareText);
|
|
143
|
-
const key = `${todoIds[0]}\u0000${todoIds[1]}\u0000${entry.resource_id}`;
|
|
144
|
-
if (conflictKeys.has(key)) contractError('conflictが重複している');
|
|
145
|
-
conflictKeys.add(key);
|
|
146
|
-
return { todo_ids: todoIds, resource_id: entry.resource_id };
|
|
147
|
-
}).sort(conflictOrder);
|
|
148
|
-
|
|
149
|
-
requireArray(value.precedences, 'precedences');
|
|
150
|
-
const precedenceKeys = new Set();
|
|
151
|
-
const precedences = value.precedences.map((entry) => {
|
|
152
|
-
if (!exactRecord(entry, ['from_todo_id', 'to_todo_id', 'reason'])
|
|
153
|
-
|| !todoSet.has(entry.from_todo_id)
|
|
154
|
-
|| !todoSet.has(entry.to_todo_id)
|
|
155
|
-
|| entry.from_todo_id === entry.to_todo_id
|
|
156
|
-
|| !boundedText(entry.reason)) {
|
|
157
|
-
contractError('precedence relationが不正');
|
|
158
|
-
}
|
|
159
|
-
const key = `${entry.from_todo_id}\u0000${entry.to_todo_id}\u0000${entry.reason}`;
|
|
160
|
-
if (precedenceKeys.has(key)) contractError('precedenceが重複している');
|
|
161
|
-
precedenceKeys.add(key);
|
|
162
|
-
return {
|
|
163
|
-
from_todo_id: entry.from_todo_id,
|
|
164
|
-
to_todo_id: entry.to_todo_id,
|
|
165
|
-
reason: entry.reason,
|
|
166
|
-
};
|
|
167
|
-
}).sort(precedenceOrder);
|
|
168
|
-
assertAcyclic(todos, precedences);
|
|
169
|
-
|
|
170
|
-
requireArray(value.unknowns, 'unknowns');
|
|
171
|
-
const unknownKeys = new Set();
|
|
172
|
-
const unknowns = value.unknowns.map((entry) => {
|
|
173
|
-
if (!exactRecord(entry, ['todo_id', 'kind', 'reason'])
|
|
174
|
-
|| !todoSet.has(entry.todo_id)
|
|
175
|
-
|| !identifier(entry.kind)
|
|
176
|
-
|| !boundedText(entry.reason)) {
|
|
177
|
-
contractError('unknown relationが不正');
|
|
178
|
-
}
|
|
179
|
-
const key = `${entry.todo_id}\u0000${entry.kind}\u0000${entry.reason}`;
|
|
180
|
-
if (unknownKeys.has(key)) contractError('unknownが重複している');
|
|
181
|
-
unknownKeys.add(key);
|
|
182
|
-
return { todo_id: entry.todo_id, kind: entry.kind, reason: entry.reason };
|
|
183
|
-
}).sort(unknownOrder);
|
|
184
|
-
|
|
185
|
-
return {
|
|
186
|
-
schema_version: GRAPH_SCHEMA,
|
|
187
|
-
todos,
|
|
188
|
-
conflicts,
|
|
189
|
-
precedences,
|
|
190
|
-
unknowns,
|
|
191
|
-
capacity: value.capacity,
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function canonicalGraph(value) {
|
|
196
|
-
const normalized = normalizeGraph(value);
|
|
197
|
-
if (!isDeepStrictEqual(value, normalized)) contractError('normalized graphがcanonicalではない');
|
|
198
|
-
return normalized;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function graphDigest(graph) {
|
|
202
|
-
return sha256(JSON.stringify(graph));
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function pairwiseVerdicts(graph) {
|
|
206
|
-
return [
|
|
207
|
-
...graph.conflicts.map((conflict) => ({
|
|
208
|
-
type: 'conflict',
|
|
209
|
-
todo_ids: [...conflict.todo_ids],
|
|
210
|
-
resource_id: conflict.resource_id,
|
|
211
|
-
})),
|
|
212
|
-
...graph.precedences.map((precedence) => ({
|
|
213
|
-
type: 'precedence',
|
|
214
|
-
from_todo_id: precedence.from_todo_id,
|
|
215
|
-
to_todo_id: precedence.to_todo_id,
|
|
216
|
-
reason: precedence.reason,
|
|
217
|
-
})),
|
|
218
|
-
];
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function observationResource(resource) {
|
|
222
|
-
if (!exactRecord(resource, [
|
|
223
|
-
'resource_id',
|
|
224
|
-
'kind',
|
|
225
|
-
'target',
|
|
226
|
-
'todo_ids',
|
|
227
|
-
'provenance',
|
|
228
|
-
'status',
|
|
229
|
-
]) || (resource.status !== 'observed' && resource.status !== 'unknown')) {
|
|
230
|
-
contractError('normalized resource shapeが不正');
|
|
231
|
-
}
|
|
232
|
-
return {
|
|
233
|
-
resource_id: resource.resource_id,
|
|
234
|
-
kind: resource.kind,
|
|
235
|
-
target: resource.target,
|
|
236
|
-
todo_ids: resource.todo_ids,
|
|
237
|
-
provenance: resource.provenance,
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function validateSafely(callback) {
|
|
242
|
-
try {
|
|
243
|
-
return callback() === true;
|
|
244
|
-
} catch {
|
|
245
|
-
return false;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/** @param {unknown} graph @returns {boolean} */
|
|
250
|
-
export function validateNormalizedBoundaryGraphV2(graph) {
|
|
251
|
-
return validateSafely(() => {
|
|
252
|
-
canonicalGraph(graph);
|
|
253
|
-
return true;
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/** @param {unknown} bundle @returns {boolean} */
|
|
258
|
-
export function validateNormalizedBoundaryBundleV2(bundle) {
|
|
259
|
-
return validateSafely(() => {
|
|
260
|
-
if (!exactRecord(bundle, [
|
|
261
|
-
'schema_version',
|
|
262
|
-
'source',
|
|
263
|
-
'resources',
|
|
264
|
-
'precedences',
|
|
265
|
-
'graph',
|
|
266
|
-
'graph_digest',
|
|
267
|
-
]) || bundle.schema_version !== BUNDLE_SCHEMA
|
|
268
|
-
|| typeof bundle.graph_digest !== 'string'
|
|
269
|
-
|| !DIGEST.test(bundle.graph_digest)) {
|
|
270
|
-
return false;
|
|
271
|
-
}
|
|
272
|
-
const graph = canonicalGraph(bundle.graph);
|
|
273
|
-
requireArray(bundle.resources, 'resources');
|
|
274
|
-
requireArray(bundle.precedences, 'precedences');
|
|
275
|
-
const observationSet = {
|
|
276
|
-
schema_version: OBSERVATION_SCHEMA,
|
|
277
|
-
source: bundle.source,
|
|
278
|
-
capacity: graph.capacity,
|
|
279
|
-
todos: graph.todos,
|
|
280
|
-
resources: bundle.resources.map(observationResource),
|
|
281
|
-
precedences: bundle.precedences,
|
|
282
|
-
};
|
|
283
|
-
const recompiled = compileBoundaryObservationV2(observationSet);
|
|
284
|
-
return isDeepStrictEqual(bundle, recompiled);
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* @param {unknown} verdict
|
|
290
|
-
* @param {unknown} graph
|
|
291
|
-
* @returns {boolean}
|
|
292
|
-
*/
|
|
293
|
-
export function validateBoundaryVerdictV2(verdict, graph) {
|
|
294
|
-
return validateSafely(() => {
|
|
295
|
-
const normalizedGraph = canonicalGraph(graph);
|
|
296
|
-
if (!exactRecord(verdict, [
|
|
297
|
-
'schema_version',
|
|
298
|
-
'normalized_graph_digest',
|
|
299
|
-
'verdicts',
|
|
300
|
-
]) || verdict.schema_version !== VERDICT_SCHEMA
|
|
301
|
-
|| typeof verdict.normalized_graph_digest !== 'string'
|
|
302
|
-
|| !DIGEST.test(verdict.normalized_graph_digest)
|
|
303
|
-
|| verdict.normalized_graph_digest !== graphDigest(normalizedGraph)) {
|
|
304
|
-
return false;
|
|
305
|
-
}
|
|
306
|
-
requireArray(verdict.verdicts, 'verdicts');
|
|
307
|
-
return isDeepStrictEqual(verdict.verdicts, pairwiseVerdicts(normalizedGraph));
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* @param {unknown} plan
|
|
313
|
-
* @param {unknown} graph
|
|
314
|
-
* @param {unknown} options
|
|
315
|
-
* @returns {boolean}
|
|
316
|
-
*/
|
|
317
|
-
export function validatePlanGraphV2(plan, graph, options = {}) {
|
|
318
|
-
return validateSafely(() => {
|
|
319
|
-
const normalizedGraph = canonicalGraph(graph);
|
|
320
|
-
const result = verifySchedulabilityPlanV2(normalizedGraph, plan, options);
|
|
321
|
-
return result.outcome === 'verified'
|
|
322
|
-
&& plainRecord(plan)
|
|
323
|
-
&& result.minimum_feasible_waves === plan.minimum_feasible_waves;
|
|
324
|
-
});
|
|
325
|
-
}
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
3
|
+
|
|
4
|
+
import { compileBoundaryObservationV2 } from './boundary-observation-compiler-v2.mjs';
|
|
5
|
+
import { verifySchedulabilityPlanV2 } from './schedulability-verifier-v2.mjs';
|
|
6
|
+
|
|
7
|
+
const OBSERVATION_SCHEMA = 'lattice.boundary_observation_set.v2';
|
|
8
|
+
const BUNDLE_SCHEMA = 'lattice.normalized_boundary_bundle.v2';
|
|
9
|
+
const GRAPH_SCHEMA = 'lattice.normalized_boundary_graph.v2';
|
|
10
|
+
const VERDICT_SCHEMA = 'lattice.boundary_verdict.v2';
|
|
11
|
+
const MAX_TODOS = 256;
|
|
12
|
+
const IDENTIFIER = /^[0-9A-Za-z](?:[0-9A-Za-z._-]{0,127})$/;
|
|
13
|
+
const DIGEST = /^[0-9a-f]{64}$/;
|
|
14
|
+
|
|
15
|
+
function contractError(reason) {
|
|
16
|
+
throw new TypeError(`artifact contracts v2契約違反: ${reason}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function plainRecord(value) {
|
|
20
|
+
return value !== null
|
|
21
|
+
&& typeof value === 'object'
|
|
22
|
+
&& !Array.isArray(value)
|
|
23
|
+
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function exactRecord(value, keys) {
|
|
27
|
+
if (!plainRecord(value)) return false;
|
|
28
|
+
const actual = Object.keys(value).sort();
|
|
29
|
+
const expected = [...keys].sort();
|
|
30
|
+
return actual.length === expected.length
|
|
31
|
+
&& actual.every((key, index) => key === expected[index]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function requireArray(value, label) {
|
|
35
|
+
if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype) {
|
|
36
|
+
contractError(`${label}がarrayではない`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function identifier(value) {
|
|
41
|
+
return typeof value === 'string' && IDENTIFIER.test(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function boundedText(value) {
|
|
45
|
+
return typeof value === 'string'
|
|
46
|
+
&& value.length > 0
|
|
47
|
+
&& value === value.trim()
|
|
48
|
+
&& Buffer.byteLength(value, 'utf8') <= 4_096;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function compareText(left, right) {
|
|
52
|
+
if (left < right) return -1;
|
|
53
|
+
if (left > right) return 1;
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function sha256(value) {
|
|
58
|
+
return createHash('sha256').update(value).digest('hex');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function conflictOrder(left, right) {
|
|
62
|
+
return compareText(left.todo_ids[0], right.todo_ids[0])
|
|
63
|
+
|| compareText(left.todo_ids[1], right.todo_ids[1])
|
|
64
|
+
|| compareText(left.resource_id, right.resource_id);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function precedenceOrder(left, right) {
|
|
68
|
+
return compareText(left.from_todo_id, right.from_todo_id)
|
|
69
|
+
|| compareText(left.to_todo_id, right.to_todo_id)
|
|
70
|
+
|| compareText(left.reason, right.reason);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function unknownOrder(left, right) {
|
|
74
|
+
return compareText(left.todo_id, right.todo_id)
|
|
75
|
+
|| compareText(left.kind, right.kind)
|
|
76
|
+
|| compareText(left.reason, right.reason);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function assertAcyclic(todos, precedences) {
|
|
80
|
+
const indegree = new Map(todos.map((todoId) => [todoId, 0]));
|
|
81
|
+
const outgoing = new Map(todos.map((todoId) => [todoId, new Set()]));
|
|
82
|
+
for (const precedence of precedences) {
|
|
83
|
+
const next = outgoing.get(precedence.from_todo_id);
|
|
84
|
+
if (!next.has(precedence.to_todo_id)) {
|
|
85
|
+
next.add(precedence.to_todo_id);
|
|
86
|
+
indegree.set(precedence.to_todo_id, indegree.get(precedence.to_todo_id) + 1);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const ready = todos.filter((todoId) => indegree.get(todoId) === 0);
|
|
90
|
+
let visited = 0;
|
|
91
|
+
for (let cursor = 0; cursor < ready.length; cursor += 1) {
|
|
92
|
+
const todoId = ready[cursor];
|
|
93
|
+
visited += 1;
|
|
94
|
+
for (const next of outgoing.get(todoId)) {
|
|
95
|
+
indegree.set(next, indegree.get(next) - 1);
|
|
96
|
+
if (indegree.get(next) === 0) ready.push(next);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (visited !== todos.length) contractError('precedencesにcycleがある');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function normalizeGraph(value) {
|
|
103
|
+
if (!exactRecord(value, [
|
|
104
|
+
'schema_version',
|
|
105
|
+
'todos',
|
|
106
|
+
'conflicts',
|
|
107
|
+
'precedences',
|
|
108
|
+
'unknowns',
|
|
109
|
+
'capacity',
|
|
110
|
+
]) || value.schema_version !== GRAPH_SCHEMA) {
|
|
111
|
+
contractError('normalized graph shapeまたはschema_versionが不正');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
requireArray(value.todos, 'todos');
|
|
115
|
+
if (value.todos.length < 1
|
|
116
|
+
|| value.todos.length > MAX_TODOS
|
|
117
|
+
|| !value.todos.every(identifier)
|
|
118
|
+
|| new Set(value.todos).size !== value.todos.length) {
|
|
119
|
+
contractError('todosがbounded unique identifier arrayではない');
|
|
120
|
+
}
|
|
121
|
+
const todos = [...value.todos].sort(compareText);
|
|
122
|
+
const todoSet = new Set(todos);
|
|
123
|
+
if (!Number.isSafeInteger(value.capacity)
|
|
124
|
+
|| value.capacity < 1
|
|
125
|
+
|| value.capacity > MAX_TODOS) {
|
|
126
|
+
contractError('capacityがbounded positive integerではない');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
requireArray(value.conflicts, 'conflicts');
|
|
130
|
+
const conflictKeys = new Set();
|
|
131
|
+
const conflicts = value.conflicts.map((entry) => {
|
|
132
|
+
if (!exactRecord(entry, ['todo_ids', 'resource_id'])) {
|
|
133
|
+
contractError('conflict shapeが不正');
|
|
134
|
+
}
|
|
135
|
+
requireArray(entry.todo_ids, 'conflict.todo_ids');
|
|
136
|
+
if (entry.todo_ids.length !== 2
|
|
137
|
+
|| entry.todo_ids.some((todoId) => !todoSet.has(todoId))
|
|
138
|
+
|| entry.todo_ids[0] === entry.todo_ids[1]
|
|
139
|
+
|| !identifier(entry.resource_id)) {
|
|
140
|
+
contractError('conflict relationが不正');
|
|
141
|
+
}
|
|
142
|
+
const todoIds = [...entry.todo_ids].sort(compareText);
|
|
143
|
+
const key = `${todoIds[0]}\u0000${todoIds[1]}\u0000${entry.resource_id}`;
|
|
144
|
+
if (conflictKeys.has(key)) contractError('conflictが重複している');
|
|
145
|
+
conflictKeys.add(key);
|
|
146
|
+
return { todo_ids: todoIds, resource_id: entry.resource_id };
|
|
147
|
+
}).sort(conflictOrder);
|
|
148
|
+
|
|
149
|
+
requireArray(value.precedences, 'precedences');
|
|
150
|
+
const precedenceKeys = new Set();
|
|
151
|
+
const precedences = value.precedences.map((entry) => {
|
|
152
|
+
if (!exactRecord(entry, ['from_todo_id', 'to_todo_id', 'reason'])
|
|
153
|
+
|| !todoSet.has(entry.from_todo_id)
|
|
154
|
+
|| !todoSet.has(entry.to_todo_id)
|
|
155
|
+
|| entry.from_todo_id === entry.to_todo_id
|
|
156
|
+
|| !boundedText(entry.reason)) {
|
|
157
|
+
contractError('precedence relationが不正');
|
|
158
|
+
}
|
|
159
|
+
const key = `${entry.from_todo_id}\u0000${entry.to_todo_id}\u0000${entry.reason}`;
|
|
160
|
+
if (precedenceKeys.has(key)) contractError('precedenceが重複している');
|
|
161
|
+
precedenceKeys.add(key);
|
|
162
|
+
return {
|
|
163
|
+
from_todo_id: entry.from_todo_id,
|
|
164
|
+
to_todo_id: entry.to_todo_id,
|
|
165
|
+
reason: entry.reason,
|
|
166
|
+
};
|
|
167
|
+
}).sort(precedenceOrder);
|
|
168
|
+
assertAcyclic(todos, precedences);
|
|
169
|
+
|
|
170
|
+
requireArray(value.unknowns, 'unknowns');
|
|
171
|
+
const unknownKeys = new Set();
|
|
172
|
+
const unknowns = value.unknowns.map((entry) => {
|
|
173
|
+
if (!exactRecord(entry, ['todo_id', 'kind', 'reason'])
|
|
174
|
+
|| !todoSet.has(entry.todo_id)
|
|
175
|
+
|| !identifier(entry.kind)
|
|
176
|
+
|| !boundedText(entry.reason)) {
|
|
177
|
+
contractError('unknown relationが不正');
|
|
178
|
+
}
|
|
179
|
+
const key = `${entry.todo_id}\u0000${entry.kind}\u0000${entry.reason}`;
|
|
180
|
+
if (unknownKeys.has(key)) contractError('unknownが重複している');
|
|
181
|
+
unknownKeys.add(key);
|
|
182
|
+
return { todo_id: entry.todo_id, kind: entry.kind, reason: entry.reason };
|
|
183
|
+
}).sort(unknownOrder);
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
schema_version: GRAPH_SCHEMA,
|
|
187
|
+
todos,
|
|
188
|
+
conflicts,
|
|
189
|
+
precedences,
|
|
190
|
+
unknowns,
|
|
191
|
+
capacity: value.capacity,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function canonicalGraph(value) {
|
|
196
|
+
const normalized = normalizeGraph(value);
|
|
197
|
+
if (!isDeepStrictEqual(value, normalized)) contractError('normalized graphがcanonicalではない');
|
|
198
|
+
return normalized;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function graphDigest(graph) {
|
|
202
|
+
return sha256(JSON.stringify(graph));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function pairwiseVerdicts(graph) {
|
|
206
|
+
return [
|
|
207
|
+
...graph.conflicts.map((conflict) => ({
|
|
208
|
+
type: 'conflict',
|
|
209
|
+
todo_ids: [...conflict.todo_ids],
|
|
210
|
+
resource_id: conflict.resource_id,
|
|
211
|
+
})),
|
|
212
|
+
...graph.precedences.map((precedence) => ({
|
|
213
|
+
type: 'precedence',
|
|
214
|
+
from_todo_id: precedence.from_todo_id,
|
|
215
|
+
to_todo_id: precedence.to_todo_id,
|
|
216
|
+
reason: precedence.reason,
|
|
217
|
+
})),
|
|
218
|
+
];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function observationResource(resource) {
|
|
222
|
+
if (!exactRecord(resource, [
|
|
223
|
+
'resource_id',
|
|
224
|
+
'kind',
|
|
225
|
+
'target',
|
|
226
|
+
'todo_ids',
|
|
227
|
+
'provenance',
|
|
228
|
+
'status',
|
|
229
|
+
]) || (resource.status !== 'observed' && resource.status !== 'unknown')) {
|
|
230
|
+
contractError('normalized resource shapeが不正');
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
resource_id: resource.resource_id,
|
|
234
|
+
kind: resource.kind,
|
|
235
|
+
target: resource.target,
|
|
236
|
+
todo_ids: resource.todo_ids,
|
|
237
|
+
provenance: resource.provenance,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function validateSafely(callback) {
|
|
242
|
+
try {
|
|
243
|
+
return callback() === true;
|
|
244
|
+
} catch {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** @param {unknown} graph @returns {boolean} */
|
|
250
|
+
export function validateNormalizedBoundaryGraphV2(graph) {
|
|
251
|
+
return validateSafely(() => {
|
|
252
|
+
canonicalGraph(graph);
|
|
253
|
+
return true;
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** @param {unknown} bundle @returns {boolean} */
|
|
258
|
+
export function validateNormalizedBoundaryBundleV2(bundle) {
|
|
259
|
+
return validateSafely(() => {
|
|
260
|
+
if (!exactRecord(bundle, [
|
|
261
|
+
'schema_version',
|
|
262
|
+
'source',
|
|
263
|
+
'resources',
|
|
264
|
+
'precedences',
|
|
265
|
+
'graph',
|
|
266
|
+
'graph_digest',
|
|
267
|
+
]) || bundle.schema_version !== BUNDLE_SCHEMA
|
|
268
|
+
|| typeof bundle.graph_digest !== 'string'
|
|
269
|
+
|| !DIGEST.test(bundle.graph_digest)) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
const graph = canonicalGraph(bundle.graph);
|
|
273
|
+
requireArray(bundle.resources, 'resources');
|
|
274
|
+
requireArray(bundle.precedences, 'precedences');
|
|
275
|
+
const observationSet = {
|
|
276
|
+
schema_version: OBSERVATION_SCHEMA,
|
|
277
|
+
source: bundle.source,
|
|
278
|
+
capacity: graph.capacity,
|
|
279
|
+
todos: graph.todos,
|
|
280
|
+
resources: bundle.resources.map(observationResource),
|
|
281
|
+
precedences: bundle.precedences,
|
|
282
|
+
};
|
|
283
|
+
const recompiled = compileBoundaryObservationV2(observationSet);
|
|
284
|
+
return isDeepStrictEqual(bundle, recompiled);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* @param {unknown} verdict
|
|
290
|
+
* @param {unknown} graph
|
|
291
|
+
* @returns {boolean}
|
|
292
|
+
*/
|
|
293
|
+
export function validateBoundaryVerdictV2(verdict, graph) {
|
|
294
|
+
return validateSafely(() => {
|
|
295
|
+
const normalizedGraph = canonicalGraph(graph);
|
|
296
|
+
if (!exactRecord(verdict, [
|
|
297
|
+
'schema_version',
|
|
298
|
+
'normalized_graph_digest',
|
|
299
|
+
'verdicts',
|
|
300
|
+
]) || verdict.schema_version !== VERDICT_SCHEMA
|
|
301
|
+
|| typeof verdict.normalized_graph_digest !== 'string'
|
|
302
|
+
|| !DIGEST.test(verdict.normalized_graph_digest)
|
|
303
|
+
|| verdict.normalized_graph_digest !== graphDigest(normalizedGraph)) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
requireArray(verdict.verdicts, 'verdicts');
|
|
307
|
+
return isDeepStrictEqual(verdict.verdicts, pairwiseVerdicts(normalizedGraph));
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* @param {unknown} plan
|
|
313
|
+
* @param {unknown} graph
|
|
314
|
+
* @param {unknown} options
|
|
315
|
+
* @returns {boolean}
|
|
316
|
+
*/
|
|
317
|
+
export function validatePlanGraphV2(plan, graph, options = {}) {
|
|
318
|
+
return validateSafely(() => {
|
|
319
|
+
const normalizedGraph = canonicalGraph(graph);
|
|
320
|
+
const result = verifySchedulabilityPlanV2(normalizedGraph, plan, options);
|
|
321
|
+
return result.outcome === 'verified'
|
|
322
|
+
&& plainRecord(plan)
|
|
323
|
+
&& result.minimum_feasible_waves === plan.minimum_feasible_waves;
|
|
324
|
+
});
|
|
325
|
+
}
|