@quolu/lattice 0.52.3 → 0.53.0

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 (188) hide show
  1. package/LICENSE +147 -147
  2. package/README.ja.md +355 -355
  3. package/README.md +258 -258
  4. package/bin/lattice-bridge.mjs +25 -0
  5. package/bin/lattice-hub.mjs +67 -0
  6. package/bin/lattice-mcp.mjs +0 -0
  7. package/bin/lattice-scripted-adapter.mjs +0 -0
  8. package/bin/lattice-scripted-worker.mjs +0 -0
  9. package/bin/lattice-work-order-adapter.mjs +0 -0
  10. package/bin/lattice.mjs +0 -0
  11. package/docs/bridge-setup.md +132 -132
  12. package/docs/schemas/lattice.executor_packet.v1.schema.json +57 -57
  13. package/docs/schemas/lattice.executor_receipt.v1.schema.json +66 -66
  14. package/docs/schemas/lattice.phase_todo_revision.v3.schema.json +360 -360
  15. package/docs/schemas/lattice.plan_create_input.v1.schema.json +56 -56
  16. package/docs/schemas/lattice.plan_create_input.v2.schema.json +72 -72
  17. package/docs/schemas/lattice.plan_create_input.v3.schema.json +81 -81
  18. package/docs/schemas/lattice.plan_create_input.v4.schema.json +85 -85
  19. package/docs/schemas/lattice.run_request.v1.schema.json +238 -238
  20. package/docs/schemas/lattice.runtime_adapter_capabilities.v2.schema.json +55 -55
  21. package/docs/schemas/lattice.runtime_adapter_registration_input.v1.schema.json +78 -78
  22. package/docs/schemas/lattice.runtime_adapter_registration_input.v2.schema.json +86 -86
  23. package/docs/schemas/lattice.todo_extraction.v2.schema.json +298 -298
  24. package/docs/schemas/lattice.todo_extraction.v3.schema.json +146 -146
  25. package/docs/schemas/lattice.todo_revision.v2.schema.json +260 -260
  26. package/docs/schemas/lattice.todo_revision_set.v3.schema.json +363 -363
  27. package/package.json +103 -103
  28. package/sensor/LICENSE +21 -21
  29. package/sensor/NOTICE +19 -19
  30. package/sensor/dist/bin/lattice-sensor.js +9 -9
  31. package/sensor/dist/db/index.js +24 -24
  32. package/sensor/dist/db/migrations.js +41 -41
  33. package/sensor/dist/db/queries.js +164 -164
  34. package/sensor/dist/db/schema.sql +205 -205
  35. package/sensor/dist/directory.js +5 -5
  36. package/sensor/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
  37. package/sensor/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
  38. package/sensor/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
  39. package/sensor/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
  40. package/sensor/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
  41. package/sensor/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
  42. package/sensor/dist/extraction/wasm/tree-sitter-go.wasm +0 -0
  43. package/sensor/dist/extraction/wasm/tree-sitter-java.wasm +0 -0
  44. package/sensor/dist/extraction/wasm/tree-sitter-javascript.wasm +0 -0
  45. package/sensor/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
  46. package/sensor/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
  47. package/sensor/dist/extraction/wasm/tree-sitter-python.wasm +0 -0
  48. package/sensor/dist/extraction/wasm/tree-sitter-tsx.wasm +0 -0
  49. package/sensor/dist/extraction/wasm/tree-sitter-typescript.wasm +0 -0
  50. package/sensor/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
  51. package/sensor/dist/mcp/liveness-watchdog.js +53 -53
  52. package/sensor/dist/mcp/server-instructions.js +95 -95
  53. package/sensor/package.json +56 -56
  54. package/src/artifact-contracts-v2.mjs +325 -325
  55. package/src/artifact-contracts.mjs +895 -895
  56. package/src/boundary-compiler.mjs +712 -712
  57. package/src/boundary-observation-compiler-v2.mjs +344 -344
  58. package/src/bounded-seam.mjs +230 -230
  59. package/src/bridge-address.mjs +107 -107
  60. package/src/bridge-cli.mjs +303 -297
  61. package/src/bridge-config.mjs +382 -346
  62. package/src/bridge-daemon.mjs +378 -378
  63. package/src/bridge-hub-heartbeat.mjs +170 -0
  64. package/src/bridge-hub-protocol.mjs +198 -0
  65. package/src/bridge-hub-server.mjs +544 -0
  66. package/src/bridge-launch-agent.mjs +323 -323
  67. package/src/bridge-registrar.mjs +102 -102
  68. package/src/bridge-server.mjs +376 -376
  69. package/src/cli-help.mjs +308 -308
  70. package/src/cli-stdio.mjs +40 -40
  71. package/src/control-compiler.mjs +532 -532
  72. package/src/dag-chain.mjs +261 -261
  73. package/src/factory-diagnostics.mjs +188 -188
  74. package/src/git-process.mjs +72 -0
  75. package/src/hash-chain.mjs +76 -76
  76. package/src/hooks-cli.mjs +1057 -1057
  77. package/src/isolation-runner.mjs +409 -409
  78. package/src/node-version-guard.mjs +44 -44
  79. package/src/project-cli.mjs +3 -3
  80. package/src/project-identity.mjs +130 -130
  81. package/src/rc1-black-box-oracle.mjs +906 -906
  82. package/src/rc1-comparison.mjs +154 -154
  83. package/src/rc1-evidence-bundle.mjs +456 -456
  84. package/src/rc1-v4-campaign.mjs +708 -708
  85. package/src/rc1-v4-transform.mjs +467 -467
  86. package/src/rc1-v5-artifact-set.mjs +855 -855
  87. package/src/rc1-v5-behavior-evidence.mjs +594 -594
  88. package/src/rc1-v5-campaign.mjs +797 -797
  89. package/src/rc1-v5-transform.mjs +421 -421
  90. package/src/rc1-v6-artifact-set.mjs +807 -807
  91. package/src/rc1-v6-behavior-evidence.mjs +273 -273
  92. package/src/rc1-v6-campaign.mjs +623 -623
  93. package/src/rc1-v6-causal-binding.mjs +473 -473
  94. package/src/rc1-v6-measurement.mjs +313 -313
  95. package/src/rc2-artifact-set.mjs +1584 -1584
  96. package/src/rc2-campaign.mjs +1506 -1506
  97. package/src/rc2-delivery-policy-front-end.mjs +1079 -1079
  98. package/src/rc2-delivery-policy-oracle.mjs +134 -134
  99. package/src/rc2-delivery-policy-transform.mjs +1127 -1127
  100. package/src/rc2-rc1-transfer-front-end.mjs +511 -511
  101. package/src/rc3-actual-dogfood.mjs +652 -652
  102. package/src/rc3-dogfood-scaffold.mjs +315 -315
  103. package/src/rc3-scripted-campaign.mjs +1383 -1383
  104. package/src/rc4-stage1-dogfood.mjs +673 -673
  105. package/src/runtime-adapter-registry.mjs +524 -524
  106. package/src/runtime-cli.mjs +4588 -4588
  107. package/src/runtime-contracts.mjs +824 -824
  108. package/src/runtime-control-store.mjs +604 -604
  109. package/src/runtime-controller-protocol.mjs +587 -587
  110. package/src/runtime-decision-verifier.mjs +701 -701
  111. package/src/runtime-diff-observer.mjs +361 -361
  112. package/src/runtime-direct-os-observer.mjs +301 -301
  113. package/src/runtime-driver-state.mjs +166 -166
  114. package/src/runtime-engine.mjs +779 -779
  115. package/src/runtime-errors.mjs +356 -356
  116. package/src/runtime-event-store.mjs +189 -189
  117. package/src/runtime-front-end.mjs +925 -925
  118. package/src/runtime-gate-store.mjs +481 -481
  119. package/src/runtime-hold-recompile.mjs +916 -916
  120. package/src/runtime-io-sentinel.mjs +391 -391
  121. package/src/runtime-lifecycle-lock.mjs +294 -294
  122. package/src/runtime-managed-supervisor.mjs +1490 -1490
  123. package/src/runtime-multi-epoch-store.mjs +838 -838
  124. package/src/runtime-projection.mjs +269 -269
  125. package/src/runtime-pull-intake.mjs +1192 -1192
  126. package/src/runtime-scripted-adapter-controller.mjs +1160 -1160
  127. package/src/runtime-scripted-executor.mjs +163 -163
  128. package/src/runtime-scripted-worktree.mjs +104 -104
  129. package/src/runtime-seam-resolve.mjs +428 -428
  130. package/src/runtime-seam-treatment.mjs +173 -173
  131. package/src/runtime-socket-owner.mjs +125 -125
  132. package/src/runtime-work-order-contracts.mjs +91 -91
  133. package/src/runtime-work-order-controller.mjs +1171 -1171
  134. package/src/runtime-worktree-executor.mjs +199 -199
  135. package/src/schedulability-compiler-v2.mjs +303 -303
  136. package/src/schedulability-verifier-v2.mjs +317 -317
  137. package/src/seam-apply.mjs +549 -549
  138. package/src/seam-commit-shared.mjs +22 -22
  139. package/src/seam-commit-transform.mjs +81 -81
  140. package/src/seam-commit.mjs +18 -18
  141. package/src/seam-cost.mjs +322 -322
  142. package/src/seam-derivation.mjs +188 -188
  143. package/src/seam-gate.mjs +146 -146
  144. package/src/seam-proposal-contracts.mjs +446 -446
  145. package/src/seam-proposal-queries.mjs +521 -521
  146. package/src/seam-proposal.mjs +2011 -2011
  147. package/src/seam-ref.mjs +33 -33
  148. package/src/seam-rewrite.mjs +286 -286
  149. package/src/seam-transform.mjs +554 -554
  150. package/src/seam-verification.mjs +260 -260
  151. package/src/sensor-adapter.mjs +432 -432
  152. package/src/sensor-cli.mjs +139 -139
  153. package/src/sensor-diff.mjs +661 -661
  154. package/src/sensor-node-runtime.mjs +53 -53
  155. package/src/sensor-runtime.mjs +52 -52
  156. package/src/timestamp-contract.mjs +8 -8
  157. package/src/todo-audit-pending.mjs +91 -91
  158. package/src/todo-chain.mjs +178 -178
  159. package/src/todo-cli.mjs +8 -8
  160. package/src/todo-contracts.mjs +728 -728
  161. package/src/todo-dashboard-registry.mjs +573 -573
  162. package/src/todo-dispatch-shape.mjs +190 -190
  163. package/src/todo-gantt-html-independence.mjs +239 -239
  164. package/src/todo-gantt-html-shared.mjs +226 -226
  165. package/src/todo-gantt-html-style.mjs +131 -131
  166. package/src/todo-gantt-html.mjs +248 -248
  167. package/src/todo-gantt-layout.mjs +974 -974
  168. package/src/todo-gantt-live.mjs +361 -361
  169. package/src/todo-gantt-nested.mjs +263 -263
  170. package/src/todo-gantt-presentation.mjs +217 -217
  171. package/src/todo-gantt-scope.mjs +123 -123
  172. package/src/todo-gantt-svg.mjs +353 -353
  173. package/src/todo-independence-contracts.mjs +595 -595
  174. package/src/todo-independence-guidance.mjs +322 -322
  175. package/src/todo-independence.mjs +640 -640
  176. package/src/todo-markdown-renderer.mjs +260 -260
  177. package/src/todo-migration.mjs +448 -448
  178. package/src/todo-narrative-anchor.mjs +130 -130
  179. package/src/todo-note-store.mjs +629 -629
  180. package/src/todo-parallel-candidates.mjs +114 -114
  181. package/src/todo-revision.mjs +995 -995
  182. package/src/todo-split.mjs +472 -472
  183. package/src/todo-status.mjs +690 -690
  184. package/src/todo-store-git-transaction.mjs +418 -418
  185. package/src/todo-store.mjs +42 -21
  186. package/src/treatment-compiler.mjs +728 -728
  187. package/src/treatment-runner.mjs +656 -656
  188. 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
+ }