@quolu/lattice 0.52.0 → 0.52.2

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