@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,344 +1,344 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
|
|
3
|
-
const OBSERVATION_SCHEMA = 'lattice.boundary_observation_set.v2';
|
|
4
|
-
const BUNDLE_SCHEMA = 'lattice.normalized_boundary_bundle.v2';
|
|
5
|
-
const GRAPH_SCHEMA = 'lattice.normalized_boundary_graph.v2';
|
|
6
|
-
const MAX_TODOS = 256;
|
|
7
|
-
const IDENTIFIER = /^[0-9A-Za-z](?:[0-9A-Za-z._-]{0,127})$/;
|
|
8
|
-
const DIGEST = /^[0-9a-f]{64}$/;
|
|
9
|
-
const RESOURCE_KINDS = new Set(['symbol', 'path', 'state', 'effect', 'line', 'dynamic']);
|
|
10
|
-
const PROVENANCE_SOURCES = new Set([
|
|
11
|
-
'sensor',
|
|
12
|
-
'manual_candidate_spec',
|
|
13
|
-
'manual_state_effect',
|
|
14
|
-
]);
|
|
15
|
-
const SENSOR_STATUSES = new Set([
|
|
16
|
-
'ready',
|
|
17
|
-
'symbol_absent',
|
|
18
|
-
// 不存在path関連。宣言の有無で3つに分かれる(ADR 0136)。宣言が無いabsentはpath_absent、
|
|
19
|
-
// 宣言があるのに実在するのはcreates_path_present、fs観測が記録に無いのはcreates_unverified。
|
|
20
|
-
'path_absent',
|
|
21
|
-
'creates_path_present',
|
|
22
|
-
'creates_unverified',
|
|
23
|
-
'empty',
|
|
24
|
-
'unresolved',
|
|
25
|
-
'command_failure',
|
|
26
|
-
'invalid_json',
|
|
27
|
-
'stale',
|
|
28
|
-
'unsupported',
|
|
29
|
-
]);
|
|
30
|
-
|
|
31
|
-
function fail(reason) {
|
|
32
|
-
throw new TypeError(`boundary observation compiler v2契約違反: ${reason}`);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function plainRecord(value) {
|
|
36
|
-
return value !== null
|
|
37
|
-
&& typeof value === 'object'
|
|
38
|
-
&& !Array.isArray(value)
|
|
39
|
-
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function exactRecord(value, keys) {
|
|
43
|
-
if (!plainRecord(value)) return false;
|
|
44
|
-
const actual = Object.keys(value).sort();
|
|
45
|
-
const expected = [...keys].sort();
|
|
46
|
-
return actual.length === expected.length
|
|
47
|
-
&& actual.every((key, index) => key === expected[index]);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function requireArray(value, label) {
|
|
51
|
-
if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype) {
|
|
52
|
-
fail(`${label}がarrayではない`);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function identifier(value) {
|
|
57
|
-
return typeof value === 'string' && IDENTIFIER.test(value);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function boundedText(value) {
|
|
61
|
-
return typeof value === 'string'
|
|
62
|
-
&& value.length > 0
|
|
63
|
-
&& value === value.trim()
|
|
64
|
-
&& Buffer.byteLength(value, 'utf8') <= 4_096;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function compareText(left, right) {
|
|
68
|
-
if (left < right) return -1;
|
|
69
|
-
if (left > right) return 1;
|
|
70
|
-
return 0;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function sha256(value) {
|
|
74
|
-
return createHash('sha256').update(value).digest('hex');
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function normalizeSource(value) {
|
|
78
|
-
if (!exactRecord(value, [
|
|
79
|
-
'snapshot_digest',
|
|
80
|
-
'candidate_witness_digest',
|
|
81
|
-
'query_set_digest',
|
|
82
|
-
'manual_evidence_digest',
|
|
83
|
-
]) || !Object.values(value).every((entry) => typeof entry === 'string' && DIGEST.test(entry))) {
|
|
84
|
-
fail('source digest shapeが不正');
|
|
85
|
-
}
|
|
86
|
-
return {
|
|
87
|
-
snapshot_digest: value.snapshot_digest,
|
|
88
|
-
candidate_witness_digest: value.candidate_witness_digest,
|
|
89
|
-
query_set_digest: value.query_set_digest,
|
|
90
|
-
manual_evidence_digest: value.manual_evidence_digest,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function normalizeProvenance(value, label) {
|
|
95
|
-
requireArray(value, `${label}.provenance`);
|
|
96
|
-
if (value.length < 1 || value.length > PROVENANCE_SOURCES.size) {
|
|
97
|
-
fail(`${label}.provenance件数が不正`);
|
|
98
|
-
}
|
|
99
|
-
const sources = new Set();
|
|
100
|
-
const normalized = value.map((entry) => {
|
|
101
|
-
if (!exactRecord(entry, ['source', 'evidence_ref', 'evidence_digest', 'status'])
|
|
102
|
-
|| !PROVENANCE_SOURCES.has(entry.source)
|
|
103
|
-
|| !boundedText(entry.evidence_ref)
|
|
104
|
-
|| typeof entry.evidence_digest !== 'string'
|
|
105
|
-
|| !DIGEST.test(entry.evidence_digest)) {
|
|
106
|
-
fail(`${label}.provenance shapeが不正`);
|
|
107
|
-
}
|
|
108
|
-
if (entry.source === 'sensor') {
|
|
109
|
-
if (!SENSOR_STATUSES.has(entry.status)) fail(`${label}.LatticeSensor statusが不正`);
|
|
110
|
-
} else if (entry.status !== 'asserted') {
|
|
111
|
-
fail(`${label}.manual provenance statusがassertedではない`);
|
|
112
|
-
}
|
|
113
|
-
if (sources.has(entry.source)) fail(`${label}.provenance sourceが重複している`);
|
|
114
|
-
sources.add(entry.source);
|
|
115
|
-
return {
|
|
116
|
-
source: entry.source,
|
|
117
|
-
evidence_ref: entry.evidence_ref,
|
|
118
|
-
evidence_digest: entry.evidence_digest,
|
|
119
|
-
status: entry.status,
|
|
120
|
-
};
|
|
121
|
-
});
|
|
122
|
-
normalized.sort((left, right) => (
|
|
123
|
-
compareText(left.source, right.source)
|
|
124
|
-
|| compareText(left.status, right.status)
|
|
125
|
-
|| compareText(left.evidence_ref, right.evidence_ref)
|
|
126
|
-
|| compareText(left.evidence_digest, right.evidence_digest)
|
|
127
|
-
));
|
|
128
|
-
return { normalized, sources };
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function requireExactSources(sources, expected, label) {
|
|
132
|
-
if (sources.size !== expected.length || expected.some((source) => !sources.has(source))) {
|
|
133
|
-
fail(`${label}.provenance source集合が不正`);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function normalizeResource(value, todoSet) {
|
|
138
|
-
if (!exactRecord(value, [
|
|
139
|
-
'resource_id',
|
|
140
|
-
'kind',
|
|
141
|
-
'target',
|
|
142
|
-
'todo_ids',
|
|
143
|
-
'provenance',
|
|
144
|
-
]) || !identifier(value.resource_id)
|
|
145
|
-
|| !RESOURCE_KINDS.has(value.kind)
|
|
146
|
-
|| !boundedText(value.target)) {
|
|
147
|
-
fail('resource shapeが不正');
|
|
148
|
-
}
|
|
149
|
-
requireArray(value.todo_ids, `resource ${value.resource_id}.todo_ids`);
|
|
150
|
-
if (value.todo_ids.length < 1
|
|
151
|
-
|| value.todo_ids.length > todoSet.size
|
|
152
|
-
|| value.todo_ids.some((todoId) => !todoSet.has(todoId))
|
|
153
|
-
|| new Set(value.todo_ids).size !== value.todo_ids.length) {
|
|
154
|
-
fail(`resource ${value.resource_id}.todo_idsが不正`);
|
|
155
|
-
}
|
|
156
|
-
const todoIds = [...value.todo_ids].sort(compareText);
|
|
157
|
-
const { normalized: provenance, sources } = normalizeProvenance(
|
|
158
|
-
value.provenance,
|
|
159
|
-
`resource ${value.resource_id}`,
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
let status;
|
|
163
|
-
if (value.kind === 'symbol' || value.kind === 'path') {
|
|
164
|
-
requireExactSources(sources, ['sensor', 'manual_candidate_spec'], `resource ${value.resource_id}`);
|
|
165
|
-
status = provenance.find((entry) => entry.source === 'sensor').status === 'ready'
|
|
166
|
-
? 'observed'
|
|
167
|
-
: 'unknown';
|
|
168
|
-
} else {
|
|
169
|
-
requireExactSources(sources, ['manual_state_effect'], `resource ${value.resource_id}`);
|
|
170
|
-
status = value.kind === 'dynamic' ? 'unknown' : 'observed';
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return {
|
|
174
|
-
resource_id: value.resource_id,
|
|
175
|
-
kind: value.kind,
|
|
176
|
-
target: value.target,
|
|
177
|
-
todo_ids: todoIds,
|
|
178
|
-
provenance,
|
|
179
|
-
status,
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function normalizePrecedence(value, todoSet) {
|
|
184
|
-
if (!exactRecord(value, [
|
|
185
|
-
'from_todo_id',
|
|
186
|
-
'to_todo_id',
|
|
187
|
-
'reason',
|
|
188
|
-
'provenance',
|
|
189
|
-
]) || !todoSet.has(value.from_todo_id)
|
|
190
|
-
|| !todoSet.has(value.to_todo_id)
|
|
191
|
-
|| value.from_todo_id === value.to_todo_id
|
|
192
|
-
|| !boundedText(value.reason)) {
|
|
193
|
-
fail('precedence shapeが不正');
|
|
194
|
-
}
|
|
195
|
-
const { normalized: provenance, sources } = normalizeProvenance(value.provenance, 'precedence');
|
|
196
|
-
requireExactSources(sources, ['manual_candidate_spec'], 'precedence');
|
|
197
|
-
return {
|
|
198
|
-
from_todo_id: value.from_todo_id,
|
|
199
|
-
to_todo_id: value.to_todo_id,
|
|
200
|
-
reason: value.reason,
|
|
201
|
-
provenance,
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function assertAcyclic(todos, precedences) {
|
|
206
|
-
const indegree = new Map(todos.map((todoId) => [todoId, 0]));
|
|
207
|
-
const outgoing = new Map(todos.map((todoId) => [todoId, new Set()]));
|
|
208
|
-
for (const precedence of precedences) {
|
|
209
|
-
const next = outgoing.get(precedence.from_todo_id);
|
|
210
|
-
if (!next.has(precedence.to_todo_id)) {
|
|
211
|
-
next.add(precedence.to_todo_id);
|
|
212
|
-
indegree.set(precedence.to_todo_id, indegree.get(precedence.to_todo_id) + 1);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
const ready = todos.filter((todoId) => indegree.get(todoId) === 0);
|
|
216
|
-
let visited = 0;
|
|
217
|
-
for (let cursor = 0; cursor < ready.length; cursor += 1) {
|
|
218
|
-
const todoId = ready[cursor];
|
|
219
|
-
visited += 1;
|
|
220
|
-
for (const next of outgoing.get(todoId)) {
|
|
221
|
-
indegree.set(next, indegree.get(next) - 1);
|
|
222
|
-
if (indegree.get(next) === 0) ready.push(next);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
if (visited !== todos.length) fail('precedencesにcycleがある');
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function unknownForResource(resource) {
|
|
229
|
-
const sensor = resource.provenance.find((entry) => entry.source === 'sensor');
|
|
230
|
-
const kind = resource.kind === 'dynamic' ? 'dynamic' : `sensor_${sensor.status}`;
|
|
231
|
-
const status = resource.kind === 'dynamic' ? 'dynamic' : sensor.status;
|
|
232
|
-
return resource.todo_ids.map((todoId) => ({
|
|
233
|
-
todo_id: todoId,
|
|
234
|
-
kind,
|
|
235
|
-
reason: `resource ${resource.resource_id} is ${status}`,
|
|
236
|
-
}));
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function conflictOrder(left, right) {
|
|
240
|
-
return compareText(left.todo_ids[0], right.todo_ids[0])
|
|
241
|
-
|| compareText(left.todo_ids[1], right.todo_ids[1])
|
|
242
|
-
|| compareText(left.resource_id, right.resource_id);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function precedenceOrder(left, right) {
|
|
246
|
-
return compareText(left.from_todo_id, right.from_todo_id)
|
|
247
|
-
|| compareText(left.to_todo_id, right.to_todo_id)
|
|
248
|
-
|| compareText(left.reason, right.reason);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function unknownOrder(left, right) {
|
|
252
|
-
return compareText(left.todo_id, right.todo_id)
|
|
253
|
-
|| compareText(left.kind, right.kind)
|
|
254
|
-
|| compareText(left.reason, right.reason);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* LatticeSensorの構造観測とmanual witnessをprovenance付きbundleへ正規化する。
|
|
259
|
-
* @param {unknown} observationSet
|
|
260
|
-
* @returns {object}
|
|
261
|
-
*/
|
|
262
|
-
export function compileBoundaryObservationV2(observationSet) {
|
|
263
|
-
if (!exactRecord(observationSet, [
|
|
264
|
-
'schema_version',
|
|
265
|
-
'source',
|
|
266
|
-
'capacity',
|
|
267
|
-
'todos',
|
|
268
|
-
'resources',
|
|
269
|
-
'precedences',
|
|
270
|
-
]) || observationSet.schema_version !== OBSERVATION_SCHEMA) {
|
|
271
|
-
fail('observation set shapeまたはschema_versionが不正');
|
|
272
|
-
}
|
|
273
|
-
const source = normalizeSource(observationSet.source);
|
|
274
|
-
requireArray(observationSet.todos, 'todos');
|
|
275
|
-
if (observationSet.todos.length < 1
|
|
276
|
-
|| observationSet.todos.length > MAX_TODOS
|
|
277
|
-
|| !observationSet.todos.every(identifier)
|
|
278
|
-
|| new Set(observationSet.todos).size !== observationSet.todos.length) {
|
|
279
|
-
fail('todosがbounded unique identifier arrayではない');
|
|
280
|
-
}
|
|
281
|
-
const todos = [...observationSet.todos].sort(compareText);
|
|
282
|
-
const todoSet = new Set(todos);
|
|
283
|
-
if (!Number.isSafeInteger(observationSet.capacity)
|
|
284
|
-
|| observationSet.capacity < 1
|
|
285
|
-
|| observationSet.capacity > MAX_TODOS) {
|
|
286
|
-
fail('capacityがbounded positive integerではない');
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
requireArray(observationSet.resources, 'resources');
|
|
290
|
-
const resources = observationSet.resources.map((resource) => normalizeResource(resource, todoSet));
|
|
291
|
-
if (new Set(resources.map((resource) => resource.resource_id)).size !== resources.length) {
|
|
292
|
-
fail('resource_idが重複している');
|
|
293
|
-
}
|
|
294
|
-
resources.sort((left, right) => compareText(left.resource_id, right.resource_id));
|
|
295
|
-
|
|
296
|
-
requireArray(observationSet.precedences, 'precedences');
|
|
297
|
-
const precedences = observationSet.precedences.map((entry) => normalizePrecedence(entry, todoSet));
|
|
298
|
-
const precedenceKeys = precedences.map((entry) => (
|
|
299
|
-
`${entry.from_todo_id}\u0000${entry.to_todo_id}\u0000${entry.reason}`
|
|
300
|
-
));
|
|
301
|
-
if (new Set(precedenceKeys).size !== precedenceKeys.length) fail('precedenceが重複している');
|
|
302
|
-
precedences.sort(precedenceOrder);
|
|
303
|
-
assertAcyclic(todos, precedences);
|
|
304
|
-
|
|
305
|
-
const conflicts = [];
|
|
306
|
-
const unknowns = [];
|
|
307
|
-
for (const resource of resources) {
|
|
308
|
-
if (resource.status === 'unknown') {
|
|
309
|
-
unknowns.push(...unknownForResource(resource));
|
|
310
|
-
continue;
|
|
311
|
-
}
|
|
312
|
-
for (let left = 0; left < resource.todo_ids.length; left += 1) {
|
|
313
|
-
for (let right = left + 1; right < resource.todo_ids.length; right += 1) {
|
|
314
|
-
conflicts.push({
|
|
315
|
-
todo_ids: [resource.todo_ids[left], resource.todo_ids[right]],
|
|
316
|
-
resource_id: resource.resource_id,
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
conflicts.sort(conflictOrder);
|
|
322
|
-
unknowns.sort(unknownOrder);
|
|
323
|
-
|
|
324
|
-
const graph = {
|
|
325
|
-
schema_version: GRAPH_SCHEMA,
|
|
326
|
-
todos,
|
|
327
|
-
conflicts,
|
|
328
|
-
precedences: precedences.map((entry) => ({
|
|
329
|
-
from_todo_id: entry.from_todo_id,
|
|
330
|
-
to_todo_id: entry.to_todo_id,
|
|
331
|
-
reason: entry.reason,
|
|
332
|
-
})),
|
|
333
|
-
unknowns,
|
|
334
|
-
capacity: observationSet.capacity,
|
|
335
|
-
};
|
|
336
|
-
return {
|
|
337
|
-
schema_version: BUNDLE_SCHEMA,
|
|
338
|
-
source,
|
|
339
|
-
resources,
|
|
340
|
-
precedences,
|
|
341
|
-
graph,
|
|
342
|
-
graph_digest: sha256(JSON.stringify(graph)),
|
|
343
|
-
};
|
|
344
|
-
}
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
const OBSERVATION_SCHEMA = 'lattice.boundary_observation_set.v2';
|
|
4
|
+
const BUNDLE_SCHEMA = 'lattice.normalized_boundary_bundle.v2';
|
|
5
|
+
const GRAPH_SCHEMA = 'lattice.normalized_boundary_graph.v2';
|
|
6
|
+
const MAX_TODOS = 256;
|
|
7
|
+
const IDENTIFIER = /^[0-9A-Za-z](?:[0-9A-Za-z._-]{0,127})$/;
|
|
8
|
+
const DIGEST = /^[0-9a-f]{64}$/;
|
|
9
|
+
const RESOURCE_KINDS = new Set(['symbol', 'path', 'state', 'effect', 'line', 'dynamic']);
|
|
10
|
+
const PROVENANCE_SOURCES = new Set([
|
|
11
|
+
'sensor',
|
|
12
|
+
'manual_candidate_spec',
|
|
13
|
+
'manual_state_effect',
|
|
14
|
+
]);
|
|
15
|
+
const SENSOR_STATUSES = new Set([
|
|
16
|
+
'ready',
|
|
17
|
+
'symbol_absent',
|
|
18
|
+
// 不存在path関連。宣言の有無で3つに分かれる(ADR 0136)。宣言が無いabsentはpath_absent、
|
|
19
|
+
// 宣言があるのに実在するのはcreates_path_present、fs観測が記録に無いのはcreates_unverified。
|
|
20
|
+
'path_absent',
|
|
21
|
+
'creates_path_present',
|
|
22
|
+
'creates_unverified',
|
|
23
|
+
'empty',
|
|
24
|
+
'unresolved',
|
|
25
|
+
'command_failure',
|
|
26
|
+
'invalid_json',
|
|
27
|
+
'stale',
|
|
28
|
+
'unsupported',
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
function fail(reason) {
|
|
32
|
+
throw new TypeError(`boundary observation compiler v2契約違反: ${reason}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function plainRecord(value) {
|
|
36
|
+
return value !== null
|
|
37
|
+
&& typeof value === 'object'
|
|
38
|
+
&& !Array.isArray(value)
|
|
39
|
+
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function exactRecord(value, keys) {
|
|
43
|
+
if (!plainRecord(value)) return false;
|
|
44
|
+
const actual = Object.keys(value).sort();
|
|
45
|
+
const expected = [...keys].sort();
|
|
46
|
+
return actual.length === expected.length
|
|
47
|
+
&& actual.every((key, index) => key === expected[index]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function requireArray(value, label) {
|
|
51
|
+
if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype) {
|
|
52
|
+
fail(`${label}がarrayではない`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function identifier(value) {
|
|
57
|
+
return typeof value === 'string' && IDENTIFIER.test(value);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function boundedText(value) {
|
|
61
|
+
return typeof value === 'string'
|
|
62
|
+
&& value.length > 0
|
|
63
|
+
&& value === value.trim()
|
|
64
|
+
&& Buffer.byteLength(value, 'utf8') <= 4_096;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function compareText(left, right) {
|
|
68
|
+
if (left < right) return -1;
|
|
69
|
+
if (left > right) return 1;
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function sha256(value) {
|
|
74
|
+
return createHash('sha256').update(value).digest('hex');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizeSource(value) {
|
|
78
|
+
if (!exactRecord(value, [
|
|
79
|
+
'snapshot_digest',
|
|
80
|
+
'candidate_witness_digest',
|
|
81
|
+
'query_set_digest',
|
|
82
|
+
'manual_evidence_digest',
|
|
83
|
+
]) || !Object.values(value).every((entry) => typeof entry === 'string' && DIGEST.test(entry))) {
|
|
84
|
+
fail('source digest shapeが不正');
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
snapshot_digest: value.snapshot_digest,
|
|
88
|
+
candidate_witness_digest: value.candidate_witness_digest,
|
|
89
|
+
query_set_digest: value.query_set_digest,
|
|
90
|
+
manual_evidence_digest: value.manual_evidence_digest,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function normalizeProvenance(value, label) {
|
|
95
|
+
requireArray(value, `${label}.provenance`);
|
|
96
|
+
if (value.length < 1 || value.length > PROVENANCE_SOURCES.size) {
|
|
97
|
+
fail(`${label}.provenance件数が不正`);
|
|
98
|
+
}
|
|
99
|
+
const sources = new Set();
|
|
100
|
+
const normalized = value.map((entry) => {
|
|
101
|
+
if (!exactRecord(entry, ['source', 'evidence_ref', 'evidence_digest', 'status'])
|
|
102
|
+
|| !PROVENANCE_SOURCES.has(entry.source)
|
|
103
|
+
|| !boundedText(entry.evidence_ref)
|
|
104
|
+
|| typeof entry.evidence_digest !== 'string'
|
|
105
|
+
|| !DIGEST.test(entry.evidence_digest)) {
|
|
106
|
+
fail(`${label}.provenance shapeが不正`);
|
|
107
|
+
}
|
|
108
|
+
if (entry.source === 'sensor') {
|
|
109
|
+
if (!SENSOR_STATUSES.has(entry.status)) fail(`${label}.LatticeSensor statusが不正`);
|
|
110
|
+
} else if (entry.status !== 'asserted') {
|
|
111
|
+
fail(`${label}.manual provenance statusがassertedではない`);
|
|
112
|
+
}
|
|
113
|
+
if (sources.has(entry.source)) fail(`${label}.provenance sourceが重複している`);
|
|
114
|
+
sources.add(entry.source);
|
|
115
|
+
return {
|
|
116
|
+
source: entry.source,
|
|
117
|
+
evidence_ref: entry.evidence_ref,
|
|
118
|
+
evidence_digest: entry.evidence_digest,
|
|
119
|
+
status: entry.status,
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
normalized.sort((left, right) => (
|
|
123
|
+
compareText(left.source, right.source)
|
|
124
|
+
|| compareText(left.status, right.status)
|
|
125
|
+
|| compareText(left.evidence_ref, right.evidence_ref)
|
|
126
|
+
|| compareText(left.evidence_digest, right.evidence_digest)
|
|
127
|
+
));
|
|
128
|
+
return { normalized, sources };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function requireExactSources(sources, expected, label) {
|
|
132
|
+
if (sources.size !== expected.length || expected.some((source) => !sources.has(source))) {
|
|
133
|
+
fail(`${label}.provenance source集合が不正`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function normalizeResource(value, todoSet) {
|
|
138
|
+
if (!exactRecord(value, [
|
|
139
|
+
'resource_id',
|
|
140
|
+
'kind',
|
|
141
|
+
'target',
|
|
142
|
+
'todo_ids',
|
|
143
|
+
'provenance',
|
|
144
|
+
]) || !identifier(value.resource_id)
|
|
145
|
+
|| !RESOURCE_KINDS.has(value.kind)
|
|
146
|
+
|| !boundedText(value.target)) {
|
|
147
|
+
fail('resource shapeが不正');
|
|
148
|
+
}
|
|
149
|
+
requireArray(value.todo_ids, `resource ${value.resource_id}.todo_ids`);
|
|
150
|
+
if (value.todo_ids.length < 1
|
|
151
|
+
|| value.todo_ids.length > todoSet.size
|
|
152
|
+
|| value.todo_ids.some((todoId) => !todoSet.has(todoId))
|
|
153
|
+
|| new Set(value.todo_ids).size !== value.todo_ids.length) {
|
|
154
|
+
fail(`resource ${value.resource_id}.todo_idsが不正`);
|
|
155
|
+
}
|
|
156
|
+
const todoIds = [...value.todo_ids].sort(compareText);
|
|
157
|
+
const { normalized: provenance, sources } = normalizeProvenance(
|
|
158
|
+
value.provenance,
|
|
159
|
+
`resource ${value.resource_id}`,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
let status;
|
|
163
|
+
if (value.kind === 'symbol' || value.kind === 'path') {
|
|
164
|
+
requireExactSources(sources, ['sensor', 'manual_candidate_spec'], `resource ${value.resource_id}`);
|
|
165
|
+
status = provenance.find((entry) => entry.source === 'sensor').status === 'ready'
|
|
166
|
+
? 'observed'
|
|
167
|
+
: 'unknown';
|
|
168
|
+
} else {
|
|
169
|
+
requireExactSources(sources, ['manual_state_effect'], `resource ${value.resource_id}`);
|
|
170
|
+
status = value.kind === 'dynamic' ? 'unknown' : 'observed';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
resource_id: value.resource_id,
|
|
175
|
+
kind: value.kind,
|
|
176
|
+
target: value.target,
|
|
177
|
+
todo_ids: todoIds,
|
|
178
|
+
provenance,
|
|
179
|
+
status,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function normalizePrecedence(value, todoSet) {
|
|
184
|
+
if (!exactRecord(value, [
|
|
185
|
+
'from_todo_id',
|
|
186
|
+
'to_todo_id',
|
|
187
|
+
'reason',
|
|
188
|
+
'provenance',
|
|
189
|
+
]) || !todoSet.has(value.from_todo_id)
|
|
190
|
+
|| !todoSet.has(value.to_todo_id)
|
|
191
|
+
|| value.from_todo_id === value.to_todo_id
|
|
192
|
+
|| !boundedText(value.reason)) {
|
|
193
|
+
fail('precedence shapeが不正');
|
|
194
|
+
}
|
|
195
|
+
const { normalized: provenance, sources } = normalizeProvenance(value.provenance, 'precedence');
|
|
196
|
+
requireExactSources(sources, ['manual_candidate_spec'], 'precedence');
|
|
197
|
+
return {
|
|
198
|
+
from_todo_id: value.from_todo_id,
|
|
199
|
+
to_todo_id: value.to_todo_id,
|
|
200
|
+
reason: value.reason,
|
|
201
|
+
provenance,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function assertAcyclic(todos, precedences) {
|
|
206
|
+
const indegree = new Map(todos.map((todoId) => [todoId, 0]));
|
|
207
|
+
const outgoing = new Map(todos.map((todoId) => [todoId, new Set()]));
|
|
208
|
+
for (const precedence of precedences) {
|
|
209
|
+
const next = outgoing.get(precedence.from_todo_id);
|
|
210
|
+
if (!next.has(precedence.to_todo_id)) {
|
|
211
|
+
next.add(precedence.to_todo_id);
|
|
212
|
+
indegree.set(precedence.to_todo_id, indegree.get(precedence.to_todo_id) + 1);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const ready = todos.filter((todoId) => indegree.get(todoId) === 0);
|
|
216
|
+
let visited = 0;
|
|
217
|
+
for (let cursor = 0; cursor < ready.length; cursor += 1) {
|
|
218
|
+
const todoId = ready[cursor];
|
|
219
|
+
visited += 1;
|
|
220
|
+
for (const next of outgoing.get(todoId)) {
|
|
221
|
+
indegree.set(next, indegree.get(next) - 1);
|
|
222
|
+
if (indegree.get(next) === 0) ready.push(next);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (visited !== todos.length) fail('precedencesにcycleがある');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function unknownForResource(resource) {
|
|
229
|
+
const sensor = resource.provenance.find((entry) => entry.source === 'sensor');
|
|
230
|
+
const kind = resource.kind === 'dynamic' ? 'dynamic' : `sensor_${sensor.status}`;
|
|
231
|
+
const status = resource.kind === 'dynamic' ? 'dynamic' : sensor.status;
|
|
232
|
+
return resource.todo_ids.map((todoId) => ({
|
|
233
|
+
todo_id: todoId,
|
|
234
|
+
kind,
|
|
235
|
+
reason: `resource ${resource.resource_id} is ${status}`,
|
|
236
|
+
}));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function conflictOrder(left, right) {
|
|
240
|
+
return compareText(left.todo_ids[0], right.todo_ids[0])
|
|
241
|
+
|| compareText(left.todo_ids[1], right.todo_ids[1])
|
|
242
|
+
|| compareText(left.resource_id, right.resource_id);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function precedenceOrder(left, right) {
|
|
246
|
+
return compareText(left.from_todo_id, right.from_todo_id)
|
|
247
|
+
|| compareText(left.to_todo_id, right.to_todo_id)
|
|
248
|
+
|| compareText(left.reason, right.reason);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function unknownOrder(left, right) {
|
|
252
|
+
return compareText(left.todo_id, right.todo_id)
|
|
253
|
+
|| compareText(left.kind, right.kind)
|
|
254
|
+
|| compareText(left.reason, right.reason);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* LatticeSensorの構造観測とmanual witnessをprovenance付きbundleへ正規化する。
|
|
259
|
+
* @param {unknown} observationSet
|
|
260
|
+
* @returns {object}
|
|
261
|
+
*/
|
|
262
|
+
export function compileBoundaryObservationV2(observationSet) {
|
|
263
|
+
if (!exactRecord(observationSet, [
|
|
264
|
+
'schema_version',
|
|
265
|
+
'source',
|
|
266
|
+
'capacity',
|
|
267
|
+
'todos',
|
|
268
|
+
'resources',
|
|
269
|
+
'precedences',
|
|
270
|
+
]) || observationSet.schema_version !== OBSERVATION_SCHEMA) {
|
|
271
|
+
fail('observation set shapeまたはschema_versionが不正');
|
|
272
|
+
}
|
|
273
|
+
const source = normalizeSource(observationSet.source);
|
|
274
|
+
requireArray(observationSet.todos, 'todos');
|
|
275
|
+
if (observationSet.todos.length < 1
|
|
276
|
+
|| observationSet.todos.length > MAX_TODOS
|
|
277
|
+
|| !observationSet.todos.every(identifier)
|
|
278
|
+
|| new Set(observationSet.todos).size !== observationSet.todos.length) {
|
|
279
|
+
fail('todosがbounded unique identifier arrayではない');
|
|
280
|
+
}
|
|
281
|
+
const todos = [...observationSet.todos].sort(compareText);
|
|
282
|
+
const todoSet = new Set(todos);
|
|
283
|
+
if (!Number.isSafeInteger(observationSet.capacity)
|
|
284
|
+
|| observationSet.capacity < 1
|
|
285
|
+
|| observationSet.capacity > MAX_TODOS) {
|
|
286
|
+
fail('capacityがbounded positive integerではない');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
requireArray(observationSet.resources, 'resources');
|
|
290
|
+
const resources = observationSet.resources.map((resource) => normalizeResource(resource, todoSet));
|
|
291
|
+
if (new Set(resources.map((resource) => resource.resource_id)).size !== resources.length) {
|
|
292
|
+
fail('resource_idが重複している');
|
|
293
|
+
}
|
|
294
|
+
resources.sort((left, right) => compareText(left.resource_id, right.resource_id));
|
|
295
|
+
|
|
296
|
+
requireArray(observationSet.precedences, 'precedences');
|
|
297
|
+
const precedences = observationSet.precedences.map((entry) => normalizePrecedence(entry, todoSet));
|
|
298
|
+
const precedenceKeys = precedences.map((entry) => (
|
|
299
|
+
`${entry.from_todo_id}\u0000${entry.to_todo_id}\u0000${entry.reason}`
|
|
300
|
+
));
|
|
301
|
+
if (new Set(precedenceKeys).size !== precedenceKeys.length) fail('precedenceが重複している');
|
|
302
|
+
precedences.sort(precedenceOrder);
|
|
303
|
+
assertAcyclic(todos, precedences);
|
|
304
|
+
|
|
305
|
+
const conflicts = [];
|
|
306
|
+
const unknowns = [];
|
|
307
|
+
for (const resource of resources) {
|
|
308
|
+
if (resource.status === 'unknown') {
|
|
309
|
+
unknowns.push(...unknownForResource(resource));
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
for (let left = 0; left < resource.todo_ids.length; left += 1) {
|
|
313
|
+
for (let right = left + 1; right < resource.todo_ids.length; right += 1) {
|
|
314
|
+
conflicts.push({
|
|
315
|
+
todo_ids: [resource.todo_ids[left], resource.todo_ids[right]],
|
|
316
|
+
resource_id: resource.resource_id,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
conflicts.sort(conflictOrder);
|
|
322
|
+
unknowns.sort(unknownOrder);
|
|
323
|
+
|
|
324
|
+
const graph = {
|
|
325
|
+
schema_version: GRAPH_SCHEMA,
|
|
326
|
+
todos,
|
|
327
|
+
conflicts,
|
|
328
|
+
precedences: precedences.map((entry) => ({
|
|
329
|
+
from_todo_id: entry.from_todo_id,
|
|
330
|
+
to_todo_id: entry.to_todo_id,
|
|
331
|
+
reason: entry.reason,
|
|
332
|
+
})),
|
|
333
|
+
unknowns,
|
|
334
|
+
capacity: observationSet.capacity,
|
|
335
|
+
};
|
|
336
|
+
return {
|
|
337
|
+
schema_version: BUNDLE_SCHEMA,
|
|
338
|
+
source,
|
|
339
|
+
resources,
|
|
340
|
+
precedences,
|
|
341
|
+
graph,
|
|
342
|
+
graph_digest: sha256(JSON.stringify(graph)),
|
|
343
|
+
};
|
|
344
|
+
}
|