@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,728 +1,728 @@
|
|
|
1
|
-
import {
|
|
2
|
-
canonicalizeArtifact,
|
|
3
|
-
digestArtifact,
|
|
4
|
-
validateBoundaryManifest,
|
|
5
|
-
validateBoundaryVerdict,
|
|
6
|
-
validatePlanDiff,
|
|
7
|
-
validatePlanGraph,
|
|
8
|
-
validatePlanInput,
|
|
9
|
-
validateTransformArtifact,
|
|
10
|
-
} from './artifact-contracts.mjs';
|
|
11
|
-
import { portableSensorOutcome } from './sensor-adapter.mjs';
|
|
12
|
-
|
|
13
|
-
const SHA256 = /^[0-9a-f]{64}$/;
|
|
14
|
-
const IDENTIFIER = /^[0-9A-Za-z](?:[0-9A-Za-z._-]{0,127})$/;
|
|
15
|
-
const TREATMENT_PLAN_VERSION = 'rc1-treatment-v2';
|
|
16
|
-
const AFFECTED_TEST = 'test/research-dispatch-record.test.mjs';
|
|
17
|
-
const SEAM_BY_CONCERN = new Map([
|
|
18
|
-
['dispatch.channel_policy', {
|
|
19
|
-
symbol: 'selectDispatchChannel',
|
|
20
|
-
path: 'research/fixtures/dispatch-record/src/dispatch-channel.mjs',
|
|
21
|
-
}],
|
|
22
|
-
['dispatch.label_policy', {
|
|
23
|
-
symbol: 'formatDispatchLabel',
|
|
24
|
-
path: 'research/fixtures/dispatch-record/src/dispatch-label.mjs',
|
|
25
|
-
}],
|
|
26
|
-
]);
|
|
27
|
-
|
|
28
|
-
function fail(reason) {
|
|
29
|
-
throw new TypeError(`treatment compile契約違反: ${reason}`);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function exactRecord(value, keys) {
|
|
33
|
-
if (value === null || typeof value !== 'object' || Array.isArray(value)
|
|
34
|
-
|| Object.getPrototypeOf(value) !== Object.prototype) {
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
const actual = Object.keys(value).sort();
|
|
38
|
-
const expected = [...keys].sort();
|
|
39
|
-
return actual.length === expected.length
|
|
40
|
-
&& actual.every((key, index) => key === expected[index]);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function nonEmptyText(value) {
|
|
44
|
-
return typeof value === 'string'
|
|
45
|
-
&& value.length > 0
|
|
46
|
-
&& value === value.trim()
|
|
47
|
-
&& !/[\u0000-\u001f\u007f]/.test(value)
|
|
48
|
-
&& Buffer.byteLength(value, 'utf8') <= 4_096;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function identifier(value) {
|
|
52
|
-
return typeof value === 'string' && IDENTIFIER.test(value);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function uniqueTextArray(value) {
|
|
56
|
-
return Array.isArray(value)
|
|
57
|
-
&& value.length <= 256
|
|
58
|
-
&& value.every(nonEmptyText)
|
|
59
|
-
&& new Set(value).size === value.length;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function assertManualEvidence(planInput, manualEvidence, label) {
|
|
63
|
-
if (!exactRecord(manualEvidence, ['schema', 'evidence'])
|
|
64
|
-
|| manualEvidence.schema !== 'lattice.manual_boundary_evidence.v1'
|
|
65
|
-
|| !Array.isArray(manualEvidence.evidence)
|
|
66
|
-
|| manualEvidence.evidence.length !== planInput.todos.length) {
|
|
67
|
-
fail(`${label} manual evidence rootが不正`);
|
|
68
|
-
}
|
|
69
|
-
const expectedIds = planInput.todos.map(({ id }) => id);
|
|
70
|
-
if (manualEvidence.evidence.some((entry, index) => !exactRecord(entry, [
|
|
71
|
-
'todo_id',
|
|
72
|
-
'state_reads',
|
|
73
|
-
'state_writes',
|
|
74
|
-
'effects',
|
|
75
|
-
'unknowns',
|
|
76
|
-
])
|
|
77
|
-
|| entry.todo_id !== expectedIds[index]
|
|
78
|
-
|| !uniqueTextArray(entry.state_reads)
|
|
79
|
-
|| !uniqueTextArray(entry.state_writes)
|
|
80
|
-
|| !uniqueTextArray(entry.effects)
|
|
81
|
-
|| !uniqueTextArray(entry.unknowns))) {
|
|
82
|
-
fail(`${label} manual evidenceがplan TODOと一致しない`);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function assertQuerySet(querySet) {
|
|
87
|
-
if (!exactRecord(querySet, ['schema', 'queries'])
|
|
88
|
-
|| querySet.schema !== 'lattice.sensor_query_set.v1'
|
|
89
|
-
|| !Array.isArray(querySet.queries)
|
|
90
|
-
|| querySet.queries.length !== 10
|
|
91
|
-
|| new Set(querySet.queries.map(({ id }) => id)).size !== querySet.queries.length) {
|
|
92
|
-
fail('query setがRC1 fixed setと一致しない');
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function assertLatticeSensorEvidence(querySet, sensorEvidence) {
|
|
97
|
-
if (!exactRecord(sensorEvidence, ['cwd', 'outcomes'])
|
|
98
|
-
|| !nonEmptyText(sensorEvidence.cwd)
|
|
99
|
-
|| !Array.isArray(sensorEvidence.outcomes)
|
|
100
|
-
|| sensorEvidence.outcomes.length !== querySet.queries.length) {
|
|
101
|
-
fail('LatticeSensor evidence rootがquery setと一致しない');
|
|
102
|
-
}
|
|
103
|
-
for (let index = 0; index < querySet.queries.length; index += 1) {
|
|
104
|
-
const query = querySet.queries[index];
|
|
105
|
-
const outcome = sensorEvidence.outcomes[index];
|
|
106
|
-
if (!outcome || typeof outcome !== 'object' || Array.isArray(outcome)
|
|
107
|
-
|| outcome.id !== query.id
|
|
108
|
-
|| outcome.operation !== query.operation
|
|
109
|
-
|| !nonEmptyText(outcome.outcome)) {
|
|
110
|
-
fail(`LatticeSensor evidence ${query.id}がID/operation/順序と一致しない`);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function treatmentSeams(planInput) {
|
|
116
|
-
if (planInput.plan_version !== 'rc1-control-v1'
|
|
117
|
-
|| planInput.capacity.writers !== 2
|
|
118
|
-
|| planInput.todos.length !== 2
|
|
119
|
-
|| new Set(planInput.todos.map(({ anchor }) => anchor.symbol)).size !== 1
|
|
120
|
-
|| new Set(planInput.todos.map(({ anchor }) => anchor.path)).size !== 1
|
|
121
|
-
|| planInput.todos[0].anchor.path !== planInput.project.fixture_entry) {
|
|
122
|
-
fail('RC1 plan input topologyまたはcapacityがdriftした');
|
|
123
|
-
}
|
|
124
|
-
return planInput.todos.map((todo) => {
|
|
125
|
-
const seam = SEAM_BY_CONCERN.get(todo.concern);
|
|
126
|
-
if (!seam) fail(`treatment seamへ接続できないconcern: ${todo.concern}`);
|
|
127
|
-
return { todo_id: todo.id, ...seam };
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function findSingleQuery(querySet, operation, target) {
|
|
132
|
-
const matches = querySet.queries.filter((query) => query.operation === operation
|
|
133
|
-
&& (target === undefined || query.target === target));
|
|
134
|
-
if (matches.length !== 1) fail(`query setに${operation}:${target ?? ''}がexactly one存在しない`);
|
|
135
|
-
return matches[0];
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function assertQueryCoverage(planInput, querySet, seams) {
|
|
139
|
-
const anchorSymbol = planInput.todos[0].anchor.symbol;
|
|
140
|
-
findSingleQuery(querySet, 'status');
|
|
141
|
-
for (const operation of ['query', 'callers', 'callees', 'impact']) {
|
|
142
|
-
findSingleQuery(querySet, operation, anchorSymbol);
|
|
143
|
-
}
|
|
144
|
-
for (const seam of seams) {
|
|
145
|
-
findSingleQuery(querySet, 'query', seam.symbol);
|
|
146
|
-
findSingleQuery(querySet, 'impact', seam.symbol);
|
|
147
|
-
}
|
|
148
|
-
const affected = querySet.queries.filter(({ operation }) => operation === 'affected');
|
|
149
|
-
const expectedTargets = [planInput.project.fixture_entry, ...seams.map(({ path }) => path)];
|
|
150
|
-
if (affected.length !== 1
|
|
151
|
-
|| JSON.stringify(affected[0].targets) !== JSON.stringify(expectedTargets)) {
|
|
152
|
-
fail('same query setのaffected coverageがdriftした');
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function outcomeById(querySet, sensorEvidence) {
|
|
157
|
-
return new Map(querySet.queries.map((query, index) => [
|
|
158
|
-
query.id,
|
|
159
|
-
sensorEvidence.outcomes[index],
|
|
160
|
-
]));
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function exactSymbolAt(records, target, expectedPath) {
|
|
164
|
-
if (!Array.isArray(records)) return false;
|
|
165
|
-
const exact = records.filter((entry) => entry?.node
|
|
166
|
-
&& (entry.node.name === target || entry.node.qualifiedName === target));
|
|
167
|
-
return exact.length === 1 && exact[0].node.filePath === expectedPath;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function assertReadyOutcome(outcomes, query) {
|
|
171
|
-
const outcome = outcomes.get(query.id);
|
|
172
|
-
if (outcome?.outcome !== 'ready') {
|
|
173
|
-
fail(`LatticeSensor evidence ${query.id}はreadyでなく${outcome?.outcome ?? 'missing'}`);
|
|
174
|
-
}
|
|
175
|
-
return outcome;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function treatmentGraphFacts(planInput, querySet, sensorEvidence, seams) {
|
|
179
|
-
const outcomes = outcomeById(querySet, sensorEvidence);
|
|
180
|
-
const status = assertReadyOutcome(outcomes, findSingleQuery(querySet, 'status'));
|
|
181
|
-
if (!status.data || !nonEmptyText(status.data.version)) {
|
|
182
|
-
fail('LatticeSensor statusにversionがない');
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const anchorSymbol = planInput.todos[0].anchor.symbol;
|
|
186
|
-
const anchorPath = planInput.todos[0].anchor.path;
|
|
187
|
-
const anchorQuery = assertReadyOutcome(
|
|
188
|
-
outcomes,
|
|
189
|
-
findSingleQuery(querySet, 'query', anchorSymbol),
|
|
190
|
-
);
|
|
191
|
-
if (!exactSymbolAt(anchorQuery.data, anchorSymbol, anchorPath)) {
|
|
192
|
-
fail('anchor symbolがexpected pathへexact解決しない');
|
|
193
|
-
}
|
|
194
|
-
for (const operation of ['callers', 'callees', 'impact']) {
|
|
195
|
-
const outcome = assertReadyOutcome(
|
|
196
|
-
outcomes,
|
|
197
|
-
findSingleQuery(querySet, operation, anchorSymbol),
|
|
198
|
-
);
|
|
199
|
-
if (!exactSymbolAt(outcome.resolution, anchorSymbol, anchorPath)) {
|
|
200
|
-
fail(`${operation} anchor resolutionがexpected pathと一致しない`);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
for (const seam of seams) {
|
|
204
|
-
const query = assertReadyOutcome(outcomes, findSingleQuery(querySet, 'query', seam.symbol));
|
|
205
|
-
const impact = assertReadyOutcome(outcomes, findSingleQuery(querySet, 'impact', seam.symbol));
|
|
206
|
-
if (!exactSymbolAt(query.data, seam.symbol, seam.path)
|
|
207
|
-
|| !exactSymbolAt(impact.resolution, seam.symbol, seam.path)) {
|
|
208
|
-
fail(`seam ${seam.symbol}がexpected pathへexact解決しない`);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const affectedQuery = querySet.queries.find(({ operation }) => operation === 'affected');
|
|
213
|
-
const affected = assertReadyOutcome(outcomes, affectedQuery);
|
|
214
|
-
if (!Array.isArray(affected.targets)
|
|
215
|
-
|| affected.targets.length !== affectedQuery.targets.length
|
|
216
|
-
|| affected.targets.some((entry, index) => entry.target !== affectedQuery.targets[index]
|
|
217
|
-
|| entry.outcome !== 'ready'
|
|
218
|
-
|| !entry.data
|
|
219
|
-
|| JSON.stringify(entry.data.affectedTests) !== JSON.stringify([AFFECTED_TEST]))) {
|
|
220
|
-
fail('treatment source全体のaffected testがfixed verifierと一致しない');
|
|
221
|
-
}
|
|
222
|
-
return {
|
|
223
|
-
sensorVersion: status.data.version,
|
|
224
|
-
affectedTests: [AFFECTED_TEST],
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function assertBundle(bundle, label) {
|
|
229
|
-
if (!exactRecord(bundle, ['boundary_manifest', 'boundary_verdict', 'plan_graph'])
|
|
230
|
-
|| !validateBoundaryManifest(bundle.boundary_manifest)
|
|
231
|
-
|| !validateBoundaryVerdict(bundle.boundary_verdict)
|
|
232
|
-
|| !validatePlanGraph(bundle.plan_graph)) {
|
|
233
|
-
fail(`${label} control artifactがpublic contractを満たさない`);
|
|
234
|
-
}
|
|
235
|
-
const manifestDigest = digestArtifact(bundle.boundary_manifest);
|
|
236
|
-
if (bundle.boundary_verdict.boundary_manifest_digest !== manifestDigest
|
|
237
|
-
|| bundle.plan_graph.source_manifest_digest !== manifestDigest) {
|
|
238
|
-
fail(`${label} control artifactのdigest chainが不正`);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function assertPredecessors({
|
|
243
|
-
planInput,
|
|
244
|
-
manualNormal,
|
|
245
|
-
manualNegative,
|
|
246
|
-
querySet,
|
|
247
|
-
transformArtifact,
|
|
248
|
-
control,
|
|
249
|
-
codeSnapshotDigest,
|
|
250
|
-
}) {
|
|
251
|
-
if (!validateTransformArtifact(transformArtifact) || transformArtifact.status !== 'accepted') {
|
|
252
|
-
fail('accepted transform predecessorが必要');
|
|
253
|
-
}
|
|
254
|
-
if (!exactRecord(control, ['normal', 'negative'])) {
|
|
255
|
-
fail('control predecessor setが不正');
|
|
256
|
-
}
|
|
257
|
-
assertBundle(control.normal, 'normal');
|
|
258
|
-
assertBundle(control.negative, 'negative');
|
|
259
|
-
const planInputDigest = digestArtifact(planInput);
|
|
260
|
-
const querySetDigest = digestArtifact(querySet);
|
|
261
|
-
const normalManifestDigest = digestArtifact(control.normal.boundary_manifest);
|
|
262
|
-
const normalVerdictDigest = digestArtifact(control.normal.boundary_verdict);
|
|
263
|
-
const normalPlanDigest = digestArtifact(control.normal.plan_graph);
|
|
264
|
-
if (control.normal.boundary_manifest.plan_input_digest !== planInputDigest
|
|
265
|
-
|| control.negative.boundary_manifest.plan_input_digest !== planInputDigest
|
|
266
|
-
|| control.normal.boundary_manifest.source.query_set_digest !== querySetDigest
|
|
267
|
-
|| control.negative.boundary_manifest.source.query_set_digest !== querySetDigest
|
|
268
|
-
|| control.normal.boundary_manifest.source.manual_evidence_digest !== digestArtifact(manualNormal)
|
|
269
|
-
|| control.negative.boundary_manifest.source.manual_evidence_digest !== digestArtifact(manualNegative)
|
|
270
|
-
|| control.normal.plan_graph.plan_version !== planInput.plan_version
|
|
271
|
-
|| control.negative.plan_graph.plan_version !== planInput.plan_version
|
|
272
|
-
|| transformArtifact.source.boundary_manifest_digest !== normalManifestDigest
|
|
273
|
-
|| transformArtifact.source.boundary_verdict_digest !== normalVerdictDigest
|
|
274
|
-
|| transformArtifact.source.control_plan_digest !== normalPlanDigest
|
|
275
|
-
|| transformArtifact.source.query_set_digest !== querySetDigest
|
|
276
|
-
|| transformArtifact.source.code_snapshot_digest
|
|
277
|
-
!== control.normal.boundary_manifest.source.code_snapshot_digest) {
|
|
278
|
-
fail('control/transform predecessorのdigest chainが不正');
|
|
279
|
-
}
|
|
280
|
-
if (codeSnapshotDigest !== transformArtifact.output.snapshot_digest
|
|
281
|
-
|| transformArtifact.source.code_snapshot_digest === codeSnapshotDigest) {
|
|
282
|
-
fail('post-transform code snapshotがaccepted transformと一致しない');
|
|
283
|
-
}
|
|
284
|
-
if (control.normal.boundary_verdict.verdicts[0]?.verdict !== 'seam_candidate'
|
|
285
|
-
|| control.normal.plan_graph.minimum_feasible_waves !== 2
|
|
286
|
-
|| control.negative.boundary_verdict.verdicts[0]?.verdict !== 'intentional_serial'
|
|
287
|
-
|| !control.negative.boundary_manifest.conflicts.some(({ kind }) => kind === 'state')) {
|
|
288
|
-
fail('control predecessorの実験topologyがdriftした');
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function manualByTodo(manualEvidence) {
|
|
293
|
-
return new Map(manualEvidence.evidence.map((entry) => [entry.todo_id, entry]));
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function manualRecordId(todoId) {
|
|
297
|
-
return `manual-${todoId}`;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function resourcesForTodo(seam, manual) {
|
|
301
|
-
const owns = [
|
|
302
|
-
{ kind: 'symbol', target: seam.symbol },
|
|
303
|
-
{ kind: 'path', target: seam.path },
|
|
304
|
-
];
|
|
305
|
-
return {
|
|
306
|
-
owns,
|
|
307
|
-
reads: manual.state_reads.map((target) => ({ kind: 'state', target })),
|
|
308
|
-
writes: [
|
|
309
|
-
...owns.map((entry) => ({ ...entry })),
|
|
310
|
-
...manual.state_writes.map((target) => ({ kind: 'state', target })),
|
|
311
|
-
...manual.effects.map((target) => ({ kind: 'effect', target })),
|
|
312
|
-
],
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function sharedManualConflicts(planInput, manuals) {
|
|
317
|
-
const [leftTodo, rightTodo] = planInput.todos;
|
|
318
|
-
const left = manuals.get(leftTodo.id);
|
|
319
|
-
const right = manuals.get(rightTodo.id);
|
|
320
|
-
const conflicts = [];
|
|
321
|
-
const stateTargets = [...new Set([
|
|
322
|
-
...left.state_reads,
|
|
323
|
-
...left.state_writes,
|
|
324
|
-
].filter((target) => (right.state_reads.includes(target) || right.state_writes.includes(target))
|
|
325
|
-
&& (left.state_writes.includes(target) || right.state_writes.includes(target))))].sort();
|
|
326
|
-
for (const target of stateTargets) {
|
|
327
|
-
conflicts.push({
|
|
328
|
-
id: `shared-state-${target}`,
|
|
329
|
-
kind: 'state',
|
|
330
|
-
todo_ids: [leftTodo.id, rightTodo.id],
|
|
331
|
-
resource: { kind: 'state', target },
|
|
332
|
-
evidence_refs: [manualRecordId(leftTodo.id), manualRecordId(rightTodo.id)],
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
const effectTargets = left.effects.filter((target) => right.effects.includes(target)).sort();
|
|
336
|
-
for (const target of effectTargets) {
|
|
337
|
-
conflicts.push({
|
|
338
|
-
id: `shared-effect-${target}`,
|
|
339
|
-
kind: 'effect',
|
|
340
|
-
todo_ids: [leftTodo.id, rightTodo.id],
|
|
341
|
-
resource: { kind: 'effect', target },
|
|
342
|
-
evidence_refs: [manualRecordId(leftTodo.id), manualRecordId(rightTodo.id)],
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
if (conflicts.some(({ id }) => !identifier(id))) {
|
|
346
|
-
fail('manual conflict targetからbounded IDを作れない');
|
|
347
|
-
}
|
|
348
|
-
return conflicts;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function graphRecords(querySet, sensorEvidence) {
|
|
352
|
-
return querySet.queries.map((query, index) => ({
|
|
353
|
-
id: query.id,
|
|
354
|
-
operation: query.operation,
|
|
355
|
-
status: sensorEvidence.outcomes[index].outcome,
|
|
356
|
-
result_digest: digestArtifact(portableSensorOutcome(sensorEvidence.outcomes[index])),
|
|
357
|
-
}));
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function compileManifest({
|
|
361
|
-
planInput,
|
|
362
|
-
manualEvidence,
|
|
363
|
-
querySet,
|
|
364
|
-
sensorEvidence,
|
|
365
|
-
codeSnapshotDigest,
|
|
366
|
-
seams,
|
|
367
|
-
facts,
|
|
368
|
-
}) {
|
|
369
|
-
const manuals = manualByTodo(manualEvidence);
|
|
370
|
-
const affectedQuery = querySet.queries.find(({ operation }) => operation === 'affected');
|
|
371
|
-
const conflicts = sharedManualConflicts(planInput, manuals);
|
|
372
|
-
const manualEvidenceRecords = planInput.todos.map((todo) => ({
|
|
373
|
-
id: manualRecordId(todo.id),
|
|
374
|
-
todo_id: todo.id,
|
|
375
|
-
result_digest: digestArtifact(manuals.get(todo.id)),
|
|
376
|
-
}));
|
|
377
|
-
const todos = planInput.todos.map((todo, index) => {
|
|
378
|
-
const seam = seams[index];
|
|
379
|
-
const manual = manuals.get(todo.id);
|
|
380
|
-
const resources = resourcesForTodo(seam, manual);
|
|
381
|
-
const conflictIds = conflicts
|
|
382
|
-
.filter(({ todo_ids: todoIds }) => todoIds.includes(todo.id))
|
|
383
|
-
.map(({ id }) => id);
|
|
384
|
-
const unknowns = manual.unknowns.map((unknown) => `manual_unknown:${todo.id}:${unknown}`);
|
|
385
|
-
return {
|
|
386
|
-
id: todo.id,
|
|
387
|
-
owns: resources.owns,
|
|
388
|
-
reads: resources.reads,
|
|
389
|
-
writes: resources.writes,
|
|
390
|
-
hard_needs: [],
|
|
391
|
-
conflict_ids: conflictIds,
|
|
392
|
-
unknowns,
|
|
393
|
-
tests: facts.affectedTests,
|
|
394
|
-
evidence_refs: [
|
|
395
|
-
findSingleQuery(querySet, 'query', seam.symbol).id,
|
|
396
|
-
findSingleQuery(querySet, 'impact', seam.symbol).id,
|
|
397
|
-
affectedQuery.id,
|
|
398
|
-
manualRecordId(todo.id),
|
|
399
|
-
],
|
|
400
|
-
};
|
|
401
|
-
});
|
|
402
|
-
const manifest = {
|
|
403
|
-
schema: 'lattice.boundary_manifest.v1',
|
|
404
|
-
plan_input_digest: digestArtifact(planInput),
|
|
405
|
-
source: {
|
|
406
|
-
code_snapshot_digest: codeSnapshotDigest,
|
|
407
|
-
query_set_digest: digestArtifact(querySet),
|
|
408
|
-
manual_evidence_digest: digestArtifact(manualEvidence),
|
|
409
|
-
sensor_version: facts.sensorVersion,
|
|
410
|
-
},
|
|
411
|
-
graph_evidence: graphRecords(querySet, sensorEvidence),
|
|
412
|
-
manual_evidence: manualEvidenceRecords,
|
|
413
|
-
todos,
|
|
414
|
-
conflicts,
|
|
415
|
-
unknowns: [...new Set(todos.flatMap(({ unknowns }) => unknowns))],
|
|
416
|
-
};
|
|
417
|
-
if (!validateBoundaryManifest(manifest)) {
|
|
418
|
-
fail('生成したtreatment boundary manifestがpublic contractを満たさない');
|
|
419
|
-
}
|
|
420
|
-
return manifest;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function compileVerdict(manifest) {
|
|
424
|
-
const manualUnknown = manifest.unknowns.length > 0;
|
|
425
|
-
const nonCodeConflict = manifest.conflicts.some(({ kind }) => kind === 'state' || kind === 'effect');
|
|
426
|
-
const verdict = manualUnknown
|
|
427
|
-
? 'unknown_requires_evidence'
|
|
428
|
-
: nonCodeConflict ? 'intentional_serial' : 'parallel_ready';
|
|
429
|
-
const reasons = manualUnknown
|
|
430
|
-
? ['manual unknownが残るためtreatment independenceを確定できない']
|
|
431
|
-
: nonCodeConflict
|
|
432
|
-
? ['shared manual state/effect conflictはpath分離後も残る']
|
|
433
|
-
: ['accepted seamとpost-transform graph evidenceがnon-overlapping ownershipを示す'];
|
|
434
|
-
const artifact = {
|
|
435
|
-
schema: 'lattice.boundary_verdict.v1',
|
|
436
|
-
boundary_manifest_digest: digestArtifact(manifest),
|
|
437
|
-
verdicts: [{
|
|
438
|
-
id: 'channel-label-verdict',
|
|
439
|
-
todo_ids: manifest.todos.map(({ id }) => id),
|
|
440
|
-
verdict,
|
|
441
|
-
conflict_ids: manifest.conflicts.map(({ id }) => id),
|
|
442
|
-
seam_candidate: null,
|
|
443
|
-
reasons,
|
|
444
|
-
unknowns: manifest.unknowns,
|
|
445
|
-
}],
|
|
446
|
-
};
|
|
447
|
-
if (!validateBoundaryVerdict(artifact)) {
|
|
448
|
-
fail('生成したtreatment boundary verdictがpublic contractを満たさない');
|
|
449
|
-
}
|
|
450
|
-
return artifact;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
function compilePlan(planInput, manifest) {
|
|
454
|
-
const edgeKinds = new Map([
|
|
455
|
-
['state', 'state_conflict'],
|
|
456
|
-
['effect', 'effect_conflict'],
|
|
457
|
-
['semantic', 'semantic_conflict'],
|
|
458
|
-
['dynamic_unknown', 'dynamic_unknown'],
|
|
459
|
-
]);
|
|
460
|
-
const edges = manifest.conflicts.map((conflict) => ({
|
|
461
|
-
id: conflict.id,
|
|
462
|
-
from: conflict.todo_ids[0],
|
|
463
|
-
to: conflict.todo_ids[1],
|
|
464
|
-
kind: edgeKinds.get(conflict.kind),
|
|
465
|
-
evidence_refs: conflict.evidence_refs,
|
|
466
|
-
}));
|
|
467
|
-
const waves = edges.length === 0
|
|
468
|
-
? [{ index: 0, todo_ids: planInput.todos.map(({ id }) => id) }]
|
|
469
|
-
: planInput.todos.map((todo, index) => ({ index, todo_ids: [todo.id] }));
|
|
470
|
-
const graph = {
|
|
471
|
-
schema: 'lattice.plan_graph.v1',
|
|
472
|
-
plan_version: TREATMENT_PLAN_VERSION,
|
|
473
|
-
source_manifest_digest: digestArtifact(manifest),
|
|
474
|
-
capacity: { writers: planInput.capacity.writers },
|
|
475
|
-
nodes: planInput.todos.map((todo) => ({
|
|
476
|
-
id: todo.id,
|
|
477
|
-
outcome: todo.outcome,
|
|
478
|
-
owned_boundaries: manifest.todos.find(({ id }) => id === todo.id).owns,
|
|
479
|
-
})),
|
|
480
|
-
edges,
|
|
481
|
-
joins: [],
|
|
482
|
-
waves,
|
|
483
|
-
minimum_feasible_waves: waves.length,
|
|
484
|
-
};
|
|
485
|
-
if (!validatePlanGraph(graph)) {
|
|
486
|
-
fail('生成したtreatment plan graphがpublic contractを満たさない');
|
|
487
|
-
}
|
|
488
|
-
return graph;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
function bundle(planInput, manualEvidence, common) {
|
|
492
|
-
const boundaryManifest = compileManifest({
|
|
493
|
-
planInput,
|
|
494
|
-
manualEvidence,
|
|
495
|
-
...common,
|
|
496
|
-
});
|
|
497
|
-
const boundaryVerdict = compileVerdict(boundaryManifest);
|
|
498
|
-
const planGraph = compilePlan(planInput, boundaryManifest);
|
|
499
|
-
return {
|
|
500
|
-
boundary_manifest: boundaryManifest,
|
|
501
|
-
boundary_manifest_digest: digestArtifact(boundaryManifest),
|
|
502
|
-
boundary_verdict: boundaryVerdict,
|
|
503
|
-
boundary_verdict_digest: digestArtifact(boundaryVerdict),
|
|
504
|
-
plan_graph: planGraph,
|
|
505
|
-
plan_graph_digest: digestArtifact(planGraph),
|
|
506
|
-
};
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
function graphChanges(before, after) {
|
|
510
|
-
const beforeById = new Map(before.map((entry) => [entry.id, entry]));
|
|
511
|
-
const afterById = new Map(after.map((entry) => [entry.id, entry]));
|
|
512
|
-
return {
|
|
513
|
-
added: [...afterById.keys()].filter((id) => !beforeById.has(id)).sort(),
|
|
514
|
-
removed: [...beforeById.keys()].filter((id) => !afterById.has(id)).sort(),
|
|
515
|
-
changed: [...beforeById.keys()].filter((id) => afterById.has(id)
|
|
516
|
-
&& digestArtifact(beforeById.get(id)) !== digestArtifact(afterById.get(id))).sort(),
|
|
517
|
-
};
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
function compilePlanDiff({ controlPlan, treatmentPlan, transformArtifact, querySet }) {
|
|
521
|
-
const nodeChanges = graphChanges(controlPlan.nodes, treatmentPlan.nodes);
|
|
522
|
-
const edgeChanges = graphChanges(controlPlan.edges, treatmentPlan.edges);
|
|
523
|
-
const diff = {
|
|
524
|
-
schema: 'lattice.plan_diff.v1',
|
|
525
|
-
old_plan: { version: controlPlan.plan_version, digest: digestArtifact(controlPlan) },
|
|
526
|
-
new_plan: { version: treatmentPlan.plan_version, digest: digestArtifact(treatmentPlan) },
|
|
527
|
-
transform: {
|
|
528
|
-
status: transformArtifact.status,
|
|
529
|
-
artifact_digest: digestArtifact(transformArtifact),
|
|
530
|
-
patch_digest: transformArtifact.patch.digest,
|
|
531
|
-
verification_digest: transformArtifact.verification.digest,
|
|
532
|
-
changed_paths: transformArtifact.scope.changed_paths,
|
|
533
|
-
},
|
|
534
|
-
query_set_digest: digestArtifact(querySet),
|
|
535
|
-
snapshots: {
|
|
536
|
-
before_digest: transformArtifact.source.code_snapshot_digest,
|
|
537
|
-
after_digest: transformArtifact.output.snapshot_digest,
|
|
538
|
-
},
|
|
539
|
-
nodes: nodeChanges,
|
|
540
|
-
edges: { added: edgeChanges.added, removed: edgeChanges.removed },
|
|
541
|
-
invalidated_contexts: [
|
|
542
|
-
{
|
|
543
|
-
kind: 'old_plan',
|
|
544
|
-
ref: controlPlan.plan_version,
|
|
545
|
-
reason: 'accepted seam changed the owned boundary topology; active plan is immutable',
|
|
546
|
-
},
|
|
547
|
-
{
|
|
548
|
-
kind: 'agent_context',
|
|
549
|
-
ref: 'rc1-control-agent-context',
|
|
550
|
-
reason: 'the old shared-symbol ownership assumptions no longer apply',
|
|
551
|
-
},
|
|
552
|
-
{
|
|
553
|
-
kind: 'partial_patch',
|
|
554
|
-
ref: 'rc1-control-partial-patch-set',
|
|
555
|
-
reason: 'patches based on the old plan cannot cross the version barrier',
|
|
556
|
-
},
|
|
557
|
-
{
|
|
558
|
-
kind: 'interface_assumption',
|
|
559
|
-
ref: 'shared-buildDispatchRecord-ownership',
|
|
560
|
-
reason: 'each TODO now owns a separate policy symbol and path',
|
|
561
|
-
},
|
|
562
|
-
],
|
|
563
|
-
metrics: {
|
|
564
|
-
write_conflicts_before: controlPlan.edges.filter(({ kind }) => kind === 'write_conflict').length,
|
|
565
|
-
write_conflicts_after: treatmentPlan.edges.filter(({ kind }) => kind === 'write_conflict').length,
|
|
566
|
-
hard_precedence_before: controlPlan.edges.filter(({ kind }) => kind === 'hard_need').length,
|
|
567
|
-
hard_precedence_after: treatmentPlan.edges.filter(({ kind }) => kind === 'hard_need').length,
|
|
568
|
-
minimum_feasible_waves_before: controlPlan.minimum_feasible_waves,
|
|
569
|
-
minimum_feasible_waves_after: treatmentPlan.minimum_feasible_waves,
|
|
570
|
-
},
|
|
571
|
-
};
|
|
572
|
-
if (!validatePlanDiff(diff)) {
|
|
573
|
-
fail('生成したplan diffがpublic contractを満たさない');
|
|
574
|
-
}
|
|
575
|
-
return diff;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
function conditionSummary(bundle) {
|
|
579
|
-
return {
|
|
580
|
-
boundary_manifest_digest: bundle.boundary_manifest_digest,
|
|
581
|
-
boundary_verdict_digest: bundle.boundary_verdict_digest,
|
|
582
|
-
plan_graph_digest: bundle.plan_graph_digest,
|
|
583
|
-
verdict: bundle.boundary_verdict.verdicts[0].verdict,
|
|
584
|
-
write_conflicts: bundle.plan_graph.edges.filter(({ kind }) => kind === 'write_conflict').length,
|
|
585
|
-
state_conflicts: bundle.boundary_manifest.conflicts.filter(({ kind }) => kind === 'state').length,
|
|
586
|
-
hard_precedence: bundle.plan_graph.edges.filter(({ kind }) => kind === 'hard_need').length,
|
|
587
|
-
unknowns: bundle.boundary_manifest.unknowns.length,
|
|
588
|
-
minimum_feasible_waves: bundle.plan_graph.minimum_feasible_waves,
|
|
589
|
-
};
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
function controlSummary(bundle) {
|
|
593
|
-
return conditionSummary({
|
|
594
|
-
...bundle,
|
|
595
|
-
boundary_manifest_digest: digestArtifact(bundle.boundary_manifest),
|
|
596
|
-
boundary_verdict_digest: digestArtifact(bundle.boundary_verdict),
|
|
597
|
-
plan_graph_digest: digestArtifact(bundle.plan_graph),
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
function compileComparison({
|
|
602
|
-
planInput,
|
|
603
|
-
manualNormal,
|
|
604
|
-
manualNegative,
|
|
605
|
-
querySet,
|
|
606
|
-
transformArtifact,
|
|
607
|
-
control,
|
|
608
|
-
normal,
|
|
609
|
-
negative,
|
|
610
|
-
planDiff,
|
|
611
|
-
}) {
|
|
612
|
-
const comparison = {
|
|
613
|
-
schema: 'lattice.rc1.control_treatment_comparison.v1',
|
|
614
|
-
fixed_inputs: {
|
|
615
|
-
plan_input: digestArtifact(planInput),
|
|
616
|
-
normal_manual_evidence: digestArtifact(manualNormal),
|
|
617
|
-
negative_manual_evidence: digestArtifact(manualNegative),
|
|
618
|
-
query_set: digestArtifact(querySet),
|
|
619
|
-
capacity_writers: planInput.capacity.writers,
|
|
620
|
-
verifier: transformArtifact.verification.digest,
|
|
621
|
-
},
|
|
622
|
-
independent_variable: {
|
|
623
|
-
kind: 'accepted_seam_transform',
|
|
624
|
-
artifact_digest: digestArtifact(transformArtifact),
|
|
625
|
-
patch_digest: transformArtifact.patch.digest,
|
|
626
|
-
},
|
|
627
|
-
control: controlSummary(control.normal),
|
|
628
|
-
treatment: conditionSummary(normal),
|
|
629
|
-
negative_control: controlSummary(control.negative),
|
|
630
|
-
negative_treatment: conditionSummary(negative),
|
|
631
|
-
version_barrier: {
|
|
632
|
-
old_plan: planDiff.old_plan,
|
|
633
|
-
new_plan: planDiff.new_plan,
|
|
634
|
-
plan_diff_digest: digestArtifact(planDiff),
|
|
635
|
-
invalidated_contexts: planDiff.invalidated_contexts.map(({ kind, ref }) => ({ kind, ref })),
|
|
636
|
-
},
|
|
637
|
-
hypothesis_result: normal.boundary_verdict.verdicts[0].verdict === 'parallel_ready'
|
|
638
|
-
&& normal.plan_graph.minimum_feasible_waves === 1
|
|
639
|
-
&& negative.boundary_verdict.verdicts[0].verdict === 'intentional_serial'
|
|
640
|
-
&& negative.boundary_manifest.conflicts.some(({ kind }) => kind === 'state')
|
|
641
|
-
? 'supported_in_fixture'
|
|
642
|
-
: 'refuted_in_fixture',
|
|
643
|
-
};
|
|
644
|
-
canonicalizeArtifact(comparison);
|
|
645
|
-
return comparison;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
/**
|
|
649
|
-
* accepted RC1 seam後の同じquery setをnormal/negative boundaryと新plan versionへ再compileする。
|
|
650
|
-
* @param {object} options
|
|
651
|
-
* @returns {object}
|
|
652
|
-
*/
|
|
653
|
-
export function compileTreatmentArtifacts({
|
|
654
|
-
planInput,
|
|
655
|
-
manualNormal,
|
|
656
|
-
manualNegative,
|
|
657
|
-
querySet,
|
|
658
|
-
sensorEvidence,
|
|
659
|
-
codeSnapshotDigest,
|
|
660
|
-
transformArtifact,
|
|
661
|
-
control,
|
|
662
|
-
} = {}) {
|
|
663
|
-
for (const value of [
|
|
664
|
-
planInput,
|
|
665
|
-
manualNormal,
|
|
666
|
-
manualNegative,
|
|
667
|
-
querySet,
|
|
668
|
-
sensorEvidence,
|
|
669
|
-
transformArtifact,
|
|
670
|
-
control,
|
|
671
|
-
]) {
|
|
672
|
-
canonicalizeArtifact(value);
|
|
673
|
-
}
|
|
674
|
-
if (!validatePlanInput(planInput)) fail('plan inputがpublic contractを満たさない');
|
|
675
|
-
if (typeof codeSnapshotDigest !== 'string' || !SHA256.test(codeSnapshotDigest)) {
|
|
676
|
-
fail('code snapshot digestがSHA-256でない');
|
|
677
|
-
}
|
|
678
|
-
assertManualEvidence(planInput, manualNormal, 'normal');
|
|
679
|
-
assertManualEvidence(planInput, manualNegative, 'negative');
|
|
680
|
-
assertQuerySet(querySet);
|
|
681
|
-
assertLatticeSensorEvidence(querySet, sensorEvidence);
|
|
682
|
-
const seams = treatmentSeams(planInput);
|
|
683
|
-
assertQueryCoverage(planInput, querySet, seams);
|
|
684
|
-
assertPredecessors({
|
|
685
|
-
planInput,
|
|
686
|
-
manualNormal,
|
|
687
|
-
manualNegative,
|
|
688
|
-
querySet,
|
|
689
|
-
transformArtifact,
|
|
690
|
-
control,
|
|
691
|
-
codeSnapshotDigest,
|
|
692
|
-
});
|
|
693
|
-
const facts = treatmentGraphFacts(planInput, querySet, sensorEvidence, seams);
|
|
694
|
-
const common = {
|
|
695
|
-
querySet,
|
|
696
|
-
sensorEvidence,
|
|
697
|
-
codeSnapshotDigest,
|
|
698
|
-
seams,
|
|
699
|
-
facts,
|
|
700
|
-
};
|
|
701
|
-
const normal = bundle(planInput, manualNormal, common);
|
|
702
|
-
const negative = bundle(planInput, manualNegative, common);
|
|
703
|
-
const planDiff = compilePlanDiff({
|
|
704
|
-
controlPlan: control.normal.plan_graph,
|
|
705
|
-
treatmentPlan: normal.plan_graph,
|
|
706
|
-
transformArtifact,
|
|
707
|
-
querySet,
|
|
708
|
-
});
|
|
709
|
-
const comparison = compileComparison({
|
|
710
|
-
planInput,
|
|
711
|
-
manualNormal,
|
|
712
|
-
manualNegative,
|
|
713
|
-
querySet,
|
|
714
|
-
transformArtifact,
|
|
715
|
-
control,
|
|
716
|
-
normal,
|
|
717
|
-
negative,
|
|
718
|
-
planDiff,
|
|
719
|
-
});
|
|
720
|
-
return {
|
|
721
|
-
normal,
|
|
722
|
-
negative,
|
|
723
|
-
plan_diff: planDiff,
|
|
724
|
-
plan_diff_digest: digestArtifact(planDiff),
|
|
725
|
-
comparison,
|
|
726
|
-
comparison_digest: digestArtifact(comparison),
|
|
727
|
-
};
|
|
728
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
canonicalizeArtifact,
|
|
3
|
+
digestArtifact,
|
|
4
|
+
validateBoundaryManifest,
|
|
5
|
+
validateBoundaryVerdict,
|
|
6
|
+
validatePlanDiff,
|
|
7
|
+
validatePlanGraph,
|
|
8
|
+
validatePlanInput,
|
|
9
|
+
validateTransformArtifact,
|
|
10
|
+
} from './artifact-contracts.mjs';
|
|
11
|
+
import { portableSensorOutcome } from './sensor-adapter.mjs';
|
|
12
|
+
|
|
13
|
+
const SHA256 = /^[0-9a-f]{64}$/;
|
|
14
|
+
const IDENTIFIER = /^[0-9A-Za-z](?:[0-9A-Za-z._-]{0,127})$/;
|
|
15
|
+
const TREATMENT_PLAN_VERSION = 'rc1-treatment-v2';
|
|
16
|
+
const AFFECTED_TEST = 'test/research-dispatch-record.test.mjs';
|
|
17
|
+
const SEAM_BY_CONCERN = new Map([
|
|
18
|
+
['dispatch.channel_policy', {
|
|
19
|
+
symbol: 'selectDispatchChannel',
|
|
20
|
+
path: 'research/fixtures/dispatch-record/src/dispatch-channel.mjs',
|
|
21
|
+
}],
|
|
22
|
+
['dispatch.label_policy', {
|
|
23
|
+
symbol: 'formatDispatchLabel',
|
|
24
|
+
path: 'research/fixtures/dispatch-record/src/dispatch-label.mjs',
|
|
25
|
+
}],
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
function fail(reason) {
|
|
29
|
+
throw new TypeError(`treatment compile契約違反: ${reason}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function exactRecord(value, keys) {
|
|
33
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)
|
|
34
|
+
|| Object.getPrototypeOf(value) !== Object.prototype) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
const actual = Object.keys(value).sort();
|
|
38
|
+
const expected = [...keys].sort();
|
|
39
|
+
return actual.length === expected.length
|
|
40
|
+
&& actual.every((key, index) => key === expected[index]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function nonEmptyText(value) {
|
|
44
|
+
return typeof value === 'string'
|
|
45
|
+
&& value.length > 0
|
|
46
|
+
&& value === value.trim()
|
|
47
|
+
&& !/[\u0000-\u001f\u007f]/.test(value)
|
|
48
|
+
&& Buffer.byteLength(value, 'utf8') <= 4_096;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function identifier(value) {
|
|
52
|
+
return typeof value === 'string' && IDENTIFIER.test(value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function uniqueTextArray(value) {
|
|
56
|
+
return Array.isArray(value)
|
|
57
|
+
&& value.length <= 256
|
|
58
|
+
&& value.every(nonEmptyText)
|
|
59
|
+
&& new Set(value).size === value.length;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function assertManualEvidence(planInput, manualEvidence, label) {
|
|
63
|
+
if (!exactRecord(manualEvidence, ['schema', 'evidence'])
|
|
64
|
+
|| manualEvidence.schema !== 'lattice.manual_boundary_evidence.v1'
|
|
65
|
+
|| !Array.isArray(manualEvidence.evidence)
|
|
66
|
+
|| manualEvidence.evidence.length !== planInput.todos.length) {
|
|
67
|
+
fail(`${label} manual evidence rootが不正`);
|
|
68
|
+
}
|
|
69
|
+
const expectedIds = planInput.todos.map(({ id }) => id);
|
|
70
|
+
if (manualEvidence.evidence.some((entry, index) => !exactRecord(entry, [
|
|
71
|
+
'todo_id',
|
|
72
|
+
'state_reads',
|
|
73
|
+
'state_writes',
|
|
74
|
+
'effects',
|
|
75
|
+
'unknowns',
|
|
76
|
+
])
|
|
77
|
+
|| entry.todo_id !== expectedIds[index]
|
|
78
|
+
|| !uniqueTextArray(entry.state_reads)
|
|
79
|
+
|| !uniqueTextArray(entry.state_writes)
|
|
80
|
+
|| !uniqueTextArray(entry.effects)
|
|
81
|
+
|| !uniqueTextArray(entry.unknowns))) {
|
|
82
|
+
fail(`${label} manual evidenceがplan TODOと一致しない`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function assertQuerySet(querySet) {
|
|
87
|
+
if (!exactRecord(querySet, ['schema', 'queries'])
|
|
88
|
+
|| querySet.schema !== 'lattice.sensor_query_set.v1'
|
|
89
|
+
|| !Array.isArray(querySet.queries)
|
|
90
|
+
|| querySet.queries.length !== 10
|
|
91
|
+
|| new Set(querySet.queries.map(({ id }) => id)).size !== querySet.queries.length) {
|
|
92
|
+
fail('query setがRC1 fixed setと一致しない');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function assertLatticeSensorEvidence(querySet, sensorEvidence) {
|
|
97
|
+
if (!exactRecord(sensorEvidence, ['cwd', 'outcomes'])
|
|
98
|
+
|| !nonEmptyText(sensorEvidence.cwd)
|
|
99
|
+
|| !Array.isArray(sensorEvidence.outcomes)
|
|
100
|
+
|| sensorEvidence.outcomes.length !== querySet.queries.length) {
|
|
101
|
+
fail('LatticeSensor evidence rootがquery setと一致しない');
|
|
102
|
+
}
|
|
103
|
+
for (let index = 0; index < querySet.queries.length; index += 1) {
|
|
104
|
+
const query = querySet.queries[index];
|
|
105
|
+
const outcome = sensorEvidence.outcomes[index];
|
|
106
|
+
if (!outcome || typeof outcome !== 'object' || Array.isArray(outcome)
|
|
107
|
+
|| outcome.id !== query.id
|
|
108
|
+
|| outcome.operation !== query.operation
|
|
109
|
+
|| !nonEmptyText(outcome.outcome)) {
|
|
110
|
+
fail(`LatticeSensor evidence ${query.id}がID/operation/順序と一致しない`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function treatmentSeams(planInput) {
|
|
116
|
+
if (planInput.plan_version !== 'rc1-control-v1'
|
|
117
|
+
|| planInput.capacity.writers !== 2
|
|
118
|
+
|| planInput.todos.length !== 2
|
|
119
|
+
|| new Set(planInput.todos.map(({ anchor }) => anchor.symbol)).size !== 1
|
|
120
|
+
|| new Set(planInput.todos.map(({ anchor }) => anchor.path)).size !== 1
|
|
121
|
+
|| planInput.todos[0].anchor.path !== planInput.project.fixture_entry) {
|
|
122
|
+
fail('RC1 plan input topologyまたはcapacityがdriftした');
|
|
123
|
+
}
|
|
124
|
+
return planInput.todos.map((todo) => {
|
|
125
|
+
const seam = SEAM_BY_CONCERN.get(todo.concern);
|
|
126
|
+
if (!seam) fail(`treatment seamへ接続できないconcern: ${todo.concern}`);
|
|
127
|
+
return { todo_id: todo.id, ...seam };
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function findSingleQuery(querySet, operation, target) {
|
|
132
|
+
const matches = querySet.queries.filter((query) => query.operation === operation
|
|
133
|
+
&& (target === undefined || query.target === target));
|
|
134
|
+
if (matches.length !== 1) fail(`query setに${operation}:${target ?? ''}がexactly one存在しない`);
|
|
135
|
+
return matches[0];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function assertQueryCoverage(planInput, querySet, seams) {
|
|
139
|
+
const anchorSymbol = planInput.todos[0].anchor.symbol;
|
|
140
|
+
findSingleQuery(querySet, 'status');
|
|
141
|
+
for (const operation of ['query', 'callers', 'callees', 'impact']) {
|
|
142
|
+
findSingleQuery(querySet, operation, anchorSymbol);
|
|
143
|
+
}
|
|
144
|
+
for (const seam of seams) {
|
|
145
|
+
findSingleQuery(querySet, 'query', seam.symbol);
|
|
146
|
+
findSingleQuery(querySet, 'impact', seam.symbol);
|
|
147
|
+
}
|
|
148
|
+
const affected = querySet.queries.filter(({ operation }) => operation === 'affected');
|
|
149
|
+
const expectedTargets = [planInput.project.fixture_entry, ...seams.map(({ path }) => path)];
|
|
150
|
+
if (affected.length !== 1
|
|
151
|
+
|| JSON.stringify(affected[0].targets) !== JSON.stringify(expectedTargets)) {
|
|
152
|
+
fail('same query setのaffected coverageがdriftした');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function outcomeById(querySet, sensorEvidence) {
|
|
157
|
+
return new Map(querySet.queries.map((query, index) => [
|
|
158
|
+
query.id,
|
|
159
|
+
sensorEvidence.outcomes[index],
|
|
160
|
+
]));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function exactSymbolAt(records, target, expectedPath) {
|
|
164
|
+
if (!Array.isArray(records)) return false;
|
|
165
|
+
const exact = records.filter((entry) => entry?.node
|
|
166
|
+
&& (entry.node.name === target || entry.node.qualifiedName === target));
|
|
167
|
+
return exact.length === 1 && exact[0].node.filePath === expectedPath;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function assertReadyOutcome(outcomes, query) {
|
|
171
|
+
const outcome = outcomes.get(query.id);
|
|
172
|
+
if (outcome?.outcome !== 'ready') {
|
|
173
|
+
fail(`LatticeSensor evidence ${query.id}はreadyでなく${outcome?.outcome ?? 'missing'}`);
|
|
174
|
+
}
|
|
175
|
+
return outcome;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function treatmentGraphFacts(planInput, querySet, sensorEvidence, seams) {
|
|
179
|
+
const outcomes = outcomeById(querySet, sensorEvidence);
|
|
180
|
+
const status = assertReadyOutcome(outcomes, findSingleQuery(querySet, 'status'));
|
|
181
|
+
if (!status.data || !nonEmptyText(status.data.version)) {
|
|
182
|
+
fail('LatticeSensor statusにversionがない');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const anchorSymbol = planInput.todos[0].anchor.symbol;
|
|
186
|
+
const anchorPath = planInput.todos[0].anchor.path;
|
|
187
|
+
const anchorQuery = assertReadyOutcome(
|
|
188
|
+
outcomes,
|
|
189
|
+
findSingleQuery(querySet, 'query', anchorSymbol),
|
|
190
|
+
);
|
|
191
|
+
if (!exactSymbolAt(anchorQuery.data, anchorSymbol, anchorPath)) {
|
|
192
|
+
fail('anchor symbolがexpected pathへexact解決しない');
|
|
193
|
+
}
|
|
194
|
+
for (const operation of ['callers', 'callees', 'impact']) {
|
|
195
|
+
const outcome = assertReadyOutcome(
|
|
196
|
+
outcomes,
|
|
197
|
+
findSingleQuery(querySet, operation, anchorSymbol),
|
|
198
|
+
);
|
|
199
|
+
if (!exactSymbolAt(outcome.resolution, anchorSymbol, anchorPath)) {
|
|
200
|
+
fail(`${operation} anchor resolutionがexpected pathと一致しない`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
for (const seam of seams) {
|
|
204
|
+
const query = assertReadyOutcome(outcomes, findSingleQuery(querySet, 'query', seam.symbol));
|
|
205
|
+
const impact = assertReadyOutcome(outcomes, findSingleQuery(querySet, 'impact', seam.symbol));
|
|
206
|
+
if (!exactSymbolAt(query.data, seam.symbol, seam.path)
|
|
207
|
+
|| !exactSymbolAt(impact.resolution, seam.symbol, seam.path)) {
|
|
208
|
+
fail(`seam ${seam.symbol}がexpected pathへexact解決しない`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const affectedQuery = querySet.queries.find(({ operation }) => operation === 'affected');
|
|
213
|
+
const affected = assertReadyOutcome(outcomes, affectedQuery);
|
|
214
|
+
if (!Array.isArray(affected.targets)
|
|
215
|
+
|| affected.targets.length !== affectedQuery.targets.length
|
|
216
|
+
|| affected.targets.some((entry, index) => entry.target !== affectedQuery.targets[index]
|
|
217
|
+
|| entry.outcome !== 'ready'
|
|
218
|
+
|| !entry.data
|
|
219
|
+
|| JSON.stringify(entry.data.affectedTests) !== JSON.stringify([AFFECTED_TEST]))) {
|
|
220
|
+
fail('treatment source全体のaffected testがfixed verifierと一致しない');
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
sensorVersion: status.data.version,
|
|
224
|
+
affectedTests: [AFFECTED_TEST],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function assertBundle(bundle, label) {
|
|
229
|
+
if (!exactRecord(bundle, ['boundary_manifest', 'boundary_verdict', 'plan_graph'])
|
|
230
|
+
|| !validateBoundaryManifest(bundle.boundary_manifest)
|
|
231
|
+
|| !validateBoundaryVerdict(bundle.boundary_verdict)
|
|
232
|
+
|| !validatePlanGraph(bundle.plan_graph)) {
|
|
233
|
+
fail(`${label} control artifactがpublic contractを満たさない`);
|
|
234
|
+
}
|
|
235
|
+
const manifestDigest = digestArtifact(bundle.boundary_manifest);
|
|
236
|
+
if (bundle.boundary_verdict.boundary_manifest_digest !== manifestDigest
|
|
237
|
+
|| bundle.plan_graph.source_manifest_digest !== manifestDigest) {
|
|
238
|
+
fail(`${label} control artifactのdigest chainが不正`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function assertPredecessors({
|
|
243
|
+
planInput,
|
|
244
|
+
manualNormal,
|
|
245
|
+
manualNegative,
|
|
246
|
+
querySet,
|
|
247
|
+
transformArtifact,
|
|
248
|
+
control,
|
|
249
|
+
codeSnapshotDigest,
|
|
250
|
+
}) {
|
|
251
|
+
if (!validateTransformArtifact(transformArtifact) || transformArtifact.status !== 'accepted') {
|
|
252
|
+
fail('accepted transform predecessorが必要');
|
|
253
|
+
}
|
|
254
|
+
if (!exactRecord(control, ['normal', 'negative'])) {
|
|
255
|
+
fail('control predecessor setが不正');
|
|
256
|
+
}
|
|
257
|
+
assertBundle(control.normal, 'normal');
|
|
258
|
+
assertBundle(control.negative, 'negative');
|
|
259
|
+
const planInputDigest = digestArtifact(planInput);
|
|
260
|
+
const querySetDigest = digestArtifact(querySet);
|
|
261
|
+
const normalManifestDigest = digestArtifact(control.normal.boundary_manifest);
|
|
262
|
+
const normalVerdictDigest = digestArtifact(control.normal.boundary_verdict);
|
|
263
|
+
const normalPlanDigest = digestArtifact(control.normal.plan_graph);
|
|
264
|
+
if (control.normal.boundary_manifest.plan_input_digest !== planInputDigest
|
|
265
|
+
|| control.negative.boundary_manifest.plan_input_digest !== planInputDigest
|
|
266
|
+
|| control.normal.boundary_manifest.source.query_set_digest !== querySetDigest
|
|
267
|
+
|| control.negative.boundary_manifest.source.query_set_digest !== querySetDigest
|
|
268
|
+
|| control.normal.boundary_manifest.source.manual_evidence_digest !== digestArtifact(manualNormal)
|
|
269
|
+
|| control.negative.boundary_manifest.source.manual_evidence_digest !== digestArtifact(manualNegative)
|
|
270
|
+
|| control.normal.plan_graph.plan_version !== planInput.plan_version
|
|
271
|
+
|| control.negative.plan_graph.plan_version !== planInput.plan_version
|
|
272
|
+
|| transformArtifact.source.boundary_manifest_digest !== normalManifestDigest
|
|
273
|
+
|| transformArtifact.source.boundary_verdict_digest !== normalVerdictDigest
|
|
274
|
+
|| transformArtifact.source.control_plan_digest !== normalPlanDigest
|
|
275
|
+
|| transformArtifact.source.query_set_digest !== querySetDigest
|
|
276
|
+
|| transformArtifact.source.code_snapshot_digest
|
|
277
|
+
!== control.normal.boundary_manifest.source.code_snapshot_digest) {
|
|
278
|
+
fail('control/transform predecessorのdigest chainが不正');
|
|
279
|
+
}
|
|
280
|
+
if (codeSnapshotDigest !== transformArtifact.output.snapshot_digest
|
|
281
|
+
|| transformArtifact.source.code_snapshot_digest === codeSnapshotDigest) {
|
|
282
|
+
fail('post-transform code snapshotがaccepted transformと一致しない');
|
|
283
|
+
}
|
|
284
|
+
if (control.normal.boundary_verdict.verdicts[0]?.verdict !== 'seam_candidate'
|
|
285
|
+
|| control.normal.plan_graph.minimum_feasible_waves !== 2
|
|
286
|
+
|| control.negative.boundary_verdict.verdicts[0]?.verdict !== 'intentional_serial'
|
|
287
|
+
|| !control.negative.boundary_manifest.conflicts.some(({ kind }) => kind === 'state')) {
|
|
288
|
+
fail('control predecessorの実験topologyがdriftした');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function manualByTodo(manualEvidence) {
|
|
293
|
+
return new Map(manualEvidence.evidence.map((entry) => [entry.todo_id, entry]));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function manualRecordId(todoId) {
|
|
297
|
+
return `manual-${todoId}`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function resourcesForTodo(seam, manual) {
|
|
301
|
+
const owns = [
|
|
302
|
+
{ kind: 'symbol', target: seam.symbol },
|
|
303
|
+
{ kind: 'path', target: seam.path },
|
|
304
|
+
];
|
|
305
|
+
return {
|
|
306
|
+
owns,
|
|
307
|
+
reads: manual.state_reads.map((target) => ({ kind: 'state', target })),
|
|
308
|
+
writes: [
|
|
309
|
+
...owns.map((entry) => ({ ...entry })),
|
|
310
|
+
...manual.state_writes.map((target) => ({ kind: 'state', target })),
|
|
311
|
+
...manual.effects.map((target) => ({ kind: 'effect', target })),
|
|
312
|
+
],
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function sharedManualConflicts(planInput, manuals) {
|
|
317
|
+
const [leftTodo, rightTodo] = planInput.todos;
|
|
318
|
+
const left = manuals.get(leftTodo.id);
|
|
319
|
+
const right = manuals.get(rightTodo.id);
|
|
320
|
+
const conflicts = [];
|
|
321
|
+
const stateTargets = [...new Set([
|
|
322
|
+
...left.state_reads,
|
|
323
|
+
...left.state_writes,
|
|
324
|
+
].filter((target) => (right.state_reads.includes(target) || right.state_writes.includes(target))
|
|
325
|
+
&& (left.state_writes.includes(target) || right.state_writes.includes(target))))].sort();
|
|
326
|
+
for (const target of stateTargets) {
|
|
327
|
+
conflicts.push({
|
|
328
|
+
id: `shared-state-${target}`,
|
|
329
|
+
kind: 'state',
|
|
330
|
+
todo_ids: [leftTodo.id, rightTodo.id],
|
|
331
|
+
resource: { kind: 'state', target },
|
|
332
|
+
evidence_refs: [manualRecordId(leftTodo.id), manualRecordId(rightTodo.id)],
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
const effectTargets = left.effects.filter((target) => right.effects.includes(target)).sort();
|
|
336
|
+
for (const target of effectTargets) {
|
|
337
|
+
conflicts.push({
|
|
338
|
+
id: `shared-effect-${target}`,
|
|
339
|
+
kind: 'effect',
|
|
340
|
+
todo_ids: [leftTodo.id, rightTodo.id],
|
|
341
|
+
resource: { kind: 'effect', target },
|
|
342
|
+
evidence_refs: [manualRecordId(leftTodo.id), manualRecordId(rightTodo.id)],
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
if (conflicts.some(({ id }) => !identifier(id))) {
|
|
346
|
+
fail('manual conflict targetからbounded IDを作れない');
|
|
347
|
+
}
|
|
348
|
+
return conflicts;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function graphRecords(querySet, sensorEvidence) {
|
|
352
|
+
return querySet.queries.map((query, index) => ({
|
|
353
|
+
id: query.id,
|
|
354
|
+
operation: query.operation,
|
|
355
|
+
status: sensorEvidence.outcomes[index].outcome,
|
|
356
|
+
result_digest: digestArtifact(portableSensorOutcome(sensorEvidence.outcomes[index])),
|
|
357
|
+
}));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function compileManifest({
|
|
361
|
+
planInput,
|
|
362
|
+
manualEvidence,
|
|
363
|
+
querySet,
|
|
364
|
+
sensorEvidence,
|
|
365
|
+
codeSnapshotDigest,
|
|
366
|
+
seams,
|
|
367
|
+
facts,
|
|
368
|
+
}) {
|
|
369
|
+
const manuals = manualByTodo(manualEvidence);
|
|
370
|
+
const affectedQuery = querySet.queries.find(({ operation }) => operation === 'affected');
|
|
371
|
+
const conflicts = sharedManualConflicts(planInput, manuals);
|
|
372
|
+
const manualEvidenceRecords = planInput.todos.map((todo) => ({
|
|
373
|
+
id: manualRecordId(todo.id),
|
|
374
|
+
todo_id: todo.id,
|
|
375
|
+
result_digest: digestArtifact(manuals.get(todo.id)),
|
|
376
|
+
}));
|
|
377
|
+
const todos = planInput.todos.map((todo, index) => {
|
|
378
|
+
const seam = seams[index];
|
|
379
|
+
const manual = manuals.get(todo.id);
|
|
380
|
+
const resources = resourcesForTodo(seam, manual);
|
|
381
|
+
const conflictIds = conflicts
|
|
382
|
+
.filter(({ todo_ids: todoIds }) => todoIds.includes(todo.id))
|
|
383
|
+
.map(({ id }) => id);
|
|
384
|
+
const unknowns = manual.unknowns.map((unknown) => `manual_unknown:${todo.id}:${unknown}`);
|
|
385
|
+
return {
|
|
386
|
+
id: todo.id,
|
|
387
|
+
owns: resources.owns,
|
|
388
|
+
reads: resources.reads,
|
|
389
|
+
writes: resources.writes,
|
|
390
|
+
hard_needs: [],
|
|
391
|
+
conflict_ids: conflictIds,
|
|
392
|
+
unknowns,
|
|
393
|
+
tests: facts.affectedTests,
|
|
394
|
+
evidence_refs: [
|
|
395
|
+
findSingleQuery(querySet, 'query', seam.symbol).id,
|
|
396
|
+
findSingleQuery(querySet, 'impact', seam.symbol).id,
|
|
397
|
+
affectedQuery.id,
|
|
398
|
+
manualRecordId(todo.id),
|
|
399
|
+
],
|
|
400
|
+
};
|
|
401
|
+
});
|
|
402
|
+
const manifest = {
|
|
403
|
+
schema: 'lattice.boundary_manifest.v1',
|
|
404
|
+
plan_input_digest: digestArtifact(planInput),
|
|
405
|
+
source: {
|
|
406
|
+
code_snapshot_digest: codeSnapshotDigest,
|
|
407
|
+
query_set_digest: digestArtifact(querySet),
|
|
408
|
+
manual_evidence_digest: digestArtifact(manualEvidence),
|
|
409
|
+
sensor_version: facts.sensorVersion,
|
|
410
|
+
},
|
|
411
|
+
graph_evidence: graphRecords(querySet, sensorEvidence),
|
|
412
|
+
manual_evidence: manualEvidenceRecords,
|
|
413
|
+
todos,
|
|
414
|
+
conflicts,
|
|
415
|
+
unknowns: [...new Set(todos.flatMap(({ unknowns }) => unknowns))],
|
|
416
|
+
};
|
|
417
|
+
if (!validateBoundaryManifest(manifest)) {
|
|
418
|
+
fail('生成したtreatment boundary manifestがpublic contractを満たさない');
|
|
419
|
+
}
|
|
420
|
+
return manifest;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function compileVerdict(manifest) {
|
|
424
|
+
const manualUnknown = manifest.unknowns.length > 0;
|
|
425
|
+
const nonCodeConflict = manifest.conflicts.some(({ kind }) => kind === 'state' || kind === 'effect');
|
|
426
|
+
const verdict = manualUnknown
|
|
427
|
+
? 'unknown_requires_evidence'
|
|
428
|
+
: nonCodeConflict ? 'intentional_serial' : 'parallel_ready';
|
|
429
|
+
const reasons = manualUnknown
|
|
430
|
+
? ['manual unknownが残るためtreatment independenceを確定できない']
|
|
431
|
+
: nonCodeConflict
|
|
432
|
+
? ['shared manual state/effect conflictはpath分離後も残る']
|
|
433
|
+
: ['accepted seamとpost-transform graph evidenceがnon-overlapping ownershipを示す'];
|
|
434
|
+
const artifact = {
|
|
435
|
+
schema: 'lattice.boundary_verdict.v1',
|
|
436
|
+
boundary_manifest_digest: digestArtifact(manifest),
|
|
437
|
+
verdicts: [{
|
|
438
|
+
id: 'channel-label-verdict',
|
|
439
|
+
todo_ids: manifest.todos.map(({ id }) => id),
|
|
440
|
+
verdict,
|
|
441
|
+
conflict_ids: manifest.conflicts.map(({ id }) => id),
|
|
442
|
+
seam_candidate: null,
|
|
443
|
+
reasons,
|
|
444
|
+
unknowns: manifest.unknowns,
|
|
445
|
+
}],
|
|
446
|
+
};
|
|
447
|
+
if (!validateBoundaryVerdict(artifact)) {
|
|
448
|
+
fail('生成したtreatment boundary verdictがpublic contractを満たさない');
|
|
449
|
+
}
|
|
450
|
+
return artifact;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function compilePlan(planInput, manifest) {
|
|
454
|
+
const edgeKinds = new Map([
|
|
455
|
+
['state', 'state_conflict'],
|
|
456
|
+
['effect', 'effect_conflict'],
|
|
457
|
+
['semantic', 'semantic_conflict'],
|
|
458
|
+
['dynamic_unknown', 'dynamic_unknown'],
|
|
459
|
+
]);
|
|
460
|
+
const edges = manifest.conflicts.map((conflict) => ({
|
|
461
|
+
id: conflict.id,
|
|
462
|
+
from: conflict.todo_ids[0],
|
|
463
|
+
to: conflict.todo_ids[1],
|
|
464
|
+
kind: edgeKinds.get(conflict.kind),
|
|
465
|
+
evidence_refs: conflict.evidence_refs,
|
|
466
|
+
}));
|
|
467
|
+
const waves = edges.length === 0
|
|
468
|
+
? [{ index: 0, todo_ids: planInput.todos.map(({ id }) => id) }]
|
|
469
|
+
: planInput.todos.map((todo, index) => ({ index, todo_ids: [todo.id] }));
|
|
470
|
+
const graph = {
|
|
471
|
+
schema: 'lattice.plan_graph.v1',
|
|
472
|
+
plan_version: TREATMENT_PLAN_VERSION,
|
|
473
|
+
source_manifest_digest: digestArtifact(manifest),
|
|
474
|
+
capacity: { writers: planInput.capacity.writers },
|
|
475
|
+
nodes: planInput.todos.map((todo) => ({
|
|
476
|
+
id: todo.id,
|
|
477
|
+
outcome: todo.outcome,
|
|
478
|
+
owned_boundaries: manifest.todos.find(({ id }) => id === todo.id).owns,
|
|
479
|
+
})),
|
|
480
|
+
edges,
|
|
481
|
+
joins: [],
|
|
482
|
+
waves,
|
|
483
|
+
minimum_feasible_waves: waves.length,
|
|
484
|
+
};
|
|
485
|
+
if (!validatePlanGraph(graph)) {
|
|
486
|
+
fail('生成したtreatment plan graphがpublic contractを満たさない');
|
|
487
|
+
}
|
|
488
|
+
return graph;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function bundle(planInput, manualEvidence, common) {
|
|
492
|
+
const boundaryManifest = compileManifest({
|
|
493
|
+
planInput,
|
|
494
|
+
manualEvidence,
|
|
495
|
+
...common,
|
|
496
|
+
});
|
|
497
|
+
const boundaryVerdict = compileVerdict(boundaryManifest);
|
|
498
|
+
const planGraph = compilePlan(planInput, boundaryManifest);
|
|
499
|
+
return {
|
|
500
|
+
boundary_manifest: boundaryManifest,
|
|
501
|
+
boundary_manifest_digest: digestArtifact(boundaryManifest),
|
|
502
|
+
boundary_verdict: boundaryVerdict,
|
|
503
|
+
boundary_verdict_digest: digestArtifact(boundaryVerdict),
|
|
504
|
+
plan_graph: planGraph,
|
|
505
|
+
plan_graph_digest: digestArtifact(planGraph),
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function graphChanges(before, after) {
|
|
510
|
+
const beforeById = new Map(before.map((entry) => [entry.id, entry]));
|
|
511
|
+
const afterById = new Map(after.map((entry) => [entry.id, entry]));
|
|
512
|
+
return {
|
|
513
|
+
added: [...afterById.keys()].filter((id) => !beforeById.has(id)).sort(),
|
|
514
|
+
removed: [...beforeById.keys()].filter((id) => !afterById.has(id)).sort(),
|
|
515
|
+
changed: [...beforeById.keys()].filter((id) => afterById.has(id)
|
|
516
|
+
&& digestArtifact(beforeById.get(id)) !== digestArtifact(afterById.get(id))).sort(),
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function compilePlanDiff({ controlPlan, treatmentPlan, transformArtifact, querySet }) {
|
|
521
|
+
const nodeChanges = graphChanges(controlPlan.nodes, treatmentPlan.nodes);
|
|
522
|
+
const edgeChanges = graphChanges(controlPlan.edges, treatmentPlan.edges);
|
|
523
|
+
const diff = {
|
|
524
|
+
schema: 'lattice.plan_diff.v1',
|
|
525
|
+
old_plan: { version: controlPlan.plan_version, digest: digestArtifact(controlPlan) },
|
|
526
|
+
new_plan: { version: treatmentPlan.plan_version, digest: digestArtifact(treatmentPlan) },
|
|
527
|
+
transform: {
|
|
528
|
+
status: transformArtifact.status,
|
|
529
|
+
artifact_digest: digestArtifact(transformArtifact),
|
|
530
|
+
patch_digest: transformArtifact.patch.digest,
|
|
531
|
+
verification_digest: transformArtifact.verification.digest,
|
|
532
|
+
changed_paths: transformArtifact.scope.changed_paths,
|
|
533
|
+
},
|
|
534
|
+
query_set_digest: digestArtifact(querySet),
|
|
535
|
+
snapshots: {
|
|
536
|
+
before_digest: transformArtifact.source.code_snapshot_digest,
|
|
537
|
+
after_digest: transformArtifact.output.snapshot_digest,
|
|
538
|
+
},
|
|
539
|
+
nodes: nodeChanges,
|
|
540
|
+
edges: { added: edgeChanges.added, removed: edgeChanges.removed },
|
|
541
|
+
invalidated_contexts: [
|
|
542
|
+
{
|
|
543
|
+
kind: 'old_plan',
|
|
544
|
+
ref: controlPlan.plan_version,
|
|
545
|
+
reason: 'accepted seam changed the owned boundary topology; active plan is immutable',
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
kind: 'agent_context',
|
|
549
|
+
ref: 'rc1-control-agent-context',
|
|
550
|
+
reason: 'the old shared-symbol ownership assumptions no longer apply',
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
kind: 'partial_patch',
|
|
554
|
+
ref: 'rc1-control-partial-patch-set',
|
|
555
|
+
reason: 'patches based on the old plan cannot cross the version barrier',
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
kind: 'interface_assumption',
|
|
559
|
+
ref: 'shared-buildDispatchRecord-ownership',
|
|
560
|
+
reason: 'each TODO now owns a separate policy symbol and path',
|
|
561
|
+
},
|
|
562
|
+
],
|
|
563
|
+
metrics: {
|
|
564
|
+
write_conflicts_before: controlPlan.edges.filter(({ kind }) => kind === 'write_conflict').length,
|
|
565
|
+
write_conflicts_after: treatmentPlan.edges.filter(({ kind }) => kind === 'write_conflict').length,
|
|
566
|
+
hard_precedence_before: controlPlan.edges.filter(({ kind }) => kind === 'hard_need').length,
|
|
567
|
+
hard_precedence_after: treatmentPlan.edges.filter(({ kind }) => kind === 'hard_need').length,
|
|
568
|
+
minimum_feasible_waves_before: controlPlan.minimum_feasible_waves,
|
|
569
|
+
minimum_feasible_waves_after: treatmentPlan.minimum_feasible_waves,
|
|
570
|
+
},
|
|
571
|
+
};
|
|
572
|
+
if (!validatePlanDiff(diff)) {
|
|
573
|
+
fail('生成したplan diffがpublic contractを満たさない');
|
|
574
|
+
}
|
|
575
|
+
return diff;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function conditionSummary(bundle) {
|
|
579
|
+
return {
|
|
580
|
+
boundary_manifest_digest: bundle.boundary_manifest_digest,
|
|
581
|
+
boundary_verdict_digest: bundle.boundary_verdict_digest,
|
|
582
|
+
plan_graph_digest: bundle.plan_graph_digest,
|
|
583
|
+
verdict: bundle.boundary_verdict.verdicts[0].verdict,
|
|
584
|
+
write_conflicts: bundle.plan_graph.edges.filter(({ kind }) => kind === 'write_conflict').length,
|
|
585
|
+
state_conflicts: bundle.boundary_manifest.conflicts.filter(({ kind }) => kind === 'state').length,
|
|
586
|
+
hard_precedence: bundle.plan_graph.edges.filter(({ kind }) => kind === 'hard_need').length,
|
|
587
|
+
unknowns: bundle.boundary_manifest.unknowns.length,
|
|
588
|
+
minimum_feasible_waves: bundle.plan_graph.minimum_feasible_waves,
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function controlSummary(bundle) {
|
|
593
|
+
return conditionSummary({
|
|
594
|
+
...bundle,
|
|
595
|
+
boundary_manifest_digest: digestArtifact(bundle.boundary_manifest),
|
|
596
|
+
boundary_verdict_digest: digestArtifact(bundle.boundary_verdict),
|
|
597
|
+
plan_graph_digest: digestArtifact(bundle.plan_graph),
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function compileComparison({
|
|
602
|
+
planInput,
|
|
603
|
+
manualNormal,
|
|
604
|
+
manualNegative,
|
|
605
|
+
querySet,
|
|
606
|
+
transformArtifact,
|
|
607
|
+
control,
|
|
608
|
+
normal,
|
|
609
|
+
negative,
|
|
610
|
+
planDiff,
|
|
611
|
+
}) {
|
|
612
|
+
const comparison = {
|
|
613
|
+
schema: 'lattice.rc1.control_treatment_comparison.v1',
|
|
614
|
+
fixed_inputs: {
|
|
615
|
+
plan_input: digestArtifact(planInput),
|
|
616
|
+
normal_manual_evidence: digestArtifact(manualNormal),
|
|
617
|
+
negative_manual_evidence: digestArtifact(manualNegative),
|
|
618
|
+
query_set: digestArtifact(querySet),
|
|
619
|
+
capacity_writers: planInput.capacity.writers,
|
|
620
|
+
verifier: transformArtifact.verification.digest,
|
|
621
|
+
},
|
|
622
|
+
independent_variable: {
|
|
623
|
+
kind: 'accepted_seam_transform',
|
|
624
|
+
artifact_digest: digestArtifact(transformArtifact),
|
|
625
|
+
patch_digest: transformArtifact.patch.digest,
|
|
626
|
+
},
|
|
627
|
+
control: controlSummary(control.normal),
|
|
628
|
+
treatment: conditionSummary(normal),
|
|
629
|
+
negative_control: controlSummary(control.negative),
|
|
630
|
+
negative_treatment: conditionSummary(negative),
|
|
631
|
+
version_barrier: {
|
|
632
|
+
old_plan: planDiff.old_plan,
|
|
633
|
+
new_plan: planDiff.new_plan,
|
|
634
|
+
plan_diff_digest: digestArtifact(planDiff),
|
|
635
|
+
invalidated_contexts: planDiff.invalidated_contexts.map(({ kind, ref }) => ({ kind, ref })),
|
|
636
|
+
},
|
|
637
|
+
hypothesis_result: normal.boundary_verdict.verdicts[0].verdict === 'parallel_ready'
|
|
638
|
+
&& normal.plan_graph.minimum_feasible_waves === 1
|
|
639
|
+
&& negative.boundary_verdict.verdicts[0].verdict === 'intentional_serial'
|
|
640
|
+
&& negative.boundary_manifest.conflicts.some(({ kind }) => kind === 'state')
|
|
641
|
+
? 'supported_in_fixture'
|
|
642
|
+
: 'refuted_in_fixture',
|
|
643
|
+
};
|
|
644
|
+
canonicalizeArtifact(comparison);
|
|
645
|
+
return comparison;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* accepted RC1 seam後の同じquery setをnormal/negative boundaryと新plan versionへ再compileする。
|
|
650
|
+
* @param {object} options
|
|
651
|
+
* @returns {object}
|
|
652
|
+
*/
|
|
653
|
+
export function compileTreatmentArtifacts({
|
|
654
|
+
planInput,
|
|
655
|
+
manualNormal,
|
|
656
|
+
manualNegative,
|
|
657
|
+
querySet,
|
|
658
|
+
sensorEvidence,
|
|
659
|
+
codeSnapshotDigest,
|
|
660
|
+
transformArtifact,
|
|
661
|
+
control,
|
|
662
|
+
} = {}) {
|
|
663
|
+
for (const value of [
|
|
664
|
+
planInput,
|
|
665
|
+
manualNormal,
|
|
666
|
+
manualNegative,
|
|
667
|
+
querySet,
|
|
668
|
+
sensorEvidence,
|
|
669
|
+
transformArtifact,
|
|
670
|
+
control,
|
|
671
|
+
]) {
|
|
672
|
+
canonicalizeArtifact(value);
|
|
673
|
+
}
|
|
674
|
+
if (!validatePlanInput(planInput)) fail('plan inputがpublic contractを満たさない');
|
|
675
|
+
if (typeof codeSnapshotDigest !== 'string' || !SHA256.test(codeSnapshotDigest)) {
|
|
676
|
+
fail('code snapshot digestがSHA-256でない');
|
|
677
|
+
}
|
|
678
|
+
assertManualEvidence(planInput, manualNormal, 'normal');
|
|
679
|
+
assertManualEvidence(planInput, manualNegative, 'negative');
|
|
680
|
+
assertQuerySet(querySet);
|
|
681
|
+
assertLatticeSensorEvidence(querySet, sensorEvidence);
|
|
682
|
+
const seams = treatmentSeams(planInput);
|
|
683
|
+
assertQueryCoverage(planInput, querySet, seams);
|
|
684
|
+
assertPredecessors({
|
|
685
|
+
planInput,
|
|
686
|
+
manualNormal,
|
|
687
|
+
manualNegative,
|
|
688
|
+
querySet,
|
|
689
|
+
transformArtifact,
|
|
690
|
+
control,
|
|
691
|
+
codeSnapshotDigest,
|
|
692
|
+
});
|
|
693
|
+
const facts = treatmentGraphFacts(planInput, querySet, sensorEvidence, seams);
|
|
694
|
+
const common = {
|
|
695
|
+
querySet,
|
|
696
|
+
sensorEvidence,
|
|
697
|
+
codeSnapshotDigest,
|
|
698
|
+
seams,
|
|
699
|
+
facts,
|
|
700
|
+
};
|
|
701
|
+
const normal = bundle(planInput, manualNormal, common);
|
|
702
|
+
const negative = bundle(planInput, manualNegative, common);
|
|
703
|
+
const planDiff = compilePlanDiff({
|
|
704
|
+
controlPlan: control.normal.plan_graph,
|
|
705
|
+
treatmentPlan: normal.plan_graph,
|
|
706
|
+
transformArtifact,
|
|
707
|
+
querySet,
|
|
708
|
+
});
|
|
709
|
+
const comparison = compileComparison({
|
|
710
|
+
planInput,
|
|
711
|
+
manualNormal,
|
|
712
|
+
manualNegative,
|
|
713
|
+
querySet,
|
|
714
|
+
transformArtifact,
|
|
715
|
+
control,
|
|
716
|
+
normal,
|
|
717
|
+
negative,
|
|
718
|
+
planDiff,
|
|
719
|
+
});
|
|
720
|
+
return {
|
|
721
|
+
normal,
|
|
722
|
+
negative,
|
|
723
|
+
plan_diff: planDiff,
|
|
724
|
+
plan_diff_digest: digestArtifact(planDiff),
|
|
725
|
+
comparison,
|
|
726
|
+
comparison_digest: digestArtifact(comparison),
|
|
727
|
+
};
|
|
728
|
+
}
|