@quolu/lattice 0.52.2 → 0.52.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/artifact-contracts-v2.mjs +325 -325
- package/src/artifact-contracts.mjs +895 -895
- package/src/boundary-compiler.mjs +712 -712
- package/src/boundary-observation-compiler-v2.mjs +344 -344
- package/src/bounded-seam.mjs +230 -230
- package/src/bridge-address.mjs +107 -107
- package/src/bridge-cli.mjs +297 -297
- package/src/bridge-config.mjs +346 -346
- package/src/bridge-daemon.mjs +378 -378
- package/src/bridge-hub-protocol.mjs +198 -0
- package/src/bridge-launch-agent.mjs +323 -323
- package/src/bridge-registrar.mjs +102 -102
- package/src/bridge-server.mjs +376 -376
- package/src/cli-help.mjs +308 -308
- package/src/cli-stdio.mjs +40 -40
- package/src/control-compiler.mjs +532 -532
- package/src/dag-chain.mjs +261 -261
- package/src/factory-diagnostics.mjs +188 -188
- package/src/git-process.mjs +72 -0
- package/src/hash-chain.mjs +76 -76
- package/src/hooks-cli.mjs +1057 -1057
- package/src/isolation-runner.mjs +409 -409
- package/src/node-version-guard.mjs +44 -44
- package/src/project-cli.mjs +699 -697
- package/src/project-identity.mjs +130 -130
- package/src/rc1-black-box-oracle.mjs +906 -906
- package/src/rc1-comparison.mjs +154 -154
- package/src/rc1-evidence-bundle.mjs +456 -456
- package/src/rc1-v4-campaign.mjs +708 -708
- package/src/rc1-v4-transform.mjs +467 -467
- package/src/rc1-v5-artifact-set.mjs +855 -855
- package/src/rc1-v5-behavior-evidence.mjs +594 -594
- package/src/rc1-v5-campaign.mjs +797 -797
- package/src/rc1-v5-transform.mjs +421 -421
- package/src/rc1-v6-artifact-set.mjs +807 -807
- package/src/rc1-v6-behavior-evidence.mjs +273 -273
- package/src/rc1-v6-campaign.mjs +623 -623
- package/src/rc1-v6-causal-binding.mjs +473 -473
- package/src/rc1-v6-measurement.mjs +313 -313
- package/src/rc2-artifact-set.mjs +1584 -1584
- package/src/rc2-campaign.mjs +1506 -1506
- package/src/rc2-delivery-policy-front-end.mjs +1079 -1079
- package/src/rc2-delivery-policy-oracle.mjs +134 -134
- package/src/rc2-delivery-policy-transform.mjs +1127 -1127
- package/src/rc2-rc1-transfer-front-end.mjs +511 -511
- package/src/rc3-actual-dogfood.mjs +652 -652
- package/src/rc3-dogfood-scaffold.mjs +315 -315
- package/src/rc3-scripted-campaign.mjs +1383 -1383
- package/src/rc4-stage1-dogfood.mjs +673 -673
- package/src/runtime-adapter-registry.mjs +524 -524
- package/src/runtime-cli.mjs +4588 -4588
- package/src/runtime-contracts.mjs +824 -824
- package/src/runtime-control-store.mjs +604 -604
- package/src/runtime-controller-protocol.mjs +587 -587
- package/src/runtime-decision-verifier.mjs +701 -701
- package/src/runtime-diff-observer.mjs +361 -361
- package/src/runtime-direct-os-observer.mjs +301 -301
- package/src/runtime-driver-state.mjs +166 -166
- package/src/runtime-engine.mjs +779 -779
- package/src/runtime-errors.mjs +356 -356
- package/src/runtime-event-store.mjs +189 -189
- package/src/runtime-front-end.mjs +925 -925
- package/src/runtime-gate-store.mjs +481 -481
- package/src/runtime-hold-recompile.mjs +916 -916
- package/src/runtime-io-sentinel.mjs +391 -391
- package/src/runtime-lifecycle-lock.mjs +294 -294
- package/src/runtime-managed-supervisor.mjs +1490 -1490
- package/src/runtime-multi-epoch-store.mjs +838 -838
- package/src/runtime-projection.mjs +269 -269
- package/src/runtime-pull-intake.mjs +1192 -1192
- package/src/runtime-scripted-adapter-controller.mjs +1160 -1160
- package/src/runtime-scripted-executor.mjs +163 -163
- package/src/runtime-scripted-worktree.mjs +104 -104
- package/src/runtime-seam-resolve.mjs +428 -428
- package/src/runtime-seam-treatment.mjs +173 -173
- package/src/runtime-socket-owner.mjs +125 -125
- package/src/runtime-work-order-contracts.mjs +91 -91
- package/src/runtime-work-order-controller.mjs +1171 -1171
- package/src/runtime-worktree-executor.mjs +199 -199
- package/src/schedulability-compiler-v2.mjs +303 -303
- package/src/schedulability-verifier-v2.mjs +317 -317
- package/src/seam-apply.mjs +549 -549
- package/src/seam-commit-shared.mjs +22 -22
- package/src/seam-commit-transform.mjs +81 -81
- package/src/seam-commit.mjs +18 -18
- package/src/seam-cost.mjs +322 -322
- package/src/seam-derivation.mjs +188 -188
- package/src/seam-gate.mjs +146 -146
- package/src/seam-proposal-contracts.mjs +446 -446
- package/src/seam-proposal-queries.mjs +521 -521
- package/src/seam-proposal.mjs +2011 -2011
- package/src/seam-ref.mjs +33 -33
- package/src/seam-rewrite.mjs +286 -286
- package/src/seam-transform.mjs +554 -554
- package/src/seam-verification.mjs +260 -260
- package/src/sensor-adapter.mjs +432 -432
- package/src/sensor-cli.mjs +139 -139
- package/src/sensor-diff.mjs +661 -661
- package/src/sensor-node-runtime.mjs +53 -53
- package/src/sensor-runtime.mjs +52 -52
- package/src/timestamp-contract.mjs +8 -8
- package/src/todo-audit-pending.mjs +91 -91
- package/src/todo-chain.mjs +178 -178
- package/src/todo-cli.mjs +3143 -3141
- package/src/todo-contracts.mjs +728 -728
- package/src/todo-dashboard-registry.mjs +573 -573
- package/src/todo-dispatch-shape.mjs +190 -190
- package/src/todo-gantt-html-independence.mjs +239 -239
- package/src/todo-gantt-html-shared.mjs +226 -226
- package/src/todo-gantt-html-style.mjs +131 -131
- package/src/todo-gantt-html.mjs +248 -248
- package/src/todo-gantt-layout.mjs +974 -974
- package/src/todo-gantt-live.mjs +361 -361
- package/src/todo-gantt-nested.mjs +263 -263
- package/src/todo-gantt-presentation.mjs +217 -217
- package/src/todo-gantt-scope.mjs +123 -123
- package/src/todo-gantt-svg.mjs +353 -353
- package/src/todo-independence-contracts.mjs +595 -595
- package/src/todo-independence-guidance.mjs +322 -322
- package/src/todo-independence.mjs +640 -640
- package/src/todo-markdown-renderer.mjs +260 -260
- package/src/todo-migration.mjs +448 -448
- package/src/todo-narrative-anchor.mjs +130 -130
- package/src/todo-note-store.mjs +629 -629
- package/src/todo-parallel-candidates.mjs +114 -114
- package/src/todo-revision.mjs +995 -995
- package/src/todo-split.mjs +472 -472
- package/src/todo-status.mjs +690 -690
- package/src/todo-store-git-transaction.mjs +418 -418
- package/src/todo-store.mjs +4360 -4322
- package/src/treatment-compiler.mjs +728 -728
- package/src/treatment-runner.mjs +656 -656
- package/src/witness-scaffold.mjs +180 -180
|
@@ -1,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
|
+
}
|