@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.
Files changed (134) hide show
  1. package/package.json +1 -1
  2. package/src/artifact-contracts-v2.mjs +325 -325
  3. package/src/artifact-contracts.mjs +895 -895
  4. package/src/boundary-compiler.mjs +712 -712
  5. package/src/boundary-observation-compiler-v2.mjs +344 -344
  6. package/src/bounded-seam.mjs +230 -230
  7. package/src/bridge-address.mjs +107 -107
  8. package/src/bridge-cli.mjs +297 -297
  9. package/src/bridge-config.mjs +346 -346
  10. package/src/bridge-daemon.mjs +378 -378
  11. package/src/bridge-hub-protocol.mjs +198 -0
  12. package/src/bridge-launch-agent.mjs +323 -323
  13. package/src/bridge-registrar.mjs +102 -102
  14. package/src/bridge-server.mjs +376 -376
  15. package/src/cli-help.mjs +308 -308
  16. package/src/cli-stdio.mjs +40 -40
  17. package/src/control-compiler.mjs +532 -532
  18. package/src/dag-chain.mjs +261 -261
  19. package/src/factory-diagnostics.mjs +188 -188
  20. package/src/git-process.mjs +72 -0
  21. package/src/hash-chain.mjs +76 -76
  22. package/src/hooks-cli.mjs +1057 -1057
  23. package/src/isolation-runner.mjs +409 -409
  24. package/src/node-version-guard.mjs +44 -44
  25. package/src/project-cli.mjs +699 -697
  26. package/src/project-identity.mjs +130 -130
  27. package/src/rc1-black-box-oracle.mjs +906 -906
  28. package/src/rc1-comparison.mjs +154 -154
  29. package/src/rc1-evidence-bundle.mjs +456 -456
  30. package/src/rc1-v4-campaign.mjs +708 -708
  31. package/src/rc1-v4-transform.mjs +467 -467
  32. package/src/rc1-v5-artifact-set.mjs +855 -855
  33. package/src/rc1-v5-behavior-evidence.mjs +594 -594
  34. package/src/rc1-v5-campaign.mjs +797 -797
  35. package/src/rc1-v5-transform.mjs +421 -421
  36. package/src/rc1-v6-artifact-set.mjs +807 -807
  37. package/src/rc1-v6-behavior-evidence.mjs +273 -273
  38. package/src/rc1-v6-campaign.mjs +623 -623
  39. package/src/rc1-v6-causal-binding.mjs +473 -473
  40. package/src/rc1-v6-measurement.mjs +313 -313
  41. package/src/rc2-artifact-set.mjs +1584 -1584
  42. package/src/rc2-campaign.mjs +1506 -1506
  43. package/src/rc2-delivery-policy-front-end.mjs +1079 -1079
  44. package/src/rc2-delivery-policy-oracle.mjs +134 -134
  45. package/src/rc2-delivery-policy-transform.mjs +1127 -1127
  46. package/src/rc2-rc1-transfer-front-end.mjs +511 -511
  47. package/src/rc3-actual-dogfood.mjs +652 -652
  48. package/src/rc3-dogfood-scaffold.mjs +315 -315
  49. package/src/rc3-scripted-campaign.mjs +1383 -1383
  50. package/src/rc4-stage1-dogfood.mjs +673 -673
  51. package/src/runtime-adapter-registry.mjs +524 -524
  52. package/src/runtime-cli.mjs +4588 -4588
  53. package/src/runtime-contracts.mjs +824 -824
  54. package/src/runtime-control-store.mjs +604 -604
  55. package/src/runtime-controller-protocol.mjs +587 -587
  56. package/src/runtime-decision-verifier.mjs +701 -701
  57. package/src/runtime-diff-observer.mjs +361 -361
  58. package/src/runtime-direct-os-observer.mjs +301 -301
  59. package/src/runtime-driver-state.mjs +166 -166
  60. package/src/runtime-engine.mjs +779 -779
  61. package/src/runtime-errors.mjs +356 -356
  62. package/src/runtime-event-store.mjs +189 -189
  63. package/src/runtime-front-end.mjs +925 -925
  64. package/src/runtime-gate-store.mjs +481 -481
  65. package/src/runtime-hold-recompile.mjs +916 -916
  66. package/src/runtime-io-sentinel.mjs +391 -391
  67. package/src/runtime-lifecycle-lock.mjs +294 -294
  68. package/src/runtime-managed-supervisor.mjs +1490 -1490
  69. package/src/runtime-multi-epoch-store.mjs +838 -838
  70. package/src/runtime-projection.mjs +269 -269
  71. package/src/runtime-pull-intake.mjs +1192 -1192
  72. package/src/runtime-scripted-adapter-controller.mjs +1160 -1160
  73. package/src/runtime-scripted-executor.mjs +163 -163
  74. package/src/runtime-scripted-worktree.mjs +104 -104
  75. package/src/runtime-seam-resolve.mjs +428 -428
  76. package/src/runtime-seam-treatment.mjs +173 -173
  77. package/src/runtime-socket-owner.mjs +125 -125
  78. package/src/runtime-work-order-contracts.mjs +91 -91
  79. package/src/runtime-work-order-controller.mjs +1171 -1171
  80. package/src/runtime-worktree-executor.mjs +199 -199
  81. package/src/schedulability-compiler-v2.mjs +303 -303
  82. package/src/schedulability-verifier-v2.mjs +317 -317
  83. package/src/seam-apply.mjs +549 -549
  84. package/src/seam-commit-shared.mjs +22 -22
  85. package/src/seam-commit-transform.mjs +81 -81
  86. package/src/seam-commit.mjs +18 -18
  87. package/src/seam-cost.mjs +322 -322
  88. package/src/seam-derivation.mjs +188 -188
  89. package/src/seam-gate.mjs +146 -146
  90. package/src/seam-proposal-contracts.mjs +446 -446
  91. package/src/seam-proposal-queries.mjs +521 -521
  92. package/src/seam-proposal.mjs +2011 -2011
  93. package/src/seam-ref.mjs +33 -33
  94. package/src/seam-rewrite.mjs +286 -286
  95. package/src/seam-transform.mjs +554 -554
  96. package/src/seam-verification.mjs +260 -260
  97. package/src/sensor-adapter.mjs +432 -432
  98. package/src/sensor-cli.mjs +139 -139
  99. package/src/sensor-diff.mjs +661 -661
  100. package/src/sensor-node-runtime.mjs +53 -53
  101. package/src/sensor-runtime.mjs +52 -52
  102. package/src/timestamp-contract.mjs +8 -8
  103. package/src/todo-audit-pending.mjs +91 -91
  104. package/src/todo-chain.mjs +178 -178
  105. package/src/todo-cli.mjs +3143 -3141
  106. package/src/todo-contracts.mjs +728 -728
  107. package/src/todo-dashboard-registry.mjs +573 -573
  108. package/src/todo-dispatch-shape.mjs +190 -190
  109. package/src/todo-gantt-html-independence.mjs +239 -239
  110. package/src/todo-gantt-html-shared.mjs +226 -226
  111. package/src/todo-gantt-html-style.mjs +131 -131
  112. package/src/todo-gantt-html.mjs +248 -248
  113. package/src/todo-gantt-layout.mjs +974 -974
  114. package/src/todo-gantt-live.mjs +361 -361
  115. package/src/todo-gantt-nested.mjs +263 -263
  116. package/src/todo-gantt-presentation.mjs +217 -217
  117. package/src/todo-gantt-scope.mjs +123 -123
  118. package/src/todo-gantt-svg.mjs +353 -353
  119. package/src/todo-independence-contracts.mjs +595 -595
  120. package/src/todo-independence-guidance.mjs +322 -322
  121. package/src/todo-independence.mjs +640 -640
  122. package/src/todo-markdown-renderer.mjs +260 -260
  123. package/src/todo-migration.mjs +448 -448
  124. package/src/todo-narrative-anchor.mjs +130 -130
  125. package/src/todo-note-store.mjs +629 -629
  126. package/src/todo-parallel-candidates.mjs +114 -114
  127. package/src/todo-revision.mjs +995 -995
  128. package/src/todo-split.mjs +472 -472
  129. package/src/todo-status.mjs +690 -690
  130. package/src/todo-store-git-transaction.mjs +418 -418
  131. package/src/todo-store.mjs +4360 -4322
  132. package/src/treatment-compiler.mjs +728 -728
  133. package/src/treatment-runner.mjs +656 -656
  134. package/src/witness-scaffold.mjs +180 -180
