@quolu/lattice 0.52.0 → 0.52.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +147 -147
- package/README.ja.md +355 -355
- package/README.md +258 -258
- package/bin/lattice-mcp.mjs +0 -0
- package/bin/lattice-scripted-adapter.mjs +0 -0
- package/bin/lattice-scripted-worker.mjs +0 -0
- package/bin/lattice-work-order-adapter.mjs +0 -0
- package/bin/lattice.mjs +0 -0
- package/docs/bridge-setup.md +132 -132
- package/docs/schemas/lattice.executor_packet.v1.schema.json +57 -57
- package/docs/schemas/lattice.executor_receipt.v1.schema.json +66 -66
- package/docs/schemas/lattice.phase_todo_revision.v3.schema.json +360 -360
- package/docs/schemas/lattice.plan_create_input.v1.schema.json +56 -56
- package/docs/schemas/lattice.plan_create_input.v2.schema.json +72 -72
- package/docs/schemas/lattice.plan_create_input.v3.schema.json +81 -81
- package/docs/schemas/lattice.plan_create_input.v4.schema.json +85 -85
- package/docs/schemas/lattice.run_request.v1.schema.json +238 -238
- package/docs/schemas/lattice.runtime_adapter_capabilities.v2.schema.json +55 -55
- package/docs/schemas/lattice.runtime_adapter_registration_input.v1.schema.json +78 -78
- package/docs/schemas/lattice.runtime_adapter_registration_input.v2.schema.json +86 -86
- package/docs/schemas/lattice.todo_extraction.v2.schema.json +298 -298
- package/docs/schemas/lattice.todo_extraction.v3.schema.json +146 -146
- package/docs/schemas/lattice.todo_revision.v2.schema.json +260 -260
- package/docs/schemas/lattice.todo_revision_set.v3.schema.json +363 -363
- package/package.json +103 -103
- package/sensor/LICENSE +21 -21
- package/sensor/NOTICE +19 -19
- package/sensor/dist/bin/lattice-sensor.js +9 -9
- package/sensor/dist/db/index.js +24 -24
- package/sensor/dist/db/migrations.js +41 -41
- package/sensor/dist/db/queries.js +164 -164
- package/sensor/dist/db/schema.sql +205 -205
- package/sensor/dist/directory.js +5 -5
- package/sensor/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-go.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-java.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-javascript.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-python.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-tsx.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-typescript.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
- package/sensor/dist/mcp/liveness-watchdog.js +53 -53
- package/sensor/dist/mcp/server-instructions.js +95 -95
- package/sensor/package.json +56 -56
- 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-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/hash-chain.mjs +76 -76
- package/src/hooks-cli.mjs +1057 -1053
- package/src/isolation-runner.mjs +409 -409
- package/src/node-version-guard.mjs +44 -44
- package/src/project-cli.mjs +697 -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 -1502
- 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 -520
- package/src/runtime-cli.mjs +4588 -4584
- package/src/runtime-contracts.mjs +824 -824
- package/src/runtime-control-store.mjs +604 -600
- 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 -162
- 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 -477
- package/src/runtime-hold-recompile.mjs +916 -916
- package/src/runtime-io-sentinel.mjs +391 -391
- package/src/runtime-lifecycle-lock.mjs +294 -290
- package/src/runtime-managed-supervisor.mjs +1490 -1490
- package/src/runtime-multi-epoch-store.mjs +838 -834
- package/src/runtime-projection.mjs +269 -269
- package/src/runtime-pull-intake.mjs +1192 -1188
- package/src/runtime-scripted-adapter-controller.mjs +1160 -1156
- 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 -1167
- 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 +3141 -3126
- 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 -625
- 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 +4322 -4314
- package/src/treatment-compiler.mjs +728 -728
- package/src/treatment-runner.mjs +656 -656
- package/src/witness-scaffold.mjs +180 -180
|
@@ -1,511 +1,511 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
digestArtifact,
|
|
5
|
-
validateBoundaryManifest,
|
|
6
|
-
validatePlanInput,
|
|
7
|
-
} from './artifact-contracts.mjs';
|
|
8
|
-
import {
|
|
9
|
-
validateBoundaryVerdictV2,
|
|
10
|
-
validateNormalizedBoundaryBundleV2,
|
|
11
|
-
validatePlanGraphV2,
|
|
12
|
-
} from './artifact-contracts-v2.mjs';
|
|
13
|
-
import { compileBoundaryObservationV2 } from './boundary-observation-compiler-v2.mjs';
|
|
14
|
-
import { compileSchedulabilityGraphV2 } from './schedulability-compiler-v2.mjs';
|
|
15
|
-
|
|
16
|
-
const INPUT_KEYS = [
|
|
17
|
-
'planInput',
|
|
18
|
-
'candidateSpec',
|
|
19
|
-
'manualEvidence',
|
|
20
|
-
'querySet',
|
|
21
|
-
'boundaryManifest',
|
|
22
|
-
];
|
|
23
|
-
const EXPECTED_CANDIDATE_DIGEST = '3250c93a8bd0958acee046f2b7ea371170290b7e256d6392421009db2e1cd8ac';
|
|
24
|
-
const EXPECTED_CANDIDATE_ID = 'extract-dispatch-production-and-test-policies';
|
|
25
|
-
const QUERY_OPERATIONS = new Set([
|
|
26
|
-
'status',
|
|
27
|
-
'query',
|
|
28
|
-
'callers',
|
|
29
|
-
'callees',
|
|
30
|
-
'impact',
|
|
31
|
-
'affected',
|
|
32
|
-
]);
|
|
33
|
-
const V2_SENSOR_STATUSES = new Set([
|
|
34
|
-
'ready',
|
|
35
|
-
'symbol_absent',
|
|
36
|
-
'empty',
|
|
37
|
-
'unresolved',
|
|
38
|
-
'command_failure',
|
|
39
|
-
'invalid_json',
|
|
40
|
-
'stale',
|
|
41
|
-
'unsupported',
|
|
42
|
-
]);
|
|
43
|
-
|
|
44
|
-
function fail(reason) {
|
|
45
|
-
throw new TypeError(`RC1 transfer front-end契約違反: ${reason}`);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function plainRecord(value) {
|
|
49
|
-
return value !== null
|
|
50
|
-
&& typeof value === 'object'
|
|
51
|
-
&& !Array.isArray(value)
|
|
52
|
-
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function exactRecord(value, keys) {
|
|
56
|
-
if (!plainRecord(value)) return false;
|
|
57
|
-
const actual = Object.keys(value).sort();
|
|
58
|
-
const expected = [...keys].sort();
|
|
59
|
-
return actual.length === expected.length
|
|
60
|
-
&& actual.every((key, index) => key === expected[index]);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function requireArray(value, label) {
|
|
64
|
-
if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype) {
|
|
65
|
-
fail(`${label}がarrayではない`);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function compareText(left, right) {
|
|
70
|
-
if (left < right) return -1;
|
|
71
|
-
if (left > right) return 1;
|
|
72
|
-
return 0;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function boundedText(value) {
|
|
76
|
-
return typeof value === 'string'
|
|
77
|
-
&& value.length > 0
|
|
78
|
-
&& value === value.trim()
|
|
79
|
-
&& Buffer.byteLength(value, 'utf8') <= 4_096;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function uniqueBoundedStrings(value, label) {
|
|
83
|
-
requireArray(value, label);
|
|
84
|
-
if (!value.every(boundedText) || new Set(value).size !== value.length) {
|
|
85
|
-
fail(`${label}がunique bounded string arrayではない`);
|
|
86
|
-
}
|
|
87
|
-
return [...value];
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function sameStringSet(left, right) {
|
|
91
|
-
return left.length === right.length
|
|
92
|
-
&& new Set(left).size === left.length
|
|
93
|
-
&& new Set(right).size === right.length
|
|
94
|
-
&& left.every((value) => right.includes(value));
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function resourceKey(kind, target) {
|
|
98
|
-
return `${kind}\u0000${target}`;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function resourceId(kind, target) {
|
|
102
|
-
const digest = createHash('sha256').update(resourceKey(kind, target)).digest('hex');
|
|
103
|
-
return `rc1-${kind}-${digest.slice(0, 20)}`;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function validateCandidateSpec(candidateSpec) {
|
|
107
|
-
if (!plainRecord(candidateSpec)
|
|
108
|
-
|| candidateSpec.schema !== 'lattice.rc1.boundary_candidate_spec.v2'
|
|
109
|
-
|| candidateSpec.candidate_id !== EXPECTED_CANDIDATE_ID) {
|
|
110
|
-
fail('candidate spec identityが不正');
|
|
111
|
-
}
|
|
112
|
-
const candidateDigest = digestArtifact(candidateSpec);
|
|
113
|
-
if (candidateDigest !== EXPECTED_CANDIDATE_DIGEST) {
|
|
114
|
-
fail('candidate specがRC1 v6 accepted witnessと一致しない');
|
|
115
|
-
}
|
|
116
|
-
requireArray(candidateSpec.todos, 'candidateSpec.todos');
|
|
117
|
-
return candidateDigest;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function queryMapFor(querySet) {
|
|
121
|
-
if (!exactRecord(querySet, ['schema', 'queries'])
|
|
122
|
-
|| querySet.schema !== 'lattice.sensor_query_set.v2') {
|
|
123
|
-
fail('query set shapeまたはschemaが不正');
|
|
124
|
-
}
|
|
125
|
-
requireArray(querySet.queries, 'querySet.queries');
|
|
126
|
-
const byId = new Map();
|
|
127
|
-
for (const query of querySet.queries) {
|
|
128
|
-
if (!plainRecord(query)
|
|
129
|
-
|| !boundedText(query.id)
|
|
130
|
-
|| !QUERY_OPERATIONS.has(query.operation)
|
|
131
|
-
|| byId.has(query.id)) {
|
|
132
|
-
fail('query set entryが不正または重複している');
|
|
133
|
-
}
|
|
134
|
-
if (query.operation === 'status') {
|
|
135
|
-
if (!exactRecord(query, ['id', 'operation'])) fail(`query ${query.id} shapeが不正`);
|
|
136
|
-
} else if (query.operation === 'affected') {
|
|
137
|
-
if (!exactRecord(query, ['id', 'operation', 'targets'])) {
|
|
138
|
-
fail(`query ${query.id} shapeが不正`);
|
|
139
|
-
}
|
|
140
|
-
uniqueBoundedStrings(query.targets, `query ${query.id}.targets`);
|
|
141
|
-
} else if (!exactRecord(query, ['id', 'operation', 'target'])
|
|
142
|
-
|| !boundedText(query.target)) {
|
|
143
|
-
fail(`query ${query.id} shapeが不正`);
|
|
144
|
-
}
|
|
145
|
-
byId.set(query.id, query);
|
|
146
|
-
}
|
|
147
|
-
if (byId.size < 1) fail('query setが空である');
|
|
148
|
-
return byId;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function manualEvidenceByTodo(manualEvidence) {
|
|
152
|
-
if (!exactRecord(manualEvidence, ['schema', 'evidence'])
|
|
153
|
-
|| manualEvidence.schema !== 'lattice.manual_boundary_evidence.v1') {
|
|
154
|
-
fail('manual evidence shapeまたはschemaが不正');
|
|
155
|
-
}
|
|
156
|
-
requireArray(manualEvidence.evidence, 'manualEvidence.evidence');
|
|
157
|
-
const byTodo = new Map();
|
|
158
|
-
for (const entry of manualEvidence.evidence) {
|
|
159
|
-
if (!exactRecord(entry, [
|
|
160
|
-
'todo_id',
|
|
161
|
-
'state_reads',
|
|
162
|
-
'state_writes',
|
|
163
|
-
'effects',
|
|
164
|
-
'unknowns',
|
|
165
|
-
]) || !boundedText(entry.todo_id) || byTodo.has(entry.todo_id)) {
|
|
166
|
-
fail('manual evidence entryが不正またはTODO重複している');
|
|
167
|
-
}
|
|
168
|
-
uniqueBoundedStrings(entry.state_reads, `${entry.todo_id}.state_reads`);
|
|
169
|
-
uniqueBoundedStrings(entry.state_writes, `${entry.todo_id}.state_writes`);
|
|
170
|
-
uniqueBoundedStrings(entry.effects, `${entry.todo_id}.effects`);
|
|
171
|
-
uniqueBoundedStrings(entry.unknowns, `${entry.todo_id}.unknowns`);
|
|
172
|
-
if (entry.state_reads.length > 0) {
|
|
173
|
-
fail('RC1 v6 transferはread/write別を失うstate_readsを受理しない');
|
|
174
|
-
}
|
|
175
|
-
byTodo.set(entry.todo_id, entry);
|
|
176
|
-
}
|
|
177
|
-
if (byTodo.size < 1) fail('manual evidenceが空である');
|
|
178
|
-
return byTodo;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function surfaceMap(candidateTodo, mode) {
|
|
182
|
-
const modeSpec = candidateTodo[mode];
|
|
183
|
-
const surfaces = new Map();
|
|
184
|
-
const add = (kind, target, queryId, queryTarget, ref) => {
|
|
185
|
-
const key = resourceKey(kind, target);
|
|
186
|
-
const existing = surfaces.get(key);
|
|
187
|
-
const value = { queryId, queryTarget, ref };
|
|
188
|
-
if (existing && (existing.queryId !== queryId || existing.queryTarget !== queryTarget)) {
|
|
189
|
-
fail(`candidate ${candidateTodo.todo_id}/${mode} surfaceが曖昧である`);
|
|
190
|
-
}
|
|
191
|
-
surfaces.set(key, value);
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
add(
|
|
195
|
-
'symbol',
|
|
196
|
-
modeSpec.production.symbol,
|
|
197
|
-
modeSpec.production.query_id,
|
|
198
|
-
modeSpec.production.symbol,
|
|
199
|
-
`${candidateTodo.todo_id}/${mode}/production`,
|
|
200
|
-
);
|
|
201
|
-
add(
|
|
202
|
-
'path',
|
|
203
|
-
modeSpec.production.path,
|
|
204
|
-
modeSpec.production.query_id,
|
|
205
|
-
modeSpec.production.symbol,
|
|
206
|
-
`${candidateTodo.todo_id}/${mode}/production`,
|
|
207
|
-
);
|
|
208
|
-
modeSpec.tests.forEach((testSurface, index) => {
|
|
209
|
-
const queryTarget = testSurface.symbol ?? testSurface.path;
|
|
210
|
-
add(
|
|
211
|
-
'path',
|
|
212
|
-
testSurface.path,
|
|
213
|
-
testSurface.query_id,
|
|
214
|
-
queryTarget,
|
|
215
|
-
`${candidateTodo.todo_id}/${mode}/tests/${index}`,
|
|
216
|
-
);
|
|
217
|
-
if (testSurface.symbol !== null) {
|
|
218
|
-
add(
|
|
219
|
-
'symbol',
|
|
220
|
-
testSurface.symbol,
|
|
221
|
-
testSurface.query_id,
|
|
222
|
-
queryTarget,
|
|
223
|
-
`${candidateTodo.todo_id}/${mode}/tests/${index}`,
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
return surfaces;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function activeModeFor(candidateByTodo, manifestByTodo) {
|
|
231
|
-
const modes = ['current', 'proposed'].filter((mode) => {
|
|
232
|
-
for (const [todoId, candidateTodo] of candidateByTodo) {
|
|
233
|
-
const manifestTodo = manifestByTodo.get(todoId);
|
|
234
|
-
const actual = manifestTodo.writes
|
|
235
|
-
.filter(({ kind }) => kind === 'symbol' || kind === 'path')
|
|
236
|
-
.map(({ kind, target }) => resourceKey(kind, target));
|
|
237
|
-
const expected = [...surfaceMap(candidateTodo, mode).keys()];
|
|
238
|
-
if (!sameStringSet(actual, expected)) return false;
|
|
239
|
-
}
|
|
240
|
-
return true;
|
|
241
|
-
});
|
|
242
|
-
if (modes.length !== 1) {
|
|
243
|
-
fail('manifest structural writesがcurrent/proposedの一方へexact対応しない');
|
|
244
|
-
}
|
|
245
|
-
return modes[0];
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function assertTodoBindings({ planInput, candidateSpec, manualByTodo, boundaryManifest }) {
|
|
249
|
-
const planIds = planInput.todos.map(({ id }) => id);
|
|
250
|
-
const candidateIds = candidateSpec.todos.map(({ todo_id: todoId }) => todoId);
|
|
251
|
-
const manualIds = [...manualByTodo.keys()];
|
|
252
|
-
const manifestIds = boundaryManifest.todos.map(({ id }) => id);
|
|
253
|
-
if (!sameStringSet(planIds, candidateIds)
|
|
254
|
-
|| !sameStringSet(planIds, manualIds)
|
|
255
|
-
|| !sameStringSet(planIds, manifestIds)) {
|
|
256
|
-
fail('plan/candidate/manual/manifestのTODO集合が一致しない');
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const planByTodo = new Map(planInput.todos.map((todo) => [todo.id, todo]));
|
|
260
|
-
for (const candidateTodo of candidateSpec.todos) {
|
|
261
|
-
const planTodo = planByTodo.get(candidateTodo.todo_id);
|
|
262
|
-
if (candidateTodo.outcome !== planTodo.outcome
|
|
263
|
-
|| candidateTodo.current.production.symbol !== planTodo.anchor.symbol
|
|
264
|
-
|| candidateTodo.current.production.path !== planTodo.anchor.path) {
|
|
265
|
-
fail(`candidate ${candidateTodo.todo_id}がplan TODOへbindしていない`);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function graphEvidenceById(boundaryManifest, queryById) {
|
|
271
|
-
const evidenceById = new Map();
|
|
272
|
-
for (const evidence of boundaryManifest.graph_evidence) {
|
|
273
|
-
const query = queryById.get(evidence.id);
|
|
274
|
-
if (!query || query.operation !== evidence.operation || evidenceById.has(evidence.id)) {
|
|
275
|
-
fail(`graph evidence ${evidence.id}がquery setへexact bindしていない`);
|
|
276
|
-
}
|
|
277
|
-
evidenceById.set(evidence.id, evidence);
|
|
278
|
-
}
|
|
279
|
-
if (evidenceById.size !== queryById.size
|
|
280
|
-
|| [...queryById.keys()].some((queryId) => !evidenceById.has(queryId))) {
|
|
281
|
-
fail('query setとgraph evidenceのID集合が一致しない');
|
|
282
|
-
}
|
|
283
|
-
return evidenceById;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function assertManualBindings(manualByTodo, boundaryManifest) {
|
|
287
|
-
const manifestByTodo = new Map(boundaryManifest.manual_evidence
|
|
288
|
-
.map((entry) => [entry.todo_id, entry]));
|
|
289
|
-
for (const [todoId, entry] of manualByTodo) {
|
|
290
|
-
const manifestEntry = manifestByTodo.get(todoId);
|
|
291
|
-
if (!manifestEntry
|
|
292
|
-
|| manifestEntry.id !== `manual-${todoId}`
|
|
293
|
-
|| manifestEntry.result_digest !== digestArtifact(entry)) {
|
|
294
|
-
fail(`manual evidence ${todoId}がmanifest entryへbindしていない`);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function structuralResources({
|
|
300
|
-
candidateSpec,
|
|
301
|
-
candidateDigest,
|
|
302
|
-
manifestByTodo,
|
|
303
|
-
activeMode,
|
|
304
|
-
queryById,
|
|
305
|
-
evidenceById,
|
|
306
|
-
}) {
|
|
307
|
-
const groups = new Map();
|
|
308
|
-
const candidateByTodo = new Map(candidateSpec.todos
|
|
309
|
-
.map((todo) => [todo.todo_id, todo]));
|
|
310
|
-
|
|
311
|
-
for (const [todoId, manifestTodo] of manifestByTodo) {
|
|
312
|
-
const surfaces = surfaceMap(candidateByTodo.get(todoId), activeMode);
|
|
313
|
-
for (const { kind, target } of manifestTodo.writes) {
|
|
314
|
-
if (kind !== 'symbol' && kind !== 'path') continue;
|
|
315
|
-
const key = resourceKey(kind, target);
|
|
316
|
-
const surface = surfaces.get(key);
|
|
317
|
-
if (!surface) fail(`manifest resource ${kind}:${target}がcandidate surfaceにない`);
|
|
318
|
-
const query = queryById.get(surface.queryId);
|
|
319
|
-
if (!query
|
|
320
|
-
|| query.operation !== 'query'
|
|
321
|
-
|| query.target !== surface.queryTarget) {
|
|
322
|
-
fail(`candidate surface ${surface.ref}のquery bindingが不正`);
|
|
323
|
-
}
|
|
324
|
-
const group = groups.get(key) ?? {
|
|
325
|
-
kind,
|
|
326
|
-
target,
|
|
327
|
-
todoIds: new Set(),
|
|
328
|
-
queryIds: new Set(),
|
|
329
|
-
surfaceRefs: new Set(),
|
|
330
|
-
};
|
|
331
|
-
group.todoIds.add(todoId);
|
|
332
|
-
group.queryIds.add(surface.queryId);
|
|
333
|
-
group.surfaceRefs.add(surface.ref);
|
|
334
|
-
groups.set(key, group);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
return [...groups.values()].map((group) => {
|
|
339
|
-
if (group.queryIds.size !== 1) {
|
|
340
|
-
fail(`shared resource ${group.kind}:${group.target}のquery provenanceが曖昧である`);
|
|
341
|
-
}
|
|
342
|
-
const queryId = [...group.queryIds][0];
|
|
343
|
-
const evidence = evidenceById.get(queryId);
|
|
344
|
-
if (!evidence || !V2_SENSOR_STATUSES.has(evidence.status)) {
|
|
345
|
-
fail(`resource ${group.kind}:${group.target}のLatticeSensor statusをv2へ保持できない`);
|
|
346
|
-
}
|
|
347
|
-
return {
|
|
348
|
-
resource_id: resourceId(group.kind, group.target),
|
|
349
|
-
kind: group.kind,
|
|
350
|
-
target: group.target,
|
|
351
|
-
todo_ids: [...group.todoIds].sort(compareText),
|
|
352
|
-
provenance: [
|
|
353
|
-
{
|
|
354
|
-
source: 'sensor',
|
|
355
|
-
evidence_ref: `boundary-manifest.graph_evidence#${queryId}`,
|
|
356
|
-
evidence_digest: evidence.result_digest,
|
|
357
|
-
status: evidence.status,
|
|
358
|
-
},
|
|
359
|
-
{
|
|
360
|
-
source: 'manual_candidate_spec',
|
|
361
|
-
evidence_ref: `candidate-spec#${[...group.surfaceRefs].sort(compareText).join(',')}`,
|
|
362
|
-
evidence_digest: candidateDigest,
|
|
363
|
-
status: 'asserted',
|
|
364
|
-
},
|
|
365
|
-
],
|
|
366
|
-
};
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function manualResources({ manualByTodo, manualEvidenceDigest, manifestByTodo }) {
|
|
371
|
-
const groups = new Map();
|
|
372
|
-
const add = (kind, target, todoId) => {
|
|
373
|
-
const key = resourceKey(kind, target);
|
|
374
|
-
const group = groups.get(key) ?? { kind, target, todoIds: new Set() };
|
|
375
|
-
group.todoIds.add(todoId);
|
|
376
|
-
groups.set(key, group);
|
|
377
|
-
};
|
|
378
|
-
|
|
379
|
-
for (const [todoId, entry] of manualByTodo) {
|
|
380
|
-
entry.state_writes.forEach((target) => add('state', target, todoId));
|
|
381
|
-
entry.effects.forEach((target) => add('effect', target, todoId));
|
|
382
|
-
entry.unknowns.forEach((target) => add('dynamic', target, todoId));
|
|
383
|
-
|
|
384
|
-
const manifestTodo = manifestByTodo.get(todoId);
|
|
385
|
-
const actualManualWrites = manifestTodo.writes
|
|
386
|
-
.filter(({ kind }) => kind === 'state' || kind === 'effect')
|
|
387
|
-
.map(({ kind, target }) => resourceKey(kind, target));
|
|
388
|
-
const expectedManualWrites = [
|
|
389
|
-
...entry.state_writes.map((target) => resourceKey('state', target)),
|
|
390
|
-
...entry.effects.map((target) => resourceKey('effect', target)),
|
|
391
|
-
];
|
|
392
|
-
if (!sameStringSet(actualManualWrites, expectedManualWrites)
|
|
393
|
-
|| !sameStringSet(manifestTodo.unknowns, entry.unknowns)) {
|
|
394
|
-
fail(`manual evidence ${todoId}とmanifest state/effect/unknownが一致しない`);
|
|
395
|
-
}
|
|
396
|
-
const unsupportedWrites = manifestTodo.writes.filter(({ kind }) => (
|
|
397
|
-
kind !== 'symbol' && kind !== 'path' && kind !== 'state' && kind !== 'effect'
|
|
398
|
-
));
|
|
399
|
-
if (unsupportedWrites.length > 0) {
|
|
400
|
-
fail(`manifest ${todoId}に未対応write kindがある`);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
return [...groups.values()].map((group) => ({
|
|
405
|
-
resource_id: resourceId(group.kind, group.target),
|
|
406
|
-
kind: group.kind,
|
|
407
|
-
target: group.target,
|
|
408
|
-
todo_ids: [...group.todoIds].sort(compareText),
|
|
409
|
-
provenance: [{
|
|
410
|
-
source: 'manual_state_effect',
|
|
411
|
-
evidence_ref: `manual-evidence#${group.kind}/${resourceId(group.kind, group.target)}`,
|
|
412
|
-
evidence_digest: manualEvidenceDigest,
|
|
413
|
-
status: 'asserted',
|
|
414
|
-
}],
|
|
415
|
-
}));
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
/**
|
|
419
|
-
* immutable RC1 v6 evidenceをcandidate非依存のv2 bundle/verdict/planへ再compileする。
|
|
420
|
-
* @param {unknown} inputs
|
|
421
|
-
* @returns {{bundle: object, verdict: object, plan: object}}
|
|
422
|
-
*/
|
|
423
|
-
export function compileRc1TransferBundleV2(inputs) {
|
|
424
|
-
if (!exactRecord(inputs, INPUT_KEYS)) fail('input shapeが不正');
|
|
425
|
-
const {
|
|
426
|
-
planInput,
|
|
427
|
-
candidateSpec,
|
|
428
|
-
manualEvidence,
|
|
429
|
-
querySet,
|
|
430
|
-
boundaryManifest,
|
|
431
|
-
} = inputs;
|
|
432
|
-
|
|
433
|
-
if (!validatePlanInput(planInput)) fail('plan inputが不正');
|
|
434
|
-
if (!validateBoundaryManifest(boundaryManifest)) fail('boundary manifestが不正');
|
|
435
|
-
const candidateDigest = validateCandidateSpec(candidateSpec);
|
|
436
|
-
const queryById = queryMapFor(querySet);
|
|
437
|
-
const manualByTodo = manualEvidenceByTodo(manualEvidence);
|
|
438
|
-
const planInputDigest = digestArtifact(planInput);
|
|
439
|
-
const querySetDigest = digestArtifact(querySet);
|
|
440
|
-
const manualEvidenceDigest = digestArtifact(manualEvidence);
|
|
441
|
-
|
|
442
|
-
if (boundaryManifest.plan_input_digest !== planInputDigest
|
|
443
|
-
|| boundaryManifest.source.query_set_digest !== querySetDigest
|
|
444
|
-
|| boundaryManifest.source.manual_evidence_digest !== manualEvidenceDigest) {
|
|
445
|
-
fail('manifest source binding digestが入力と一致しない');
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
assertTodoBindings({ planInput, candidateSpec, manualByTodo, boundaryManifest });
|
|
449
|
-
assertManualBindings(manualByTodo, boundaryManifest);
|
|
450
|
-
const queryEvidenceById = graphEvidenceById(boundaryManifest, queryById);
|
|
451
|
-
const manifestByTodo = new Map(boundaryManifest.todos.map((todo) => [todo.id, todo]));
|
|
452
|
-
if (boundaryManifest.unknowns.length > 0) {
|
|
453
|
-
fail('RC1 v6 transferはsource不明のmanifest global unknownを受理しない');
|
|
454
|
-
}
|
|
455
|
-
if ([...manifestByTodo.values()].some((todo) => todo.reads.length > 0)) {
|
|
456
|
-
fail('RC1 v6 transferはread/write区別を失うmanifest readsを受理しない');
|
|
457
|
-
}
|
|
458
|
-
if ([...manifestByTodo.values()].some((todo) => todo.hard_needs.length > 0)) {
|
|
459
|
-
fail('RC1 v6 transferはcandidate witnessにないhard_needsを受理しない');
|
|
460
|
-
}
|
|
461
|
-
const candidateByTodo = new Map(candidateSpec.todos
|
|
462
|
-
.map((todo) => [todo.todo_id, todo]));
|
|
463
|
-
const activeMode = activeModeFor(candidateByTodo, manifestByTodo);
|
|
464
|
-
|
|
465
|
-
const resources = [
|
|
466
|
-
...structuralResources({
|
|
467
|
-
candidateSpec,
|
|
468
|
-
candidateDigest,
|
|
469
|
-
manifestByTodo,
|
|
470
|
-
activeMode,
|
|
471
|
-
queryById,
|
|
472
|
-
evidenceById: queryEvidenceById,
|
|
473
|
-
}),
|
|
474
|
-
...manualResources({ manualByTodo, manualEvidenceDigest, manifestByTodo }),
|
|
475
|
-
];
|
|
476
|
-
if (new Set(resources.map(({ resource_id: resourceIdValue }) => resourceIdValue)).size
|
|
477
|
-
!== resources.length) {
|
|
478
|
-
fail('resource ID collisionを検出した');
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
const bundle = compileBoundaryObservationV2({
|
|
482
|
-
schema_version: 'lattice.boundary_observation_set.v2',
|
|
483
|
-
source: {
|
|
484
|
-
snapshot_digest: boundaryManifest.source.code_snapshot_digest,
|
|
485
|
-
candidate_witness_digest: candidateDigest,
|
|
486
|
-
query_set_digest: querySetDigest,
|
|
487
|
-
manual_evidence_digest: manualEvidenceDigest,
|
|
488
|
-
},
|
|
489
|
-
capacity: planInput.capacity.writers,
|
|
490
|
-
todos: planInput.todos.map(({ id }) => id),
|
|
491
|
-
resources,
|
|
492
|
-
precedences: [],
|
|
493
|
-
});
|
|
494
|
-
const compiled = compileSchedulabilityGraphV2(bundle.graph);
|
|
495
|
-
if (compiled.outcome !== 'compiled') {
|
|
496
|
-
throw new Error(`RC1 transfer v2 compile失敗: ${compiled.code ?? compiled.outcome}`);
|
|
497
|
-
}
|
|
498
|
-
const verdict = {
|
|
499
|
-
schema_version: 'lattice.boundary_verdict.v2',
|
|
500
|
-
normalized_graph_digest: bundle.graph_digest,
|
|
501
|
-
verdicts: compiled.pairwise_verdicts,
|
|
502
|
-
};
|
|
503
|
-
const plan = compiled.plan;
|
|
504
|
-
|
|
505
|
-
if (!validateNormalizedBoundaryBundleV2(bundle)
|
|
506
|
-
|| !validateBoundaryVerdictV2(verdict, bundle.graph)
|
|
507
|
-
|| !validatePlanGraphV2(plan, bundle.graph)) {
|
|
508
|
-
throw new Error('RC1 transferが生成したv2 artifactの独立検証に失敗した');
|
|
509
|
-
}
|
|
510
|
-
return { bundle, verdict, plan };
|
|
511
|
-
}
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
digestArtifact,
|
|
5
|
+
validateBoundaryManifest,
|
|
6
|
+
validatePlanInput,
|
|
7
|
+
} from './artifact-contracts.mjs';
|
|
8
|
+
import {
|
|
9
|
+
validateBoundaryVerdictV2,
|
|
10
|
+
validateNormalizedBoundaryBundleV2,
|
|
11
|
+
validatePlanGraphV2,
|
|
12
|
+
} from './artifact-contracts-v2.mjs';
|
|
13
|
+
import { compileBoundaryObservationV2 } from './boundary-observation-compiler-v2.mjs';
|
|
14
|
+
import { compileSchedulabilityGraphV2 } from './schedulability-compiler-v2.mjs';
|
|
15
|
+
|
|
16
|
+
const INPUT_KEYS = [
|
|
17
|
+
'planInput',
|
|
18
|
+
'candidateSpec',
|
|
19
|
+
'manualEvidence',
|
|
20
|
+
'querySet',
|
|
21
|
+
'boundaryManifest',
|
|
22
|
+
];
|
|
23
|
+
const EXPECTED_CANDIDATE_DIGEST = '3250c93a8bd0958acee046f2b7ea371170290b7e256d6392421009db2e1cd8ac';
|
|
24
|
+
const EXPECTED_CANDIDATE_ID = 'extract-dispatch-production-and-test-policies';
|
|
25
|
+
const QUERY_OPERATIONS = new Set([
|
|
26
|
+
'status',
|
|
27
|
+
'query',
|
|
28
|
+
'callers',
|
|
29
|
+
'callees',
|
|
30
|
+
'impact',
|
|
31
|
+
'affected',
|
|
32
|
+
]);
|
|
33
|
+
const V2_SENSOR_STATUSES = new Set([
|
|
34
|
+
'ready',
|
|
35
|
+
'symbol_absent',
|
|
36
|
+
'empty',
|
|
37
|
+
'unresolved',
|
|
38
|
+
'command_failure',
|
|
39
|
+
'invalid_json',
|
|
40
|
+
'stale',
|
|
41
|
+
'unsupported',
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
function fail(reason) {
|
|
45
|
+
throw new TypeError(`RC1 transfer front-end契約違反: ${reason}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function plainRecord(value) {
|
|
49
|
+
return value !== null
|
|
50
|
+
&& typeof value === 'object'
|
|
51
|
+
&& !Array.isArray(value)
|
|
52
|
+
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function exactRecord(value, keys) {
|
|
56
|
+
if (!plainRecord(value)) return false;
|
|
57
|
+
const actual = Object.keys(value).sort();
|
|
58
|
+
const expected = [...keys].sort();
|
|
59
|
+
return actual.length === expected.length
|
|
60
|
+
&& actual.every((key, index) => key === expected[index]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function requireArray(value, label) {
|
|
64
|
+
if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype) {
|
|
65
|
+
fail(`${label}がarrayではない`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function compareText(left, right) {
|
|
70
|
+
if (left < right) return -1;
|
|
71
|
+
if (left > right) return 1;
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function boundedText(value) {
|
|
76
|
+
return typeof value === 'string'
|
|
77
|
+
&& value.length > 0
|
|
78
|
+
&& value === value.trim()
|
|
79
|
+
&& Buffer.byteLength(value, 'utf8') <= 4_096;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function uniqueBoundedStrings(value, label) {
|
|
83
|
+
requireArray(value, label);
|
|
84
|
+
if (!value.every(boundedText) || new Set(value).size !== value.length) {
|
|
85
|
+
fail(`${label}がunique bounded string arrayではない`);
|
|
86
|
+
}
|
|
87
|
+
return [...value];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function sameStringSet(left, right) {
|
|
91
|
+
return left.length === right.length
|
|
92
|
+
&& new Set(left).size === left.length
|
|
93
|
+
&& new Set(right).size === right.length
|
|
94
|
+
&& left.every((value) => right.includes(value));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function resourceKey(kind, target) {
|
|
98
|
+
return `${kind}\u0000${target}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function resourceId(kind, target) {
|
|
102
|
+
const digest = createHash('sha256').update(resourceKey(kind, target)).digest('hex');
|
|
103
|
+
return `rc1-${kind}-${digest.slice(0, 20)}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function validateCandidateSpec(candidateSpec) {
|
|
107
|
+
if (!plainRecord(candidateSpec)
|
|
108
|
+
|| candidateSpec.schema !== 'lattice.rc1.boundary_candidate_spec.v2'
|
|
109
|
+
|| candidateSpec.candidate_id !== EXPECTED_CANDIDATE_ID) {
|
|
110
|
+
fail('candidate spec identityが不正');
|
|
111
|
+
}
|
|
112
|
+
const candidateDigest = digestArtifact(candidateSpec);
|
|
113
|
+
if (candidateDigest !== EXPECTED_CANDIDATE_DIGEST) {
|
|
114
|
+
fail('candidate specがRC1 v6 accepted witnessと一致しない');
|
|
115
|
+
}
|
|
116
|
+
requireArray(candidateSpec.todos, 'candidateSpec.todos');
|
|
117
|
+
return candidateDigest;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function queryMapFor(querySet) {
|
|
121
|
+
if (!exactRecord(querySet, ['schema', 'queries'])
|
|
122
|
+
|| querySet.schema !== 'lattice.sensor_query_set.v2') {
|
|
123
|
+
fail('query set shapeまたはschemaが不正');
|
|
124
|
+
}
|
|
125
|
+
requireArray(querySet.queries, 'querySet.queries');
|
|
126
|
+
const byId = new Map();
|
|
127
|
+
for (const query of querySet.queries) {
|
|
128
|
+
if (!plainRecord(query)
|
|
129
|
+
|| !boundedText(query.id)
|
|
130
|
+
|| !QUERY_OPERATIONS.has(query.operation)
|
|
131
|
+
|| byId.has(query.id)) {
|
|
132
|
+
fail('query set entryが不正または重複している');
|
|
133
|
+
}
|
|
134
|
+
if (query.operation === 'status') {
|
|
135
|
+
if (!exactRecord(query, ['id', 'operation'])) fail(`query ${query.id} shapeが不正`);
|
|
136
|
+
} else if (query.operation === 'affected') {
|
|
137
|
+
if (!exactRecord(query, ['id', 'operation', 'targets'])) {
|
|
138
|
+
fail(`query ${query.id} shapeが不正`);
|
|
139
|
+
}
|
|
140
|
+
uniqueBoundedStrings(query.targets, `query ${query.id}.targets`);
|
|
141
|
+
} else if (!exactRecord(query, ['id', 'operation', 'target'])
|
|
142
|
+
|| !boundedText(query.target)) {
|
|
143
|
+
fail(`query ${query.id} shapeが不正`);
|
|
144
|
+
}
|
|
145
|
+
byId.set(query.id, query);
|
|
146
|
+
}
|
|
147
|
+
if (byId.size < 1) fail('query setが空である');
|
|
148
|
+
return byId;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function manualEvidenceByTodo(manualEvidence) {
|
|
152
|
+
if (!exactRecord(manualEvidence, ['schema', 'evidence'])
|
|
153
|
+
|| manualEvidence.schema !== 'lattice.manual_boundary_evidence.v1') {
|
|
154
|
+
fail('manual evidence shapeまたはschemaが不正');
|
|
155
|
+
}
|
|
156
|
+
requireArray(manualEvidence.evidence, 'manualEvidence.evidence');
|
|
157
|
+
const byTodo = new Map();
|
|
158
|
+
for (const entry of manualEvidence.evidence) {
|
|
159
|
+
if (!exactRecord(entry, [
|
|
160
|
+
'todo_id',
|
|
161
|
+
'state_reads',
|
|
162
|
+
'state_writes',
|
|
163
|
+
'effects',
|
|
164
|
+
'unknowns',
|
|
165
|
+
]) || !boundedText(entry.todo_id) || byTodo.has(entry.todo_id)) {
|
|
166
|
+
fail('manual evidence entryが不正またはTODO重複している');
|
|
167
|
+
}
|
|
168
|
+
uniqueBoundedStrings(entry.state_reads, `${entry.todo_id}.state_reads`);
|
|
169
|
+
uniqueBoundedStrings(entry.state_writes, `${entry.todo_id}.state_writes`);
|
|
170
|
+
uniqueBoundedStrings(entry.effects, `${entry.todo_id}.effects`);
|
|
171
|
+
uniqueBoundedStrings(entry.unknowns, `${entry.todo_id}.unknowns`);
|
|
172
|
+
if (entry.state_reads.length > 0) {
|
|
173
|
+
fail('RC1 v6 transferはread/write別を失うstate_readsを受理しない');
|
|
174
|
+
}
|
|
175
|
+
byTodo.set(entry.todo_id, entry);
|
|
176
|
+
}
|
|
177
|
+
if (byTodo.size < 1) fail('manual evidenceが空である');
|
|
178
|
+
return byTodo;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function surfaceMap(candidateTodo, mode) {
|
|
182
|
+
const modeSpec = candidateTodo[mode];
|
|
183
|
+
const surfaces = new Map();
|
|
184
|
+
const add = (kind, target, queryId, queryTarget, ref) => {
|
|
185
|
+
const key = resourceKey(kind, target);
|
|
186
|
+
const existing = surfaces.get(key);
|
|
187
|
+
const value = { queryId, queryTarget, ref };
|
|
188
|
+
if (existing && (existing.queryId !== queryId || existing.queryTarget !== queryTarget)) {
|
|
189
|
+
fail(`candidate ${candidateTodo.todo_id}/${mode} surfaceが曖昧である`);
|
|
190
|
+
}
|
|
191
|
+
surfaces.set(key, value);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
add(
|
|
195
|
+
'symbol',
|
|
196
|
+
modeSpec.production.symbol,
|
|
197
|
+
modeSpec.production.query_id,
|
|
198
|
+
modeSpec.production.symbol,
|
|
199
|
+
`${candidateTodo.todo_id}/${mode}/production`,
|
|
200
|
+
);
|
|
201
|
+
add(
|
|
202
|
+
'path',
|
|
203
|
+
modeSpec.production.path,
|
|
204
|
+
modeSpec.production.query_id,
|
|
205
|
+
modeSpec.production.symbol,
|
|
206
|
+
`${candidateTodo.todo_id}/${mode}/production`,
|
|
207
|
+
);
|
|
208
|
+
modeSpec.tests.forEach((testSurface, index) => {
|
|
209
|
+
const queryTarget = testSurface.symbol ?? testSurface.path;
|
|
210
|
+
add(
|
|
211
|
+
'path',
|
|
212
|
+
testSurface.path,
|
|
213
|
+
testSurface.query_id,
|
|
214
|
+
queryTarget,
|
|
215
|
+
`${candidateTodo.todo_id}/${mode}/tests/${index}`,
|
|
216
|
+
);
|
|
217
|
+
if (testSurface.symbol !== null) {
|
|
218
|
+
add(
|
|
219
|
+
'symbol',
|
|
220
|
+
testSurface.symbol,
|
|
221
|
+
testSurface.query_id,
|
|
222
|
+
queryTarget,
|
|
223
|
+
`${candidateTodo.todo_id}/${mode}/tests/${index}`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
return surfaces;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function activeModeFor(candidateByTodo, manifestByTodo) {
|
|
231
|
+
const modes = ['current', 'proposed'].filter((mode) => {
|
|
232
|
+
for (const [todoId, candidateTodo] of candidateByTodo) {
|
|
233
|
+
const manifestTodo = manifestByTodo.get(todoId);
|
|
234
|
+
const actual = manifestTodo.writes
|
|
235
|
+
.filter(({ kind }) => kind === 'symbol' || kind === 'path')
|
|
236
|
+
.map(({ kind, target }) => resourceKey(kind, target));
|
|
237
|
+
const expected = [...surfaceMap(candidateTodo, mode).keys()];
|
|
238
|
+
if (!sameStringSet(actual, expected)) return false;
|
|
239
|
+
}
|
|
240
|
+
return true;
|
|
241
|
+
});
|
|
242
|
+
if (modes.length !== 1) {
|
|
243
|
+
fail('manifest structural writesがcurrent/proposedの一方へexact対応しない');
|
|
244
|
+
}
|
|
245
|
+
return modes[0];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function assertTodoBindings({ planInput, candidateSpec, manualByTodo, boundaryManifest }) {
|
|
249
|
+
const planIds = planInput.todos.map(({ id }) => id);
|
|
250
|
+
const candidateIds = candidateSpec.todos.map(({ todo_id: todoId }) => todoId);
|
|
251
|
+
const manualIds = [...manualByTodo.keys()];
|
|
252
|
+
const manifestIds = boundaryManifest.todos.map(({ id }) => id);
|
|
253
|
+
if (!sameStringSet(planIds, candidateIds)
|
|
254
|
+
|| !sameStringSet(planIds, manualIds)
|
|
255
|
+
|| !sameStringSet(planIds, manifestIds)) {
|
|
256
|
+
fail('plan/candidate/manual/manifestのTODO集合が一致しない');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const planByTodo = new Map(planInput.todos.map((todo) => [todo.id, todo]));
|
|
260
|
+
for (const candidateTodo of candidateSpec.todos) {
|
|
261
|
+
const planTodo = planByTodo.get(candidateTodo.todo_id);
|
|
262
|
+
if (candidateTodo.outcome !== planTodo.outcome
|
|
263
|
+
|| candidateTodo.current.production.symbol !== planTodo.anchor.symbol
|
|
264
|
+
|| candidateTodo.current.production.path !== planTodo.anchor.path) {
|
|
265
|
+
fail(`candidate ${candidateTodo.todo_id}がplan TODOへbindしていない`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function graphEvidenceById(boundaryManifest, queryById) {
|
|
271
|
+
const evidenceById = new Map();
|
|
272
|
+
for (const evidence of boundaryManifest.graph_evidence) {
|
|
273
|
+
const query = queryById.get(evidence.id);
|
|
274
|
+
if (!query || query.operation !== evidence.operation || evidenceById.has(evidence.id)) {
|
|
275
|
+
fail(`graph evidence ${evidence.id}がquery setへexact bindしていない`);
|
|
276
|
+
}
|
|
277
|
+
evidenceById.set(evidence.id, evidence);
|
|
278
|
+
}
|
|
279
|
+
if (evidenceById.size !== queryById.size
|
|
280
|
+
|| [...queryById.keys()].some((queryId) => !evidenceById.has(queryId))) {
|
|
281
|
+
fail('query setとgraph evidenceのID集合が一致しない');
|
|
282
|
+
}
|
|
283
|
+
return evidenceById;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function assertManualBindings(manualByTodo, boundaryManifest) {
|
|
287
|
+
const manifestByTodo = new Map(boundaryManifest.manual_evidence
|
|
288
|
+
.map((entry) => [entry.todo_id, entry]));
|
|
289
|
+
for (const [todoId, entry] of manualByTodo) {
|
|
290
|
+
const manifestEntry = manifestByTodo.get(todoId);
|
|
291
|
+
if (!manifestEntry
|
|
292
|
+
|| manifestEntry.id !== `manual-${todoId}`
|
|
293
|
+
|| manifestEntry.result_digest !== digestArtifact(entry)) {
|
|
294
|
+
fail(`manual evidence ${todoId}がmanifest entryへbindしていない`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function structuralResources({
|
|
300
|
+
candidateSpec,
|
|
301
|
+
candidateDigest,
|
|
302
|
+
manifestByTodo,
|
|
303
|
+
activeMode,
|
|
304
|
+
queryById,
|
|
305
|
+
evidenceById,
|
|
306
|
+
}) {
|
|
307
|
+
const groups = new Map();
|
|
308
|
+
const candidateByTodo = new Map(candidateSpec.todos
|
|
309
|
+
.map((todo) => [todo.todo_id, todo]));
|
|
310
|
+
|
|
311
|
+
for (const [todoId, manifestTodo] of manifestByTodo) {
|
|
312
|
+
const surfaces = surfaceMap(candidateByTodo.get(todoId), activeMode);
|
|
313
|
+
for (const { kind, target } of manifestTodo.writes) {
|
|
314
|
+
if (kind !== 'symbol' && kind !== 'path') continue;
|
|
315
|
+
const key = resourceKey(kind, target);
|
|
316
|
+
const surface = surfaces.get(key);
|
|
317
|
+
if (!surface) fail(`manifest resource ${kind}:${target}がcandidate surfaceにない`);
|
|
318
|
+
const query = queryById.get(surface.queryId);
|
|
319
|
+
if (!query
|
|
320
|
+
|| query.operation !== 'query'
|
|
321
|
+
|| query.target !== surface.queryTarget) {
|
|
322
|
+
fail(`candidate surface ${surface.ref}のquery bindingが不正`);
|
|
323
|
+
}
|
|
324
|
+
const group = groups.get(key) ?? {
|
|
325
|
+
kind,
|
|
326
|
+
target,
|
|
327
|
+
todoIds: new Set(),
|
|
328
|
+
queryIds: new Set(),
|
|
329
|
+
surfaceRefs: new Set(),
|
|
330
|
+
};
|
|
331
|
+
group.todoIds.add(todoId);
|
|
332
|
+
group.queryIds.add(surface.queryId);
|
|
333
|
+
group.surfaceRefs.add(surface.ref);
|
|
334
|
+
groups.set(key, group);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return [...groups.values()].map((group) => {
|
|
339
|
+
if (group.queryIds.size !== 1) {
|
|
340
|
+
fail(`shared resource ${group.kind}:${group.target}のquery provenanceが曖昧である`);
|
|
341
|
+
}
|
|
342
|
+
const queryId = [...group.queryIds][0];
|
|
343
|
+
const evidence = evidenceById.get(queryId);
|
|
344
|
+
if (!evidence || !V2_SENSOR_STATUSES.has(evidence.status)) {
|
|
345
|
+
fail(`resource ${group.kind}:${group.target}のLatticeSensor statusをv2へ保持できない`);
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
resource_id: resourceId(group.kind, group.target),
|
|
349
|
+
kind: group.kind,
|
|
350
|
+
target: group.target,
|
|
351
|
+
todo_ids: [...group.todoIds].sort(compareText),
|
|
352
|
+
provenance: [
|
|
353
|
+
{
|
|
354
|
+
source: 'sensor',
|
|
355
|
+
evidence_ref: `boundary-manifest.graph_evidence#${queryId}`,
|
|
356
|
+
evidence_digest: evidence.result_digest,
|
|
357
|
+
status: evidence.status,
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
source: 'manual_candidate_spec',
|
|
361
|
+
evidence_ref: `candidate-spec#${[...group.surfaceRefs].sort(compareText).join(',')}`,
|
|
362
|
+
evidence_digest: candidateDigest,
|
|
363
|
+
status: 'asserted',
|
|
364
|
+
},
|
|
365
|
+
],
|
|
366
|
+
};
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function manualResources({ manualByTodo, manualEvidenceDigest, manifestByTodo }) {
|
|
371
|
+
const groups = new Map();
|
|
372
|
+
const add = (kind, target, todoId) => {
|
|
373
|
+
const key = resourceKey(kind, target);
|
|
374
|
+
const group = groups.get(key) ?? { kind, target, todoIds: new Set() };
|
|
375
|
+
group.todoIds.add(todoId);
|
|
376
|
+
groups.set(key, group);
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
for (const [todoId, entry] of manualByTodo) {
|
|
380
|
+
entry.state_writes.forEach((target) => add('state', target, todoId));
|
|
381
|
+
entry.effects.forEach((target) => add('effect', target, todoId));
|
|
382
|
+
entry.unknowns.forEach((target) => add('dynamic', target, todoId));
|
|
383
|
+
|
|
384
|
+
const manifestTodo = manifestByTodo.get(todoId);
|
|
385
|
+
const actualManualWrites = manifestTodo.writes
|
|
386
|
+
.filter(({ kind }) => kind === 'state' || kind === 'effect')
|
|
387
|
+
.map(({ kind, target }) => resourceKey(kind, target));
|
|
388
|
+
const expectedManualWrites = [
|
|
389
|
+
...entry.state_writes.map((target) => resourceKey('state', target)),
|
|
390
|
+
...entry.effects.map((target) => resourceKey('effect', target)),
|
|
391
|
+
];
|
|
392
|
+
if (!sameStringSet(actualManualWrites, expectedManualWrites)
|
|
393
|
+
|| !sameStringSet(manifestTodo.unknowns, entry.unknowns)) {
|
|
394
|
+
fail(`manual evidence ${todoId}とmanifest state/effect/unknownが一致しない`);
|
|
395
|
+
}
|
|
396
|
+
const unsupportedWrites = manifestTodo.writes.filter(({ kind }) => (
|
|
397
|
+
kind !== 'symbol' && kind !== 'path' && kind !== 'state' && kind !== 'effect'
|
|
398
|
+
));
|
|
399
|
+
if (unsupportedWrites.length > 0) {
|
|
400
|
+
fail(`manifest ${todoId}に未対応write kindがある`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return [...groups.values()].map((group) => ({
|
|
405
|
+
resource_id: resourceId(group.kind, group.target),
|
|
406
|
+
kind: group.kind,
|
|
407
|
+
target: group.target,
|
|
408
|
+
todo_ids: [...group.todoIds].sort(compareText),
|
|
409
|
+
provenance: [{
|
|
410
|
+
source: 'manual_state_effect',
|
|
411
|
+
evidence_ref: `manual-evidence#${group.kind}/${resourceId(group.kind, group.target)}`,
|
|
412
|
+
evidence_digest: manualEvidenceDigest,
|
|
413
|
+
status: 'asserted',
|
|
414
|
+
}],
|
|
415
|
+
}));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* immutable RC1 v6 evidenceをcandidate非依存のv2 bundle/verdict/planへ再compileする。
|
|
420
|
+
* @param {unknown} inputs
|
|
421
|
+
* @returns {{bundle: object, verdict: object, plan: object}}
|
|
422
|
+
*/
|
|
423
|
+
export function compileRc1TransferBundleV2(inputs) {
|
|
424
|
+
if (!exactRecord(inputs, INPUT_KEYS)) fail('input shapeが不正');
|
|
425
|
+
const {
|
|
426
|
+
planInput,
|
|
427
|
+
candidateSpec,
|
|
428
|
+
manualEvidence,
|
|
429
|
+
querySet,
|
|
430
|
+
boundaryManifest,
|
|
431
|
+
} = inputs;
|
|
432
|
+
|
|
433
|
+
if (!validatePlanInput(planInput)) fail('plan inputが不正');
|
|
434
|
+
if (!validateBoundaryManifest(boundaryManifest)) fail('boundary manifestが不正');
|
|
435
|
+
const candidateDigest = validateCandidateSpec(candidateSpec);
|
|
436
|
+
const queryById = queryMapFor(querySet);
|
|
437
|
+
const manualByTodo = manualEvidenceByTodo(manualEvidence);
|
|
438
|
+
const planInputDigest = digestArtifact(planInput);
|
|
439
|
+
const querySetDigest = digestArtifact(querySet);
|
|
440
|
+
const manualEvidenceDigest = digestArtifact(manualEvidence);
|
|
441
|
+
|
|
442
|
+
if (boundaryManifest.plan_input_digest !== planInputDigest
|
|
443
|
+
|| boundaryManifest.source.query_set_digest !== querySetDigest
|
|
444
|
+
|| boundaryManifest.source.manual_evidence_digest !== manualEvidenceDigest) {
|
|
445
|
+
fail('manifest source binding digestが入力と一致しない');
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
assertTodoBindings({ planInput, candidateSpec, manualByTodo, boundaryManifest });
|
|
449
|
+
assertManualBindings(manualByTodo, boundaryManifest);
|
|
450
|
+
const queryEvidenceById = graphEvidenceById(boundaryManifest, queryById);
|
|
451
|
+
const manifestByTodo = new Map(boundaryManifest.todos.map((todo) => [todo.id, todo]));
|
|
452
|
+
if (boundaryManifest.unknowns.length > 0) {
|
|
453
|
+
fail('RC1 v6 transferはsource不明のmanifest global unknownを受理しない');
|
|
454
|
+
}
|
|
455
|
+
if ([...manifestByTodo.values()].some((todo) => todo.reads.length > 0)) {
|
|
456
|
+
fail('RC1 v6 transferはread/write区別を失うmanifest readsを受理しない');
|
|
457
|
+
}
|
|
458
|
+
if ([...manifestByTodo.values()].some((todo) => todo.hard_needs.length > 0)) {
|
|
459
|
+
fail('RC1 v6 transferはcandidate witnessにないhard_needsを受理しない');
|
|
460
|
+
}
|
|
461
|
+
const candidateByTodo = new Map(candidateSpec.todos
|
|
462
|
+
.map((todo) => [todo.todo_id, todo]));
|
|
463
|
+
const activeMode = activeModeFor(candidateByTodo, manifestByTodo);
|
|
464
|
+
|
|
465
|
+
const resources = [
|
|
466
|
+
...structuralResources({
|
|
467
|
+
candidateSpec,
|
|
468
|
+
candidateDigest,
|
|
469
|
+
manifestByTodo,
|
|
470
|
+
activeMode,
|
|
471
|
+
queryById,
|
|
472
|
+
evidenceById: queryEvidenceById,
|
|
473
|
+
}),
|
|
474
|
+
...manualResources({ manualByTodo, manualEvidenceDigest, manifestByTodo }),
|
|
475
|
+
];
|
|
476
|
+
if (new Set(resources.map(({ resource_id: resourceIdValue }) => resourceIdValue)).size
|
|
477
|
+
!== resources.length) {
|
|
478
|
+
fail('resource ID collisionを検出した');
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const bundle = compileBoundaryObservationV2({
|
|
482
|
+
schema_version: 'lattice.boundary_observation_set.v2',
|
|
483
|
+
source: {
|
|
484
|
+
snapshot_digest: boundaryManifest.source.code_snapshot_digest,
|
|
485
|
+
candidate_witness_digest: candidateDigest,
|
|
486
|
+
query_set_digest: querySetDigest,
|
|
487
|
+
manual_evidence_digest: manualEvidenceDigest,
|
|
488
|
+
},
|
|
489
|
+
capacity: planInput.capacity.writers,
|
|
490
|
+
todos: planInput.todos.map(({ id }) => id),
|
|
491
|
+
resources,
|
|
492
|
+
precedences: [],
|
|
493
|
+
});
|
|
494
|
+
const compiled = compileSchedulabilityGraphV2(bundle.graph);
|
|
495
|
+
if (compiled.outcome !== 'compiled') {
|
|
496
|
+
throw new Error(`RC1 transfer v2 compile失敗: ${compiled.code ?? compiled.outcome}`);
|
|
497
|
+
}
|
|
498
|
+
const verdict = {
|
|
499
|
+
schema_version: 'lattice.boundary_verdict.v2',
|
|
500
|
+
normalized_graph_digest: bundle.graph_digest,
|
|
501
|
+
verdicts: compiled.pairwise_verdicts,
|
|
502
|
+
};
|
|
503
|
+
const plan = compiled.plan;
|
|
504
|
+
|
|
505
|
+
if (!validateNormalizedBoundaryBundleV2(bundle)
|
|
506
|
+
|| !validateBoundaryVerdictV2(verdict, bundle.graph)
|
|
507
|
+
|| !validatePlanGraphV2(plan, bundle.graph)) {
|
|
508
|
+
throw new Error('RC1 transferが生成したv2 artifactの独立検証に失敗した');
|
|
509
|
+
}
|
|
510
|
+
return { bundle, verdict, plan };
|
|
511
|
+
}
|