@@ -1,1079 +1,1079 @@
1
- import { createHash } from 'node:crypto';
2
-
3
- import {
4
- digestArtifact,
5
- validatePlanInput,
6
- } from './artifact-contracts.mjs';
7
- import { compileBoundaryObservationV2 } from './boundary-observation-compiler-v2.mjs';
8
-
9
- const INPUT_KEYS = [
10
- 'planInput',
11
- 'candidateSpec',
12
- 'manualEvidence',
13
- 'querySet',
14
- 'sourceSnapshot',
15
- 'sensorEvidence',
16
- ];
17
- const EXPECTED_CANDIDATE_ID = 'shard-delivery-policy-registry-by-channel';
18
- const EXPECTED_ORACLE_DIGEST_BY_CANDIDATE_DIGEST = Object.freeze({
19
- '30ee67852f7ab5fb0d9bf82f2a4c55b6569a76507b0df5b329290c84d29b49f5':
20
- 'c4012dfc00cc5b0194bd1a87be4a4e0b20d45e784d49a987768eea1b9932fafe',
21
- '4cc5d7bb428a8899353d18524c25105742fa90f89ee55d36064c4be3c52e2907':
22
- 'c68a7ff9a7c9c4a181ceda6396d5fcbf27084de18680018d244a27998041652c',
23
- });
24
- const ORACLE_RUNNER = 'runRc2DeliveryPolicyOracle';
25
- const QUERY_OPERATIONS = new Set([
26
- 'status',
27
- 'query',
28
- 'callers',
29
- 'callees',
30
- 'impact',
31
- 'affected',
32
- ]);
33
- const SENSOR_OUTCOMES = new Set([
34
- 'ready',
35
- 'symbol_absent',
36
- 'empty',
37
- 'unresolved',
38
- 'command_failure',
39
- 'invalid_json',
40
- 'stale',
41
- 'unsupported',
42
- ]);
43
- const DIGEST = /^[0-9a-f]{64}$/;
44
-
45
- function fail(reason) {
46
- throw new TypeError(`delivery policy front-end契約違反: ${reason}`);
47
- }
48
-
49
- function plainRecord(value) {
50
- return value !== null
51
- && typeof value === 'object'
52
- && !Array.isArray(value)
53
- && Object.getPrototypeOf(value) === Object.prototype;
54
- }
55
-
56
- function exactRecord(value, keys) {
57
- if (!plainRecord(value)) return false;
58
- const actual = Object.keys(value).sort();
59
- const expected = [...keys].sort();
60
- return actual.length === expected.length
61
- && actual.every((key, index) => key === expected[index]);
62
- }
63
-
64
- function requireArray(value, label) {
65
- if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype) {
66
- fail(`${label}がarrayではない`);
67
- }
68
- }
69
-
70
- function boundedText(value) {
71
- return typeof value === 'string'
72
- && value.length > 0
73
- && value === value.trim()
74
- && Buffer.byteLength(value, 'utf8') <= 4_096;
75
- }
76
-
77
- function uniqueBoundedStrings(value, label) {
78
- requireArray(value, label);
79
- if (!value.every(boundedText) || new Set(value).size !== value.length) {
80
- fail(`${label}がunique bounded string arrayではない`);
81
- }
82
- return [...value];
83
- }
84
-
85
- function compareText(left, right) {
86
- if (left < right) return -1;
87
- if (left > right) return 1;
88
- return 0;
89
- }
90
-
91
- function sameStringSet(left, right) {
92
- return left.length === right.length
93
- && new Set(left).size === left.length
94
- && new Set(right).size === right.length
95
- && left.every((entry) => right.includes(entry));
96
- }
97
-
98
- function resourceKey(kind, target) {
99
- return `${kind}\u0000${target}`;
100
- }
101
-
102
- function resourceId(kind, target) {
103
- const digest = createHash('sha256').update(resourceKey(kind, target)).digest('hex');
104
- return `rc2-${kind}-${digest.slice(0, 20)}`;
105
- }
106
-
107
- function queryMapFor(querySet) {
108
- if (!exactRecord(querySet, ['schema', 'queries'])
109
- || querySet.schema !== 'lattice.sensor_query_set.v2') {
110
- fail('query set shapeまたはschemaが不正');
111
- }
112
- requireArray(querySet.queries, 'querySet.queries');
113
- const byId = new Map();
114
- for (const query of querySet.queries) {
115
- if (!plainRecord(query)
116
- || !boundedText(query.id)
117
- || !QUERY_OPERATIONS.has(query.operation)
118
- || byId.has(query.id)) {
119
- fail('query set entryが不正または重複している');
120
- }
121
- if (query.operation === 'status') {
122
- if (!exactRecord(query, ['id', 'operation'])) fail(`query ${query.id} shapeが不正`);
123
- } else if (query.operation === 'affected') {
124
- if (!exactRecord(query, ['id', 'operation', 'targets'])) {
125
- fail(`query ${query.id} shapeが不正`);
126
- }
127
- uniqueBoundedStrings(query.targets, `query ${query.id}.targets`);
128
- } else if (!exactRecord(query, ['id', 'operation', 'target'])
129
- || !boundedText(query.target)) {
130
- fail(`query ${query.id} shapeが不正`);
131
- }
132
- byId.set(query.id, query);
133
- }
134
- if (byId.size < 1) fail('query setが空である');
135
- return byId;
136
- }
137
-
138
- function collectQueryReferences(value, references = new Set()) {
139
- if (Array.isArray(value)) {
140
- value.forEach((entry) => collectQueryReferences(entry, references));
141
- return references;
142
- }
143
- if (!plainRecord(value)) return references;
144
- for (const [key, entry] of Object.entries(value)) {
145
- if ((key === 'query_id' || key === 'impact_id' || key.endsWith('_query_id'))
146
- && typeof entry === 'string') {
147
- references.add(entry);
148
- }
149
- collectQueryReferences(entry, references);
150
- }
151
- return references;
152
- }
153
-
154
- function expectQuery(queryById, id, operation, target, label) {
155
- const query = queryById.get(id);
156
- if (!query
157
- || query.operation !== operation
158
- || (operation !== 'status'
159
- && operation !== 'affected'
160
- && query.target !== target)) {
161
- fail(`${label}のquery bindingが不正`);
162
- }
163
- return query;
164
- }
165
-
166
- function surfaceQueryIds(surface) {
167
- return [
168
- surface.query_id,
169
- surface.callers_query_id,
170
- surface.callees_query_id,
171
- surface.impact_id,
172
- surface.composition_link_query_id,
173
- ].filter((entry) => entry !== undefined);
174
- }
175
-
176
- function validateSurfaceShape(surface, label, { proposedProduction = false } = {}) {
177
- const basicKeys = ['symbol', 'path', 'query_id', 'impact_id'];
178
- const linkedKeys = [...basicKeys, 'callers_query_id', 'callees_query_id'];
179
- const productionKeys = proposedProduction
180
- ? [...linkedKeys, 'composition_link_query_id']
181
- : linkedKeys;
182
- if ((!exactRecord(surface, basicKeys)
183
- && !exactRecord(surface, linkedKeys)
184
- && !exactRecord(surface, productionKeys))
185
- || (surface.symbol !== null && !boundedText(surface.symbol))
186
- || !boundedText(surface.path)
187
- || !boundedText(surface.query_id)
188
- || !boundedText(surface.impact_id)) {
189
- fail(`${label} surface shapeが不正`);
190
- }
191
- for (const id of surfaceQueryIds(surface)) {
192
- if (!boundedText(id)) fail(`${label} query referenceが不正`);
193
- }
194
- }
195
-
196
- function validateSurfaceQueries(surface, queryById, label, fixedOracle) {
197
- const target = surface.symbol ?? surface.path;
198
- expectQuery(queryById, surface.query_id, 'query', target, `${label}.query_id`);
199
- expectQuery(queryById, surface.impact_id, 'impact', target, `${label}.impact_id`);
200
- if (surface.callers_query_id !== undefined) {
201
- expectQuery(
202
- queryById,
203
- surface.callers_query_id,
204
- 'callers',
205
- fixedOracle ? ORACLE_RUNNER : target,
206
- `${label}.callers_query_id`,
207
- );
208
- }
209
- if (surface.callees_query_id !== undefined) {
210
- expectQuery(
211
- queryById,
212
- surface.callees_query_id,
213
- 'callees',
214
- fixedOracle ? ORACLE_RUNNER : target,
215
- `${label}.callees_query_id`,
216
- );
217
- }
218
- if (surface.composition_link_query_id !== undefined) {
219
- expectQuery(
220
- queryById,
221
- surface.composition_link_query_id,
222
- 'callees',
223
- 'resolveDeliveryPolicy',
224
- `${label}.composition_link_query_id`,
225
- );
226
- }
227
- }
228
-
229
- function validateCandidateSpec(candidateSpec, queryById) {
230
- if (!exactRecord(candidateSpec, [
231
- 'schema',
232
- 'candidate_id',
233
- 'plan_input_ref',
234
- 'query_set_ref',
235
- 'compiler_contract',
236
- 'fixed_oracle',
237
- 'stable_surfaces',
238
- 'todos',
239
- ])
240
- || candidateSpec.schema !== 'lattice.rc2.boundary_candidate_spec.v1'
241
- || candidateSpec.candidate_id !== EXPECTED_CANDIDATE_ID) {
242
- fail('candidate spec identityまたはshapeが不正');
243
- }
244
- const candidateDigest = digestArtifact(candidateSpec);
245
- const expectedOracleDigest = EXPECTED_ORACLE_DIGEST_BY_CANDIDATE_DIGEST[candidateDigest];
246
- if (expectedOracleDigest === undefined) {
247
- fail('candidate specがaccepted RC2 witnessと一致しない');
248
- }
249
- if (!exactRecord(candidateSpec.compiler_contract, [
250
- 'condition_selector',
251
- 'surface_resolution',
252
- 'unresolved_policy',
253
- 'status_query_id',
254
- 'affected_query_id',
255
- ])
256
- || candidateSpec.compiler_contract.condition_selector !== 'forbidden'
257
- || candidateSpec.compiler_contract.surface_resolution !== 'exact_name_path_and_link'
258
- || candidateSpec.compiler_contract.unresolved_policy !== 'typed_unknown') {
259
- fail('candidate compiler contractが不正');
260
- }
261
- expectQuery(
262
- queryById,
263
- candidateSpec.compiler_contract.status_query_id,
264
- 'status',
265
- undefined,
266
- 'candidate compiler status',
267
- );
268
- const affected = queryById.get(candidateSpec.compiler_contract.affected_query_id);
269
- if (!affected || affected.operation !== 'affected') {
270
- fail('candidate compiler affected query bindingが不正');
271
- }
272
-
273
- if (!exactRecord(candidateSpec.fixed_oracle, [
274
- 'path',
275
- 'source_digest',
276
- 'case_ids',
277
- 'path_query_id',
278
- 'runner_query_id',
279
- 'callers_query_id',
280
- 'callees_query_id',
281
- 'impact_id',
282
- ])
283
- || !boundedText(candidateSpec.fixed_oracle.path)
284
- || candidateSpec.fixed_oracle.source_digest !== expectedOracleDigest) {
285
- fail('candidate fixed oracle identityが不正');
286
- }
287
- uniqueBoundedStrings(candidateSpec.fixed_oracle.case_ids, 'fixed_oracle.case_ids');
288
- expectQuery(
289
- queryById,
290
- candidateSpec.fixed_oracle.path_query_id,
291
- 'query',
292
- candidateSpec.fixed_oracle.path,
293
- 'fixed oracle path',
294
- );
295
- expectQuery(
296
- queryById,
297
- candidateSpec.fixed_oracle.runner_query_id,
298
- 'query',
299
- ORACLE_RUNNER,
300
- 'fixed oracle runner',
301
- );
302
- expectQuery(
303
- queryById,
304
- candidateSpec.fixed_oracle.callers_query_id,
305
- 'callers',
306
- ORACLE_RUNNER,
307
- 'fixed oracle callers',
308
- );
309
- expectQuery(
310
- queryById,
311
- candidateSpec.fixed_oracle.callees_query_id,
312
- 'callees',
313
- ORACLE_RUNNER,
314
- 'fixed oracle callees',
315
- );
316
- expectQuery(
317
- queryById,
318
- candidateSpec.fixed_oracle.impact_id,
319
- 'impact',
320
- candidateSpec.fixed_oracle.path,
321
- 'fixed oracle impact',
322
- );
323
-
324
- requireArray(candidateSpec.stable_surfaces, 'candidateSpec.stable_surfaces');
325
- if (candidateSpec.stable_surfaces.length !== 3) fail('stable surface件数が不正');
326
- const stableRoles = new Set();
327
- for (const surface of candidateSpec.stable_surfaces) {
328
- if (!exactRecord(surface, [
329
- 'kind',
330
- 'target',
331
- 'path',
332
- 'role',
333
- 'active_when',
334
- 'query_id',
335
- 'callers_query_id',
336
- 'callees_query_id',
337
- 'impact_id',
338
- ])
339
- || !new Set(['symbol', 'path']).has(surface.kind)
340
- || !boundedText(surface.target)
341
- || !boundedText(surface.path)
342
- || !new Set(['proposed', 'always']).has(surface.active_when)
343
- || stableRoles.has(surface.role)) {
344
- fail('stable surface shapeまたはroleが不正');
345
- }
346
- stableRoles.add(surface.role);
347
- const fixedOracle = surface.role === 'fixed-oracle';
348
- expectQuery(queryById, surface.query_id, 'query', surface.target, `${surface.role}.query`);
349
- expectQuery(
350
- queryById,
351
- surface.callers_query_id,
352
- 'callers',
353
- fixedOracle ? ORACLE_RUNNER : surface.target,
354
- `${surface.role}.callers`,
355
- );
356
- expectQuery(
357
- queryById,
358
- surface.callees_query_id,
359
- 'callees',
360
- fixedOracle ? ORACLE_RUNNER : surface.target,
361
- `${surface.role}.callees`,
362
- );
363
- expectQuery(queryById, surface.impact_id, 'impact', surface.target, `${surface.role}.impact`);
364
- }
365
- if (!sameStringSet(
366
- [...stableRoles],
367
- ['composition-entry', 'composition-test', 'fixed-oracle'],
368
- )) {
369
- fail('stable surface role集合が不正');
370
- }
371
-
372
- requireArray(candidateSpec.todos, 'candidateSpec.todos');
373
- if (candidateSpec.todos.length !== 3) fail('candidate TODO件数が不正');
374
- const todoIds = new Set();
375
- const caseIds = [];
376
- for (const candidateTodo of candidateSpec.todos) {
377
- if (!exactRecord(candidateTodo, [
378
- 'todo_id',
379
- 'outcome',
380
- 'case_ids',
381
- 'current',
382
- 'proposed',
383
- ])
384
- || !boundedText(candidateTodo.todo_id)
385
- || !boundedText(candidateTodo.outcome)
386
- || todoIds.has(candidateTodo.todo_id)) {
387
- fail('candidate TODO shapeまたはIDが不正');
388
- }
389
- todoIds.add(candidateTodo.todo_id);
390
- caseIds.push(...uniqueBoundedStrings(candidateTodo.case_ids, `${candidateTodo.todo_id}.case_ids`));
391
- for (const mode of ['current', 'proposed']) {
392
- const modeSpec = candidateTodo[mode];
393
- if (!exactRecord(modeSpec, ['production', 'tests'])) {
394
- fail(`${candidateTodo.todo_id}/${mode} shapeが不正`);
395
- }
396
- validateSurfaceShape(
397
- modeSpec.production,
398
- `${candidateTodo.todo_id}/${mode}/production`,
399
- { proposedProduction: mode === 'proposed' },
400
- );
401
- requireArray(modeSpec.tests, `${candidateTodo.todo_id}/${mode}.tests`);
402
- const expectedTests = mode === 'current' ? 2 : 1;
403
- if (modeSpec.tests.length !== expectedTests) {
404
- fail(`${candidateTodo.todo_id}/${mode} test surface件数が不正`);
405
- }
406
- modeSpec.tests.forEach((surface, index) => {
407
- validateSurfaceShape(surface, `${candidateTodo.todo_id}/${mode}/tests/${index}`);
408
- });
409
- validateSurfaceQueries(
410
- modeSpec.production,
411
- queryById,
412
- `${candidateTodo.todo_id}/${mode}/production`,
413
- false,
414
- );
415
- modeSpec.tests.forEach((surface, index) => validateSurfaceQueries(
416
- surface,
417
- queryById,
418
- `${candidateTodo.todo_id}/${mode}/tests/${index}`,
419
- surface.path === candidateSpec.fixed_oracle.path,
420
- ));
421
- }
422
- }
423
- if (new Set(caseIds).size !== caseIds.length
424
- || !sameStringSet(caseIds, candidateSpec.fixed_oracle.case_ids)) {
425
- fail('candidate case IDがfixed oracleのexact partitionではない');
426
- }
427
-
428
- const queryReferences = [...collectQueryReferences(candidateSpec)];
429
- if (!sameStringSet(queryReferences, [...queryById.keys()])) {
430
- fail('candidate query referenceとquery setがexact/exhaustiveではない');
431
- }
432
- return candidateDigest;
433
- }
434
-
435
- function manualEvidenceByTodo(manualEvidence) {
436
- if (!exactRecord(manualEvidence, ['schema', 'evidence'])
437
- || manualEvidence.schema !== 'lattice.manual_boundary_evidence.v1') {
438
- fail('manual evidence shapeまたはschemaが不正');
439
- }
440
- requireArray(manualEvidence.evidence, 'manualEvidence.evidence');
441
- const byTodo = new Map();
442
- for (const entry of manualEvidence.evidence) {
443
- if (!exactRecord(entry, [
444
- 'todo_id',
445
- 'state_reads',
446
- 'state_writes',
447
- 'effects',
448
- 'unknowns',
449
- ])
450
- || !boundedText(entry.todo_id)
451
- || byTodo.has(entry.todo_id)) {
452
- fail('manual evidence entryが不正またはTODO重複している');
453
- }
454
- uniqueBoundedStrings(entry.state_reads, `${entry.todo_id}.state_reads`);
455
- uniqueBoundedStrings(entry.state_writes, `${entry.todo_id}.state_writes`);
456
- uniqueBoundedStrings(entry.effects, `${entry.todo_id}.effects`);
457
- uniqueBoundedStrings(entry.unknowns, `${entry.todo_id}.unknowns`);
458
- if (entry.state_reads.length > 0) {
459
- fail('manual state_readsをwrite conflictへ損失変換できない');
460
- }
461
- byTodo.set(entry.todo_id, entry);
462
- }
463
- if (byTodo.size < 1) fail('manual evidenceが空である');
464
- return byTodo;
465
- }
466
-
467
- function assertTodoBindings(planInput, candidateSpec, manualByTodo) {
468
- const planIds = planInput.todos.map(({ id }) => id);
469
- const candidateIds = candidateSpec.todos.map(({ todo_id: todoId }) => todoId);
470
- const manualIds = [...manualByTodo.keys()];
471
- if (!sameStringSet(planIds, candidateIds) || !sameStringSet(planIds, manualIds)) {
472
- fail('plan/candidate/manualのTODO集合が一致しない');
473
- }
474
- if (planInput.query_set_ref !== candidateSpec.query_set_ref
475
- || planInput.project.fixture_entry !== candidateSpec.todos[0].current.production.path) {
476
- fail('plan inputがcandidate fixture/query setへbindしていない');
477
- }
478
- const planByTodo = new Map(planInput.todos.map((todo) => [todo.id, todo]));
479
- for (const candidateTodo of candidateSpec.todos) {
480
- const planTodo = planByTodo.get(candidateTodo.todo_id);
481
- if (planTodo.outcome !== candidateTodo.outcome
482
- || planTodo.anchor.symbol !== candidateTodo.current.production.symbol
483
- || planTodo.anchor.path !== candidateTodo.current.production.path) {
484
- fail(`candidate ${candidateTodo.todo_id}がplan TODOへbindしていない`);
485
- }
486
- }
487
- }
488
-
489
- function candidatePaths(candidateSpec) {
490
- const current = new Set([candidateSpec.fixed_oracle.path]);
491
- const all = new Set([candidateSpec.fixed_oracle.path]);
492
- for (const candidateTodo of candidateSpec.todos) {
493
- current.add(candidateTodo.current.production.path);
494
- all.add(candidateTodo.current.production.path);
495
- for (const testSurface of candidateTodo.current.tests) {
496
- current.add(testSurface.path);
497
- all.add(testSurface.path);
498
- }
499
- all.add(candidateTodo.proposed.production.path);
500
- for (const testSurface of candidateTodo.proposed.tests) all.add(testSurface.path);
501
- }
502
- for (const stable of candidateSpec.stable_surfaces) all.add(stable.path);
503
- return {
504
- current: [...current].sort(compareText),
505
- all: [...all].sort(compareText),
506
- };
507
- }
508
-
509
- function validateSourceSnapshot(sourceSnapshot, candidateSpec) {
510
- if (!exactRecord(sourceSnapshot, ['schema_version', 'files'])
511
- || sourceSnapshot.schema_version !== 'lattice.rc2.source_snapshot.v1') {
512
- fail('sourceSnapshot shapeまたはschemaが不正');
513
- }
514
- requireArray(sourceSnapshot.files, 'sourceSnapshot.files');
515
- const paths = candidatePaths(candidateSpec);
516
- if (sourceSnapshot.files.length !== paths.all.length) {
517
- fail('sourceSnapshot path件数がcandidateと一致しない');
518
- }
519
- const byPath = new Map();
520
- for (const file of sourceSnapshot.files) {
521
- if (!exactRecord(file, ['path', 'state', 'content_digest'])
522
- || !boundedText(file.path)
523
- || !new Set(['file', 'absent']).has(file.state)
524
- || (file.state === 'file' && (typeof file.content_digest !== 'string'
525
- || !DIGEST.test(file.content_digest)))
526
- || (file.state === 'absent' && file.content_digest !== null)
527
- || byPath.has(file.path)) {
528
- fail('sourceSnapshot file entryが不正または重複している');
529
- }
530
- byPath.set(file.path, file);
531
- }
532
- if (!sameStringSet([...byPath.keys()], paths.all)) {
533
- fail('sourceSnapshot path集合がcandidate exact surfaceと一致しない');
534
- }
535
- const oracle = byPath.get(candidateSpec.fixed_oracle.path);
536
- if (oracle.state !== 'file'
537
- || oracle.content_digest !== candidateSpec.fixed_oracle.source_digest) {
538
- fail('sourceSnapshot fixed oracle digestがcandidateと一致しない');
539
- }
540
- const present = [...byPath.values()]
541
- .filter(({ state }) => state === 'file')
542
- .map(({ path }) => path)
543
- .sort(compareText);
544
- if (sameStringSet(present, paths.current)) {
545
- return { topology: 'current', filesByPath: byPath, paths };
546
- }
547
- if (sameStringSet(present, paths.all)) {
548
- return { topology: 'proposed', filesByPath: byPath, paths };
549
- }
550
- fail('sourceSnapshotがcurrent/proposed exact topologyのどちらでもない');
551
- }
552
-
553
- function validatePortableOutcomeShape(outcome, query) {
554
- if (!plainRecord(outcome)
555
- || outcome.id !== query.id
556
- || outcome.operation !== query.operation
557
- || !SENSOR_OUTCOMES.has(outcome.outcome)) {
558
- fail(`portable outcome ${query.id} identityが不正`);
559
- }
560
- if (query.operation === 'status') {
561
- if (outcome.outcome !== 'ready'
562
- || !exactRecord(outcome, ['id', 'operation', 'outcome', 'data'])
563
- || !plainRecord(outcome.data)) {
564
- fail('portable statusがready preimageではない');
565
- }
566
- return;
567
- }
568
- if (query.operation === 'affected') {
569
- if (!exactRecord(outcome, ['id', 'operation', 'outcome', 'targets'])) {
570
- fail('portable affected shapeが不正');
571
- }
572
- requireArray(outcome.targets, `portable ${query.id}.targets`);
573
- return;
574
- }
575
- if (outcome.target !== query.target) {
576
- fail(`portable outcome ${query.id} targetがqueryと一致しない`);
577
- }
578
- if (outcome.outcome === 'ready') {
579
- const keys = query.operation === 'query'
580
- ? ['id', 'operation', 'target', 'outcome', 'data']
581
- : ['id', 'operation', 'target', 'outcome', 'data', 'resolution'];
582
- if (!exactRecord(outcome, keys)) fail(`portable ready outcome ${query.id} shapeが不正`);
583
- if (query.operation === 'query') requireArray(outcome.data, `${query.id}.data`);
584
- else {
585
- if (!plainRecord(outcome.data)) fail(`${query.id}.dataがrecordではない`);
586
- requireArray(outcome.resolution, `${query.id}.resolution`);
587
- }
588
- return;
589
- }
590
- if (outcome.outcome === 'symbol_absent') {
591
- const keys = query.operation === 'query'
592
- ? ['id', 'operation', 'target', 'outcome', 'data']
593
- : ['id', 'operation', 'target', 'outcome'];
594
- if (!exactRecord(outcome, keys)
595
- || (query.operation === 'query' && (!Array.isArray(outcome.data)
596
- || outcome.data.length !== 0))) {
597
- fail(`portable symbol_absent outcome ${query.id} shapeが不正`);
598
- }
599
- }
600
- }
601
-
602
- function assertFreshStatus(statusOutcome) {
603
- const status = statusOutcome.data;
604
- if (status.initialized !== true
605
- || !exactRecord(status.pendingChanges, ['added', 'modified', 'removed'])
606
- || Object.values(status.pendingChanges).some((value) => value !== 0)
607
- || status.worktreeMismatch !== null
608
- || !plainRecord(status.index)
609
- || status.index.state !== 'complete'
610
- || status.index.pendingRefs !== 0
611
- || status.index.reindexRecommended !== false) {
612
- fail('portable LatticeSensor statusがfresh/completeではない');
613
- }
614
- }
615
-
616
- function validateAffectedOutcome(outcome, query, filesByPath) {
617
- if (outcome.targets.length !== query.targets.length) {
618
- fail('portable affected target件数がqueryと一致しない');
619
- }
620
- for (let index = 0; index < query.targets.length; index += 1) {
621
- const target = query.targets[index];
622
- const result = outcome.targets[index];
623
- const present = filesByPath.get(target)?.state === 'file';
624
- if (!exactRecord(result, ['target', 'outcome', 'data'])
625
- || result.target !== target
626
- || result.outcome !== (present ? 'ready' : 'empty')
627
- || !plainRecord(result.data)
628
- || !Array.isArray(result.data.changedFiles)
629
- || !sameStringSet(result.data.changedFiles, [target])
630
- || !Array.isArray(result.data.affectedTests)
631
- || (present && result.data.affectedTests.length < 1)
632
- || (!present && result.data.affectedTests.length !== 0)) {
633
- fail(`portable affected ${target}がsnapshot存在状態へbindしていない`);
634
- }
635
- }
636
- const expected = outcome.targets.every(({ outcome: status }) => status === 'ready')
637
- ? 'ready'
638
- : 'unresolved';
639
- if (outcome.outcome !== expected) {
640
- fail('portable affected aggregate outcomeがtarget outcomesと一致しない');
641
- }
642
- }
643
-
644
- function validatePortableEvidence({
645
- sensorEvidence,
646
- querySet,
647
- queryById,
648
- candidateSpec,
649
- filesByPath,
650
- }) {
651
- if (!exactRecord(sensorEvidence, [
652
- 'schema',
653
- 'projection',
654
- 'query_set_digest',
655
- 'outcomes',
656
- 'per_query',
657
- 'aggregate_digest',
658
- ])
659
- || sensorEvidence.schema !== 'lattice.sensor_portable_preimage.v1'
660
- || sensorEvidence.projection !== 'lattice.sensor_portable_outcome.v1'
661
- || sensorEvidence.query_set_digest !== digestArtifact(querySet)) {
662
- fail('portable evidence identity/query bindingが不正');
663
- }
664
- requireArray(sensorEvidence.outcomes, 'portable.outcomes');
665
- requireArray(sensorEvidence.per_query, 'portable.per_query');
666
- if (sensorEvidence.outcomes.length !== querySet.queries.length
667
- || sensorEvidence.per_query.length !== querySet.queries.length) {
668
- fail('portable outcome件数がquery setと一致しない');
669
- }
670
- const byId = new Map();
671
- for (let index = 0; index < querySet.queries.length; index += 1) {
672
- const query = querySet.queries[index];
673
- const outcome = sensorEvidence.outcomes[index];
674
- const receipt = sensorEvidence.per_query[index];
675
- validatePortableOutcomeShape(outcome, query);
676
- if (!exactRecord(receipt, ['id', 'operation', 'outcome', 'result_digest'])
677
- || receipt.id !== query.id
678
- || receipt.operation !== query.operation
679
- || receipt.outcome !== outcome.outcome
680
- || receipt.result_digest !== digestArtifact(outcome)) {
681
- fail(`portable per-query digest ${query.id}がoutcomeと一致しない`);
682
- }
683
- byId.set(query.id, outcome);
684
- }
685
- const aggregatePreimage = {
686
- projection: sensorEvidence.projection,
687
- query_set_digest: sensorEvidence.query_set_digest,
688
- outcomes: sensorEvidence.outcomes,
689
- };
690
- if (!DIGEST.test(sensorEvidence.aggregate_digest)
691
- || sensorEvidence.aggregate_digest !== digestArtifact(aggregatePreimage)) {
692
- fail('portable aggregate digestがfull preimageから再計算できない');
693
- }
694
- assertFreshStatus(byId.get(candidateSpec.compiler_contract.status_query_id));
695
- const affectedQuery = queryById.get(candidateSpec.compiler_contract.affected_query_id);
696
- validateAffectedOutcome(
697
- byId.get(candidateSpec.compiler_contract.affected_query_id),
698
- affectedQuery,
699
- filesByPath,
700
- );
701
- return byId;
702
- }
703
-
704
- function isExactNodeCandidate(entry, target, path) {
705
- const node = entry?.node;
706
- if (!plainRecord(node) || node.filePath !== path) return false;
707
- if (target.includes('/')) return node.qualifiedName === target;
708
- return node.name === target || node.qualifiedName === target;
709
- }
710
-
711
- function assertReadyQuery(outcomeById, id, target, path, label) {
712
- const outcome = outcomeById.get(id);
713
- if (!outcome || outcome.outcome !== 'ready' || outcome.operation !== 'query') {
714
- fail(`${label} queryがreadyではない`);
715
- }
716
- const exact = outcome.data.filter((entry) => isExactNodeCandidate(entry, target, path));
717
- if (exact.length !== 1) fail(`${label} exact name/path resolutionが一意でない`);
718
- }
719
-
720
- function assertReadyResolution(outcomeById, id, target, path, operation, label) {
721
- const outcome = outcomeById.get(id);
722
- if (!outcome || outcome.outcome !== 'ready' || outcome.operation !== operation) {
723
- fail(`${label} ${operation}がreadyではない`);
724
- }
725
- const exact = outcome.resolution.filter((entry) => isExactNodeCandidate(entry, target, path));
726
- if (exact.length !== 1) fail(`${label} ${operation} exact resolutionが一意でない`);
727
- }
728
-
729
- function assertSurfaceReady(outcomeById, surface, label, fixedOracle = false) {
730
- const target = surface.symbol ?? surface.path;
731
- assertReadyQuery(outcomeById, surface.query_id, target, surface.path, label);
732
- assertReadyResolution(
733
- outcomeById,
734
- surface.impact_id,
735
- target,
736
- surface.path,
737
- 'impact',
738
- label,
739
- );
740
- if (surface.callers_query_id !== undefined) {
741
- assertReadyResolution(
742
- outcomeById,
743
- surface.callers_query_id,
744
- fixedOracle ? ORACLE_RUNNER : target,
745
- surface.path,
746
- 'callers',
747
- label,
748
- );
749
- }
750
- if (surface.callees_query_id !== undefined) {
751
- assertReadyResolution(
752
- outcomeById,
753
- surface.callees_query_id,
754
- fixedOracle ? ORACLE_RUNNER : target,
755
- surface.path,
756
- 'callees',
757
- label,
758
- );
759
- }
760
- }
761
-
762
- function containsExactSummary(entries, target, path) {
763
- return Array.isArray(entries)
764
- && entries.some((entry) => plainRecord(entry)
765
- && entry.name === target
766
- && entry.filePath === path);
767
- }
768
-
769
- function assertCalleeLink(outcomeById, queryId, target, path, label) {
770
- const outcome = outcomeById.get(queryId);
771
- if (!outcome
772
- || outcome.outcome !== 'ready'
773
- || outcome.operation !== 'callees'
774
- || !containsExactSummary(outcome.data?.callees, target, path)) {
775
- fail(`${label} exact callee linkが観測されない`);
776
- }
777
- }
778
-
779
- function stableByRole(candidateSpec, role) {
780
- return candidateSpec.stable_surfaces.find((surface) => surface.role === role);
781
- }
782
-
783
- function assertActivation(candidateSpec, topology, outcomeById, queryById) {
784
- const fixed = candidateSpec.fixed_oracle;
785
- const fixedSurface = {
786
- symbol: null,
787
- path: fixed.path,
788
- query_id: fixed.path_query_id,
789
- callers_query_id: fixed.callers_query_id,
790
- callees_query_id: fixed.callees_query_id,
791
- impact_id: fixed.impact_id,
792
- };
793
- assertSurfaceReady(outcomeById, fixedSurface, 'fixed oracle', true);
794
- assertReadyQuery(outcomeById, fixed.runner_query_id, ORACLE_RUNNER, fixed.path, 'fixed oracle runner');
795
-
796
- if (topology === 'current') {
797
- const inactive = new Set();
798
- for (const candidateTodo of candidateSpec.todos) {
799
- assertSurfaceReady(
800
- outcomeById,
801
- candidateTodo.current.production,
802
- `${candidateTodo.todo_id}/current/production`,
803
- );
804
- candidateTodo.current.tests.forEach((surface, index) => assertSurfaceReady(
805
- outcomeById,
806
- surface,
807
- `${candidateTodo.todo_id}/current/tests/${index}`,
808
- surface.path === fixed.path,
809
- ));
810
- const proposed = candidateTodo.proposed;
811
- [proposed.production, ...proposed.tests].forEach((surface) => {
812
- surfaceQueryIds(surface)
813
- .filter((id) => id !== proposed.production.composition_link_query_id)
814
- .forEach((id) => inactive.add(id));
815
- });
816
- }
817
- const compositionTest = stableByRole(candidateSpec, 'composition-test');
818
- surfaceQueryIds(compositionTest).forEach((id) => inactive.add(id));
819
- for (const id of inactive) {
820
- const outcome = outcomeById.get(id);
821
- if (!outcome || outcome.outcome !== 'symbol_absent') {
822
- fail(`current topologyのinactive proposed query ${id}がsymbol_absentではない`);
823
- }
824
- }
825
- return;
826
- }
827
-
828
- for (const query of queryById.values()) {
829
- if (query.operation === 'status' || query.operation === 'affected') continue;
830
- if (outcomeById.get(query.id)?.outcome !== 'ready') {
831
- fail(`proposed topologyのquery ${query.id}がreadyではない`);
832
- }
833
- }
834
- const compositionEntry = stableByRole(candidateSpec, 'composition-entry');
835
- const compositionTest = stableByRole(candidateSpec, 'composition-test');
836
- assertSurfaceReady(outcomeById, {
837
- symbol: compositionEntry.target,
838
- path: compositionEntry.path,
839
- query_id: compositionEntry.query_id,
840
- callers_query_id: compositionEntry.callers_query_id,
841
- callees_query_id: compositionEntry.callees_query_id,
842
- impact_id: compositionEntry.impact_id,
843
- }, 'composition entry');
844
- assertSurfaceReady(outcomeById, {
845
- symbol: compositionTest.target,
846
- path: compositionTest.path,
847
- query_id: compositionTest.query_id,
848
- callers_query_id: compositionTest.callers_query_id,
849
- callees_query_id: compositionTest.callees_query_id,
850
- impact_id: compositionTest.impact_id,
851
- }, 'composition test');
852
- for (const candidateTodo of candidateSpec.todos) {
853
- const production = candidateTodo.proposed.production;
854
- const testSurface = candidateTodo.proposed.tests[0];
855
- assertSurfaceReady(
856
- outcomeById,
857
- production,
858
- `${candidateTodo.todo_id}/proposed/production`,
859
- );
860
- assertSurfaceReady(
861
- outcomeById,
862
- testSurface,
863
- `${candidateTodo.todo_id}/proposed/test`,
864
- );
865
- assertCalleeLink(
866
- outcomeById,
867
- production.composition_link_query_id,
868
- production.symbol,
869
- production.path,
870
- `${candidateTodo.todo_id}/composition-entry`,
871
- );
872
- assertCalleeLink(
873
- outcomeById,
874
- testSurface.callees_query_id,
875
- production.symbol,
876
- production.path,
877
- `${candidateTodo.todo_id}/dedicated-test`,
878
- );
879
- }
880
- assertCalleeLink(
881
- outcomeById,
882
- compositionTest.callees_query_id,
883
- compositionEntry.target,
884
- compositionEntry.path,
885
- 'composition-test/entry',
886
- );
887
- }
888
-
889
- function evidenceIdsForSurface(surface, candidateSpec) {
890
- const ids = surfaceQueryIds(surface);
891
- if (surface.path === candidateSpec.fixed_oracle.path) {
892
- ids.push(candidateSpec.fixed_oracle.runner_query_id);
893
- }
894
- return [...new Set(ids)].sort(compareText);
895
- }
896
-
897
- function structuralResources({
898
- candidateSpec,
899
- candidateDigest,
900
- topology,
901
- outcomeById,
902
- sensorEvidence,
903
- }) {
904
- const groups = new Map();
905
- const add = (kind, target, todoId, surface, ref) => {
906
- const key = resourceKey(kind, target);
907
- const group = groups.get(key) ?? {
908
- kind,
909
- target,
910
- todoIds: new Set(),
911
- queryIds: new Set(),
912
- candidateRefs: new Set(),
913
- };
914
- group.todoIds.add(todoId);
915
- evidenceIdsForSurface(surface, candidateSpec).forEach((id) => group.queryIds.add(id));
916
- group.candidateRefs.add(ref);
917
- groups.set(key, group);
918
- };
919
-
920
- for (const candidateTodo of candidateSpec.todos) {
921
- const mode = topology === 'current' ? 'current' : 'proposed';
922
- const modeSpec = candidateTodo[mode];
923
- add(
924
- 'symbol',
925
- modeSpec.production.symbol,
926
- candidateTodo.todo_id,
927
- modeSpec.production,
928
- `${candidateTodo.todo_id}/${mode}/production/symbol`,
929
- );
930
- add(
931
- 'path',
932
- modeSpec.production.path,
933
- candidateTodo.todo_id,
934
- modeSpec.production,
935
- `${candidateTodo.todo_id}/${mode}/production/path`,
936
- );
937
- modeSpec.tests.forEach((surface, index) => {
938
- add(
939
- 'path',
940
- surface.path,
941
- candidateTodo.todo_id,
942
- surface,
943
- `${candidateTodo.todo_id}/${mode}/tests/${index}/path`,
944
- );
945
- if (surface.symbol !== null) {
946
- add(
947
- 'symbol',
948
- surface.symbol,
949
- candidateTodo.todo_id,
950
- surface,
951
- `${candidateTodo.todo_id}/${mode}/tests/${index}/symbol`,
952
- );
953
- }
954
- });
955
- }
956
-
957
- return [...groups.values()].map((group) => {
958
- const queryIds = [...group.queryIds].sort(compareText);
959
- const outcomes = queryIds.map((id) => outcomeById.get(id));
960
- if (outcomes.some((outcome) => !outcome || outcome.outcome !== 'ready')) {
961
- fail(`active resource ${group.kind}:${group.target}のLatticeSensor evidenceがreadyではない`);
962
- }
963
- const evidenceDigest = digestArtifact({
964
- schema: 'lattice.rc2.sensor_resource_evidence.v1',
965
- topology,
966
- portable_aggregate_digest: sensorEvidence.aggregate_digest,
967
- query_ids: queryIds,
968
- outcomes,
969
- });
970
- return {
971
- resource_id: resourceId(group.kind, group.target),
972
- kind: group.kind,
973
- target: group.target,
974
- todo_ids: [...group.todoIds].sort(compareText),
975
- provenance: [
976
- {
977
- source: 'sensor',
978
- evidence_ref: `portable-sensor#${queryIds.join(',')}`,
979
- evidence_digest: evidenceDigest,
980
- status: 'ready',
981
- },
982
- {
983
- source: 'manual_candidate_spec',
984
- evidence_ref: `candidate-spec#${[...group.candidateRefs].sort(compareText).join(',')}`,
985
- evidence_digest: candidateDigest,
986
- status: 'asserted',
987
- },
988
- ],
989
- };
990
- });
991
- }
992
-
993
- function manualResources(manualByTodo, manualEvidenceDigest) {
994
- const groups = new Map();
995
- const add = (kind, target, todoId) => {
996
- const key = resourceKey(kind, target);
997
- const group = groups.get(key) ?? { kind, target, todoIds: new Set() };
998
- group.todoIds.add(todoId);
999
- groups.set(key, group);
1000
- };
1001
- for (const [todoId, entry] of manualByTodo) {
1002
- entry.state_writes.forEach((target) => add('state', target, todoId));
1003
- entry.effects.forEach((target) => add('effect', target, todoId));
1004
- entry.unknowns.forEach((target) => add('dynamic', target, todoId));
1005
- }
1006
- return [...groups.values()].map((group) => ({
1007
- resource_id: resourceId(group.kind, group.target),
1008
- kind: group.kind,
1009
- target: group.target,
1010
- todo_ids: [...group.todoIds].sort(compareText),
1011
- provenance: [{
1012
- source: 'manual_state_effect',
1013
- evidence_ref: `manual-evidence#${group.kind}/${resourceId(group.kind, group.target)}`,
1014
- evidence_digest: manualEvidenceDigest,
1015
- status: 'asserted',
1016
- }],
1017
- }));
1018
- }
1019
-
1020
- /**
1021
- * RC2 delivery-policy witnessをprovenance付きnormalized boundary bundleへ変換する。
1022
- * @param {unknown} inputs
1023
- * @returns {object}
1024
- */
1025
- export function compileDeliveryPolicyBoundaryBundleV2(inputs) {
1026
- if (!exactRecord(inputs, INPUT_KEYS)) fail('input shapeが不正');
1027
- const {
1028
- planInput,
1029
- candidateSpec,
1030
- manualEvidence,
1031
- querySet,
1032
- sourceSnapshot,
1033
- sensorEvidence,
1034
- } = inputs;
1035
-
1036
- if (!validatePlanInput(planInput)) fail('plan inputが不正');
1037
- const queryById = queryMapFor(querySet);
1038
- const candidateDigest = validateCandidateSpec(candidateSpec, queryById);
1039
- const manualByTodo = manualEvidenceByTodo(manualEvidence);
1040
- assertTodoBindings(planInput, candidateSpec, manualByTodo);
1041
- const snapshot = validateSourceSnapshot(sourceSnapshot, candidateSpec);
1042
- const outcomeById = validatePortableEvidence({
1043
- sensorEvidence,
1044
- querySet,
1045
- queryById,
1046
- candidateSpec,
1047
- filesByPath: snapshot.filesByPath,
1048
- });
1049
- assertActivation(candidateSpec, snapshot.topology, outcomeById, queryById);
1050
-
1051
- const manualEvidenceDigest = digestArtifact(manualEvidence);
1052
- const resources = [
1053
- ...structuralResources({
1054
- candidateSpec,
1055
- candidateDigest,
1056
- topology: snapshot.topology,
1057
- outcomeById,
1058
- sensorEvidence,
1059
- }),
1060
- ...manualResources(manualByTodo, manualEvidenceDigest),
1061
- ];
1062
- if (new Set(resources.map(({ resource_id: id }) => id)).size !== resources.length) {
1063
- fail('resource ID collisionを検出した');
1064
- }
1065
-
1066
- return compileBoundaryObservationV2({
1067
- schema_version: 'lattice.boundary_observation_set.v2',
1068
- source: {
1069
- snapshot_digest: digestArtifact(sourceSnapshot),
1070
- candidate_witness_digest: candidateDigest,
1071
- query_set_digest: digestArtifact(querySet),
1072
- manual_evidence_digest: manualEvidenceDigest,
1073
- },
1074
- capacity: planInput.capacity.writers,
1075
- todos: planInput.todos.map(({ id }) => id),
1076
- resources,
1077
- precedences: [],
1078
- });
1079
- }
1
+ import { createHash } from 'node:crypto';
2
+
3
+ import {
4
+ digestArtifact,
5
+ validatePlanInput,
6
+ } from './artifact-contracts.mjs';
7
+ import { compileBoundaryObservationV2 } from './boundary-observation-compiler-v2.mjs';
8
+
9
+ const INPUT_KEYS = [
10
+ 'planInput',
11
+ 'candidateSpec',
12
+ 'manualEvidence',
13
+ 'querySet',
14
+ 'sourceSnapshot',
15
+ 'sensorEvidence',
16
+ ];
17
+ const EXPECTED_CANDIDATE_ID = 'shard-delivery-policy-registry-by-channel';
18
+ const EXPECTED_ORACLE_DIGEST_BY_CANDIDATE_DIGEST = Object.freeze({
19
+ '30ee67852f7ab5fb0d9bf82f2a4c55b6569a76507b0df5b329290c84d29b49f5':
20
+ 'c4012dfc00cc5b0194bd1a87be4a4e0b20d45e784d49a987768eea1b9932fafe',
21
+ '4cc5d7bb428a8899353d18524c25105742fa90f89ee55d36064c4be3c52e2907':
22
+ 'c68a7ff9a7c9c4a181ceda6396d5fcbf27084de18680018d244a27998041652c',
23
+ });
24
+ const ORACLE_RUNNER = 'runRc2DeliveryPolicyOracle';
25
+ const QUERY_OPERATIONS = new Set([
26
+ 'status',
27
+ 'query',
28
+ 'callers',
29
+ 'callees',
30
+ 'impact',
31
+ 'affected',
32
+ ]);
33
+ const SENSOR_OUTCOMES = new Set([
34
+ 'ready',
35
+ 'symbol_absent',
36
+ 'empty',
37
+ 'unresolved',
38
+ 'command_failure',
39
+ 'invalid_json',
40
+ 'stale',
41
+ 'unsupported',
42
+ ]);
43
+ const DIGEST = /^[0-9a-f]{64}$/;
44
+
45
+ function fail(reason) {
46
+ throw new TypeError(`delivery policy front-end契約違反: ${reason}`);
47
+ }
48
+
49
+ function plainRecord(value) {
50
+ return value !== null
51
+ && typeof value === 'object'
52
+ && !Array.isArray(value)
53
+ && Object.getPrototypeOf(value) === Object.prototype;
54
+ }
55
+
56
+ function exactRecord(value, keys) {
57
+ if (!plainRecord(value)) return false;
58
+ const actual = Object.keys(value).sort();
59
+ const expected = [...keys].sort();
60
+ return actual.length === expected.length
61
+ && actual.every((key, index) => key === expected[index]);
62
+ }
63
+
64
+ function requireArray(value, label) {
65
+ if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype) {
66
+ fail(`${label}がarrayではない`);
67
+ }
68
+ }
69
+
70
+ function boundedText(value) {
71
+ return typeof value === 'string'
72
+ && value.length > 0
73
+ && value === value.trim()
74
+ && Buffer.byteLength(value, 'utf8') <= 4_096;
75
+ }
76
+
77
+ function uniqueBoundedStrings(value, label) {
78
+ requireArray(value, label);
79
+ if (!value.every(boundedText) || new Set(value).size !== value.length) {
80
+ fail(`${label}がunique bounded string arrayではない`);
81
+ }
82
+ return [...value];
83
+ }
84
+
85
+ function compareText(left, right) {
86
+ if (left < right) return -1;
87
+ if (left > right) return 1;
88
+ return 0;
89
+ }
90
+
91
+ function sameStringSet(left, right) {
92
+ return left.length === right.length
93
+ && new Set(left).size === left.length
94
+ && new Set(right).size === right.length
95
+ && left.every((entry) => right.includes(entry));
96
+ }
97
+
98
+ function resourceKey(kind, target) {
99
+ return `${kind}\u0000${target}`;
100
+ }
101
+
102
+ function resourceId(kind, target) {
103
+ const digest = createHash('sha256').update(resourceKey(kind, target)).digest('hex');
104
+ return `rc2-${kind}-${digest.slice(0, 20)}`;
105
+ }
106
+
107
+ function queryMapFor(querySet) {
108
+ if (!exactRecord(querySet, ['schema', 'queries'])
109
+ || querySet.schema !== 'lattice.sensor_query_set.v2') {
110
+ fail('query set shapeまたはschemaが不正');
111
+ }
112
+ requireArray(querySet.queries, 'querySet.queries');
113
+ const byId = new Map();
114
+ for (const query of querySet.queries) {
115
+ if (!plainRecord(query)
116
+ || !boundedText(query.id)
117
+ || !QUERY_OPERATIONS.has(query.operation)
118
+ || byId.has(query.id)) {
119
+ fail('query set entryが不正または重複している');
120
+ }
121
+ if (query.operation === 'status') {
122
+ if (!exactRecord(query, ['id', 'operation'])) fail(`query ${query.id} shapeが不正`);
123
+ } else if (query.operation === 'affected') {
124
+ if (!exactRecord(query, ['id', 'operation', 'targets'])) {
125
+ fail(`query ${query.id} shapeが不正`);
126
+ }
127
+ uniqueBoundedStrings(query.targets, `query ${query.id}.targets`);
128
+ } else if (!exactRecord(query, ['id', 'operation', 'target'])
129
+ || !boundedText(query.target)) {
130
+ fail(`query ${query.id} shapeが不正`);
131
+ }
132
+ byId.set(query.id, query);
133
+ }
134
+ if (byId.size < 1) fail('query setが空である');
135
+ return byId;
136
+ }
137
+
138
+ function collectQueryReferences(value, references = new Set()) {
139
+ if (Array.isArray(value)) {
140
+ value.forEach((entry) => collectQueryReferences(entry, references));
141
+ return references;
142
+ }
143
+ if (!plainRecord(value)) return references;
144
+ for (const [key, entry] of Object.entries(value)) {
145
+ if ((key === 'query_id' || key === 'impact_id' || key.endsWith('_query_id'))
146
+ && typeof entry === 'string') {
147
+ references.add(entry);
148
+ }
149
+ collectQueryReferences(entry, references);
150
+ }
151
+ return references;
152
+ }
153
+
154
+ function expectQuery(queryById, id, operation, target, label) {
155
+ const query = queryById.get(id);
156
+ if (!query
157
+ || query.operation !== operation
158
+ || (operation !== 'status'
159
+ && operation !== 'affected'
160
+ && query.target !== target)) {
161
+ fail(`${label}のquery bindingが不正`);
162
+ }
163
+ return query;
164
+ }
165
+
166
+ function surfaceQueryIds(surface) {
167
+ return [
168
+ surface.query_id,
169
+ surface.callers_query_id,
170
+ surface.callees_query_id,
171
+ surface.impact_id,
172
+ surface.composition_link_query_id,
173
+ ].filter((entry) => entry !== undefined);
174
+ }
175
+
176
+ function validateSurfaceShape(surface, label, { proposedProduction = false } = {}) {
177
+ const basicKeys = ['symbol', 'path', 'query_id', 'impact_id'];
178
+ const linkedKeys = [...basicKeys, 'callers_query_id', 'callees_query_id'];
179
+ const productionKeys = proposedProduction
180
+ ? [...linkedKeys, 'composition_link_query_id']
181
+ : linkedKeys;
182
+ if ((!exactRecord(surface, basicKeys)
183
+ && !exactRecord(surface, linkedKeys)
184
+ && !exactRecord(surface, productionKeys))
185
+ || (surface.symbol !== null && !boundedText(surface.symbol))
186
+ || !boundedText(surface.path)
187
+ || !boundedText(surface.query_id)
188
+ || !boundedText(surface.impact_id)) {
189
+ fail(`${label} surface shapeが不正`);
190
+ }
191
+ for (const id of surfaceQueryIds(surface)) {
192
+ if (!boundedText(id)) fail(`${label} query referenceが不正`);
193
+ }
194
+ }
195
+
196
+ function validateSurfaceQueries(surface, queryById, label, fixedOracle) {
197
+ const target = surface.symbol ?? surface.path;
198
+ expectQuery(queryById, surface.query_id, 'query', target, `${label}.query_id`);
199
+ expectQuery(queryById, surface.impact_id, 'impact', target, `${label}.impact_id`);
200
+ if (surface.callers_query_id !== undefined) {
201
+ expectQuery(
202
+ queryById,
203
+ surface.callers_query_id,
204
+ 'callers',
205
+ fixedOracle ? ORACLE_RUNNER : target,
206
+ `${label}.callers_query_id`,
207
+ );
208
+ }
209
+ if (surface.callees_query_id !== undefined) {
210
+ expectQuery(
211
+ queryById,
212
+ surface.callees_query_id,
213
+ 'callees',
214
+ fixedOracle ? ORACLE_RUNNER : target,
215
+ `${label}.callees_query_id`,
216
+ );
217
+ }
218
+ if (surface.composition_link_query_id !== undefined) {
219
+ expectQuery(
220
+ queryById,
221
+ surface.composition_link_query_id,
222
+ 'callees',
223
+ 'resolveDeliveryPolicy',
224
+ `${label}.composition_link_query_id`,
225
+ );
226
+ }
227
+ }
228
+
229
+ function validateCandidateSpec(candidateSpec, queryById) {
230
+ if (!exactRecord(candidateSpec, [
231
+ 'schema',
232
+ 'candidate_id',
233
+ 'plan_input_ref',
234
+ 'query_set_ref',
235
+ 'compiler_contract',
236
+ 'fixed_oracle',
237
+ 'stable_surfaces',
238
+ 'todos',
239
+ ])
240
+ || candidateSpec.schema !== 'lattice.rc2.boundary_candidate_spec.v1'
241
+ || candidateSpec.candidate_id !== EXPECTED_CANDIDATE_ID) {
242
+ fail('candidate spec identityまたはshapeが不正');
243
+ }
244
+ const candidateDigest = digestArtifact(candidateSpec);
245
+ const expectedOracleDigest = EXPECTED_ORACLE_DIGEST_BY_CANDIDATE_DIGEST[candidateDigest];
246
+ if (expectedOracleDigest === undefined) {
247
+ fail('candidate specがaccepted RC2 witnessと一致しない');
248
+ }
249
+ if (!exactRecord(candidateSpec.compiler_contract, [
250
+ 'condition_selector',
251
+ 'surface_resolution',
252
+ 'unresolved_policy',
253
+ 'status_query_id',
254
+ 'affected_query_id',
255
+ ])
256
+ || candidateSpec.compiler_contract.condition_selector !== 'forbidden'
257
+ || candidateSpec.compiler_contract.surface_resolution !== 'exact_name_path_and_link'
258
+ || candidateSpec.compiler_contract.unresolved_policy !== 'typed_unknown') {
259
+ fail('candidate compiler contractが不正');
260
+ }
261
+ expectQuery(
262
+ queryById,
263
+ candidateSpec.compiler_contract.status_query_id,
264
+ 'status',
265
+ undefined,
266
+ 'candidate compiler status',
267
+ );
268
+ const affected = queryById.get(candidateSpec.compiler_contract.affected_query_id);
269
+ if (!affected || affected.operation !== 'affected') {
270
+ fail('candidate compiler affected query bindingが不正');
271
+ }
272
+
273
+ if (!exactRecord(candidateSpec.fixed_oracle, [
274
+ 'path',
275
+ 'source_digest',
276
+ 'case_ids',
277
+ 'path_query_id',
278
+ 'runner_query_id',
279
+ 'callers_query_id',
280
+ 'callees_query_id',
281
+ 'impact_id',
282
+ ])
283
+ || !boundedText(candidateSpec.fixed_oracle.path)
284
+ || candidateSpec.fixed_oracle.source_digest !== expectedOracleDigest) {
285
+ fail('candidate fixed oracle identityが不正');
286
+ }
287
+ uniqueBoundedStrings(candidateSpec.fixed_oracle.case_ids, 'fixed_oracle.case_ids');
288
+ expectQuery(
289
+ queryById,
290
+ candidateSpec.fixed_oracle.path_query_id,
291
+ 'query',
292
+ candidateSpec.fixed_oracle.path,
293
+ 'fixed oracle path',
294
+ );
295
+ expectQuery(
296
+ queryById,
297
+ candidateSpec.fixed_oracle.runner_query_id,
298
+ 'query',
299
+ ORACLE_RUNNER,
300
+ 'fixed oracle runner',
301
+ );
302
+ expectQuery(
303
+ queryById,
304
+ candidateSpec.fixed_oracle.callers_query_id,
305
+ 'callers',
306
+ ORACLE_RUNNER,
307
+ 'fixed oracle callers',
308
+ );
309
+ expectQuery(
310
+ queryById,
311
+ candidateSpec.fixed_oracle.callees_query_id,
312
+ 'callees',
313
+ ORACLE_RUNNER,
314
+ 'fixed oracle callees',
315
+ );
316
+ expectQuery(
317
+ queryById,
318
+ candidateSpec.fixed_oracle.impact_id,
319
+ 'impact',
320
+ candidateSpec.fixed_oracle.path,
321
+ 'fixed oracle impact',
322
+ );
323
+
324
+ requireArray(candidateSpec.stable_surfaces, 'candidateSpec.stable_surfaces');
325
+ if (candidateSpec.stable_surfaces.length !== 3) fail('stable surface件数が不正');
326
+ const stableRoles = new Set();
327
+ for (const surface of candidateSpec.stable_surfaces) {
328
+ if (!exactRecord(surface, [
329
+ 'kind',
330
+ 'target',
331
+ 'path',
332
+ 'role',
333
+ 'active_when',
334
+ 'query_id',
335
+ 'callers_query_id',
336
+ 'callees_query_id',
337
+ 'impact_id',
338
+ ])
339
+ || !new Set(['symbol', 'path']).has(surface.kind)
340
+ || !boundedText(surface.target)
341
+ || !boundedText(surface.path)
342
+ || !new Set(['proposed', 'always']).has(surface.active_when)
343
+ || stableRoles.has(surface.role)) {
344
+ fail('stable surface shapeまたはroleが不正');
345
+ }
346
+ stableRoles.add(surface.role);
347
+ const fixedOracle = surface.role === 'fixed-oracle';
348
+ expectQuery(queryById, surface.query_id, 'query', surface.target, `${surface.role}.query`);
349
+ expectQuery(
350
+ queryById,
351
+ surface.callers_query_id,
352
+ 'callers',
353
+ fixedOracle ? ORACLE_RUNNER : surface.target,
354
+ `${surface.role}.callers`,
355
+ );
356
+ expectQuery(
357
+ queryById,
358
+ surface.callees_query_id,
359
+ 'callees',
360
+ fixedOracle ? ORACLE_RUNNER : surface.target,
361
+ `${surface.role}.callees`,
362
+ );
363
+ expectQuery(queryById, surface.impact_id, 'impact', surface.target, `${surface.role}.impact`);
364
+ }
365
+ if (!sameStringSet(
366
+ [...stableRoles],
367
+ ['composition-entry', 'composition-test', 'fixed-oracle'],
368
+ )) {
369
+ fail('stable surface role集合が不正');
370
+ }
371
+
372
+ requireArray(candidateSpec.todos, 'candidateSpec.todos');
373
+ if (candidateSpec.todos.length !== 3) fail('candidate TODO件数が不正');
374
+ const todoIds = new Set();
375
+ const caseIds = [];
376
+ for (const candidateTodo of candidateSpec.todos) {
377
+ if (!exactRecord(candidateTodo, [
378
+ 'todo_id',
379
+ 'outcome',
380
+ 'case_ids',
381
+ 'current',
382
+ 'proposed',
383
+ ])
384
+ || !boundedText(candidateTodo.todo_id)
385
+ || !boundedText(candidateTodo.outcome)
386
+ || todoIds.has(candidateTodo.todo_id)) {
387
+ fail('candidate TODO shapeまたはIDが不正');
388
+ }
389
+ todoIds.add(candidateTodo.todo_id);
390
+ caseIds.push(...uniqueBoundedStrings(candidateTodo.case_ids, `${candidateTodo.todo_id}.case_ids`));
391
+ for (const mode of ['current', 'proposed']) {
392
+ const modeSpec = candidateTodo[mode];
393
+ if (!exactRecord(modeSpec, ['production', 'tests'])) {
394
+ fail(`${candidateTodo.todo_id}/${mode} shapeが不正`);
395
+ }
396
+ validateSurfaceShape(
397
+ modeSpec.production,
398
+ `${candidateTodo.todo_id}/${mode}/production`,
399
+ { proposedProduction: mode === 'proposed' },
400
+ );
401
+ requireArray(modeSpec.tests, `${candidateTodo.todo_id}/${mode}.tests`);
402
+ const expectedTests = mode === 'current' ? 2 : 1;
403
+ if (modeSpec.tests.length !== expectedTests) {
404
+ fail(`${candidateTodo.todo_id}/${mode} test surface件数が不正`);
405
+ }
406
+ modeSpec.tests.forEach((surface, index) => {
407
+ validateSurfaceShape(surface, `${candidateTodo.todo_id}/${mode}/tests/${index}`);
408
+ });
409
+ validateSurfaceQueries(
410
+ modeSpec.production,
411
+ queryById,
412
+ `${candidateTodo.todo_id}/${mode}/production`,
413
+ false,
414
+ );
415
+ modeSpec.tests.forEach((surface, index) => validateSurfaceQueries(
416
+ surface,
417
+ queryById,
418
+ `${candidateTodo.todo_id}/${mode}/tests/${index}`,
419
+ surface.path === candidateSpec.fixed_oracle.path,
420
+ ));
421
+ }
422
+ }
423
+ if (new Set(caseIds).size !== caseIds.length
424
+ || !sameStringSet(caseIds, candidateSpec.fixed_oracle.case_ids)) {
425
+ fail('candidate case IDがfixed oracleのexact partitionではない');
426
+ }
427
+
428
+ const queryReferences = [...collectQueryReferences(candidateSpec)];
429
+ if (!sameStringSet(queryReferences, [...queryById.keys()])) {
430
+ fail('candidate query referenceとquery setがexact/exhaustiveではない');
431
+ }
432
+ return candidateDigest;
433
+ }
434
+
435
+ function manualEvidenceByTodo(manualEvidence) {
436
+ if (!exactRecord(manualEvidence, ['schema', 'evidence'])
437
+ || manualEvidence.schema !== 'lattice.manual_boundary_evidence.v1') {
438
+ fail('manual evidence shapeまたはschemaが不正');
439
+ }
440
+ requireArray(manualEvidence.evidence, 'manualEvidence.evidence');
441
+ const byTodo = new Map();
442
+ for (const entry of manualEvidence.evidence) {
443
+ if (!exactRecord(entry, [
444
+ 'todo_id',
445
+ 'state_reads',
446
+ 'state_writes',
447
+ 'effects',
448
+ 'unknowns',
449
+ ])
450
+ || !boundedText(entry.todo_id)
451
+ || byTodo.has(entry.todo_id)) {
452
+ fail('manual evidence entryが不正またはTODO重複している');
453
+ }
454
+ uniqueBoundedStrings(entry.state_reads, `${entry.todo_id}.state_reads`);
455
+ uniqueBoundedStrings(entry.state_writes, `${entry.todo_id}.state_writes`);
456
+ uniqueBoundedStrings(entry.effects, `${entry.todo_id}.effects`);
457
+ uniqueBoundedStrings(entry.unknowns, `${entry.todo_id}.unknowns`);
458
+ if (entry.state_reads.length > 0) {
459
+ fail('manual state_readsをwrite conflictへ損失変換できない');
460
+ }
461
+ byTodo.set(entry.todo_id, entry);
462
+ }
463
+ if (byTodo.size < 1) fail('manual evidenceが空である');
464
+ return byTodo;
465
+ }
466
+
467
+ function assertTodoBindings(planInput, candidateSpec, manualByTodo) {
468
+ const planIds = planInput.todos.map(({ id }) => id);
469
+ const candidateIds = candidateSpec.todos.map(({ todo_id: todoId }) => todoId);
470
+ const manualIds = [...manualByTodo.keys()];
471
+ if (!sameStringSet(planIds, candidateIds) || !sameStringSet(planIds, manualIds)) {
472
+ fail('plan/candidate/manualのTODO集合が一致しない');
473
+ }
474
+ if (planInput.query_set_ref !== candidateSpec.query_set_ref
475
+ || planInput.project.fixture_entry !== candidateSpec.todos[0].current.production.path) {
476
+ fail('plan inputがcandidate fixture/query setへbindしていない');
477
+ }
478
+ const planByTodo = new Map(planInput.todos.map((todo) => [todo.id, todo]));
479
+ for (const candidateTodo of candidateSpec.todos) {
480
+ const planTodo = planByTodo.get(candidateTodo.todo_id);
481
+ if (planTodo.outcome !== candidateTodo.outcome
482
+ || planTodo.anchor.symbol !== candidateTodo.current.production.symbol
483
+ || planTodo.anchor.path !== candidateTodo.current.production.path) {
484
+ fail(`candidate ${candidateTodo.todo_id}がplan TODOへbindしていない`);
485
+ }
486
+ }
487
+ }
488
+
489
+ function candidatePaths(candidateSpec) {
490
+ const current = new Set([candidateSpec.fixed_oracle.path]);
491
+ const all = new Set([candidateSpec.fixed_oracle.path]);
492
+ for (const candidateTodo of candidateSpec.todos) {
493
+ current.add(candidateTodo.current.production.path);
494
+ all.add(candidateTodo.current.production.path);
495
+ for (const testSurface of candidateTodo.current.tests) {
496
+ current.add(testSurface.path);
497
+ all.add(testSurface.path);
498
+ }
499
+ all.add(candidateTodo.proposed.production.path);
500
+ for (const testSurface of candidateTodo.proposed.tests) all.add(testSurface.path);
501
+ }
502
+ for (const stable of candidateSpec.stable_surfaces) all.add(stable.path);
503
+ return {
504
+ current: [...current].sort(compareText),
505
+ all: [...all].sort(compareText),
506
+ };
507
+ }
508
+
509
+ function validateSourceSnapshot(sourceSnapshot, candidateSpec) {
510
+ if (!exactRecord(sourceSnapshot, ['schema_version', 'files'])
511
+ || sourceSnapshot.schema_version !== 'lattice.rc2.source_snapshot.v1') {
512
+ fail('sourceSnapshot shapeまたはschemaが不正');
513
+ }
514
+ requireArray(sourceSnapshot.files, 'sourceSnapshot.files');
515
+ const paths = candidatePaths(candidateSpec);
516
+ if (sourceSnapshot.files.length !== paths.all.length) {
517
+ fail('sourceSnapshot path件数がcandidateと一致しない');
518
+ }
519
+ const byPath = new Map();
520
+ for (const file of sourceSnapshot.files) {
521
+ if (!exactRecord(file, ['path', 'state', 'content_digest'])
522
+ || !boundedText(file.path)
523
+ || !new Set(['file', 'absent']).has(file.state)
524
+ || (file.state === 'file' && (typeof file.content_digest !== 'string'
525
+ || !DIGEST.test(file.content_digest)))
526
+ || (file.state === 'absent' && file.content_digest !== null)
527
+ || byPath.has(file.path)) {
528
+ fail('sourceSnapshot file entryが不正または重複している');
529
+ }
530
+ byPath.set(file.path, file);
531
+ }
532
+ if (!sameStringSet([...byPath.keys()], paths.all)) {
533
+ fail('sourceSnapshot path集合がcandidate exact surfaceと一致しない');
534
+ }
535
+ const oracle = byPath.get(candidateSpec.fixed_oracle.path);
536
+ if (oracle.state !== 'file'
537
+ || oracle.content_digest !== candidateSpec.fixed_oracle.source_digest) {
538
+ fail('sourceSnapshot fixed oracle digestがcandidateと一致しない');
539
+ }
540
+ const present = [...byPath.values()]
541
+ .filter(({ state }) => state === 'file')
542
+ .map(({ path }) => path)
543
+ .sort(compareText);
544
+ if (sameStringSet(present, paths.current)) {
545
+ return { topology: 'current', filesByPath: byPath, paths };
546
+ }
547
+ if (sameStringSet(present, paths.all)) {
548
+ return { topology: 'proposed', filesByPath: byPath, paths };
549
+ }
550
+ fail('sourceSnapshotがcurrent/proposed exact topologyのどちらでもない');
551
+ }
552
+
553
+ function validatePortableOutcomeShape(outcome, query) {
554
+ if (!plainRecord(outcome)
555
+ || outcome.id !== query.id
556
+ || outcome.operation !== query.operation
557
+ || !SENSOR_OUTCOMES.has(outcome.outcome)) {
558
+ fail(`portable outcome ${query.id} identityが不正`);
559
+ }
560
+ if (query.operation === 'status') {
561
+ if (outcome.outcome !== 'ready'
562
+ || !exactRecord(outcome, ['id', 'operation', 'outcome', 'data'])
563
+ || !plainRecord(outcome.data)) {
564
+ fail('portable statusがready preimageではない');
565
+ }
566
+ return;
567
+ }
568
+ if (query.operation === 'affected') {
569
+ if (!exactRecord(outcome, ['id', 'operation', 'outcome', 'targets'])) {
570
+ fail('portable affected shapeが不正');
571
+ }
572
+ requireArray(outcome.targets, `portable ${query.id}.targets`);
573
+ return;
574
+ }
575
+ if (outcome.target !== query.target) {
576
+ fail(`portable outcome ${query.id} targetがqueryと一致しない`);
577
+ }
578
+ if (outcome.outcome === 'ready') {
579
+ const keys = query.operation === 'query'
580
+ ? ['id', 'operation', 'target', 'outcome', 'data']
581
+ : ['id', 'operation', 'target', 'outcome', 'data', 'resolution'];
582
+ if (!exactRecord(outcome, keys)) fail(`portable ready outcome ${query.id} shapeが不正`);
583
+ if (query.operation === 'query') requireArray(outcome.data, `${query.id}.data`);
584
+ else {
585
+ if (!plainRecord(outcome.data)) fail(`${query.id}.dataがrecordではない`);
586
+ requireArray(outcome.resolution, `${query.id}.resolution`);
587
+ }
588
+ return;
589
+ }
590
+ if (outcome.outcome === 'symbol_absent') {
591
+ const keys = query.operation === 'query'
592
+ ? ['id', 'operation', 'target', 'outcome', 'data']
593
+ : ['id', 'operation', 'target', 'outcome'];
594
+ if (!exactRecord(outcome, keys)
595
+ || (query.operation === 'query' && (!Array.isArray(outcome.data)
596
+ || outcome.data.length !== 0))) {
597
+ fail(`portable symbol_absent outcome ${query.id} shapeが不正`);
598
+ }
599
+ }
600
+ }
601
+
602
+ function assertFreshStatus(statusOutcome) {
603
+ const status = statusOutcome.data;
604
+ if (status.initialized !== true
605
+ || !exactRecord(status.pendingChanges, ['added', 'modified', 'removed'])
606
+ || Object.values(status.pendingChanges).some((value) => value !== 0)
607
+ || status.worktreeMismatch !== null
608
+ || !plainRecord(status.index)
609
+ || status.index.state !== 'complete'
610
+ || status.index.pendingRefs !== 0
611
+ || status.index.reindexRecommended !== false) {
612
+ fail('portable LatticeSensor statusがfresh/completeではない');
613
+ }
614
+ }
615
+
616
+ function validateAffectedOutcome(outcome, query, filesByPath) {
617
+ if (outcome.targets.length !== query.targets.length) {
618
+ fail('portable affected target件数がqueryと一致しない');
619
+ }
620
+ for (let index = 0; index < query.targets.length; index += 1) {
621
+ const target = query.targets[index];
622
+ const result = outcome.targets[index];
623
+ const present = filesByPath.get(target)?.state === 'file';
624
+ if (!exactRecord(result, ['target', 'outcome', 'data'])
625
+ || result.target !== target
626
+ || result.outcome !== (present ? 'ready' : 'empty')
627
+ || !plainRecord(result.data)
628
+ || !Array.isArray(result.data.changedFiles)
629
+ || !sameStringSet(result.data.changedFiles, [target])
630
+ || !Array.isArray(result.data.affectedTests)
631
+ || (present && result.data.affectedTests.length < 1)
632
+ || (!present && result.data.affectedTests.length !== 0)) {
633
+ fail(`portable affected ${target}がsnapshot存在状態へbindしていない`);
634
+ }
635
+ }
636
+ const expected = outcome.targets.every(({ outcome: status }) => status === 'ready')
637
+ ? 'ready'
638
+ : 'unresolved';
639
+ if (outcome.outcome !== expected) {
640
+ fail('portable affected aggregate outcomeがtarget outcomesと一致しない');
641
+ }
642
+ }
643
+
644
+ function validatePortableEvidence({
645
+ sensorEvidence,
646
+ querySet,
647
+ queryById,
648
+ candidateSpec,
649
+ filesByPath,
650
+ }) {
651
+ if (!exactRecord(sensorEvidence, [
652
+ 'schema',
653
+ 'projection',
654
+ 'query_set_digest',
655
+ 'outcomes',
656
+ 'per_query',
657
+ 'aggregate_digest',
658
+ ])
659
+ || sensorEvidence.schema !== 'lattice.sensor_portable_preimage.v1'
660
+ || sensorEvidence.projection !== 'lattice.sensor_portable_outcome.v1'
661
+ || sensorEvidence.query_set_digest !== digestArtifact(querySet)) {
662
+ fail('portable evidence identity/query bindingが不正');
663
+ }
664
+ requireArray(sensorEvidence.outcomes, 'portable.outcomes');
665
+ requireArray(sensorEvidence.per_query, 'portable.per_query');
666
+ if (sensorEvidence.outcomes.length !== querySet.queries.length
667
+ || sensorEvidence.per_query.length !== querySet.queries.length) {
668
+ fail('portable outcome件数がquery setと一致しない');
669
+ }
670
+ const byId = new Map();
671
+ for (let index = 0; index < querySet.queries.length; index += 1) {
672
+ const query = querySet.queries[index];
673
+ const outcome = sensorEvidence.outcomes[index];
674
+ const receipt = sensorEvidence.per_query[index];
675
+ validatePortableOutcomeShape(outcome, query);
676
+ if (!exactRecord(receipt, ['id', 'operation', 'outcome', 'result_digest'])
677
+ || receipt.id !== query.id
678
+ || receipt.operation !== query.operation
679
+ || receipt.outcome !== outcome.outcome
680
+ || receipt.result_digest !== digestArtifact(outcome)) {
681
+ fail(`portable per-query digest ${query.id}がoutcomeと一致しない`);
682
+ }
683
+ byId.set(query.id, outcome);
684
+ }
685
+ const aggregatePreimage = {
686
+ projection: sensorEvidence.projection,
687
+ query_set_digest: sensorEvidence.query_set_digest,
688
+ outcomes: sensorEvidence.outcomes,
689
+ };
690
+ if (!DIGEST.test(sensorEvidence.aggregate_digest)
691
+ || sensorEvidence.aggregate_digest !== digestArtifact(aggregatePreimage)) {
692
+ fail('portable aggregate digestがfull preimageから再計算できない');
693
+ }
694
+ assertFreshStatus(byId.get(candidateSpec.compiler_contract.status_query_id));
695
+ const affectedQuery = queryById.get(candidateSpec.compiler_contract.affected_query_id);
696
+ validateAffectedOutcome(
697
+ byId.get(candidateSpec.compiler_contract.affected_query_id),
698
+ affectedQuery,
699
+ filesByPath,
700
+ );
701
+ return byId;
702
+ }
703
+
704
+ function isExactNodeCandidate(entry, target, path) {
705
+ const node = entry?.node;
706
+ if (!plainRecord(node) || node.filePath !== path) return false;
707
+ if (target.includes('/')) return node.qualifiedName === target;
708
+ return node.name === target || node.qualifiedName === target;
709
+ }
710
+
711
+ function assertReadyQuery(outcomeById, id, target, path, label) {
712
+ const outcome = outcomeById.get(id);
713
+ if (!outcome || outcome.outcome !== 'ready' || outcome.operation !== 'query') {
714
+ fail(`${label} queryがreadyではない`);
715
+ }
716
+ const exact = outcome.data.filter((entry) => isExactNodeCandidate(entry, target, path));
717
+ if (exact.length !== 1) fail(`${label} exact name/path resolutionが一意でない`);
718
+ }
719
+
720
+ function assertReadyResolution(outcomeById, id, target, path, operation, label) {
721
+ const outcome = outcomeById.get(id);
722
+ if (!outcome || outcome.outcome !== 'ready' || outcome.operation !== operation) {
723
+ fail(`${label} ${operation}がreadyではない`);
724
+ }
725
+ const exact = outcome.resolution.filter((entry) => isExactNodeCandidate(entry, target, path));
726
+ if (exact.length !== 1) fail(`${label} ${operation} exact resolutionが一意でない`);
727
+ }
728
+
729
+ function assertSurfaceReady(outcomeById, surface, label, fixedOracle = false) {
730
+ const target = surface.symbol ?? surface.path;
731
+ assertReadyQuery(outcomeById, surface.query_id, target, surface.path, label);
732
+ assertReadyResolution(
733
+ outcomeById,
734
+ surface.impact_id,
735
+ target,
736
+ surface.path,
737
+ 'impact',
738
+ label,
739
+ );
740
+ if (surface.callers_query_id !== undefined) {
741
+ assertReadyResolution(
742
+ outcomeById,
743
+ surface.callers_query_id,
744
+ fixedOracle ? ORACLE_RUNNER : target,
745
+ surface.path,
746
+ 'callers',
747
+ label,
748
+ );
749
+ }
750
+ if (surface.callees_query_id !== undefined) {
751
+ assertReadyResolution(
752
+ outcomeById,
753
+ surface.callees_query_id,
754
+ fixedOracle ? ORACLE_RUNNER : target,
755
+ surface.path,
756
+ 'callees',
757
+ label,
758
+ );
759
+ }
760
+ }
761
+
762
+ function containsExactSummary(entries, target, path) {
763
+ return Array.isArray(entries)
764
+ && entries.some((entry) => plainRecord(entry)
765
+ && entry.name === target
766
+ && entry.filePath === path);
767
+ }
768
+
769
+ function assertCalleeLink(outcomeById, queryId, target, path, label) {
770
+ const outcome = outcomeById.get(queryId);
771
+ if (!outcome
772
+ || outcome.outcome !== 'ready'
773
+ || outcome.operation !== 'callees'
774
+ || !containsExactSummary(outcome.data?.callees, target, path)) {
775
+ fail(`${label} exact callee linkが観測されない`);
776
+ }
777
+ }
778
+
779
+ function stableByRole(candidateSpec, role) {
780
+ return candidateSpec.stable_surfaces.find((surface) => surface.role === role);
781
+ }
782
+
783
+ function assertActivation(candidateSpec, topology, outcomeById, queryById) {
784
+ const fixed = candidateSpec.fixed_oracle;
785
+ const fixedSurface = {
786
+ symbol: null,
787
+ path: fixed.path,
788
+ query_id: fixed.path_query_id,
789
+ callers_query_id: fixed.callers_query_id,
790
+ callees_query_id: fixed.callees_query_id,
791
+ impact_id: fixed.impact_id,
792
+ };
793
+ assertSurfaceReady(outcomeById, fixedSurface, 'fixed oracle', true);
794
+ assertReadyQuery(outcomeById, fixed.runner_query_id, ORACLE_RUNNER, fixed.path, 'fixed oracle runner');
795
+
796
+ if (topology === 'current') {
797
+ const inactive = new Set();
798
+ for (const candidateTodo of candidateSpec.todos) {
799
+ assertSurfaceReady(
800
+ outcomeById,
801
+ candidateTodo.current.production,
802
+ `${candidateTodo.todo_id}/current/production`,
803
+ );
804
+ candidateTodo.current.tests.forEach((surface, index) => assertSurfaceReady(
805
+ outcomeById,
806
+ surface,
807
+ `${candidateTodo.todo_id}/current/tests/${index}`,
808
+ surface.path === fixed.path,
809
+ ));
810
+ const proposed = candidateTodo.proposed;
811
+ [proposed.production, ...proposed.tests].forEach((surface) => {
812
+ surfaceQueryIds(surface)
813
+ .filter((id) => id !== proposed.production.composition_link_query_id)
814
+ .forEach((id) => inactive.add(id));
815
+ });
816
+ }
817
+ const compositionTest = stableByRole(candidateSpec, 'composition-test');
818
+ surfaceQueryIds(compositionTest).forEach((id) => inactive.add(id));
819
+ for (const id of inactive) {
820
+ const outcome = outcomeById.get(id);
821
+ if (!outcome || outcome.outcome !== 'symbol_absent') {
822
+ fail(`current topologyのinactive proposed query ${id}がsymbol_absentではない`);
823
+ }
824
+ }
825
+ return;
826
+ }
827
+
828
+ for (const query of queryById.values()) {
829
+ if (query.operation === 'status' || query.operation === 'affected') continue;
830
+ if (outcomeById.get(query.id)?.outcome !== 'ready') {
831
+ fail(`proposed topologyのquery ${query.id}がreadyではない`);
832
+ }
833
+ }
834
+ const compositionEntry = stableByRole(candidateSpec, 'composition-entry');
835
+ const compositionTest = stableByRole(candidateSpec, 'composition-test');
836
+ assertSurfaceReady(outcomeById, {
837
+ symbol: compositionEntry.target,
838
+ path: compositionEntry.path,
839
+ query_id: compositionEntry.query_id,
840
+ callers_query_id: compositionEntry.callers_query_id,
841
+ callees_query_id: compositionEntry.callees_query_id,
842
+ impact_id: compositionEntry.impact_id,
843
+ }, 'composition entry');
844
+ assertSurfaceReady(outcomeById, {
845
+ symbol: compositionTest.target,
846
+ path: compositionTest.path,
847
+ query_id: compositionTest.query_id,
848
+ callers_query_id: compositionTest.callers_query_id,
849
+ callees_query_id: compositionTest.callees_query_id,
850
+ impact_id: compositionTest.impact_id,
851
+ }, 'composition test');
852
+ for (const candidateTodo of candidateSpec.todos) {
853
+ const production = candidateTodo.proposed.production;
854
+ const testSurface = candidateTodo.proposed.tests[0];
855
+ assertSurfaceReady(
856
+ outcomeById,
857
+ production,
858
+ `${candidateTodo.todo_id}/proposed/production`,
859
+ );
860
+ assertSurfaceReady(
861
+ outcomeById,
862
+ testSurface,
863
+ `${candidateTodo.todo_id}/proposed/test`,
864
+ );
865
+ assertCalleeLink(
866
+ outcomeById,
867
+ production.composition_link_query_id,
868
+ production.symbol,
869
+ production.path,
870
+ `${candidateTodo.todo_id}/composition-entry`,
871
+ );
872
+ assertCalleeLink(
873
+ outcomeById,
874
+ testSurface.callees_query_id,
875
+ production.symbol,
876
+ production.path,
877
+ `${candidateTodo.todo_id}/dedicated-test`,
878
+ );
879
+ }
880
+ assertCalleeLink(
881
+ outcomeById,
882
+ compositionTest.callees_query_id,
883
+ compositionEntry.target,
884
+ compositionEntry.path,
885
+ 'composition-test/entry',
886
+ );
887
+ }
888
+
889
+ function evidenceIdsForSurface(surface, candidateSpec) {
890
+ const ids = surfaceQueryIds(surface);
891
+ if (surface.path === candidateSpec.fixed_oracle.path) {
892
+ ids.push(candidateSpec.fixed_oracle.runner_query_id);
893
+ }
894
+ return [...new Set(ids)].sort(compareText);
895
+ }
896
+
897
+ function structuralResources({
898
+ candidateSpec,
899
+ candidateDigest,
900
+ topology,
901
+ outcomeById,
902
+ sensorEvidence,
903
+ }) {
904
+ const groups = new Map();
905
+ const add = (kind, target, todoId, surface, ref) => {
906
+ const key = resourceKey(kind, target);
907
+ const group = groups.get(key) ?? {
908
+ kind,
909
+ target,
910
+ todoIds: new Set(),
911
+ queryIds: new Set(),
912
+ candidateRefs: new Set(),
913
+ };
914
+ group.todoIds.add(todoId);
915
+ evidenceIdsForSurface(surface, candidateSpec).forEach((id) => group.queryIds.add(id));
916
+ group.candidateRefs.add(ref);
917
+ groups.set(key, group);
918
+ };
919
+
920
+ for (const candidateTodo of candidateSpec.todos) {
921
+ const mode = topology === 'current' ? 'current' : 'proposed';
922
+ const modeSpec = candidateTodo[mode];
923
+ add(
924
+ 'symbol',
925
+ modeSpec.production.symbol,
926
+ candidateTodo.todo_id,
927
+ modeSpec.production,
928
+ `${candidateTodo.todo_id}/${mode}/production/symbol`,
929
+ );
930
+ add(
931
+ 'path',
932
+ modeSpec.production.path,
933
+ candidateTodo.todo_id,
934
+ modeSpec.production,
935
+ `${candidateTodo.todo_id}/${mode}/production/path`,
936
+ );
937
+ modeSpec.tests.forEach((surface, index) => {
938
+ add(
939
+ 'path',
940
+ surface.path,
941
+ candidateTodo.todo_id,
942
+ surface,
943
+ `${candidateTodo.todo_id}/${mode}/tests/${index}/path`,
944
+ );
945
+ if (surface.symbol !== null) {
946
+ add(
947
+ 'symbol',
948
+ surface.symbol,
949
+ candidateTodo.todo_id,
950
+ surface,
951
+ `${candidateTodo.todo_id}/${mode}/tests/${index}/symbol`,
952
+ );
953
+ }
954
+ });
955
+ }
956
+
957
+ return [...groups.values()].map((group) => {
958
+ const queryIds = [...group.queryIds].sort(compareText);
959
+ const outcomes = queryIds.map((id) => outcomeById.get(id));
960
+ if (outcomes.some((outcome) => !outcome || outcome.outcome !== 'ready')) {
961
+ fail(`active resource ${group.kind}:${group.target}のLatticeSensor evidenceがreadyではない`);
962
+ }
963
+ const evidenceDigest = digestArtifact({
964
+ schema: 'lattice.rc2.sensor_resource_evidence.v1',
965
+ topology,
966
+ portable_aggregate_digest: sensorEvidence.aggregate_digest,
967
+ query_ids: queryIds,
968
+ outcomes,
969
+ });
970
+ return {
971
+ resource_id: resourceId(group.kind, group.target),
972
+ kind: group.kind,
973
+ target: group.target,
974
+ todo_ids: [...group.todoIds].sort(compareText),
975
+ provenance: [
976
+ {
977
+ source: 'sensor',
978
+ evidence_ref: `portable-sensor#${queryIds.join(',')}`,
979
+ evidence_digest: evidenceDigest,
980
+ status: 'ready',
981
+ },
982
+ {
983
+ source: 'manual_candidate_spec',
984
+ evidence_ref: `candidate-spec#${[...group.candidateRefs].sort(compareText).join(',')}`,
985
+ evidence_digest: candidateDigest,
986
+ status: 'asserted',
987
+ },
988
+ ],
989
+ };
990
+ });
991
+ }
992
+
993
+ function manualResources(manualByTodo, manualEvidenceDigest) {
994
+ const groups = new Map();
995
+ const add = (kind, target, todoId) => {
996
+ const key = resourceKey(kind, target);
997
+ const group = groups.get(key) ?? { kind, target, todoIds: new Set() };
998
+ group.todoIds.add(todoId);
999
+ groups.set(key, group);
1000
+ };
1001
+ for (const [todoId, entry] of manualByTodo) {
1002
+ entry.state_writes.forEach((target) => add('state', target, todoId));
1003
+ entry.effects.forEach((target) => add('effect', target, todoId));
1004
+ entry.unknowns.forEach((target) => add('dynamic', target, todoId));
1005
+ }
1006
+ return [...groups.values()].map((group) => ({
1007
+ resource_id: resourceId(group.kind, group.target),
1008
+ kind: group.kind,
1009
+ target: group.target,
1010
+ todo_ids: [...group.todoIds].sort(compareText),
1011
+ provenance: [{
1012
+ source: 'manual_state_effect',
1013
+ evidence_ref: `manual-evidence#${group.kind}/${resourceId(group.kind, group.target)}`,
1014
+ evidence_digest: manualEvidenceDigest,
1015
+ status: 'asserted',
1016
+ }],
1017
+ }));
1018
+ }
1019
+
1020
+ /**
1021
+ * RC2 delivery-policy witnessをprovenance付きnormalized boundary bundleへ変換する。
1022
+ * @param {unknown} inputs
1023
+ * @returns {object}
1024
+ */
1025
+ export function compileDeliveryPolicyBoundaryBundleV2(inputs) {
1026
+ if (!exactRecord(inputs, INPUT_KEYS)) fail('input shapeが不正');
1027
+ const {
1028
+ planInput,
1029
+ candidateSpec,
1030
+ manualEvidence,
1031
+ querySet,
1032
+ sourceSnapshot,
1033
+ sensorEvidence,
1034
+ } = inputs;
1035
+
1036
+ if (!validatePlanInput(planInput)) fail('plan inputが不正');
1037
+ const queryById = queryMapFor(querySet);
1038
+ const candidateDigest = validateCandidateSpec(candidateSpec, queryById);
1039
+ const manualByTodo = manualEvidenceByTodo(manualEvidence);
1040
+ assertTodoBindings(planInput, candidateSpec, manualByTodo);
1041
+ const snapshot = validateSourceSnapshot(sourceSnapshot, candidateSpec);
1042
+ const outcomeById = validatePortableEvidence({
1043
+ sensorEvidence,
1044
+ querySet,
1045
+ queryById,
1046
+ candidateSpec,
1047
+ filesByPath: snapshot.filesByPath,
1048
+ });
1049
+ assertActivation(candidateSpec, snapshot.topology, outcomeById, queryById);
1050
+
1051
+ const manualEvidenceDigest = digestArtifact(manualEvidence);
1052
+ const resources = [
1053
+ ...structuralResources({
1054
+ candidateSpec,
1055
+ candidateDigest,
1056
+ topology: snapshot.topology,
1057
+ outcomeById,
1058
+ sensorEvidence,
1059
+ }),
1060
+ ...manualResources(manualByTodo, manualEvidenceDigest),
1061
+ ];
1062
+ if (new Set(resources.map(({ resource_id: id }) => id)).size !== resources.length) {
1063
+ fail('resource ID collisionを検出した');
1064
+ }
1065
+
1066
+ return compileBoundaryObservationV2({
1067
+ schema_version: 'lattice.boundary_observation_set.v2',
1068
+ source: {
1069
+ snapshot_digest: digestArtifact(sourceSnapshot),
1070
+ candidate_witness_digest: candidateDigest,
1071
+ query_set_digest: digestArtifact(querySet),
1072
+ manual_evidence_digest: manualEvidenceDigest,
1073
+ },
1074
+ capacity: planInput.capacity.writers,
1075
+ todos: planInput.todos.map(({ id }) => id),
1076
+ resources,
1077
+ precedences: [],
1078
+ });
1079
+ }