@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.
Files changed (134) hide show
  1. package/package.json +1 -1
  2. package/src/artifact-contracts-v2.mjs +325 -325
  3. package/src/artifact-contracts.mjs +895 -895
  4. package/src/boundary-compiler.mjs +712 -712
  5. package/src/boundary-observation-compiler-v2.mjs +344 -344
  6. package/src/bounded-seam.mjs +230 -230
  7. package/src/bridge-address.mjs +107 -107
  8. package/src/bridge-cli.mjs +297 -297
  9. package/src/bridge-config.mjs +346 -346
  10. package/src/bridge-daemon.mjs +378 -378
  11. package/src/bridge-hub-protocol.mjs +198 -0
  12. package/src/bridge-launch-agent.mjs +323 -323
  13. package/src/bridge-registrar.mjs +102 -102
  14. package/src/bridge-server.mjs +376 -376
  15. package/src/cli-help.mjs +308 -308
  16. package/src/cli-stdio.mjs +40 -40
  17. package/src/control-compiler.mjs +532 -532
  18. package/src/dag-chain.mjs +261 -261
  19. package/src/factory-diagnostics.mjs +188 -188
  20. package/src/git-process.mjs +72 -0
  21. package/src/hash-chain.mjs +76 -76
  22. package/src/hooks-cli.mjs +1057 -1057
  23. package/src/isolation-runner.mjs +409 -409
  24. package/src/node-version-guard.mjs +44 -44
  25. package/src/project-cli.mjs +699 -697
  26. package/src/project-identity.mjs +130 -130
  27. package/src/rc1-black-box-oracle.mjs +906 -906
  28. package/src/rc1-comparison.mjs +154 -154
  29. package/src/rc1-evidence-bundle.mjs +456 -456
  30. package/src/rc1-v4-campaign.mjs +708 -708
  31. package/src/rc1-v4-transform.mjs +467 -467
  32. package/src/rc1-v5-artifact-set.mjs +855 -855
  33. package/src/rc1-v5-behavior-evidence.mjs +594 -594
  34. package/src/rc1-v5-campaign.mjs +797 -797
  35. package/src/rc1-v5-transform.mjs +421 -421
  36. package/src/rc1-v6-artifact-set.mjs +807 -807
  37. package/src/rc1-v6-behavior-evidence.mjs +273 -273
  38. package/src/rc1-v6-campaign.mjs +623 -623
  39. package/src/rc1-v6-causal-binding.mjs +473 -473
  40. package/src/rc1-v6-measurement.mjs +313 -313
  41. package/src/rc2-artifact-set.mjs +1584 -1584
  42. package/src/rc2-campaign.mjs +1506 -1506
  43. package/src/rc2-delivery-policy-front-end.mjs +1079 -1079
  44. package/src/rc2-delivery-policy-oracle.mjs +134 -134
  45. package/src/rc2-delivery-policy-transform.mjs +1127 -1127
  46. package/src/rc2-rc1-transfer-front-end.mjs +511 -511
  47. package/src/rc3-actual-dogfood.mjs +652 -652
  48. package/src/rc3-dogfood-scaffold.mjs +315 -315
  49. package/src/rc3-scripted-campaign.mjs +1383 -1383
  50. package/src/rc4-stage1-dogfood.mjs +673 -673
  51. package/src/runtime-adapter-registry.mjs +524 -524
  52. package/src/runtime-cli.mjs +4588 -4588
  53. package/src/runtime-contracts.mjs +824 -824
  54. package/src/runtime-control-store.mjs +604 -604
  55. package/src/runtime-controller-protocol.mjs +587 -587
  56. package/src/runtime-decision-verifier.mjs +701 -701
  57. package/src/runtime-diff-observer.mjs +361 -361
  58. package/src/runtime-direct-os-observer.mjs +301 -301
  59. package/src/runtime-driver-state.mjs +166 -166
  60. package/src/runtime-engine.mjs +779 -779
  61. package/src/runtime-errors.mjs +356 -356
  62. package/src/runtime-event-store.mjs +189 -189
  63. package/src/runtime-front-end.mjs +925 -925
  64. package/src/runtime-gate-store.mjs +481 -481
  65. package/src/runtime-hold-recompile.mjs +916 -916
  66. package/src/runtime-io-sentinel.mjs +391 -391
  67. package/src/runtime-lifecycle-lock.mjs +294 -294
  68. package/src/runtime-managed-supervisor.mjs +1490 -1490
  69. package/src/runtime-multi-epoch-store.mjs +838 -838
  70. package/src/runtime-projection.mjs +269 -269
  71. package/src/runtime-pull-intake.mjs +1192 -1192
  72. package/src/runtime-scripted-adapter-controller.mjs +1160 -1160
  73. package/src/runtime-scripted-executor.mjs +163 -163
  74. package/src/runtime-scripted-worktree.mjs +104 -104
  75. package/src/runtime-seam-resolve.mjs +428 -428
  76. package/src/runtime-seam-treatment.mjs +173 -173
  77. package/src/runtime-socket-owner.mjs +125 -125
  78. package/src/runtime-work-order-contracts.mjs +91 -91
  79. package/src/runtime-work-order-controller.mjs +1171 -1171
  80. package/src/runtime-worktree-executor.mjs +199 -199
  81. package/src/schedulability-compiler-v2.mjs +303 -303
  82. package/src/schedulability-verifier-v2.mjs +317 -317
  83. package/src/seam-apply.mjs +549 -549
  84. package/src/seam-commit-shared.mjs +22 -22
  85. package/src/seam-commit-transform.mjs +81 -81
  86. package/src/seam-commit.mjs +18 -18
  87. package/src/seam-cost.mjs +322 -322
  88. package/src/seam-derivation.mjs +188 -188
  89. package/src/seam-gate.mjs +146 -146
  90. package/src/seam-proposal-contracts.mjs +446 -446
  91. package/src/seam-proposal-queries.mjs +521 -521
  92. package/src/seam-proposal.mjs +2011 -2011
  93. package/src/seam-ref.mjs +33 -33
  94. package/src/seam-rewrite.mjs +286 -286
  95. package/src/seam-transform.mjs +554 -554
  96. package/src/seam-verification.mjs +260 -260
  97. package/src/sensor-adapter.mjs +432 -432
  98. package/src/sensor-cli.mjs +139 -139
  99. package/src/sensor-diff.mjs +661 -661
  100. package/src/sensor-node-runtime.mjs +53 -53
  101. package/src/sensor-runtime.mjs +52 -52
  102. package/src/timestamp-contract.mjs +8 -8
  103. package/src/todo-audit-pending.mjs +91 -91
  104. package/src/todo-chain.mjs +178 -178
  105. package/src/todo-cli.mjs +3143 -3141
  106. package/src/todo-contracts.mjs +728 -728
  107. package/src/todo-dashboard-registry.mjs +573 -573
  108. package/src/todo-dispatch-shape.mjs +190 -190
  109. package/src/todo-gantt-html-independence.mjs +239 -239
  110. package/src/todo-gantt-html-shared.mjs +226 -226
  111. package/src/todo-gantt-html-style.mjs +131 -131
  112. package/src/todo-gantt-html.mjs +248 -248
  113. package/src/todo-gantt-layout.mjs +974 -974
  114. package/src/todo-gantt-live.mjs +361 -361
  115. package/src/todo-gantt-nested.mjs +263 -263
  116. package/src/todo-gantt-presentation.mjs +217 -217
  117. package/src/todo-gantt-scope.mjs +123 -123
  118. package/src/todo-gantt-svg.mjs +353 -353
  119. package/src/todo-independence-contracts.mjs +595 -595
  120. package/src/todo-independence-guidance.mjs +322 -322
  121. package/src/todo-independence.mjs +640 -640
  122. package/src/todo-markdown-renderer.mjs +260 -260
  123. package/src/todo-migration.mjs +448 -448
  124. package/src/todo-narrative-anchor.mjs +130 -130
  125. package/src/todo-note-store.mjs +629 -629
  126. package/src/todo-parallel-candidates.mjs +114 -114
  127. package/src/todo-revision.mjs +995 -995
  128. package/src/todo-split.mjs +472 -472
  129. package/src/todo-status.mjs +690 -690
  130. package/src/todo-store-git-transaction.mjs +418 -418
  131. package/src/todo-store.mjs +4360 -4322
  132. package/src/treatment-compiler.mjs +728 -728
  133. package/src/treatment-runner.mjs +656 -656
  134. package/src/witness-scaffold.mjs +180 -180
@@ -1,1383 +1,1383 @@
1
- import { mkdir, mkdtemp, readFile, rename, writeFile } from 'node:fs/promises';
2
- import { tmpdir } from 'node:os';
3
- import path from 'node:path';
4
-
5
- import { digestArtifact } from './artifact-contracts.mjs';
6
- import { collectSensorEvidence } from './sensor-adapter.mjs';
7
- import { spawnSensorCliSync } from './sensor-runtime.mjs';
8
- import { scaffoldRc3DogfoodRepo, verifyRc3DogfoodScaffold } from './rc3-dogfood-scaffold.mjs';
9
- import { compileRuntimePlanV1, evidenceFromCollectedOutcomes } from './runtime-front-end.mjs';
10
- import {
11
- adjudicatePendingReceipts,
12
- buildNextRunEvent,
13
- buildExecutorPackets,
14
- classifyCheckpointObservation,
15
- closeRunIfComplete,
16
- dispatchReadyFrontier,
17
- initializeRunEvents,
18
- observeExecutor,
19
- } from './runtime-engine.mjs';
20
- import {
21
- decideHoldAndCarryOver,
22
- recompileNextEpochPlan,
23
- routeConflictTreatment,
24
- } from './runtime-hold-recompile.mjs';
25
- import { detectCheckpointFindings } from './runtime-diff-observer.mjs';
26
- import { createWorktreeExecutorAdapter } from './runtime-worktree-executor.mjs';
27
- import {
28
- computeReadyFrontier,
29
- recomputeHoldDecision,
30
- recomputeReceiptDecisions,
31
- } from './runtime-decision-verifier.mjs';
32
- import { verifyRunEventChain } from './runtime-event-store.mjs';
33
- import { projectRuntimeState } from './runtime-projection.mjs';
34
- import { selfDigest } from './runtime-contracts.mjs';
35
-
36
- /**
37
- * RC3-H scripted closed-loop campaign(plan RC3-H、ADR 0044 Decision 10・11)。
38
- *
39
- * Lattice-owned dogfood scaffoldから条件ごとにdisposable repoを作り、正解集合が
40
- * 既知の8条件を同一base(同一fixture bytes)・同一runtime identityで実行する。
41
- * 各条件でevents・plan・hold・receipt・期待/実測比較を保存し、summary値を信じず
42
- * artifact-only verifierが保存bytesから全decisionを再計算する。
43
- *
44
- * 条件(plan「Primary: scripted late-conflict campaign」の8行):
45
- * clean_parallel / late_path_conflict / scope_violation / semantic_unknown /
46
- * stale_receipt / irreducible_conflict / accepted_seam / event_corruption
47
- */
48
-
49
- const RUN_TIMESTAMP = '2026-07-17T00:00:00.000Z';
50
- // dogfood scaffoldの実在3 file(LatticeSensor観測がreadyになる対象)をTODO所有面にする。
51
- const DOC_A = 'research/fixtures/delivery-policy-registry/src/delivery-policy-registry.mjs';
52
- const DOC_B = 'src/rc2-delivery-policy-oracle.mjs';
53
- const DOC_C = 'test/rc2-delivery-policy-fixture.test.mjs';
54
- const MAX_SCRIPTED_REBINDS = 64;
55
-
56
- function fail(reason) {
57
- throw new TypeError(`rc3 campaign契約違反: ${reason}`);
58
- }
59
-
60
- function docWitness(todoId, docPath, affectedTests, { extraWrites = [], unknowns = [], stateEffects = [] } = {}) {
61
- return {
62
- owns: [{ kind: 'path', target: docPath }],
63
- reads: [],
64
- writes: [docPath, ...extraWrites],
65
- resources: stateEffects.map(({ resource_id: id }) => id),
66
- state_effects: stateEffects,
67
- sensor_provenance: {
68
- queries: [{ query_id: `q-aff-${todoId}`, expect: { kind: 'affected', path: docPath } }],
69
- },
70
- affected_tests: affectedTests,
71
- unknowns,
72
- };
73
- }
74
-
75
- /**
76
- * manual witnessのaffected test宣言をfresh LatticeSensor観測から導出する
77
- * (宣言と観測のexact一致契約の下で、正解宣言を機械的に得る)。
78
- */
79
- async function probeAffectedTests(repoRoot, targets) {
80
- const querySet = {
81
- queries: targets.map((target, index) => ({ id: `probe-${index}`, operation: 'affected', target })),
82
- };
83
- const collected = await collectSensorEvidence({ cwd: repoRoot, querySet });
84
- const byTarget = {};
85
- collected.outcomes.forEach((outcome, index) => {
86
- const entry = Array.isArray(outcome.targets) ? outcome.targets[0] : null;
87
- byTarget[targets[index]] = Array.isArray(entry?.data?.affectedTests)
88
- ? [...entry.data.affectedTests].sort()
89
- : [];
90
- });
91
- return byTarget;
92
- }
93
-
94
- /**
95
- * 8条件のうちscaffold上の6条件は同一TODO集合(TA/TB/TC)・同一capacity・
96
- * 同一query構造のrequest templateを使い、条件の注入(unknowns・state_effects・
97
- * 実write)だけを可変にする(plan「同一base、同一request、同一runtime identity」の
98
- * 運用解釈。字義どおりの単一request bytesは注入設計と両立しないため、
99
- * templateの同一性とbase/identityの同一性で担保し、evidenceに解釈を記録する)。
100
- */
101
- function buildDocRequest({ requestId, baseSha, todos, capacity }) {
102
- const queries = [{ id: 'q-status', operation: 'status' }];
103
- for (const [todoId, witness] of Object.entries(todos)) {
104
- void witness;
105
- queries.push({
106
- id: `q-aff-${todoId}`,
107
- operation: 'affected',
108
- target: todos[todoId].owns[0].target,
109
- });
110
- }
111
- const request = {
112
- schema: 'lattice.run_request.v1',
113
- request_id: requestId,
114
- repo: { base_sha: baseSha, root_kind: 'git-worktree' },
115
- capacity: { executors: capacity },
116
- todos: Object.keys(todos).map((todoId) => ({ todo_id: todoId })),
117
- manual_witness: todos,
118
- sensor_query_set: { queries },
119
- executor_capability: { adapters: ['isolated-worktree'] },
120
- claim_mode: 'exact_minimum',
121
- };
122
- request.request_digest = selfDigest(request, 'request_digest');
123
- return request;
124
- }
125
-
126
- async function compileForRepo({ request, repoRoot }) {
127
- const collected = await collectSensorEvidence({
128
- cwd: repoRoot,
129
- querySet: request.sensor_query_set,
130
- });
131
- const sensorEvidence = evidenceFromCollectedOutcomes({
132
- querySet: request.sensor_query_set,
133
- collected,
134
- });
135
- return compileRuntimePlanV1({
136
- request,
137
- sensorEvidence,
138
- planRef: `plan-${request.request_id}-e1`,
139
- planEpoch: 1,
140
- predecessorRefs: [],
141
- });
142
- }
143
-
144
- function docWriter(docPath, body) {
145
- return async ({ worktreePath }) => {
146
- await mkdir(path.dirname(path.join(worktreePath, docPath)), { recursive: true });
147
- await writeFile(path.join(worktreePath, docPath), body);
148
- };
149
- }
150
-
151
- /** 全running executorを1回ずつ観測し、checkpointがあれば分類する共通driver。 */
152
- async function observeAndClassifyAll({ runId, plan, events, packets, manifests, adapter }) {
153
- let next = events;
154
- const scopeExpanded = [];
155
- const state = projectRuntimeState({ events: next });
156
- for (const todoId of state.running) {
157
- const observed = await observeExecutor({
158
- runId, plan, events: next, adapter, todoId, recordedAt: RUN_TIMESTAMP,
159
- });
160
- next = observed.events;
161
- if (observed.observation.state === 'checkpoint_ready') {
162
- const classified = classifyCheckpointObservation({
163
- runId, plan, events: next, packets, manifests, todoId,
164
- detect: detectCheckpointFindings, recordedAt: RUN_TIMESTAMP,
165
- });
166
- next = classified.events;
167
- scopeExpanded.push(...classified.scope_expanded);
168
- if (classified.findings.length > 0) {
169
- return { events: next, frozen: true, scope_expanded: scopeExpanded };
170
- }
171
- }
172
- }
173
- return { events: next, frozen: false, scope_expanded: scopeExpanded };
174
- }
175
-
176
- /** 競合なし前提でclosed loopまで回す(clean/irreducible串行の完走用)。 */
177
- async function driveToClose({ runId, plan, events, packets, manifests, adapter, maxRounds = 16 }) {
178
- let next = events;
179
- const scopeExpanded = [];
180
- for (let round = 0; round < maxRounds; round += 1) {
181
- const dispatched = await dispatchReadyFrontier({
182
- runId, plan, events: next, packets, manifests, adapter, recordedAt: RUN_TIMESTAMP,
183
- });
184
- if (dispatched.failure) fail(`dispatch失敗: ${dispatched.failure.message}`);
185
- next = dispatched.events;
186
- const observed = await observeAndClassifyAll({
187
- runId, plan, events: next, packets, manifests, adapter,
188
- });
189
- if (observed.frozen) fail('競合なし条件でfreezeが発生した');
190
- next = observed.events;
191
- scopeExpanded.push(...observed.scope_expanded);
192
- const adjudicated = adjudicatePendingReceipts({ runId, plan, events: next, recordedAt: RUN_TIMESTAMP });
193
- next = adjudicated.events;
194
- const closed = closeRunIfComplete({ runId, plan, events: next, recordedAt: RUN_TIMESTAMP });
195
- next = closed.events;
196
- if (closed.closed) return { events: next, scope_expanded: scopeExpanded };
197
- }
198
- fail('closed loopがmaxRounds内に完走しない');
199
- return null;
200
- }
201
-
202
- /**
203
- * legacy scripted campaign内だけで、controller rebindとactivation gateの完了を模擬する。
204
- * productionのACK/gate検証を迂回する入口ではなく、既にscriptで完了させたactivationを
205
- * event contractへ投影するための有界fixture helperである。
206
- */
207
- function activateScriptedRecompile({ runId, events, plan, planDiff, rebindPackets }) {
208
- const entries = Object.entries(rebindPackets).sort(([left], [right]) => left.localeCompare(right));
209
- if (entries.length > MAX_SCRIPTED_REBINDS) {
210
- fail(`scripted rebind数が上限を超えた: ${entries.length}`);
211
- }
212
- let next = [...events];
213
- for (const [todoId, packet] of entries) {
214
- if (packet?.todo_id !== todoId || packet?.new_plan_epoch !== plan.plan_epoch) {
215
- fail(`scripted rebind packetの帰属が不正: ${todoId}`);
216
- }
217
- next.push(buildNextRunEvent({
218
- events: next,
219
- runId,
220
- kind: 'epoch_rebound',
221
- planEpoch: plan.plan_epoch,
222
- subject: { kind: 'todo', ref: todoId },
223
- payload: packet,
224
- recordedAt: RUN_TIMESTAMP,
225
- }));
226
- }
227
- next.push(buildNextRunEvent({
228
- events: next,
229
- runId,
230
- kind: 'intake_resumed',
231
- planEpoch: plan.plan_epoch,
232
- subject: { kind: 'runtime_plan', ref: plan.plan_ref },
233
- payload: { plan_diff_digest: planDiff.diff_digest },
234
- recordedAt: RUN_TIMESTAMP,
235
- }));
236
- return next;
237
- }
238
-
239
- function decisionReplayChecks({ plan, events }) {
240
- const checks = [];
241
- events.forEach((event, index) => {
242
- if (event.kind !== 'dispatch_decided') return;
243
- // dispatch decisionはそのdecisionが属するepochのplanでだけ再計算できる
244
- //(frontier規則はepoch-scoped)。別epochのdecisionは当該epochのplanで検証する。
245
- if (event.plan_epoch !== plan.plan_epoch) return;
246
- const recomputed = computeReadyFrontier({ plan, events: events.slice(0, index) });
247
- checks.push({
248
- sequence: event.sequence,
249
- kind: 'dispatch_decided',
250
- match: JSON.stringify(event.payload.dispatchable) === JSON.stringify(recomputed.dispatchable),
251
- });
252
- });
253
- return checks;
254
- }
255
-
256
- /** 条件結果recordを組み立てる(期待と実測、および再計算一致)。 */
257
- function conditionRecord({ condition, expected, actual, events, plan }) {
258
- const chain = verifyRunEventChain({ events });
259
- const replay = decisionReplayChecks({ plan, events });
260
- const record = {
261
- schema: 'lattice.rc3.campaign_condition.v1',
262
- condition,
263
- expected,
264
- actual,
265
- matched: JSON.stringify(expected) === JSON.stringify(actual),
266
- event_chain_valid: chain.valid,
267
- dispatch_replay_matched: replay.every(({ match }) => match),
268
- event_count: events.length,
269
- events_digest: digestArtifact(events.map(({ event_digest: digest }) => digest)),
270
- };
271
- if (!record.matched) fail(`条件${condition}の期待/実測が一致しない: ${JSON.stringify({ expected, actual })}`);
272
- if (!record.event_chain_valid) fail(`条件${condition}のevent chainが不正`);
273
- if (!record.dispatch_replay_matched) fail(`条件${condition}のdispatch replayが一致しない`);
274
- return record;
275
- }
276
-
277
- async function runCleanParallel({ scaffold }) {
278
- const runId = 'rc3h-clean';
279
- const affected = await probeAffectedTests(scaffold.repoRoot, [DOC_A, DOC_B, DOC_C]);
280
- const request = buildDocRequest({
281
- requestId: 'rc3h-clean',
282
- baseSha: scaffold.target.base_sha,
283
- capacity: 3,
284
- todos: {
285
- TA: docWitness('TA', DOC_A, affected[DOC_A]),
286
- TB: docWitness('TB', DOC_B, affected[DOC_B]),
287
- TC: docWitness('TC', DOC_C, affected[DOC_C]),
288
- },
289
- });
290
- const compiled = await compileForRepo({ request, repoRoot: scaffold.repoRoot });
291
- if (compiled.outcome !== 'dispatchable') fail(`clean条件がdispatchableにならない: ${compiled.code}`);
292
- const { plan, manifests } = compiled;
293
- const packets = buildExecutorPackets({ plan, manifests });
294
- const adapter = createWorktreeExecutorAdapter({
295
- repoRoot: scaffold.repoRoot,
296
- work: {
297
- TA: docWriter(DOC_A, 'alpha\n'),
298
- TB: docWriter(DOC_B, 'beta\n'),
299
- TC: docWriter(DOC_C, 'gamma\n'),
300
- },
301
- });
302
- let events = initializeRunEvents({ runId, request, plan, manifests, recordedAt: RUN_TIMESTAMP });
303
- const driven = await driveToClose({ runId, plan, events, packets, manifests, adapter });
304
- events = driven.events;
305
- const state = projectRuntimeState({ events });
306
- return {
307
- request,
308
- plan,
309
- events,
310
- scope_expanded: driven.scope_expanded,
311
- record: conditionRecord({
312
- condition: 'clean_parallel',
313
- expected: { hold: [], continue: ['TA', 'TB', 'TC'], accepted: ['TA', 'TB', 'TC'], closed: true },
314
- actual: {
315
- hold: state.holds.flatMap((hold) => hold.hold_set ?? []),
316
- continue: state.accepted,
317
- accepted: state.accepted,
318
- closed: state.closed,
319
- },
320
- events,
321
- plan,
322
- }),
323
- };
324
- }
325
-
326
- async function runLateConflict({ scaffold }) {
327
- const runId = 'rc3h-late';
328
- const affected = await probeAffectedTests(scaffold.repoRoot, [DOC_A, DOC_B, DOC_C]);
329
- const request = buildDocRequest({
330
- requestId: 'rc3h-late',
331
- baseSha: scaffold.target.base_sha,
332
- capacity: 3,
333
- todos: {
334
- TA: docWitness('TA', DOC_A, affected[DOC_A]),
335
- TB: docWitness('TB', DOC_B, affected[DOC_B]),
336
- TC: docWitness('TC', DOC_C, affected[DOC_C]),
337
- },
338
- });
339
- const compiled = await compileForRepo({ request, repoRoot: scaffold.repoRoot });
340
- if (compiled.outcome !== 'dispatchable') fail(`late条件がdispatchableにならない: ${compiled.code}`);
341
- const { plan, manifests } = compiled;
342
- const packets = buildExecutorPackets({ plan, manifests });
343
- const adapter = createWorktreeExecutorAdapter({
344
- repoRoot: scaffold.repoRoot,
345
- work: {
346
- // TAが宣言外にTBのdeclared write(DOC_B)へも書く=後発path conflict。
347
- TA: async ({ worktreePath }) => {
348
- await docWriter(DOC_A, 'alpha\n')({ worktreePath });
349
- await docWriter(DOC_B, 'rogue write\n')({ worktreePath });
350
- },
351
- TB: docWriter(DOC_B, 'beta\n'),
352
- TC: docWriter(DOC_C, 'gamma\n'),
353
- },
354
- });
355
- let events = initializeRunEvents({ runId, request, plan, manifests, recordedAt: RUN_TIMESTAMP });
356
- const dispatched = await dispatchReadyFrontier({
357
- runId, plan, events, packets, manifests, adapter, recordedAt: RUN_TIMESTAMP,
358
- });
359
- events = dispatched.events;
360
- // TCを先に観測(正常checkpoint)、次にTA(競合露呈→freeze)。
361
- for (const todoId of ['TC', 'TA']) {
362
- const observed = await observeExecutor({ runId, plan, events, adapter, todoId, recordedAt: RUN_TIMESTAMP });
363
- events = observed.events;
364
- const classified = classifyCheckpointObservation({
365
- runId, plan, events, packets, manifests, todoId,
366
- detect: detectCheckpointFindings, recordedAt: RUN_TIMESTAMP,
367
- });
368
- events = classified.events;
369
- }
370
- const held = decideHoldAndCarryOver({
371
- runId, request, plan, manifests, packets, events, recordedAt: RUN_TIMESTAMP,
372
- });
373
- events = held.events;
374
- const lane = routeConflictTreatment({
375
- finding: held.holdDecision.finding,
376
- predeclaredTreatments: [],
377
- });
378
- const recompiled = recompileNextEpochPlan({
379
- runId, request, plan, manifests, packets, events,
380
- holdDecision: held.holdDecision,
381
- additionalConflicts: [{ todo_ids: ['TA', 'TB'], resource_id: 'late-doc-b' }],
382
- recordedAt: RUN_TIMESTAMP,
383
- });
384
- events = recompiled.events;
385
- // 継続TODO(TC)のoutputをvN+1へbind: rebindを適用し、terminal→受理まで進める
386
- // (成功条件12「無関係TODOの継続にはcarry-over witnessが必須」の完走証明)。
387
- await adapter.rebind({
388
- executor_handle: projectRuntimeState({ events }).dispatches.TC.payload.executor_handle,
389
- rebind: recompiled.rebindPackets.TC,
390
- });
391
- events = activateScriptedRecompile({
392
- runId,
393
- events,
394
- plan: recompiled.newPlan,
395
- planDiff: recompiled.planDiff,
396
- rebindPackets: recompiled.rebindPackets,
397
- });
398
- {
399
- const tcTerminal = await observeExecutor({
400
- runId, plan: recompiled.newPlan, events, adapter, todoId: 'TC', recordedAt: RUN_TIMESTAMP,
401
- });
402
- events = tcTerminal.events;
403
- const adjudicated = adjudicatePendingReceipts({
404
- runId, plan: recompiled.newPlan, events, recordedAt: RUN_TIMESTAMP,
405
- });
406
- events = adjudicated.events;
407
- }
408
- const state = projectRuntimeState({ events });
409
- return {
410
- request,
411
- plan,
412
- manifests,
413
- newPlan: recompiled.newPlan,
414
- planDiff: recompiled.planDiff,
415
- events,
416
- record: conditionRecord({
417
- condition: 'late_path_conflict',
418
- expected: {
419
- hold: ['TA', 'TB'],
420
- continue: ['TC'],
421
- lane: 'intentional_serial',
422
- carried_over: ['TC'],
423
- redispatched: ['TA', 'TB'],
424
- new_epoch: 2,
425
- carried_output_accepted: true,
426
- },
427
- actual: {
428
- hold: held.holdDecision.hold_set,
429
- continue: held.holdDecision.continue_set,
430
- lane: lane.lane,
431
- carried_over: recompiled.planDiff.carried_over,
432
- redispatched: recompiled.planDiff.redispatched,
433
- new_epoch: recompiled.newPlan.plan_epoch,
434
- carried_output_accepted: state.accepted.includes('TC'),
435
- },
436
- events,
437
- plan,
438
- }),
439
- holdDecision: held.holdDecision,
440
- stateAfter: state,
441
- };
442
- }
443
-
444
- async function runScopeViolation({ scaffold }) {
445
- const runId = 'rc3h-scope';
446
- const affected = await probeAffectedTests(scaffold.repoRoot, [DOC_A, DOC_B]);
447
- const request = buildDocRequest({
448
- requestId: 'rc3h-scope',
449
- baseSha: scaffold.target.base_sha,
450
- capacity: 2,
451
- todos: {
452
- TA: docWitness('TA', DOC_A, affected[DOC_A]),
453
- TB: docWitness('TB', DOC_B, affected[DOC_B]),
454
- },
455
- });
456
- const compiled = await compileForRepo({ request, repoRoot: scaffold.repoRoot });
457
- if (compiled.outcome !== 'dispatchable') fail(`scope条件がdispatchableにならない: ${compiled.code}`);
458
- const { plan, manifests } = compiled;
459
- const packets = buildExecutorPackets({ plan, manifests });
460
- const adapter = createWorktreeExecutorAdapter({
461
- repoRoot: scaffold.repoRoot,
462
- work: {
463
- TA: async ({ worktreePath }) => {
464
- await docWriter(DOC_A, 'alpha\n')({ worktreePath });
465
- await docWriter('docs/rogue.md', 'undeclared\n')({ worktreePath });
466
- },
467
- TB: docWriter(DOC_B, 'beta\n'),
468
- },
469
- });
470
- let events = initializeRunEvents({ runId, request, plan, manifests, recordedAt: RUN_TIMESTAMP });
471
- const dispatched = await dispatchReadyFrontier({
472
- runId, plan, events, packets, manifests, adapter, recordedAt: RUN_TIMESTAMP,
473
- });
474
- events = dispatched.events;
475
- const observed = await observeExecutor({ runId, plan, events, adapter, todoId: 'TA', recordedAt: RUN_TIMESTAMP });
476
- events = observed.events;
477
- const classified = classifyCheckpointObservation({
478
- runId, plan, events, packets, manifests, todoId: 'TA',
479
- detect: detectCheckpointFindings, recordedAt: RUN_TIMESTAMP,
480
- });
481
- events = classified.events;
482
- const driven = await driveToClose({ runId, plan, events, packets, manifests, adapter });
483
- events = driven.events;
484
- const scopeExpanded = [...classified.scope_expanded, ...driven.scope_expanded];
485
- const state = projectRuntimeState({ events });
486
- return {
487
- request,
488
- plan,
489
- manifests,
490
- events,
491
- scope_expanded: scopeExpanded,
492
- record: conditionRecord({
493
- condition: 'scope_violation',
494
- // directory名はRC3 artifact互換のため維持する。現契約では単独の予測超過は
495
- // conflictではなく、観測を残したまま有効なreceiptを受理する。
496
- expected: {
497
- observation_kinds: ['prediction_excess'],
498
- scope_expanded: [{
499
- task_id: 'TA',
500
- compared_witness_digest: null,
501
- first_seen_path_count: 1,
502
- path_count: 2,
503
- added_paths: ['docs/rogue.md'],
504
- removed_paths: [],
505
- growth_events: 1,
506
- gate_shape: false,
507
- }],
508
- conflict_finding_kinds: [],
509
- frozen: false,
510
- accepted: ['TA', 'TB'],
511
- closed: true,
512
- },
513
- actual: {
514
- observation_kinds: [...new Set(classified.observations.map(({ kind }) => kind))],
515
- scope_expanded: scopeExpanded,
516
- conflict_finding_kinds: [...new Set(classified.findings.map(({ kind }) => kind))],
517
- frozen: state.freeze !== null,
518
- accepted: state.accepted,
519
- closed: state.closed,
520
- },
521
- events,
522
- plan,
523
- }),
524
- };
525
- }
526
-
527
- async function runSemanticUnknown({ scaffold }) {
528
- const affected = await probeAffectedTests(scaffold.repoRoot, [DOC_A, DOC_B]);
529
- const request = buildDocRequest({
530
- requestId: 'rc3h-semantic',
531
- baseSha: scaffold.target.base_sha,
532
- capacity: 2,
533
- todos: {
534
- TA: docWitness('TA', DOC_A, affected[DOC_A], {
535
- unknowns: [{ kind: 'semantic_probe', ref: 'shared delivery-policy invariant?' }],
536
- }),
537
- TB: docWitness('TB', DOC_B, affected[DOC_B]),
538
- },
539
- });
540
- const compiled = await compileForRepo({ request, repoRoot: scaffold.repoRoot });
541
- // drift実効性の対照: 固定の誤ったaffected宣言はAFFECTED_TEST_DRIFTになる
542
- //(probe由来宣言がdrift検査を無意味化していないことの証明)。
543
- const driftRequest = buildDocRequest({
544
- requestId: 'rc3h-semantic-drift',
545
- baseSha: scaffold.target.base_sha,
546
- capacity: 2,
547
- todos: {
548
- TA: docWitness('TA', DOC_A, ['test/fixed-wrong-declaration.test.mjs']),
549
- TB: docWitness('TB', DOC_B, affected[DOC_B]),
550
- },
551
- });
552
- const driftCompiled = await compileForRepo({ request: driftRequest, repoRoot: scaffold.repoRoot });
553
- const record = {
554
- schema: 'lattice.rc3.campaign_condition.v1',
555
- condition: 'semantic_unknown',
556
- expected: {
557
- outcome: 'non_dispatchable',
558
- code: 'BOUNDARY_UNKNOWN',
559
- drift_control: { outcome: 'non_dispatchable', code: 'AFFECTED_TEST_DRIFT' },
560
- },
561
- actual: {
562
- outcome: compiled.outcome,
563
- code: compiled.code ?? null,
564
- drift_control: { outcome: driftCompiled.outcome, code: driftCompiled.code ?? null },
565
- },
566
- matched: compiled.outcome === 'non_dispatchable' && compiled.code === 'BOUNDARY_UNKNOWN'
567
- && driftCompiled.outcome === 'non_dispatchable' && driftCompiled.code === 'AFFECTED_TEST_DRIFT',
568
- event_chain_valid: true,
569
- dispatch_replay_matched: true,
570
- event_count: 0,
571
- events_digest: digestArtifact([]),
572
- };
573
- if (!record.matched) fail(`semantic unknown条件が期待どおりでない: ${JSON.stringify(record.actual)}`);
574
- return { request, record, nonDispatchable: { outcome: compiled.outcome, code: compiled.code } };
575
- }
576
-
577
- async function runStaleReceipt({ scaffold }) {
578
- // late conflictの流れでvN+1まで進め、rebindなしの旧epoch receipt
579
- // (held TODOの旧executor由来)をvN+1へ提出してtyped rejectを確認する。
580
- const late = await runLateConflict({ scaffold });
581
- const { buildNextRunEvent } = await import('./runtime-engine.mjs');
582
- const packets = buildExecutorPackets({ plan: late.plan, manifests: late.manifests });
583
- const state = late.stateAfter;
584
- const dispatchTB = state.dispatches.TB;
585
- const staleReceipt = {
586
- schema: 'lattice.executor_receipt.v1',
587
- receipt_id: 'TB-stale-r1',
588
- executor_handle: dispatchTB.payload.executor_handle,
589
- worktree_id: dispatchTB.payload.worktree_id,
590
- base_sha: late.plan.base_sha,
591
- plan_epoch: late.plan.plan_epoch,
592
- packet_digest: packets.TB.packet_digest,
593
- todo_id: 'TB',
594
- checkpoint_digest: '1'.repeat(64),
595
- observed_diff: [{ path: DOC_B, change: 'modified' }],
596
- };
597
- staleReceipt.receipt_digest = selfDigest(staleReceipt, 'receipt_digest');
598
- let events = [...late.events];
599
- events.push(buildNextRunEvent({
600
- events,
601
- runId: 'rc3h-late',
602
- kind: 'receipt_recorded',
603
- planEpoch: staleReceipt.plan_epoch,
604
- subject: { kind: 'todo', ref: 'TB' },
605
- payload: staleReceipt,
606
- recordedAt: RUN_TIMESTAMP,
607
- }));
608
- const acceptedBefore = projectRuntimeState({ events: late.events }).accepted;
609
- const adjudicated = adjudicatePendingReceipts({
610
- runId: 'rc3h-late', plan: late.newPlan, events, recordedAt: RUN_TIMESTAMP,
611
- });
612
- events = adjudicated.events;
613
- const decision = adjudicated.decisions.find(({ receipt_id: id }) => id === 'TB-stale-r1');
614
- const recomputed = recomputeReceiptDecisions({ plan: late.newPlan, events })
615
- .decisions.find(({ receipt_id: id }) => id === 'TB-stale-r1');
616
- const acceptedAfter = projectRuntimeState({ events }).accepted;
617
- const acceptedDelta = acceptedAfter.filter((todoId) => !acceptedBefore.includes(todoId));
618
- const record = {
619
- schema: 'lattice.rc3.campaign_condition.v1',
620
- condition: 'stale_receipt',
621
- expected: {
622
- decision: 'rejected',
623
- reason: 'stale_context',
624
- verifier_decision: 'rejected',
625
- accepted_outputs_from_stale: [],
626
- },
627
- actual: {
628
- decision: decision?.decision ?? null,
629
- reason: 'stale_context',
630
- verifier_decision: recomputed?.decision ?? null,
631
- accepted_outputs_from_stale: acceptedDelta,
632
- },
633
- matched: decision?.decision === 'rejected' && recomputed?.decision === 'rejected'
634
- && acceptedDelta.length === 0,
635
- event_chain_valid: verifyRunEventChain({ events }).valid,
636
- dispatch_replay_matched: true,
637
- event_count: events.length,
638
- events_digest: digestArtifact(events.map(({ event_digest: digest }) => digest)),
639
- };
640
- if (!record.matched) fail(`stale receipt条件が期待どおりでない: ${JSON.stringify(record.actual)}`);
641
- return {
642
- request: late.request,
643
- plan: late.newPlan,
644
- manifests: late.manifests,
645
- events,
646
- record,
647
- };
648
- }
649
-
650
- async function runAcceptedSeam({ latticeRoot }) {
651
- // 独立したscaffold上でpredeclared seam treatmentを実行し、accepted後に
652
- // fresh reindex→conflict減少→new plan発行を確認する(ADR 0044 Decision 11.3)。
653
- const workRoot = await mkdtemp(path.join(tmpdir(), 'lattice-rc3h-seam-'));
654
- const scaffold = await scaffoldRc3DogfoodRepo({ latticeRoot, workRoot });
655
- const entryPath = 'research/fixtures/delivery-policy-registry/src/delivery-policy-registry.mjs';
656
-
657
- // pre-seam: 2 TODOが同じfixture entryを所有→write conflict→2 waves。
658
- const preAffected = await probeAffectedTests(scaffold.repoRoot, [entryPath]);
659
- const preWitness = () => ({
660
- owns: [
661
- { kind: 'symbol', target: 'resolveDeliveryPolicy' },
662
- { kind: 'path', target: entryPath },
663
- ],
664
- reads: [],
665
- writes: [entryPath],
666
- resources: [],
667
- state_effects: [],
668
- sensor_provenance: {
669
- queries: [
670
- { query_id: 'q-entry', expect: { kind: 'symbol', name: 'resolveDeliveryPolicy', path: entryPath } },
671
- { query_id: 'q-aff-entry', expect: { kind: 'affected', path: entryPath } },
672
- ],
673
- },
674
- affected_tests: preAffected[entryPath],
675
- unknowns: [],
676
- });
677
- const preRequest = {
678
- schema: 'lattice.run_request.v1',
679
- request_id: 'rc3h-seam-pre',
680
- repo: { base_sha: scaffold.target.base_sha, root_kind: 'git-worktree' },
681
- capacity: { executors: 2 },
682
- todos: [{ todo_id: 'Temail' }, { todo_id: 'Tsms' }],
683
- manual_witness: { Temail: preWitness(), Tsms: preWitness() },
684
- sensor_query_set: {
685
- queries: [
686
- { id: 'q-status', operation: 'status' },
687
- { id: 'q-entry', operation: 'query', target: 'resolveDeliveryPolicy' },
688
- { id: 'q-aff-entry', operation: 'affected', target: entryPath },
689
- ],
690
- },
691
- executor_capability: { adapters: ['isolated-worktree'] },
692
- claim_mode: 'exact_minimum',
693
- };
694
- preRequest.request_digest = selfDigest(preRequest, 'request_digest');
695
- const preCompiled = await compileForRepo({ request: preRequest, repoRoot: scaffold.repoRoot });
696
- if (preCompiled.outcome !== 'dispatchable') fail(`seam pre条件がdispatchableにならない: ${preCompiled.code}`);
697
- const preWaves = preCompiled.schedule.minimum_feasible_waves;
698
- const preConflicts = preCompiled.plan.conflicts.length;
699
-
700
- // predeclared treatmentのroute判定とRC2無改変adapterでの実行。
701
- const candidateSpec = JSON.parse(await readFile(
702
- path.join(latticeRoot, 'research/campaigns/rc2/inputs/candidate-spec-v1.json'), 'utf8',
703
- ));
704
- const lane = routeConflictTreatment({
705
- finding: { kind: 'observed_write_conflict', todo_ids: ['Temail', 'Tsms'], path: entryPath },
706
- predeclaredTreatments: [{
707
- treatment_id: scaffold.predeclared_treatment.epoch,
708
- covered_paths: [entryPath],
709
- candidate_digest: scaffold.predeclared_treatment.candidate_digest,
710
- }],
711
- });
712
- if (lane.lane !== 'seam_transform') fail('predeclared treatmentがseam laneへroutingされない');
713
-
714
- const { runRc2DeliveryPolicySeamTransform } = await import('./rc2-delivery-policy-transform.mjs');
715
- const transformResult = await runRc2DeliveryPolicySeamTransform({
716
- repoRoot: scaffold.repoRoot,
717
- baseRef: scaffold.target.base_sha,
718
- candidateSpec,
719
- });
720
- if (transformResult.artifact.status !== 'accepted') {
721
- fail(`seam transformがacceptedにならない: ${JSON.stringify(transformResult.artifact.rejection)}`);
722
- }
723
-
724
- // accepted patchを新baseとしてcommitし、fresh reindex後にper-channel witnessで再compile。
725
- const { spawnSync } = await import('node:child_process');
726
- const runGit = (args) => {
727
- const result = spawnSync('git', args, { cwd: scaffold.repoRoot, encoding: 'utf8' });
728
- if (result.status !== 0) fail(`git ${args[0]} failed: ${result.stderr}`);
729
- return result.stdout;
730
- };
731
- const patchPath = path.join(workRoot, 'accepted-seam.patch');
732
- await writeFile(patchPath, transformResult.patch);
733
- runGit(['apply', '--binary', patchPath]);
734
- runGit(['-c', 'user.email=rc3h@lattice.invalid', '-c', 'user.name=rc3h', 'add', '.']);
735
- runGit(['-c', 'user.email=rc3h@lattice.invalid', '-c', 'user.name=rc3h', 'commit', '--quiet', '-m', 'accepted seam']);
736
- const newBase = runGit(['rev-parse', 'HEAD']).trim();
737
- // fresh reindex(accepted seam後は必ず再index。syncで新規fileを収載する)。
738
- const syncResult = spawnSensorCliSync(['sync', '.'], { cwd: scaffold.repoRoot, encoding: 'utf8' });
739
- if (syncResult.status !== 0) fail(`sensor reindex(sync)が失敗: ${syncResult.stderr}`);
740
-
741
- const emailPath = 'research/fixtures/delivery-policy-registry/src/email-policy.mjs';
742
- const smsPath = 'research/fixtures/delivery-policy-registry/src/sms-policy.mjs';
743
- const postAffected = await probeAffectedTests(scaffold.repoRoot, [emailPath, smsPath]);
744
- const postWitness = (todoId, symbol, filePath) => ({
745
- owns: [{ kind: 'symbol', target: symbol }, { kind: 'path', target: filePath }],
746
- reads: [],
747
- writes: [filePath],
748
- resources: [],
749
- state_effects: [],
750
- sensor_provenance: {
751
- queries: [
752
- { query_id: `q-${todoId}`, expect: { kind: 'symbol', name: symbol, path: filePath } },
753
- { query_id: `q-aff-${todoId}`, expect: { kind: 'affected', path: filePath } },
754
- ],
755
- },
756
- affected_tests: postAffected[filePath],
757
- unknowns: [],
758
- });
759
- const postRequest = {
760
- schema: 'lattice.run_request.v1',
761
- request_id: 'rc3h-seam-post',
762
- repo: { base_sha: newBase, root_kind: 'git-worktree' },
763
- capacity: { executors: 2 },
764
- todos: [{ todo_id: 'Temail' }, { todo_id: 'Tsms' }],
765
- manual_witness: {
766
- Temail: postWitness('Temail', 'resolveEmailPolicy', emailPath),
767
- Tsms: postWitness('Tsms', 'resolveSmsPolicy', smsPath),
768
- },
769
- sensor_query_set: {
770
- queries: [
771
- { id: 'q-status', operation: 'status' },
772
- { id: 'q-Temail', operation: 'query', target: 'resolveEmailPolicy' },
773
- { id: 'q-aff-Temail', operation: 'affected', target: emailPath },
774
- { id: 'q-Tsms', operation: 'query', target: 'resolveSmsPolicy' },
775
- { id: 'q-aff-Tsms', operation: 'affected', target: smsPath },
776
- ],
777
- },
778
- executor_capability: { adapters: ['isolated-worktree'] },
779
- claim_mode: 'exact_minimum',
780
- };
781
- postRequest.request_digest = selfDigest(postRequest, 'request_digest');
782
- // post-seam planはgenesisではなく、accepted transformとpre planをpredecessorに
783
- // 持つepoch 2 planとしてcompileする(Decision 11.2/plan barrier)。
784
- const transformArtifactDigest = digestArtifact(transformResult.artifact);
785
- const postCollected = await collectSensorEvidence({
786
- cwd: scaffold.repoRoot, querySet: postRequest.sensor_query_set,
787
- });
788
- const postCompiled = compileRuntimePlanV1({
789
- request: postRequest,
790
- sensorEvidence: evidenceFromCollectedOutcomes({
791
- querySet: postRequest.sensor_query_set, collected: postCollected,
792
- }),
793
- planRef: `plan-${postRequest.request_id}-e2`,
794
- planEpoch: 2,
795
- predecessorRefs: [preCompiled.plan.plan_ref, `seam:${transformArtifactDigest}`],
796
- });
797
- if (postCompiled.outcome !== 'dispatchable') fail(`seam post条件がdispatchableにならない: ${postCompiled.code}`);
798
-
799
- const record = {
800
- schema: 'lattice.rc3.campaign_condition.v1',
801
- condition: 'accepted_seam',
802
- expected: {
803
- lane: 'seam_transform',
804
- transform_status: 'accepted',
805
- conflict_reduced: true,
806
- wave_reduced: true,
807
- new_plan_issued: true,
808
- },
809
- actual: {
810
- lane: lane.lane,
811
- transform_status: transformResult.artifact.status,
812
- conflict_reduced: postCompiled.plan.conflicts.length < preConflicts,
813
- wave_reduced: postCompiled.schedule.minimum_feasible_waves < preWaves,
814
- new_plan_issued: postCompiled.plan.plan_digest !== preCompiled.plan.plan_digest,
815
- },
816
- matched: false,
817
- event_chain_valid: true,
818
- dispatch_replay_matched: true,
819
- event_count: 0,
820
- events_digest: digestArtifact([]),
821
- seam: {
822
- pre: { conflicts: preConflicts, minimum_feasible_waves: preWaves, base_sha: scaffold.target.base_sha },
823
- post: {
824
- conflicts: postCompiled.plan.conflicts.length,
825
- minimum_feasible_waves: postCompiled.schedule.minimum_feasible_waves,
826
- base_sha: newBase,
827
- },
828
- transform_artifact_digest: transformArtifactDigest,
829
- patch_digest: transformResult.artifact.patch.digest,
830
- predecessor: scaffold.predeclared_treatment,
831
- post_plan_predecessor_refs: postCompiled.plan.predecessor_refs,
832
- namespaces: {
833
- lattice_source: scaffold.lattice_source,
834
- target: { root_kind: scaffold.target.root_kind, pre_base_sha: scaffold.target.base_sha, post_base_sha: newBase },
835
- },
836
- },
837
- };
838
- record.matched = JSON.stringify(record.expected) === JSON.stringify(record.actual);
839
- if (!record.matched) fail(`accepted seam条件が期待どおりでない: ${JSON.stringify(record.actual)}`);
840
- return {
841
- request: postRequest,
842
- record,
843
- seamExtras: {
844
- transformArtifact: transformResult.artifact,
845
- patchBytes: transformResult.patch,
846
- postPlan: postCompiled.plan,
847
- prePlan: preCompiled.plan,
848
- },
849
- };
850
- }
851
-
852
- async function runIrreducibleConflict({ scaffold }) {
853
- // 実行中に発見されたseamなしshared state(semantic conflict)を
854
- // intentional serialへ再compileする(plan条件表の「再compile」読み)。
855
- const runId = 'rc3h-serial';
856
- const affected = await probeAffectedTests(scaffold.repoRoot, [DOC_A, DOC_B, DOC_C]);
857
- const request = buildDocRequest({
858
- requestId: 'rc3h-serial',
859
- baseSha: scaffold.target.base_sha,
860
- capacity: 3,
861
- todos: {
862
- TA: docWitness('TA', DOC_A, affected[DOC_A]),
863
- TB: docWitness('TB', DOC_B, affected[DOC_B]),
864
- TC: docWitness('TC', DOC_C, affected[DOC_C]),
865
- },
866
- });
867
- const compiled = await compileForRepo({ request, repoRoot: scaffold.repoRoot });
868
- if (compiled.outcome !== 'dispatchable') fail(`serial条件がdispatchableにならない: ${compiled.code}`);
869
- const { plan, manifests } = compiled;
870
- const packets = buildExecutorPackets({ plan, manifests });
871
- const { createScriptedExecutorAdapter } = await import('./runtime-scripted-executor.mjs');
872
- const adapter = createScriptedExecutorAdapter({
873
- script: {
874
- TA: [{
875
- kind: 'hold_request',
876
- finding: {
877
- kind: 'semantic_conflict_unknown',
878
- todo_ids: ['TA', 'TB'],
879
- resource_id: 'discovered-shared-state',
880
- },
881
- }],
882
- TB: [{ kind: 'stall' }],
883
- TC: [{ kind: 'terminal' }],
884
- },
885
- });
886
- let events = initializeRunEvents({ runId, request, plan, manifests, recordedAt: RUN_TIMESTAMP });
887
- const dispatched = await dispatchReadyFrontier({
888
- runId, plan, events, packets, manifests, adapter, recordedAt: RUN_TIMESTAMP,
889
- });
890
- events = dispatched.events;
891
- // TCはfreeze前に完走・受理される。
892
- const tcTerminal = await observeExecutor({ runId, plan, events, adapter, todoId: 'TC', recordedAt: RUN_TIMESTAMP });
893
- events = tcTerminal.events;
894
- const tcAccepted = adjudicatePendingReceipts({ runId, plan, events, recordedAt: RUN_TIMESTAMP });
895
- events = tcAccepted.events;
896
- // TAが実行中にshared stateを発見して通報→conflict_found+intake_frozen。
897
- const held0 = await observeExecutor({ runId, plan, events, adapter, todoId: 'TA', recordedAt: RUN_TIMESTAMP });
898
- events = held0.events;
899
- const held = decideHoldAndCarryOver({
900
- runId, request, plan, manifests, packets, events, recordedAt: RUN_TIMESTAMP,
901
- });
902
- events = held.events;
903
- const lane = routeConflictTreatment({
904
- finding: held.holdDecision.finding,
905
- predeclaredTreatments: [],
906
- });
907
- const recompiled = recompileNextEpochPlan({
908
- runId, request, plan, manifests, packets, events,
909
- holdDecision: held.holdDecision,
910
- additionalConflicts: [{ todo_ids: ['TA', 'TB'], resource_id: 'discovered-shared-state' }],
911
- recordedAt: RUN_TIMESTAMP,
912
- });
913
- events = recompiled.events;
914
- const newPlan = recompiled.newPlan;
915
- events = activateScriptedRecompile({
916
- runId,
917
- events,
918
- plan: newPlan,
919
- planDiff: recompiled.planDiff,
920
- rebindPackets: recompiled.rebindPackets,
921
- });
922
- // vN+1でserial redispatch(conflict pairは同時に載らない)。
923
- const epoch2Adapter = createScriptedExecutorAdapter({
924
- script: { TA: [{ kind: 'terminal' }], TB: [{ kind: 'terminal' }] },
925
- });
926
- for (let round = 0; round < 6; round += 1) {
927
- const redispatched = await dispatchReadyFrontier({
928
- runId, plan: newPlan, events, packets: recompiled.redispatchPackets,
929
- manifests, adapter: epoch2Adapter, recordedAt: RUN_TIMESTAMP,
930
- });
931
- events = redispatched.events;
932
- const state = projectRuntimeState({ events });
933
- for (const todoId of state.running) {
934
- const observed = await observeExecutor({
935
- runId, plan: newPlan, events, adapter: epoch2Adapter, todoId, recordedAt: RUN_TIMESTAMP,
936
- });
937
- events = observed.events;
938
- }
939
- const adjudicated = adjudicatePendingReceipts({ runId, plan: newPlan, events, recordedAt: RUN_TIMESTAMP });
940
- events = adjudicated.events;
941
- const closed = closeRunIfComplete({ runId, plan: newPlan, events, recordedAt: RUN_TIMESTAMP });
942
- events = closed.events;
943
- if (closed.closed) break;
944
- }
945
- const state = projectRuntimeState({ events });
946
- const taAccepted2 = events.find((e) => (
947
- e.kind === 'receipt_accepted' && e.subject.ref === 'TA' && e.plan_epoch === 2
948
- ));
949
- const tbDispatch2 = events.find((e) => (
950
- e.kind === 'executor_dispatched' && e.subject.ref === 'TB' && e.plan_epoch === 2
951
- ));
952
- return {
953
- request,
954
- plan,
955
- manifests,
956
- newPlan,
957
- planDiff: recompiled.planDiff,
958
- holdDecision: held.holdDecision,
959
- events,
960
- record: conditionRecord({
961
- condition: 'irreducible_conflict',
962
- expected: {
963
- lane: 'intentional_serial',
964
- hold: ['TA', 'TB'],
965
- recompiled_with_conflict: true,
966
- precedence_not_faked: true,
967
- serialized_in_new_epoch: true,
968
- accepted: ['TA', 'TB', 'TC'],
969
- closed: true,
970
- },
971
- actual: {
972
- lane: lane.lane,
973
- hold: held.holdDecision.hold_set,
974
- recompiled_with_conflict: newPlan.conflicts.some(
975
- (conflict) => conflict.resource_id === 'discovered-shared-state',
976
- ),
977
- precedence_not_faked: newPlan.precedence.length === plan.precedence.length,
978
- serialized_in_new_epoch: taAccepted2 !== undefined && tbDispatch2 !== undefined
979
- && taAccepted2.sequence < tbDispatch2.sequence,
980
- accepted: state.accepted,
981
- closed: state.closed,
982
- },
983
- events,
984
- plan,
985
- }),
986
- };
987
- }
988
-
989
- async function runEventCorruption({ cleanResult }) {
990
- const { events } = cleanResult;
991
- const variants = {};
992
- // 改竄: payload書換え(digest不一致)。
993
- const tampered = structuredClone(events);
994
- tampered[2].payload = { ...tampered[2].payload, injected: true };
995
- variants.tamper = { events: tampered, expected_failed: ['event_digest_mismatch'] };
996
- // 順序欠落。
997
- const gapped = structuredClone(events);
998
- gapped.splice(3, 1);
999
- variants.gap = { events: gapped, expected_failed: ['sequence_gap', 'digest_chain_mismatch'] };
1000
- // fork: 同一sequenceに別内容。
1001
- const forked = structuredClone(events);
1002
- const forkedEvent = structuredClone(forked[4]);
1003
- forkedEvent.payload = { ...forkedEvent.payload, fork: true };
1004
- forkedEvent.event_digest = selfDigest(forkedEvent, 'event_digest');
1005
- forked.splice(5, 0, forkedEvent);
1006
- variants.fork = { events: forked, expected_failed: ['sequence_fork', 'storage_order'] };
1007
- // 重複: 同一eventの再挿入。
1008
- const duplicated = structuredClone(events);
1009
- duplicated.splice(5, 0, structuredClone(duplicated[4]));
1010
- variants.duplicate = { events: duplicated, expected_failed: ['duplicate_event', 'storage_order'] };
1011
- // 未知kind。
1012
- const unknownKind = structuredClone(events);
1013
- unknownKind[3] = { ...unknownKind[3], kind: 'mystery_event' };
1014
- unknownKind[3].event_digest = selfDigest(unknownKind[3], 'event_digest');
1015
- variants.unknown_kind = {
1016
- events: unknownKind,
1017
- expected_failed: ['unknown_kind', 'digest_chain_mismatch'],
1018
- };
1019
-
1020
- const results = {};
1021
- for (const [name, variant] of Object.entries(variants)) {
1022
- const verification = verifyRunEventChain({ events: variant.events });
1023
- results[name] = {
1024
- valid: verification.valid,
1025
- failed_conditions: verification.failed_conditions,
1026
- expected_failed: variant.expected_failed,
1027
- typed_match: variant.expected_failed.every(
1028
- (condition) => verification.failed_conditions.includes(condition),
1029
- ),
1030
- };
1031
- }
1032
- const record = {
1033
- schema: 'lattice.rc3.campaign_condition.v1',
1034
- condition: 'event_corruption',
1035
- expected: { all_rejected: true, all_typed: true },
1036
- actual: {
1037
- all_rejected: Object.values(results).every(({ valid }) => valid === false),
1038
- all_typed: Object.values(results).every(({ typed_match: match }) => match === true),
1039
- },
1040
- matched: false,
1041
- event_chain_valid: true,
1042
- dispatch_replay_matched: true,
1043
- event_count: 0,
1044
- events_digest: digestArtifact([]),
1045
- corruption_results: results,
1046
- corrupted_source_events_digest: digestArtifact(events.map(({ event_digest: digest }) => digest)),
1047
- };
1048
- record.matched = record.actual.all_rejected && record.actual.all_typed;
1049
- if (!record.matched) fail(`corruption条件がtyped rejectされない: ${JSON.stringify(results)}`);
1050
- // 変異bytes自体をartifactへ保存し、verifierが保存bytesから再判定できるようにする。
1051
- return { record, corruptionVariants: variants };
1052
- }
1053
-
1054
- /** artifactを保存bytesだけから再検証する(summary値を信じない)。 */
1055
- const EXPECTED_CONDITIONS = Object.freeze([
1056
- 'clean_parallel',
1057
- 'late_path_conflict',
1058
- 'scope_violation',
1059
- 'semantic_unknown',
1060
- 'stale_receipt',
1061
- 'irreducible_conflict',
1062
- 'accepted_seam',
1063
- 'event_corruption',
1064
- ]);
1065
-
1066
- function containedRelativePath(value) {
1067
- return typeof value === 'string'
1068
- && value.length > 0
1069
- && !value.startsWith('/')
1070
- && !value.includes('..')
1071
- && !value.includes('\\');
1072
- }
1073
-
1074
- export async function verifyRc3CampaignArtifactsOnDisk({ artifactRoot }) {
1075
- const manifest = JSON.parse(await readFile(path.join(artifactRoot, 'campaign-manifest.json'), 'utf8'));
1076
- const checks = [];
1077
- // manifest自体の完全性: schemaと、8条件の固定順・非重複・過不足なし。
1078
- checks.push({
1079
- id: 'manifest:schema',
1080
- passed: manifest?.schema === 'lattice.rc3.campaign_manifest.v1',
1081
- });
1082
- checks.push({
1083
- id: 'manifest:conditions_exact',
1084
- passed: Array.isArray(manifest?.conditions)
1085
- && JSON.stringify(manifest.conditions.map(({ condition }) => condition))
1086
- === JSON.stringify(EXPECTED_CONDITIONS),
1087
- });
1088
- const entries = Array.isArray(manifest?.conditions) ? manifest.conditions : [];
1089
- for (const entry of entries) {
1090
- const paths = [entry.record_path, entry.events_path, entry.plan_path,
1091
- entry.manifests_path, entry.hold_decision_path, entry.corruption_variants_path]
1092
- .filter((value) => value !== null && value !== undefined);
1093
- if (!paths.every(containedRelativePath)) {
1094
- checks.push({ id: `${entry.condition}:path_containment`, passed: false });
1095
- continue;
1096
- }
1097
- const record = JSON.parse(await readFile(
1098
- path.join(artifactRoot, entry.record_path), 'utf8',
1099
- ));
1100
- checks.push({ id: `${entry.condition}:record_digest`, passed: digestArtifact(record) === entry.record_digest });
1101
- // 期待/実測の再比較(保存されたmatchedを信じない)。
1102
- checks.push({
1103
- id: `${entry.condition}:expected_actual_recomputed`,
1104
- passed: record.matched === true
1105
- && JSON.stringify(record.expected) === JSON.stringify(record.actual),
1106
- });
1107
- if (entry.corruption_variants_path !== undefined && entry.corruption_variants_path !== null) {
1108
- // 保存済み変異bytesから改竄拒否を再判定する。
1109
- const variants = JSON.parse(await readFile(
1110
- path.join(artifactRoot, entry.corruption_variants_path), 'utf8',
1111
- ));
1112
- for (const [name, variant] of Object.entries(variants)) {
1113
- const verification = verifyRunEventChain({ events: variant.events });
1114
- checks.push({
1115
- id: `${entry.condition}:${name}_rejected`,
1116
- passed: verification.valid === false
1117
- && variant.expected_failed.every(
1118
- (condition) => verification.failed_conditions.includes(condition),
1119
- ),
1120
- });
1121
- }
1122
- }
1123
- if (entry.events_path === null) {
1124
- checks.push({ id: `${entry.condition}:non_dispatchable`, passed: record.event_count === 0 });
1125
- continue;
1126
- }
1127
- const events = JSON.parse(await readFile(path.join(artifactRoot, entry.events_path), 'utf8'));
1128
- const chain = verifyRunEventChain({ events });
1129
- checks.push({ id: `${entry.condition}:event_chain`, passed: chain.valid });
1130
- checks.push({
1131
- id: `${entry.condition}:events_digest`,
1132
- passed: digestArtifact(events.map(({ event_digest: digest }) => digest)) === record.events_digest,
1133
- });
1134
- const plan = JSON.parse(await readFile(path.join(artifactRoot, entry.plan_path), 'utf8'));
1135
- const replay = decisionReplayChecks({ plan, events });
1136
- checks.push({ id: `${entry.condition}:dispatch_replay`, passed: replay.every(({ match }) => match) });
1137
- // record.actualの主要主張をevents自体から再計算して照合する(expected/actual/
1138
- // matched/record_digestを整合的に書き換えるcoordinated改竄の遮断)。
1139
- {
1140
- const stateFromEvents = projectRuntimeState({ events });
1141
- const actual = record.actual ?? {};
1142
- const claims = [];
1143
- if (Object.hasOwn(actual, 'hold')) {
1144
- claims.push(JSON.stringify(actual.hold)
1145
- === JSON.stringify([...new Set(stateFromEvents.holds.flatMap((hold) => hold.hold_set ?? []))].sort()));
1146
- }
1147
- if (Object.hasOwn(actual, 'accepted')) {
1148
- claims.push(JSON.stringify(actual.accepted) === JSON.stringify(stateFromEvents.accepted));
1149
- }
1150
- if (Object.hasOwn(actual, 'closed')) {
1151
- claims.push(actual.closed === stateFromEvents.closed);
1152
- }
1153
- if (Object.hasOwn(actual, 'carried_over')) {
1154
- claims.push(JSON.stringify(actual.carried_over)
1155
- === JSON.stringify(Object.keys(stateFromEvents.rebinds).sort()));
1156
- }
1157
- checks.push({
1158
- id: `${entry.condition}:actual_recomputed_from_events`,
1159
- passed: claims.every((claim) => claim === true),
1160
- });
1161
- }
1162
- // receipt裁定の再計算: accepted集合の完全一致(片側包含にしない)。
1163
- const receiptCheck = recomputeReceiptDecisions({ plan, events });
1164
- const acceptedFromEvents = [...new Set(projectRuntimeState({ events }).accepted)]
1165
- .filter((todoId) => plan.nodes.some((node) => node.todo_id === todoId)
1166
- && events.some((event) => (
1167
- event.kind === 'receipt_accepted'
1168
- && event.subject?.ref === todoId
1169
- && event.plan_epoch === plan.plan_epoch
1170
- )))
1171
- .sort();
1172
- const acceptedFromDecisions = [...new Set(receiptCheck.decisions
1173
- .filter(({ decision }) => decision === 'accepted')
1174
- .map(({ todo_id: todoId }) => todoId))].sort();
1175
- checks.push({
1176
- id: `${entry.condition}:receipt_replay`,
1177
- passed: acceptedFromEvents.every((todoId) => acceptedFromDecisions.includes(todoId))
1178
- && acceptedFromDecisions.every((todoId) => (
1179
- projectRuntimeState({ events }).accepted.includes(todoId)
1180
- )),
1181
- });
1182
- if (entry.hold_decision_path !== null) {
1183
- const holdDecision = JSON.parse(await readFile(path.join(artifactRoot, entry.hold_decision_path), 'utf8'));
1184
- // hold裁定はhold_decidedの直前prefix(witnessまで含む)から再計算する。
1185
- const holdIndex = events.findIndex((event) => event.kind === 'hold_decided');
1186
- const recomputed = recomputeHoldDecision({
1187
- plan,
1188
- events: events.slice(0, holdIndex),
1189
- manifests: JSON.parse(await readFile(path.join(artifactRoot, entry.manifests_path), 'utf8')),
1190
- });
1191
- checks.push({
1192
- id: `${entry.condition}:hold_replay`,
1193
- passed: JSON.stringify(recomputed.hold_set) === JSON.stringify(holdDecision.hold_set)
1194
- && JSON.stringify(recomputed.continue_set) === JSON.stringify(holdDecision.continue_set)
1195
- && recomputed.finding?.kind === holdDecision.finding?.kind
1196
- // affected closureはhold集合に必ず含まれる(partition整合の最低条件)。
1197
- && holdDecision.affected_closure.every(
1198
- (todoId) => recomputed.hold_set.includes(todoId),
1199
- ),
1200
- });
1201
- }
1202
- }
1203
- const failed = checks.filter(({ passed }) => !passed).map(({ id }) => id);
1204
- return {
1205
- schema: 'lattice.rc3.campaign_artifact_verification.v1',
1206
- valid: failed.length === 0,
1207
- checks,
1208
- failed_conditions: failed,
1209
- };
1210
- }
1211
-
1212
- async function writeArtifacts({ artifactRoot, scaffold, results, costs = [] }) {
1213
- // stagingはartifactRootと同一filesystem(親directory配下)に置く(EXDEV回避)。
1214
- // 排他はstagingのexclusive mkdirで取り、発行はrename一回で行う。
1215
- await mkdir(path.dirname(artifactRoot), { recursive: true });
1216
- const staging = `${artifactRoot}.staging`;
1217
- try {
1218
- await mkdir(staging);
1219
- } catch (error) {
1220
- fail(`staging directoryを排他作成できない(並行発行または残骸): ${String(error?.message ?? error)}`);
1221
- }
1222
- const conditions = [];
1223
- for (const result of results) {
1224
- const dir = result.record.condition;
1225
- await mkdir(path.join(staging, dir), { recursive: true });
1226
- const entry = {
1227
- condition: result.record.condition,
1228
- record_path: `${dir}/condition-record.json`,
1229
- record_digest: null,
1230
- events_path: null,
1231
- plan_path: null,
1232
- manifests_path: null,
1233
- hold_decision_path: null,
1234
- };
1235
- await writeFile(path.join(staging, entry.record_path), `${JSON.stringify(result.record, null, 1)}\n`);
1236
- entry.record_digest = digestArtifact(result.record);
1237
- if (result.events !== undefined) {
1238
- entry.events_path = `${dir}/events.json`;
1239
- await writeFile(path.join(staging, entry.events_path), `${JSON.stringify(result.events, null, 1)}\n`);
1240
- entry.plan_path = `${dir}/plan.json`;
1241
- await writeFile(path.join(staging, entry.plan_path), `${JSON.stringify(result.plan, null, 1)}\n`);
1242
- if (result.manifests !== undefined) {
1243
- entry.manifests_path = `${dir}/manifests.json`;
1244
- await writeFile(path.join(staging, entry.manifests_path), `${JSON.stringify(result.manifests, null, 1)}\n`);
1245
- }
1246
- if (result.holdDecision !== undefined) {
1247
- entry.hold_decision_path = `${dir}/hold-decision.json`;
1248
- await writeFile(path.join(staging, entry.hold_decision_path), `${JSON.stringify(result.holdDecision, null, 1)}\n`);
1249
- }
1250
- if (result.planDiff !== undefined) {
1251
- await writeFile(path.join(staging, `${dir}/plan-diff.json`), `${JSON.stringify(result.planDiff, null, 1)}\n`);
1252
- }
1253
- if (result.newPlan !== undefined) {
1254
- await writeFile(path.join(staging, `${dir}/new-plan.json`), `${JSON.stringify(result.newPlan, null, 1)}\n`);
1255
- }
1256
- }
1257
- if (result.request !== undefined) {
1258
- await writeFile(path.join(staging, `${dir}/request.json`), `${JSON.stringify(result.request, null, 1)}\n`);
1259
- }
1260
- if (result.seamExtras !== undefined) {
1261
- await writeFile(
1262
- path.join(staging, `${dir}/transform-artifact.json`),
1263
- `${JSON.stringify(result.seamExtras.transformArtifact, null, 1)}\n`,
1264
- );
1265
- await writeFile(
1266
- path.join(staging, `${dir}/pre-plan.json`),
1267
- `${JSON.stringify(result.seamExtras.prePlan, null, 1)}\n`,
1268
- );
1269
- await writeFile(
1270
- path.join(staging, `${dir}/post-plan.json`),
1271
- `${JSON.stringify(result.seamExtras.postPlan, null, 1)}\n`,
1272
- );
1273
- // patch bytes(source級payload)はidentity/配下だけに置く(Decision 10.3)。
1274
- await mkdir(path.join(staging, 'identity'), { recursive: true });
1275
- await writeFile(path.join(staging, 'identity/accepted-seam.patch'), result.seamExtras.patchBytes);
1276
- }
1277
- if (result.corruptionVariants !== undefined) {
1278
- entry.corruption_variants_path = `${dir}/corruption-variants.json`;
1279
- await writeFile(
1280
- path.join(staging, entry.corruption_variants_path),
1281
- `${JSON.stringify(
1282
- Object.fromEntries(Object.entries(result.corruptionVariants).map(([name, variant]) => (
1283
- [name, { events: variant.events, expected_failed: variant.expected_failed }]
1284
- ))),
1285
- null,
1286
- 1,
1287
- )}\n`,
1288
- );
1289
- }
1290
- conditions.push(entry);
1291
- }
1292
- const campaignManifest = {
1293
- schema: 'lattice.rc3.campaign_manifest.v1',
1294
- cost_observations: costs,
1295
- scaffold: {
1296
- lattice_source_base_sha: scaffold.lattice_source.base_sha,
1297
- target_base_sha: scaffold.target.base_sha,
1298
- predeclared_treatment: scaffold.predeclared_treatment,
1299
- path_digests: scaffold.target.path_digests,
1300
- },
1301
- conditions,
1302
- };
1303
- await writeFile(
1304
- path.join(staging, 'campaign-manifest.json'),
1305
- `${JSON.stringify(campaignManifest, null, 1)}\n`,
1306
- );
1307
- // atomic no-overwrite発行: 目標rootの不在を確認してからrename一回。
1308
- // 失敗時はstagingを残さない(残骸掃除を試み、掃除失敗はmessageへ載せる)。
1309
- try {
1310
- let exists = true;
1311
- try {
1312
- await readFile(path.join(artifactRoot, 'campaign-manifest.json'));
1313
- } catch (error) {
1314
- exists = error?.code !== 'ENOENT' ? true : false;
1315
- }
1316
- if (exists) throw new TypeError('既存artifact rootが存在する');
1317
- await rename(staging, artifactRoot);
1318
- } catch (error) {
1319
- let residual = '';
1320
- try {
1321
- const { rm } = await import('node:fs/promises');
1322
- await rm(staging, { recursive: true, force: true });
1323
- } catch (cleanupError) {
1324
- residual = `(staging残骸: ${staging}: ${String(cleanupError?.message ?? cleanupError)})`;
1325
- }
1326
- fail(`artifact rootへのatomic発行に失敗(既存rootの上書きは禁止): ${String(error?.message ?? error)}${residual}`);
1327
- }
1328
- return campaignManifest;
1329
- }
1330
-
1331
- /**
1332
- * 8条件scripted campaignを実行し、immutable artifact rootへ発行して
1333
- * artifact-only verificationまで行う。
1334
- */
1335
- export async function runRc3ScriptedCampaign(options = {}) {
1336
- const { latticeRoot, artifactRoot } = options;
1337
- if (typeof latticeRoot !== 'string' || latticeRoot.length === 0
1338
- || typeof artifactRoot !== 'string' || artifactRoot.length === 0) {
1339
- fail('latticeRoot/artifactRootが必要');
1340
- }
1341
-
1342
- const workRoot = await mkdtemp(path.join(tmpdir(), 'lattice-rc3h-'));
1343
- const scaffold = await scaffoldRc3DogfoodRepo({ latticeRoot, workRoot });
1344
- const scaffoldVerified = await verifyRc3DogfoodScaffold({
1345
- latticeRoot, repoRoot: scaffold.repoRoot, expected: scaffold,
1346
- });
1347
- if (scaffoldVerified.outcome !== 'verified') {
1348
- fail(`scaffold検証が失敗: ${JSON.stringify(scaffoldVerified.violations)}`);
1349
- }
1350
-
1351
- // cost観測: 各条件の実wall-clockを保存する(未実測を0へ丸めない)。
1352
- const costs = [];
1353
- const timed = async (label, thunk) => {
1354
- const started = performance.now();
1355
- const value = await thunk();
1356
- costs.push({ condition: label, elapsed_ms: Math.round((performance.now() - started) * 1000) / 1000 });
1357
- return value;
1358
- };
1359
- const clean = await timed('clean_parallel', () => runCleanParallel({ scaffold }));
1360
- const late = await timed('late_path_conflict', () => runLateConflict({ scaffold }));
1361
- // 条件名`scope_violation`はRC3 manifestのdirectory名として凍結されているので動かさない。
1362
- // 中では単独の予測超過を観測し、conflictへ昇格しない現契約を検証する。
1363
- const scope = await timed('scope_violation', () => runScopeViolation({ scaffold }));
1364
- const semantic = await timed('semantic_unknown', () => runSemanticUnknown({ scaffold }));
1365
- const stale = await timed('stale_receipt', () => runStaleReceipt({ scaffold }));
1366
- const serial = await timed('irreducible_conflict', () => runIrreducibleConflict({ scaffold }));
1367
- const seam = await timed('accepted_seam', () => runAcceptedSeam({ latticeRoot }));
1368
- const corruption = await timed('event_corruption', () => runEventCorruption({ cleanResult: clean }));
1369
-
1370
- const results = [clean, late, scope, semantic, stale, serial, seam, corruption];
1371
- const campaignManifest = await writeArtifacts({ artifactRoot, scaffold, results, costs });
1372
- const verification = await verifyRc3CampaignArtifactsOnDisk({ artifactRoot });
1373
- if (!verification.valid) {
1374
- fail(`artifact-only verificationが失敗: ${JSON.stringify(verification.failed_conditions)}`);
1375
- }
1376
- return {
1377
- scaffold,
1378
- campaignManifest,
1379
- verification,
1380
- conditions: results.map(({ record }) => record.condition),
1381
- scope_expanded: results.flatMap((result) => result.scope_expanded ?? []),
1382
- };
1383
- }
1
+ import { mkdir, mkdtemp, readFile, rename, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ import { digestArtifact } from './artifact-contracts.mjs';
6
+ import { collectSensorEvidence } from './sensor-adapter.mjs';
7
+ import { spawnSensorCliSync } from './sensor-runtime.mjs';
8
+ import { scaffoldRc3DogfoodRepo, verifyRc3DogfoodScaffold } from './rc3-dogfood-scaffold.mjs';
9
+ import { compileRuntimePlanV1, evidenceFromCollectedOutcomes } from './runtime-front-end.mjs';
10
+ import {
11
+ adjudicatePendingReceipts,
12
+ buildNextRunEvent,
13
+ buildExecutorPackets,
14
+ classifyCheckpointObservation,
15
+ closeRunIfComplete,
16
+ dispatchReadyFrontier,
17
+ initializeRunEvents,
18
+ observeExecutor,
19
+ } from './runtime-engine.mjs';
20
+ import {
21
+ decideHoldAndCarryOver,
22
+ recompileNextEpochPlan,
23
+ routeConflictTreatment,
24
+ } from './runtime-hold-recompile.mjs';
25
+ import { detectCheckpointFindings } from './runtime-diff-observer.mjs';
26
+ import { createWorktreeExecutorAdapter } from './runtime-worktree-executor.mjs';
27
+ import {
28
+ computeReadyFrontier,
29
+ recomputeHoldDecision,
30
+ recomputeReceiptDecisions,
31
+ } from './runtime-decision-verifier.mjs';
32
+ import { verifyRunEventChain } from './runtime-event-store.mjs';
33
+ import { projectRuntimeState } from './runtime-projection.mjs';
34
+ import { selfDigest } from './runtime-contracts.mjs';
35
+
36
+ /**
37
+ * RC3-H scripted closed-loop campaign(plan RC3-H、ADR 0044 Decision 10・11)。
38
+ *
39
+ * Lattice-owned dogfood scaffoldから条件ごとにdisposable repoを作り、正解集合が
40
+ * 既知の8条件を同一base(同一fixture bytes)・同一runtime identityで実行する。
41
+ * 各条件でevents・plan・hold・receipt・期待/実測比較を保存し、summary値を信じず
42
+ * artifact-only verifierが保存bytesから全decisionを再計算する。
43
+ *
44
+ * 条件(plan「Primary: scripted late-conflict campaign」の8行):
45
+ * clean_parallel / late_path_conflict / scope_violation / semantic_unknown /
46
+ * stale_receipt / irreducible_conflict / accepted_seam / event_corruption
47
+ */
48
+
49
+ const RUN_TIMESTAMP = '2026-07-17T00:00:00.000Z';
50
+ // dogfood scaffoldの実在3 file(LatticeSensor観測がreadyになる対象)をTODO所有面にする。
51
+ const DOC_A = 'research/fixtures/delivery-policy-registry/src/delivery-policy-registry.mjs';
52
+ const DOC_B = 'src/rc2-delivery-policy-oracle.mjs';
53
+ const DOC_C = 'test/rc2-delivery-policy-fixture.test.mjs';
54
+ const MAX_SCRIPTED_REBINDS = 64;
55
+
56
+ function fail(reason) {
57
+ throw new TypeError(`rc3 campaign契約違反: ${reason}`);
58
+ }
59
+
60
+ function docWitness(todoId, docPath, affectedTests, { extraWrites = [], unknowns = [], stateEffects = [] } = {}) {
61
+ return {
62
+ owns: [{ kind: 'path', target: docPath }],
63
+ reads: [],
64
+ writes: [docPath, ...extraWrites],
65
+ resources: stateEffects.map(({ resource_id: id }) => id),
66
+ state_effects: stateEffects,
67
+ sensor_provenance: {
68
+ queries: [{ query_id: `q-aff-${todoId}`, expect: { kind: 'affected', path: docPath } }],
69
+ },
70
+ affected_tests: affectedTests,
71
+ unknowns,
72
+ };
73
+ }
74
+
75
+ /**
76
+ * manual witnessのaffected test宣言をfresh LatticeSensor観測から導出する
77
+ * (宣言と観測のexact一致契約の下で、正解宣言を機械的に得る)。
78
+ */
79
+ async function probeAffectedTests(repoRoot, targets) {
80
+ const querySet = {
81
+ queries: targets.map((target, index) => ({ id: `probe-${index}`, operation: 'affected', target })),
82
+ };
83
+ const collected = await collectSensorEvidence({ cwd: repoRoot, querySet });
84
+ const byTarget = {};
85
+ collected.outcomes.forEach((outcome, index) => {
86
+ const entry = Array.isArray(outcome.targets) ? outcome.targets[0] : null;
87
+ byTarget[targets[index]] = Array.isArray(entry?.data?.affectedTests)
88
+ ? [...entry.data.affectedTests].sort()
89
+ : [];
90
+ });
91
+ return byTarget;
92
+ }
93
+
94
+ /**
95
+ * 8条件のうちscaffold上の6条件は同一TODO集合(TA/TB/TC)・同一capacity・
96
+ * 同一query構造のrequest templateを使い、条件の注入(unknowns・state_effects・
97
+ * 実write)だけを可変にする(plan「同一base、同一request、同一runtime identity」の
98
+ * 運用解釈。字義どおりの単一request bytesは注入設計と両立しないため、
99
+ * templateの同一性とbase/identityの同一性で担保し、evidenceに解釈を記録する)。
100
+ */
101
+ function buildDocRequest({ requestId, baseSha, todos, capacity }) {
102
+ const queries = [{ id: 'q-status', operation: 'status' }];
103
+ for (const [todoId, witness] of Object.entries(todos)) {
104
+ void witness;
105
+ queries.push({
106
+ id: `q-aff-${todoId}`,
107
+ operation: 'affected',
108
+ target: todos[todoId].owns[0].target,
109
+ });
110
+ }
111
+ const request = {
112
+ schema: 'lattice.run_request.v1',
113
+ request_id: requestId,
114
+ repo: { base_sha: baseSha, root_kind: 'git-worktree' },
115
+ capacity: { executors: capacity },
116
+ todos: Object.keys(todos).map((todoId) => ({ todo_id: todoId })),
117
+ manual_witness: todos,
118
+ sensor_query_set: { queries },
119
+ executor_capability: { adapters: ['isolated-worktree'] },
120
+ claim_mode: 'exact_minimum',
121
+ };
122
+ request.request_digest = selfDigest(request, 'request_digest');
123
+ return request;
124
+ }
125
+
126
+ async function compileForRepo({ request, repoRoot }) {
127
+ const collected = await collectSensorEvidence({
128
+ cwd: repoRoot,
129
+ querySet: request.sensor_query_set,
130
+ });
131
+ const sensorEvidence = evidenceFromCollectedOutcomes({
132
+ querySet: request.sensor_query_set,
133
+ collected,
134
+ });
135
+ return compileRuntimePlanV1({
136
+ request,
137
+ sensorEvidence,
138
+ planRef: `plan-${request.request_id}-e1`,
139
+ planEpoch: 1,
140
+ predecessorRefs: [],
141
+ });
142
+ }
143
+
144
+ function docWriter(docPath, body) {
145
+ return async ({ worktreePath }) => {
146
+ await mkdir(path.dirname(path.join(worktreePath, docPath)), { recursive: true });
147
+ await writeFile(path.join(worktreePath, docPath), body);
148
+ };
149
+ }
150
+
151
+ /** 全running executorを1回ずつ観測し、checkpointがあれば分類する共通driver。 */
152
+ async function observeAndClassifyAll({ runId, plan, events, packets, manifests, adapter }) {
153
+ let next = events;
154
+ const scopeExpanded = [];
155
+ const state = projectRuntimeState({ events: next });
156
+ for (const todoId of state.running) {
157
+ const observed = await observeExecutor({
158
+ runId, plan, events: next, adapter, todoId, recordedAt: RUN_TIMESTAMP,
159
+ });
160
+ next = observed.events;
161
+ if (observed.observation.state === 'checkpoint_ready') {
162
+ const classified = classifyCheckpointObservation({
163
+ runId, plan, events: next, packets, manifests, todoId,
164
+ detect: detectCheckpointFindings, recordedAt: RUN_TIMESTAMP,
165
+ });
166
+ next = classified.events;
167
+ scopeExpanded.push(...classified.scope_expanded);
168
+ if (classified.findings.length > 0) {
169
+ return { events: next, frozen: true, scope_expanded: scopeExpanded };
170
+ }
171
+ }
172
+ }
173
+ return { events: next, frozen: false, scope_expanded: scopeExpanded };
174
+ }
175
+
176
+ /** 競合なし前提でclosed loopまで回す(clean/irreducible串行の完走用)。 */
177
+ async function driveToClose({ runId, plan, events, packets, manifests, adapter, maxRounds = 16 }) {
178
+ let next = events;
179
+ const scopeExpanded = [];
180
+ for (let round = 0; round < maxRounds; round += 1) {
181
+ const dispatched = await dispatchReadyFrontier({
182
+ runId, plan, events: next, packets, manifests, adapter, recordedAt: RUN_TIMESTAMP,
183
+ });
184
+ if (dispatched.failure) fail(`dispatch失敗: ${dispatched.failure.message}`);
185
+ next = dispatched.events;
186
+ const observed = await observeAndClassifyAll({
187
+ runId, plan, events: next, packets, manifests, adapter,
188
+ });
189
+ if (observed.frozen) fail('競合なし条件でfreezeが発生した');
190
+ next = observed.events;
191
+ scopeExpanded.push(...observed.scope_expanded);
192
+ const adjudicated = adjudicatePendingReceipts({ runId, plan, events: next, recordedAt: RUN_TIMESTAMP });
193
+ next = adjudicated.events;
194
+ const closed = closeRunIfComplete({ runId, plan, events: next, recordedAt: RUN_TIMESTAMP });
195
+ next = closed.events;
196
+ if (closed.closed) return { events: next, scope_expanded: scopeExpanded };
197
+ }
198
+ fail('closed loopがmaxRounds内に完走しない');
199
+ return null;
200
+ }
201
+
202
+ /**
203
+ * legacy scripted campaign内だけで、controller rebindとactivation gateの完了を模擬する。
204
+ * productionのACK/gate検証を迂回する入口ではなく、既にscriptで完了させたactivationを
205
+ * event contractへ投影するための有界fixture helperである。
206
+ */
207
+ function activateScriptedRecompile({ runId, events, plan, planDiff, rebindPackets }) {
208
+ const entries = Object.entries(rebindPackets).sort(([left], [right]) => left.localeCompare(right));
209
+ if (entries.length > MAX_SCRIPTED_REBINDS) {
210
+ fail(`scripted rebind数が上限を超えた: ${entries.length}`);
211
+ }
212
+ let next = [...events];
213
+ for (const [todoId, packet] of entries) {
214
+ if (packet?.todo_id !== todoId || packet?.new_plan_epoch !== plan.plan_epoch) {
215
+ fail(`scripted rebind packetの帰属が不正: ${todoId}`);
216
+ }
217
+ next.push(buildNextRunEvent({
218
+ events: next,
219
+ runId,
220
+ kind: 'epoch_rebound',
221
+ planEpoch: plan.plan_epoch,
222
+ subject: { kind: 'todo', ref: todoId },
223
+ payload: packet,
224
+ recordedAt: RUN_TIMESTAMP,
225
+ }));
226
+ }
227
+ next.push(buildNextRunEvent({
228
+ events: next,
229
+ runId,
230
+ kind: 'intake_resumed',
231
+ planEpoch: plan.plan_epoch,
232
+ subject: { kind: 'runtime_plan', ref: plan.plan_ref },
233
+ payload: { plan_diff_digest: planDiff.diff_digest },
234
+ recordedAt: RUN_TIMESTAMP,
235
+ }));
236
+ return next;
237
+ }
238
+
239
+ function decisionReplayChecks({ plan, events }) {
240
+ const checks = [];
241
+ events.forEach((event, index) => {
242
+ if (event.kind !== 'dispatch_decided') return;
243
+ // dispatch decisionはそのdecisionが属するepochのplanでだけ再計算できる
244
+ //(frontier規則はepoch-scoped)。別epochのdecisionは当該epochのplanで検証する。
245
+ if (event.plan_epoch !== plan.plan_epoch) return;
246
+ const recomputed = computeReadyFrontier({ plan, events: events.slice(0, index) });
247
+ checks.push({
248
+ sequence: event.sequence,
249
+ kind: 'dispatch_decided',
250
+ match: JSON.stringify(event.payload.dispatchable) === JSON.stringify(recomputed.dispatchable),
251
+ });
252
+ });
253
+ return checks;
254
+ }
255
+
256
+ /** 条件結果recordを組み立てる(期待と実測、および再計算一致)。 */
257
+ function conditionRecord({ condition, expected, actual, events, plan }) {
258
+ const chain = verifyRunEventChain({ events });
259
+ const replay = decisionReplayChecks({ plan, events });
260
+ const record = {
261
+ schema: 'lattice.rc3.campaign_condition.v1',
262
+ condition,
263
+ expected,
264
+ actual,
265
+ matched: JSON.stringify(expected) === JSON.stringify(actual),
266
+ event_chain_valid: chain.valid,
267
+ dispatch_replay_matched: replay.every(({ match }) => match),
268
+ event_count: events.length,
269
+ events_digest: digestArtifact(events.map(({ event_digest: digest }) => digest)),
270
+ };
271
+ if (!record.matched) fail(`条件${condition}の期待/実測が一致しない: ${JSON.stringify({ expected, actual })}`);
272
+ if (!record.event_chain_valid) fail(`条件${condition}のevent chainが不正`);
273
+ if (!record.dispatch_replay_matched) fail(`条件${condition}のdispatch replayが一致しない`);
274
+ return record;
275
+ }
276
+
277
+ async function runCleanParallel({ scaffold }) {
278
+ const runId = 'rc3h-clean';
279
+ const affected = await probeAffectedTests(scaffold.repoRoot, [DOC_A, DOC_B, DOC_C]);
280
+ const request = buildDocRequest({
281
+ requestId: 'rc3h-clean',
282
+ baseSha: scaffold.target.base_sha,
283
+ capacity: 3,
284
+ todos: {
285
+ TA: docWitness('TA', DOC_A, affected[DOC_A]),
286
+ TB: docWitness('TB', DOC_B, affected[DOC_B]),
287
+ TC: docWitness('TC', DOC_C, affected[DOC_C]),
288
+ },
289
+ });
290
+ const compiled = await compileForRepo({ request, repoRoot: scaffold.repoRoot });
291
+ if (compiled.outcome !== 'dispatchable') fail(`clean条件がdispatchableにならない: ${compiled.code}`);
292
+ const { plan, manifests } = compiled;
293
+ const packets = buildExecutorPackets({ plan, manifests });
294
+ const adapter = createWorktreeExecutorAdapter({
295
+ repoRoot: scaffold.repoRoot,
296
+ work: {
297
+ TA: docWriter(DOC_A, 'alpha\n'),
298
+ TB: docWriter(DOC_B, 'beta\n'),
299
+ TC: docWriter(DOC_C, 'gamma\n'),
300
+ },
301
+ });
302
+ let events = initializeRunEvents({ runId, request, plan, manifests, recordedAt: RUN_TIMESTAMP });
303
+ const driven = await driveToClose({ runId, plan, events, packets, manifests, adapter });
304
+ events = driven.events;
305
+ const state = projectRuntimeState({ events });
306
+ return {
307
+ request,
308
+ plan,
309
+ events,
310
+ scope_expanded: driven.scope_expanded,
311
+ record: conditionRecord({
312
+ condition: 'clean_parallel',
313
+ expected: { hold: [], continue: ['TA', 'TB', 'TC'], accepted: ['TA', 'TB', 'TC'], closed: true },
314
+ actual: {
315
+ hold: state.holds.flatMap((hold) => hold.hold_set ?? []),
316
+ continue: state.accepted,
317
+ accepted: state.accepted,
318
+ closed: state.closed,
319
+ },
320
+ events,
321
+ plan,
322
+ }),
323
+ };
324
+ }
325
+
326
+ async function runLateConflict({ scaffold }) {
327
+ const runId = 'rc3h-late';
328
+ const affected = await probeAffectedTests(scaffold.repoRoot, [DOC_A, DOC_B, DOC_C]);
329
+ const request = buildDocRequest({
330
+ requestId: 'rc3h-late',
331
+ baseSha: scaffold.target.base_sha,
332
+ capacity: 3,
333
+ todos: {
334
+ TA: docWitness('TA', DOC_A, affected[DOC_A]),
335
+ TB: docWitness('TB', DOC_B, affected[DOC_B]),
336
+ TC: docWitness('TC', DOC_C, affected[DOC_C]),
337
+ },
338
+ });
339
+ const compiled = await compileForRepo({ request, repoRoot: scaffold.repoRoot });
340
+ if (compiled.outcome !== 'dispatchable') fail(`late条件がdispatchableにならない: ${compiled.code}`);
341
+ const { plan, manifests } = compiled;
342
+ const packets = buildExecutorPackets({ plan, manifests });
343
+ const adapter = createWorktreeExecutorAdapter({
344
+ repoRoot: scaffold.repoRoot,
345
+ work: {
346
+ // TAが宣言外にTBのdeclared write(DOC_B)へも書く=後発path conflict。
347
+ TA: async ({ worktreePath }) => {
348
+ await docWriter(DOC_A, 'alpha\n')({ worktreePath });
349
+ await docWriter(DOC_B, 'rogue write\n')({ worktreePath });
350
+ },
351
+ TB: docWriter(DOC_B, 'beta\n'),
352
+ TC: docWriter(DOC_C, 'gamma\n'),
353
+ },
354
+ });
355
+ let events = initializeRunEvents({ runId, request, plan, manifests, recordedAt: RUN_TIMESTAMP });
356
+ const dispatched = await dispatchReadyFrontier({
357
+ runId, plan, events, packets, manifests, adapter, recordedAt: RUN_TIMESTAMP,
358
+ });
359
+ events = dispatched.events;
360
+ // TCを先に観測(正常checkpoint)、次にTA(競合露呈→freeze)。
361
+ for (const todoId of ['TC', 'TA']) {
362
+ const observed = await observeExecutor({ runId, plan, events, adapter, todoId, recordedAt: RUN_TIMESTAMP });
363
+ events = observed.events;
364
+ const classified = classifyCheckpointObservation({
365
+ runId, plan, events, packets, manifests, todoId,
366
+ detect: detectCheckpointFindings, recordedAt: RUN_TIMESTAMP,
367
+ });
368
+ events = classified.events;
369
+ }
370
+ const held = decideHoldAndCarryOver({
371
+ runId, request, plan, manifests, packets, events, recordedAt: RUN_TIMESTAMP,
372
+ });
373
+ events = held.events;
374
+ const lane = routeConflictTreatment({
375
+ finding: held.holdDecision.finding,
376
+ predeclaredTreatments: [],
377
+ });
378
+ const recompiled = recompileNextEpochPlan({
379
+ runId, request, plan, manifests, packets, events,
380
+ holdDecision: held.holdDecision,
381
+ additionalConflicts: [{ todo_ids: ['TA', 'TB'], resource_id: 'late-doc-b' }],
382
+ recordedAt: RUN_TIMESTAMP,
383
+ });
384
+ events = recompiled.events;
385
+ // 継続TODO(TC)のoutputをvN+1へbind: rebindを適用し、terminal→受理まで進める
386
+ // (成功条件12「無関係TODOの継続にはcarry-over witnessが必須」の完走証明)。
387
+ await adapter.rebind({
388
+ executor_handle: projectRuntimeState({ events }).dispatches.TC.payload.executor_handle,
389
+ rebind: recompiled.rebindPackets.TC,
390
+ });
391
+ events = activateScriptedRecompile({
392
+ runId,
393
+ events,
394
+ plan: recompiled.newPlan,
395
+ planDiff: recompiled.planDiff,
396
+ rebindPackets: recompiled.rebindPackets,
397
+ });
398
+ {
399
+ const tcTerminal = await observeExecutor({
400
+ runId, plan: recompiled.newPlan, events, adapter, todoId: 'TC', recordedAt: RUN_TIMESTAMP,
401
+ });
402
+ events = tcTerminal.events;
403
+ const adjudicated = adjudicatePendingReceipts({
404
+ runId, plan: recompiled.newPlan, events, recordedAt: RUN_TIMESTAMP,
405
+ });
406
+ events = adjudicated.events;
407
+ }
408
+ const state = projectRuntimeState({ events });
409
+ return {
410
+ request,
411
+ plan,
412
+ manifests,
413
+ newPlan: recompiled.newPlan,
414
+ planDiff: recompiled.planDiff,
415
+ events,
416
+ record: conditionRecord({
417
+ condition: 'late_path_conflict',
418
+ expected: {
419
+ hold: ['TA', 'TB'],
420
+ continue: ['TC'],
421
+ lane: 'intentional_serial',
422
+ carried_over: ['TC'],
423
+ redispatched: ['TA', 'TB'],
424
+ new_epoch: 2,
425
+ carried_output_accepted: true,
426
+ },
427
+ actual: {
428
+ hold: held.holdDecision.hold_set,
429
+ continue: held.holdDecision.continue_set,
430
+ lane: lane.lane,
431
+ carried_over: recompiled.planDiff.carried_over,
432
+ redispatched: recompiled.planDiff.redispatched,
433
+ new_epoch: recompiled.newPlan.plan_epoch,
434
+ carried_output_accepted: state.accepted.includes('TC'),
435
+ },
436
+ events,
437
+ plan,
438
+ }),
439
+ holdDecision: held.holdDecision,
440
+ stateAfter: state,
441
+ };
442
+ }
443
+
444
+ async function runScopeViolation({ scaffold }) {
445
+ const runId = 'rc3h-scope';
446
+ const affected = await probeAffectedTests(scaffold.repoRoot, [DOC_A, DOC_B]);
447
+ const request = buildDocRequest({
448
+ requestId: 'rc3h-scope',
449
+ baseSha: scaffold.target.base_sha,
450
+ capacity: 2,
451
+ todos: {
452
+ TA: docWitness('TA', DOC_A, affected[DOC_A]),
453
+ TB: docWitness('TB', DOC_B, affected[DOC_B]),
454
+ },
455
+ });
456
+ const compiled = await compileForRepo({ request, repoRoot: scaffold.repoRoot });
457
+ if (compiled.outcome !== 'dispatchable') fail(`scope条件がdispatchableにならない: ${compiled.code}`);
458
+ const { plan, manifests } = compiled;
459
+ const packets = buildExecutorPackets({ plan, manifests });
460
+ const adapter = createWorktreeExecutorAdapter({
461
+ repoRoot: scaffold.repoRoot,
462
+ work: {
463
+ TA: async ({ worktreePath }) => {
464
+ await docWriter(DOC_A, 'alpha\n')({ worktreePath });
465
+ await docWriter('docs/rogue.md', 'undeclared\n')({ worktreePath });
466
+ },
467
+ TB: docWriter(DOC_B, 'beta\n'),
468
+ },
469
+ });
470
+ let events = initializeRunEvents({ runId, request, plan, manifests, recordedAt: RUN_TIMESTAMP });
471
+ const dispatched = await dispatchReadyFrontier({
472
+ runId, plan, events, packets, manifests, adapter, recordedAt: RUN_TIMESTAMP,
473
+ });
474
+ events = dispatched.events;
475
+ const observed = await observeExecutor({ runId, plan, events, adapter, todoId: 'TA', recordedAt: RUN_TIMESTAMP });
476
+ events = observed.events;
477
+ const classified = classifyCheckpointObservation({
478
+ runId, plan, events, packets, manifests, todoId: 'TA',
479
+ detect: detectCheckpointFindings, recordedAt: RUN_TIMESTAMP,
480
+ });
481
+ events = classified.events;
482
+ const driven = await driveToClose({ runId, plan, events, packets, manifests, adapter });
483
+ events = driven.events;
484
+ const scopeExpanded = [...classified.scope_expanded, ...driven.scope_expanded];
485
+ const state = projectRuntimeState({ events });
486
+ return {
487
+ request,
488
+ plan,
489
+ manifests,
490
+ events,
491
+ scope_expanded: scopeExpanded,
492
+ record: conditionRecord({
493
+ condition: 'scope_violation',
494
+ // directory名はRC3 artifact互換のため維持する。現契約では単独の予測超過は
495
+ // conflictではなく、観測を残したまま有効なreceiptを受理する。
496
+ expected: {
497
+ observation_kinds: ['prediction_excess'],
498
+ scope_expanded: [{
499
+ task_id: 'TA',
500
+ compared_witness_digest: null,
501
+ first_seen_path_count: 1,
502
+ path_count: 2,
503
+ added_paths: ['docs/rogue.md'],
504
+ removed_paths: [],
505
+ growth_events: 1,
506
+ gate_shape: false,
507
+ }],
508
+ conflict_finding_kinds: [],
509
+ frozen: false,
510
+ accepted: ['TA', 'TB'],
511
+ closed: true,
512
+ },
513
+ actual: {
514
+ observation_kinds: [...new Set(classified.observations.map(({ kind }) => kind))],
515
+ scope_expanded: scopeExpanded,
516
+ conflict_finding_kinds: [...new Set(classified.findings.map(({ kind }) => kind))],
517
+ frozen: state.freeze !== null,
518
+ accepted: state.accepted,
519
+ closed: state.closed,
520
+ },
521
+ events,
522
+ plan,
523
+ }),
524
+ };
525
+ }
526
+
527
+ async function runSemanticUnknown({ scaffold }) {
528
+ const affected = await probeAffectedTests(scaffold.repoRoot, [DOC_A, DOC_B]);
529
+ const request = buildDocRequest({
530
+ requestId: 'rc3h-semantic',
531
+ baseSha: scaffold.target.base_sha,
532
+ capacity: 2,
533
+ todos: {
534
+ TA: docWitness('TA', DOC_A, affected[DOC_A], {
535
+ unknowns: [{ kind: 'semantic_probe', ref: 'shared delivery-policy invariant?' }],
536
+ }),
537
+ TB: docWitness('TB', DOC_B, affected[DOC_B]),
538
+ },
539
+ });
540
+ const compiled = await compileForRepo({ request, repoRoot: scaffold.repoRoot });
541
+ // drift実効性の対照: 固定の誤ったaffected宣言はAFFECTED_TEST_DRIFTになる
542
+ //(probe由来宣言がdrift検査を無意味化していないことの証明)。
543
+ const driftRequest = buildDocRequest({
544
+ requestId: 'rc3h-semantic-drift',
545
+ baseSha: scaffold.target.base_sha,
546
+ capacity: 2,
547
+ todos: {
548
+ TA: docWitness('TA', DOC_A, ['test/fixed-wrong-declaration.test.mjs']),
549
+ TB: docWitness('TB', DOC_B, affected[DOC_B]),
550
+ },
551
+ });
552
+ const driftCompiled = await compileForRepo({ request: driftRequest, repoRoot: scaffold.repoRoot });
553
+ const record = {
554
+ schema: 'lattice.rc3.campaign_condition.v1',
555
+ condition: 'semantic_unknown',
556
+ expected: {
557
+ outcome: 'non_dispatchable',
558
+ code: 'BOUNDARY_UNKNOWN',
559
+ drift_control: { outcome: 'non_dispatchable', code: 'AFFECTED_TEST_DRIFT' },
560
+ },
561
+ actual: {
562
+ outcome: compiled.outcome,
563
+ code: compiled.code ?? null,
564
+ drift_control: { outcome: driftCompiled.outcome, code: driftCompiled.code ?? null },
565
+ },
566
+ matched: compiled.outcome === 'non_dispatchable' && compiled.code === 'BOUNDARY_UNKNOWN'
567
+ && driftCompiled.outcome === 'non_dispatchable' && driftCompiled.code === 'AFFECTED_TEST_DRIFT',
568
+ event_chain_valid: true,
569
+ dispatch_replay_matched: true,
570
+ event_count: 0,
571
+ events_digest: digestArtifact([]),
572
+ };
573
+ if (!record.matched) fail(`semantic unknown条件が期待どおりでない: ${JSON.stringify(record.actual)}`);
574
+ return { request, record, nonDispatchable: { outcome: compiled.outcome, code: compiled.code } };
575
+ }
576
+
577
+ async function runStaleReceipt({ scaffold }) {
578
+ // late conflictの流れでvN+1まで進め、rebindなしの旧epoch receipt
579
+ // (held TODOの旧executor由来)をvN+1へ提出してtyped rejectを確認する。
580
+ const late = await runLateConflict({ scaffold });
581
+ const { buildNextRunEvent } = await import('./runtime-engine.mjs');
582
+ const packets = buildExecutorPackets({ plan: late.plan, manifests: late.manifests });
583
+ const state = late.stateAfter;
584
+ const dispatchTB = state.dispatches.TB;
585
+ const staleReceipt = {
586
+ schema: 'lattice.executor_receipt.v1',
587
+ receipt_id: 'TB-stale-r1',
588
+ executor_handle: dispatchTB.payload.executor_handle,
589
+ worktree_id: dispatchTB.payload.worktree_id,
590
+ base_sha: late.plan.base_sha,
591
+ plan_epoch: late.plan.plan_epoch,
592
+ packet_digest: packets.TB.packet_digest,
593
+ todo_id: 'TB',
594
+ checkpoint_digest: '1'.repeat(64),
595
+ observed_diff: [{ path: DOC_B, change: 'modified' }],
596
+ };
597
+ staleReceipt.receipt_digest = selfDigest(staleReceipt, 'receipt_digest');
598
+ let events = [...late.events];
599
+ events.push(buildNextRunEvent({
600
+ events,
601
+ runId: 'rc3h-late',
602
+ kind: 'receipt_recorded',
603
+ planEpoch: staleReceipt.plan_epoch,
604
+ subject: { kind: 'todo', ref: 'TB' },
605
+ payload: staleReceipt,
606
+ recordedAt: RUN_TIMESTAMP,
607
+ }));
608
+ const acceptedBefore = projectRuntimeState({ events: late.events }).accepted;
609
+ const adjudicated = adjudicatePendingReceipts({
610
+ runId: 'rc3h-late', plan: late.newPlan, events, recordedAt: RUN_TIMESTAMP,
611
+ });
612
+ events = adjudicated.events;
613
+ const decision = adjudicated.decisions.find(({ receipt_id: id }) => id === 'TB-stale-r1');
614
+ const recomputed = recomputeReceiptDecisions({ plan: late.newPlan, events })
615
+ .decisions.find(({ receipt_id: id }) => id === 'TB-stale-r1');
616
+ const acceptedAfter = projectRuntimeState({ events }).accepted;
617
+ const acceptedDelta = acceptedAfter.filter((todoId) => !acceptedBefore.includes(todoId));
618
+ const record = {
619
+ schema: 'lattice.rc3.campaign_condition.v1',
620
+ condition: 'stale_receipt',
621
+ expected: {
622
+ decision: 'rejected',
623
+ reason: 'stale_context',
624
+ verifier_decision: 'rejected',
625
+ accepted_outputs_from_stale: [],
626
+ },
627
+ actual: {
628
+ decision: decision?.decision ?? null,
629
+ reason: 'stale_context',
630
+ verifier_decision: recomputed?.decision ?? null,
631
+ accepted_outputs_from_stale: acceptedDelta,
632
+ },
633
+ matched: decision?.decision === 'rejected' && recomputed?.decision === 'rejected'
634
+ && acceptedDelta.length === 0,
635
+ event_chain_valid: verifyRunEventChain({ events }).valid,
636
+ dispatch_replay_matched: true,
637
+ event_count: events.length,
638
+ events_digest: digestArtifact(events.map(({ event_digest: digest }) => digest)),
639
+ };
640
+ if (!record.matched) fail(`stale receipt条件が期待どおりでない: ${JSON.stringify(record.actual)}`);
641
+ return {
642
+ request: late.request,
643
+ plan: late.newPlan,
644
+ manifests: late.manifests,
645
+ events,
646
+ record,
647
+ };
648
+ }
649
+
650
+ async function runAcceptedSeam({ latticeRoot }) {
651
+ // 独立したscaffold上でpredeclared seam treatmentを実行し、accepted後に
652
+ // fresh reindex→conflict減少→new plan発行を確認する(ADR 0044 Decision 11.3)。
653
+ const workRoot = await mkdtemp(path.join(tmpdir(), 'lattice-rc3h-seam-'));
654
+ const scaffold = await scaffoldRc3DogfoodRepo({ latticeRoot, workRoot });
655
+ const entryPath = 'research/fixtures/delivery-policy-registry/src/delivery-policy-registry.mjs';
656
+
657
+ // pre-seam: 2 TODOが同じfixture entryを所有→write conflict→2 waves。
658
+ const preAffected = await probeAffectedTests(scaffold.repoRoot, [entryPath]);
659
+ const preWitness = () => ({
660
+ owns: [
661
+ { kind: 'symbol', target: 'resolveDeliveryPolicy' },
662
+ { kind: 'path', target: entryPath },
663
+ ],
664
+ reads: [],
665
+ writes: [entryPath],
666
+ resources: [],
667
+ state_effects: [],
668
+ sensor_provenance: {
669
+ queries: [
670
+ { query_id: 'q-entry', expect: { kind: 'symbol', name: 'resolveDeliveryPolicy', path: entryPath } },
671
+ { query_id: 'q-aff-entry', expect: { kind: 'affected', path: entryPath } },
672
+ ],
673
+ },
674
+ affected_tests: preAffected[entryPath],
675
+ unknowns: [],
676
+ });
677
+ const preRequest = {
678
+ schema: 'lattice.run_request.v1',
679
+ request_id: 'rc3h-seam-pre',
680
+ repo: { base_sha: scaffold.target.base_sha, root_kind: 'git-worktree' },
681
+ capacity: { executors: 2 },
682
+ todos: [{ todo_id: 'Temail' }, { todo_id: 'Tsms' }],
683
+ manual_witness: { Temail: preWitness(), Tsms: preWitness() },
684
+ sensor_query_set: {
685
+ queries: [
686
+ { id: 'q-status', operation: 'status' },
687
+ { id: 'q-entry', operation: 'query', target: 'resolveDeliveryPolicy' },
688
+ { id: 'q-aff-entry', operation: 'affected', target: entryPath },
689
+ ],
690
+ },
691
+ executor_capability: { adapters: ['isolated-worktree'] },
692
+ claim_mode: 'exact_minimum',
693
+ };
694
+ preRequest.request_digest = selfDigest(preRequest, 'request_digest');
695
+ const preCompiled = await compileForRepo({ request: preRequest, repoRoot: scaffold.repoRoot });
696
+ if (preCompiled.outcome !== 'dispatchable') fail(`seam pre条件がdispatchableにならない: ${preCompiled.code}`);
697
+ const preWaves = preCompiled.schedule.minimum_feasible_waves;
698
+ const preConflicts = preCompiled.plan.conflicts.length;
699
+
700
+ // predeclared treatmentのroute判定とRC2無改変adapterでの実行。
701
+ const candidateSpec = JSON.parse(await readFile(
702
+ path.join(latticeRoot, 'research/campaigns/rc2/inputs/candidate-spec-v1.json'), 'utf8',
703
+ ));
704
+ const lane = routeConflictTreatment({
705
+ finding: { kind: 'observed_write_conflict', todo_ids: ['Temail', 'Tsms'], path: entryPath },
706
+ predeclaredTreatments: [{
707
+ treatment_id: scaffold.predeclared_treatment.epoch,
708
+ covered_paths: [entryPath],
709
+ candidate_digest: scaffold.predeclared_treatment.candidate_digest,
710
+ }],
711
+ });
712
+ if (lane.lane !== 'seam_transform') fail('predeclared treatmentがseam laneへroutingされない');
713
+
714
+ const { runRc2DeliveryPolicySeamTransform } = await import('./rc2-delivery-policy-transform.mjs');
715
+ const transformResult = await runRc2DeliveryPolicySeamTransform({
716
+ repoRoot: scaffold.repoRoot,
717
+ baseRef: scaffold.target.base_sha,
718
+ candidateSpec,
719
+ });
720
+ if (transformResult.artifact.status !== 'accepted') {
721
+ fail(`seam transformがacceptedにならない: ${JSON.stringify(transformResult.artifact.rejection)}`);
722
+ }
723
+
724
+ // accepted patchを新baseとしてcommitし、fresh reindex後にper-channel witnessで再compile。
725
+ const { spawnSync } = await import('node:child_process');
726
+ const runGit = (args) => {
727
+ const result = spawnSync('git', args, { cwd: scaffold.repoRoot, encoding: 'utf8' });
728
+ if (result.status !== 0) fail(`git ${args[0]} failed: ${result.stderr}`);
729
+ return result.stdout;
730
+ };
731
+ const patchPath = path.join(workRoot, 'accepted-seam.patch');
732
+ await writeFile(patchPath, transformResult.patch);
733
+ runGit(['apply', '--binary', patchPath]);
734
+ runGit(['-c', 'user.email=rc3h@lattice.invalid', '-c', 'user.name=rc3h', 'add', '.']);
735
+ runGit(['-c', 'user.email=rc3h@lattice.invalid', '-c', 'user.name=rc3h', 'commit', '--quiet', '-m', 'accepted seam']);
736
+ const newBase = runGit(['rev-parse', 'HEAD']).trim();
737
+ // fresh reindex(accepted seam後は必ず再index。syncで新規fileを収載する)。
738
+ const syncResult = spawnSensorCliSync(['sync', '.'], { cwd: scaffold.repoRoot, encoding: 'utf8' });
739
+ if (syncResult.status !== 0) fail(`sensor reindex(sync)が失敗: ${syncResult.stderr}`);
740
+
741
+ const emailPath = 'research/fixtures/delivery-policy-registry/src/email-policy.mjs';
742
+ const smsPath = 'research/fixtures/delivery-policy-registry/src/sms-policy.mjs';
743
+ const postAffected = await probeAffectedTests(scaffold.repoRoot, [emailPath, smsPath]);
744
+ const postWitness = (todoId, symbol, filePath) => ({
745
+ owns: [{ kind: 'symbol', target: symbol }, { kind: 'path', target: filePath }],
746
+ reads: [],
747
+ writes: [filePath],
748
+ resources: [],
749
+ state_effects: [],
750
+ sensor_provenance: {
751
+ queries: [
752
+ { query_id: `q-${todoId}`, expect: { kind: 'symbol', name: symbol, path: filePath } },
753
+ { query_id: `q-aff-${todoId}`, expect: { kind: 'affected', path: filePath } },
754
+ ],
755
+ },
756
+ affected_tests: postAffected[filePath],
757
+ unknowns: [],
758
+ });
759
+ const postRequest = {
760
+ schema: 'lattice.run_request.v1',
761
+ request_id: 'rc3h-seam-post',
762
+ repo: { base_sha: newBase, root_kind: 'git-worktree' },
763
+ capacity: { executors: 2 },
764
+ todos: [{ todo_id: 'Temail' }, { todo_id: 'Tsms' }],
765
+ manual_witness: {
766
+ Temail: postWitness('Temail', 'resolveEmailPolicy', emailPath),
767
+ Tsms: postWitness('Tsms', 'resolveSmsPolicy', smsPath),
768
+ },
769
+ sensor_query_set: {
770
+ queries: [
771
+ { id: 'q-status', operation: 'status' },
772
+ { id: 'q-Temail', operation: 'query', target: 'resolveEmailPolicy' },
773
+ { id: 'q-aff-Temail', operation: 'affected', target: emailPath },
774
+ { id: 'q-Tsms', operation: 'query', target: 'resolveSmsPolicy' },
775
+ { id: 'q-aff-Tsms', operation: 'affected', target: smsPath },
776
+ ],
777
+ },
778
+ executor_capability: { adapters: ['isolated-worktree'] },
779
+ claim_mode: 'exact_minimum',
780
+ };
781
+ postRequest.request_digest = selfDigest(postRequest, 'request_digest');
782
+ // post-seam planはgenesisではなく、accepted transformとpre planをpredecessorに
783
+ // 持つepoch 2 planとしてcompileする(Decision 11.2/plan barrier)。
784
+ const transformArtifactDigest = digestArtifact(transformResult.artifact);
785
+ const postCollected = await collectSensorEvidence({
786
+ cwd: scaffold.repoRoot, querySet: postRequest.sensor_query_set,
787
+ });
788
+ const postCompiled = compileRuntimePlanV1({
789
+ request: postRequest,
790
+ sensorEvidence: evidenceFromCollectedOutcomes({
791
+ querySet: postRequest.sensor_query_set, collected: postCollected,
792
+ }),
793
+ planRef: `plan-${postRequest.request_id}-e2`,
794
+ planEpoch: 2,
795
+ predecessorRefs: [preCompiled.plan.plan_ref, `seam:${transformArtifactDigest}`],
796
+ });
797
+ if (postCompiled.outcome !== 'dispatchable') fail(`seam post条件がdispatchableにならない: ${postCompiled.code}`);
798
+
799
+ const record = {
800
+ schema: 'lattice.rc3.campaign_condition.v1',
801
+ condition: 'accepted_seam',
802
+ expected: {
803
+ lane: 'seam_transform',
804
+ transform_status: 'accepted',
805
+ conflict_reduced: true,
806
+ wave_reduced: true,
807
+ new_plan_issued: true,
808
+ },
809
+ actual: {
810
+ lane: lane.lane,
811
+ transform_status: transformResult.artifact.status,
812
+ conflict_reduced: postCompiled.plan.conflicts.length < preConflicts,
813
+ wave_reduced: postCompiled.schedule.minimum_feasible_waves < preWaves,
814
+ new_plan_issued: postCompiled.plan.plan_digest !== preCompiled.plan.plan_digest,
815
+ },
816
+ matched: false,
817
+ event_chain_valid: true,
818
+ dispatch_replay_matched: true,
819
+ event_count: 0,
820
+ events_digest: digestArtifact([]),
821
+ seam: {
822
+ pre: { conflicts: preConflicts, minimum_feasible_waves: preWaves, base_sha: scaffold.target.base_sha },
823
+ post: {
824
+ conflicts: postCompiled.plan.conflicts.length,
825
+ minimum_feasible_waves: postCompiled.schedule.minimum_feasible_waves,
826
+ base_sha: newBase,
827
+ },
828
+ transform_artifact_digest: transformArtifactDigest,
829
+ patch_digest: transformResult.artifact.patch.digest,
830
+ predecessor: scaffold.predeclared_treatment,
831
+ post_plan_predecessor_refs: postCompiled.plan.predecessor_refs,
832
+ namespaces: {
833
+ lattice_source: scaffold.lattice_source,
834
+ target: { root_kind: scaffold.target.root_kind, pre_base_sha: scaffold.target.base_sha, post_base_sha: newBase },
835
+ },
836
+ },
837
+ };
838
+ record.matched = JSON.stringify(record.expected) === JSON.stringify(record.actual);
839
+ if (!record.matched) fail(`accepted seam条件が期待どおりでない: ${JSON.stringify(record.actual)}`);
840
+ return {
841
+ request: postRequest,
842
+ record,
843
+ seamExtras: {
844
+ transformArtifact: transformResult.artifact,
845
+ patchBytes: transformResult.patch,
846
+ postPlan: postCompiled.plan,
847
+ prePlan: preCompiled.plan,
848
+ },
849
+ };
850
+ }
851
+
852
+ async function runIrreducibleConflict({ scaffold }) {
853
+ // 実行中に発見されたseamなしshared state(semantic conflict)を
854
+ // intentional serialへ再compileする(plan条件表の「再compile」読み)。
855
+ const runId = 'rc3h-serial';
856
+ const affected = await probeAffectedTests(scaffold.repoRoot, [DOC_A, DOC_B, DOC_C]);
857
+ const request = buildDocRequest({
858
+ requestId: 'rc3h-serial',
859
+ baseSha: scaffold.target.base_sha,
860
+ capacity: 3,
861
+ todos: {
862
+ TA: docWitness('TA', DOC_A, affected[DOC_A]),
863
+ TB: docWitness('TB', DOC_B, affected[DOC_B]),
864
+ TC: docWitness('TC', DOC_C, affected[DOC_C]),
865
+ },
866
+ });
867
+ const compiled = await compileForRepo({ request, repoRoot: scaffold.repoRoot });
868
+ if (compiled.outcome !== 'dispatchable') fail(`serial条件がdispatchableにならない: ${compiled.code}`);
869
+ const { plan, manifests } = compiled;
870
+ const packets = buildExecutorPackets({ plan, manifests });
871
+ const { createScriptedExecutorAdapter } = await import('./runtime-scripted-executor.mjs');
872
+ const adapter = createScriptedExecutorAdapter({
873
+ script: {
874
+ TA: [{
875
+ kind: 'hold_request',
876
+ finding: {
877
+ kind: 'semantic_conflict_unknown',
878
+ todo_ids: ['TA', 'TB'],
879
+ resource_id: 'discovered-shared-state',
880
+ },
881
+ }],
882
+ TB: [{ kind: 'stall' }],
883
+ TC: [{ kind: 'terminal' }],
884
+ },
885
+ });
886
+ let events = initializeRunEvents({ runId, request, plan, manifests, recordedAt: RUN_TIMESTAMP });
887
+ const dispatched = await dispatchReadyFrontier({
888
+ runId, plan, events, packets, manifests, adapter, recordedAt: RUN_TIMESTAMP,
889
+ });
890
+ events = dispatched.events;
891
+ // TCはfreeze前に完走・受理される。
892
+ const tcTerminal = await observeExecutor({ runId, plan, events, adapter, todoId: 'TC', recordedAt: RUN_TIMESTAMP });
893
+ events = tcTerminal.events;
894
+ const tcAccepted = adjudicatePendingReceipts({ runId, plan, events, recordedAt: RUN_TIMESTAMP });
895
+ events = tcAccepted.events;
896
+ // TAが実行中にshared stateを発見して通報→conflict_found+intake_frozen。
897
+ const held0 = await observeExecutor({ runId, plan, events, adapter, todoId: 'TA', recordedAt: RUN_TIMESTAMP });
898
+ events = held0.events;
899
+ const held = decideHoldAndCarryOver({
900
+ runId, request, plan, manifests, packets, events, recordedAt: RUN_TIMESTAMP,
901
+ });
902
+ events = held.events;
903
+ const lane = routeConflictTreatment({
904
+ finding: held.holdDecision.finding,
905
+ predeclaredTreatments: [],
906
+ });
907
+ const recompiled = recompileNextEpochPlan({
908
+ runId, request, plan, manifests, packets, events,
909
+ holdDecision: held.holdDecision,
910
+ additionalConflicts: [{ todo_ids: ['TA', 'TB'], resource_id: 'discovered-shared-state' }],
911
+ recordedAt: RUN_TIMESTAMP,
912
+ });
913
+ events = recompiled.events;
914
+ const newPlan = recompiled.newPlan;
915
+ events = activateScriptedRecompile({
916
+ runId,
917
+ events,
918
+ plan: newPlan,
919
+ planDiff: recompiled.planDiff,
920
+ rebindPackets: recompiled.rebindPackets,
921
+ });
922
+ // vN+1でserial redispatch(conflict pairは同時に載らない)。
923
+ const epoch2Adapter = createScriptedExecutorAdapter({
924
+ script: { TA: [{ kind: 'terminal' }], TB: [{ kind: 'terminal' }] },
925
+ });
926
+ for (let round = 0; round < 6; round += 1) {
927
+ const redispatched = await dispatchReadyFrontier({
928
+ runId, plan: newPlan, events, packets: recompiled.redispatchPackets,
929
+ manifests, adapter: epoch2Adapter, recordedAt: RUN_TIMESTAMP,
930
+ });
931
+ events = redispatched.events;
932
+ const state = projectRuntimeState({ events });
933
+ for (const todoId of state.running) {
934
+ const observed = await observeExecutor({
935
+ runId, plan: newPlan, events, adapter: epoch2Adapter, todoId, recordedAt: RUN_TIMESTAMP,
936
+ });
937
+ events = observed.events;
938
+ }
939
+ const adjudicated = adjudicatePendingReceipts({ runId, plan: newPlan, events, recordedAt: RUN_TIMESTAMP });
940
+ events = adjudicated.events;
941
+ const closed = closeRunIfComplete({ runId, plan: newPlan, events, recordedAt: RUN_TIMESTAMP });
942
+ events = closed.events;
943
+ if (closed.closed) break;
944
+ }
945
+ const state = projectRuntimeState({ events });
946
+ const taAccepted2 = events.find((e) => (
947
+ e.kind === 'receipt_accepted' && e.subject.ref === 'TA' && e.plan_epoch === 2
948
+ ));
949
+ const tbDispatch2 = events.find((e) => (
950
+ e.kind === 'executor_dispatched' && e.subject.ref === 'TB' && e.plan_epoch === 2
951
+ ));
952
+ return {
953
+ request,
954
+ plan,
955
+ manifests,
956
+ newPlan,
957
+ planDiff: recompiled.planDiff,
958
+ holdDecision: held.holdDecision,
959
+ events,
960
+ record: conditionRecord({
961
+ condition: 'irreducible_conflict',
962
+ expected: {
963
+ lane: 'intentional_serial',
964
+ hold: ['TA', 'TB'],
965
+ recompiled_with_conflict: true,
966
+ precedence_not_faked: true,
967
+ serialized_in_new_epoch: true,
968
+ accepted: ['TA', 'TB', 'TC'],
969
+ closed: true,
970
+ },
971
+ actual: {
972
+ lane: lane.lane,
973
+ hold: held.holdDecision.hold_set,
974
+ recompiled_with_conflict: newPlan.conflicts.some(
975
+ (conflict) => conflict.resource_id === 'discovered-shared-state',
976
+ ),
977
+ precedence_not_faked: newPlan.precedence.length === plan.precedence.length,
978
+ serialized_in_new_epoch: taAccepted2 !== undefined && tbDispatch2 !== undefined
979
+ && taAccepted2.sequence < tbDispatch2.sequence,
980
+ accepted: state.accepted,
981
+ closed: state.closed,
982
+ },
983
+ events,
984
+ plan,
985
+ }),
986
+ };
987
+ }
988
+
989
+ async function runEventCorruption({ cleanResult }) {
990
+ const { events } = cleanResult;
991
+ const variants = {};
992
+ // 改竄: payload書換え(digest不一致)。
993
+ const tampered = structuredClone(events);
994
+ tampered[2].payload = { ...tampered[2].payload, injected: true };
995
+ variants.tamper = { events: tampered, expected_failed: ['event_digest_mismatch'] };
996
+ // 順序欠落。
997
+ const gapped = structuredClone(events);
998
+ gapped.splice(3, 1);
999
+ variants.gap = { events: gapped, expected_failed: ['sequence_gap', 'digest_chain_mismatch'] };
1000
+ // fork: 同一sequenceに別内容。
1001
+ const forked = structuredClone(events);
1002
+ const forkedEvent = structuredClone(forked[4]);
1003
+ forkedEvent.payload = { ...forkedEvent.payload, fork: true };
1004
+ forkedEvent.event_digest = selfDigest(forkedEvent, 'event_digest');
1005
+ forked.splice(5, 0, forkedEvent);
1006
+ variants.fork = { events: forked, expected_failed: ['sequence_fork', 'storage_order'] };
1007
+ // 重複: 同一eventの再挿入。
1008
+ const duplicated = structuredClone(events);
1009
+ duplicated.splice(5, 0, structuredClone(duplicated[4]));
1010
+ variants.duplicate = { events: duplicated, expected_failed: ['duplicate_event', 'storage_order'] };
1011
+ // 未知kind。
1012
+ const unknownKind = structuredClone(events);
1013
+ unknownKind[3] = { ...unknownKind[3], kind: 'mystery_event' };
1014
+ unknownKind[3].event_digest = selfDigest(unknownKind[3], 'event_digest');
1015
+ variants.unknown_kind = {
1016
+ events: unknownKind,
1017
+ expected_failed: ['unknown_kind', 'digest_chain_mismatch'],
1018
+ };
1019
+
1020
+ const results = {};
1021
+ for (const [name, variant] of Object.entries(variants)) {
1022
+ const verification = verifyRunEventChain({ events: variant.events });
1023
+ results[name] = {
1024
+ valid: verification.valid,
1025
+ failed_conditions: verification.failed_conditions,
1026
+ expected_failed: variant.expected_failed,
1027
+ typed_match: variant.expected_failed.every(
1028
+ (condition) => verification.failed_conditions.includes(condition),
1029
+ ),
1030
+ };
1031
+ }
1032
+ const record = {
1033
+ schema: 'lattice.rc3.campaign_condition.v1',
1034
+ condition: 'event_corruption',
1035
+ expected: { all_rejected: true, all_typed: true },
1036
+ actual: {
1037
+ all_rejected: Object.values(results).every(({ valid }) => valid === false),
1038
+ all_typed: Object.values(results).every(({ typed_match: match }) => match === true),
1039
+ },
1040
+ matched: false,
1041
+ event_chain_valid: true,
1042
+ dispatch_replay_matched: true,
1043
+ event_count: 0,
1044
+ events_digest: digestArtifact([]),
1045
+ corruption_results: results,
1046
+ corrupted_source_events_digest: digestArtifact(events.map(({ event_digest: digest }) => digest)),
1047
+ };
1048
+ record.matched = record.actual.all_rejected && record.actual.all_typed;
1049
+ if (!record.matched) fail(`corruption条件がtyped rejectされない: ${JSON.stringify(results)}`);
1050
+ // 変異bytes自体をartifactへ保存し、verifierが保存bytesから再判定できるようにする。
1051
+ return { record, corruptionVariants: variants };
1052
+ }
1053
+
1054
+ /** artifactを保存bytesだけから再検証する(summary値を信じない)。 */
1055
+ const EXPECTED_CONDITIONS = Object.freeze([
1056
+ 'clean_parallel',
1057
+ 'late_path_conflict',
1058
+ 'scope_violation',
1059
+ 'semantic_unknown',
1060
+ 'stale_receipt',
1061
+ 'irreducible_conflict',
1062
+ 'accepted_seam',
1063
+ 'event_corruption',
1064
+ ]);
1065
+
1066
+ function containedRelativePath(value) {
1067
+ return typeof value === 'string'
1068
+ && value.length > 0
1069
+ && !value.startsWith('/')
1070
+ && !value.includes('..')
1071
+ && !value.includes('\\');
1072
+ }
1073
+
1074
+ export async function verifyRc3CampaignArtifactsOnDisk({ artifactRoot }) {
1075
+ const manifest = JSON.parse(await readFile(path.join(artifactRoot, 'campaign-manifest.json'), 'utf8'));
1076
+ const checks = [];
1077
+ // manifest自体の完全性: schemaと、8条件の固定順・非重複・過不足なし。
1078
+ checks.push({
1079
+ id: 'manifest:schema',
1080
+ passed: manifest?.schema === 'lattice.rc3.campaign_manifest.v1',
1081
+ });
1082
+ checks.push({
1083
+ id: 'manifest:conditions_exact',
1084
+ passed: Array.isArray(manifest?.conditions)
1085
+ && JSON.stringify(manifest.conditions.map(({ condition }) => condition))
1086
+ === JSON.stringify(EXPECTED_CONDITIONS),
1087
+ });
1088
+ const entries = Array.isArray(manifest?.conditions) ? manifest.conditions : [];
1089
+ for (const entry of entries) {
1090
+ const paths = [entry.record_path, entry.events_path, entry.plan_path,
1091
+ entry.manifests_path, entry.hold_decision_path, entry.corruption_variants_path]
1092
+ .filter((value) => value !== null && value !== undefined);
1093
+ if (!paths.every(containedRelativePath)) {
1094
+ checks.push({ id: `${entry.condition}:path_containment`, passed: false });
1095
+ continue;
1096
+ }
1097
+ const record = JSON.parse(await readFile(
1098
+ path.join(artifactRoot, entry.record_path), 'utf8',
1099
+ ));
1100
+ checks.push({ id: `${entry.condition}:record_digest`, passed: digestArtifact(record) === entry.record_digest });
1101
+ // 期待/実測の再比較(保存されたmatchedを信じない)。
1102
+ checks.push({
1103
+ id: `${entry.condition}:expected_actual_recomputed`,
1104
+ passed: record.matched === true
1105
+ && JSON.stringify(record.expected) === JSON.stringify(record.actual),
1106
+ });
1107
+ if (entry.corruption_variants_path !== undefined && entry.corruption_variants_path !== null) {
1108
+ // 保存済み変異bytesから改竄拒否を再判定する。
1109
+ const variants = JSON.parse(await readFile(
1110
+ path.join(artifactRoot, entry.corruption_variants_path), 'utf8',
1111
+ ));
1112
+ for (const [name, variant] of Object.entries(variants)) {
1113
+ const verification = verifyRunEventChain({ events: variant.events });
1114
+ checks.push({
1115
+ id: `${entry.condition}:${name}_rejected`,
1116
+ passed: verification.valid === false
1117
+ && variant.expected_failed.every(
1118
+ (condition) => verification.failed_conditions.includes(condition),
1119
+ ),
1120
+ });
1121
+ }
1122
+ }
1123
+ if (entry.events_path === null) {
1124
+ checks.push({ id: `${entry.condition}:non_dispatchable`, passed: record.event_count === 0 });
1125
+ continue;
1126
+ }
1127
+ const events = JSON.parse(await readFile(path.join(artifactRoot, entry.events_path), 'utf8'));
1128
+ const chain = verifyRunEventChain({ events });
1129
+ checks.push({ id: `${entry.condition}:event_chain`, passed: chain.valid });
1130
+ checks.push({
1131
+ id: `${entry.condition}:events_digest`,
1132
+ passed: digestArtifact(events.map(({ event_digest: digest }) => digest)) === record.events_digest,
1133
+ });
1134
+ const plan = JSON.parse(await readFile(path.join(artifactRoot, entry.plan_path), 'utf8'));
1135
+ const replay = decisionReplayChecks({ plan, events });
1136
+ checks.push({ id: `${entry.condition}:dispatch_replay`, passed: replay.every(({ match }) => match) });
1137
+ // record.actualの主要主張をevents自体から再計算して照合する(expected/actual/
1138
+ // matched/record_digestを整合的に書き換えるcoordinated改竄の遮断)。
1139
+ {
1140
+ const stateFromEvents = projectRuntimeState({ events });
1141
+ const actual = record.actual ?? {};
1142
+ const claims = [];
1143
+ if (Object.hasOwn(actual, 'hold')) {
1144
+ claims.push(JSON.stringify(actual.hold)
1145
+ === JSON.stringify([...new Set(stateFromEvents.holds.flatMap((hold) => hold.hold_set ?? []))].sort()));
1146
+ }
1147
+ if (Object.hasOwn(actual, 'accepted')) {
1148
+ claims.push(JSON.stringify(actual.accepted) === JSON.stringify(stateFromEvents.accepted));
1149
+ }
1150
+ if (Object.hasOwn(actual, 'closed')) {
1151
+ claims.push(actual.closed === stateFromEvents.closed);
1152
+ }
1153
+ if (Object.hasOwn(actual, 'carried_over')) {
1154
+ claims.push(JSON.stringify(actual.carried_over)
1155
+ === JSON.stringify(Object.keys(stateFromEvents.rebinds).sort()));
1156
+ }
1157
+ checks.push({
1158
+ id: `${entry.condition}:actual_recomputed_from_events`,
1159
+ passed: claims.every((claim) => claim === true),
1160
+ });
1161
+ }
1162
+ // receipt裁定の再計算: accepted集合の完全一致(片側包含にしない)。
1163
+ const receiptCheck = recomputeReceiptDecisions({ plan, events });
1164
+ const acceptedFromEvents = [...new Set(projectRuntimeState({ events }).accepted)]
1165
+ .filter((todoId) => plan.nodes.some((node) => node.todo_id === todoId)
1166
+ && events.some((event) => (
1167
+ event.kind === 'receipt_accepted'
1168
+ && event.subject?.ref === todoId
1169
+ && event.plan_epoch === plan.plan_epoch
1170
+ )))
1171
+ .sort();
1172
+ const acceptedFromDecisions = [...new Set(receiptCheck.decisions
1173
+ .filter(({ decision }) => decision === 'accepted')
1174
+ .map(({ todo_id: todoId }) => todoId))].sort();
1175
+ checks.push({
1176
+ id: `${entry.condition}:receipt_replay`,
1177
+ passed: acceptedFromEvents.every((todoId) => acceptedFromDecisions.includes(todoId))
1178
+ && acceptedFromDecisions.every((todoId) => (
1179
+ projectRuntimeState({ events }).accepted.includes(todoId)
1180
+ )),
1181
+ });
1182
+ if (entry.hold_decision_path !== null) {
1183
+ const holdDecision = JSON.parse(await readFile(path.join(artifactRoot, entry.hold_decision_path), 'utf8'));
1184
+ // hold裁定はhold_decidedの直前prefix(witnessまで含む)から再計算する。
1185
+ const holdIndex = events.findIndex((event) => event.kind === 'hold_decided');
1186
+ const recomputed = recomputeHoldDecision({
1187
+ plan,
1188
+ events: events.slice(0, holdIndex),
1189
+ manifests: JSON.parse(await readFile(path.join(artifactRoot, entry.manifests_path), 'utf8')),
1190
+ });
1191
+ checks.push({
1192
+ id: `${entry.condition}:hold_replay`,
1193
+ passed: JSON.stringify(recomputed.hold_set) === JSON.stringify(holdDecision.hold_set)
1194
+ && JSON.stringify(recomputed.continue_set) === JSON.stringify(holdDecision.continue_set)
1195
+ && recomputed.finding?.kind === holdDecision.finding?.kind
1196
+ // affected closureはhold集合に必ず含まれる(partition整合の最低条件)。
1197
+ && holdDecision.affected_closure.every(
1198
+ (todoId) => recomputed.hold_set.includes(todoId),
1199
+ ),
1200
+ });
1201
+ }
1202
+ }
1203
+ const failed = checks.filter(({ passed }) => !passed).map(({ id }) => id);
1204
+ return {
1205
+ schema: 'lattice.rc3.campaign_artifact_verification.v1',
1206
+ valid: failed.length === 0,
1207
+ checks,
1208
+ failed_conditions: failed,
1209
+ };
1210
+ }
1211
+
1212
+ async function writeArtifacts({ artifactRoot, scaffold, results, costs = [] }) {
1213
+ // stagingはartifactRootと同一filesystem(親directory配下)に置く(EXDEV回避)。
1214
+ // 排他はstagingのexclusive mkdirで取り、発行はrename一回で行う。
1215
+ await mkdir(path.dirname(artifactRoot), { recursive: true });
1216
+ const staging = `${artifactRoot}.staging`;
1217
+ try {
1218
+ await mkdir(staging);
1219
+ } catch (error) {
1220
+ fail(`staging directoryを排他作成できない(並行発行または残骸): ${String(error?.message ?? error)}`);
1221
+ }
1222
+ const conditions = [];
1223
+ for (const result of results) {
1224
+ const dir = result.record.condition;
1225
+ await mkdir(path.join(staging, dir), { recursive: true });
1226
+ const entry = {
1227
+ condition: result.record.condition,
1228
+ record_path: `${dir}/condition-record.json`,
1229
+ record_digest: null,
1230
+ events_path: null,
1231
+ plan_path: null,
1232
+ manifests_path: null,
1233
+ hold_decision_path: null,
1234
+ };
1235
+ await writeFile(path.join(staging, entry.record_path), `${JSON.stringify(result.record, null, 1)}\n`);
1236
+ entry.record_digest = digestArtifact(result.record);
1237
+ if (result.events !== undefined) {
1238
+ entry.events_path = `${dir}/events.json`;
1239
+ await writeFile(path.join(staging, entry.events_path), `${JSON.stringify(result.events, null, 1)}\n`);
1240
+ entry.plan_path = `${dir}/plan.json`;
1241
+ await writeFile(path.join(staging, entry.plan_path), `${JSON.stringify(result.plan, null, 1)}\n`);
1242
+ if (result.manifests !== undefined) {
1243
+ entry.manifests_path = `${dir}/manifests.json`;
1244
+ await writeFile(path.join(staging, entry.manifests_path), `${JSON.stringify(result.manifests, null, 1)}\n`);
1245
+ }
1246
+ if (result.holdDecision !== undefined) {
1247
+ entry.hold_decision_path = `${dir}/hold-decision.json`;
1248
+ await writeFile(path.join(staging, entry.hold_decision_path), `${JSON.stringify(result.holdDecision, null, 1)}\n`);
1249
+ }
1250
+ if (result.planDiff !== undefined) {
1251
+ await writeFile(path.join(staging, `${dir}/plan-diff.json`), `${JSON.stringify(result.planDiff, null, 1)}\n`);
1252
+ }
1253
+ if (result.newPlan !== undefined) {
1254
+ await writeFile(path.join(staging, `${dir}/new-plan.json`), `${JSON.stringify(result.newPlan, null, 1)}\n`);
1255
+ }
1256
+ }
1257
+ if (result.request !== undefined) {
1258
+ await writeFile(path.join(staging, `${dir}/request.json`), `${JSON.stringify(result.request, null, 1)}\n`);
1259
+ }
1260
+ if (result.seamExtras !== undefined) {
1261
+ await writeFile(
1262
+ path.join(staging, `${dir}/transform-artifact.json`),
1263
+ `${JSON.stringify(result.seamExtras.transformArtifact, null, 1)}\n`,
1264
+ );
1265
+ await writeFile(
1266
+ path.join(staging, `${dir}/pre-plan.json`),
1267
+ `${JSON.stringify(result.seamExtras.prePlan, null, 1)}\n`,
1268
+ );
1269
+ await writeFile(
1270
+ path.join(staging, `${dir}/post-plan.json`),
1271
+ `${JSON.stringify(result.seamExtras.postPlan, null, 1)}\n`,
1272
+ );
1273
+ // patch bytes(source級payload)はidentity/配下だけに置く(Decision 10.3)。
1274
+ await mkdir(path.join(staging, 'identity'), { recursive: true });
1275
+ await writeFile(path.join(staging, 'identity/accepted-seam.patch'), result.seamExtras.patchBytes);
1276
+ }
1277
+ if (result.corruptionVariants !== undefined) {
1278
+ entry.corruption_variants_path = `${dir}/corruption-variants.json`;
1279
+ await writeFile(
1280
+ path.join(staging, entry.corruption_variants_path),
1281
+ `${JSON.stringify(
1282
+ Object.fromEntries(Object.entries(result.corruptionVariants).map(([name, variant]) => (
1283
+ [name, { events: variant.events, expected_failed: variant.expected_failed }]
1284
+ ))),
1285
+ null,
1286
+ 1,
1287
+ )}\n`,
1288
+ );
1289
+ }
1290
+ conditions.push(entry);
1291
+ }
1292
+ const campaignManifest = {
1293
+ schema: 'lattice.rc3.campaign_manifest.v1',
1294
+ cost_observations: costs,
1295
+ scaffold: {
1296
+ lattice_source_base_sha: scaffold.lattice_source.base_sha,
1297
+ target_base_sha: scaffold.target.base_sha,
1298
+ predeclared_treatment: scaffold.predeclared_treatment,
1299
+ path_digests: scaffold.target.path_digests,
1300
+ },
1301
+ conditions,
1302
+ };
1303
+ await writeFile(
1304
+ path.join(staging, 'campaign-manifest.json'),
1305
+ `${JSON.stringify(campaignManifest, null, 1)}\n`,
1306
+ );
1307
+ // atomic no-overwrite発行: 目標rootの不在を確認してからrename一回。
1308
+ // 失敗時はstagingを残さない(残骸掃除を試み、掃除失敗はmessageへ載せる)。
1309
+ try {
1310
+ let exists = true;
1311
+ try {
1312
+ await readFile(path.join(artifactRoot, 'campaign-manifest.json'));
1313
+ } catch (error) {
1314
+ exists = error?.code !== 'ENOENT' ? true : false;
1315
+ }
1316
+ if (exists) throw new TypeError('既存artifact rootが存在する');
1317
+ await rename(staging, artifactRoot);
1318
+ } catch (error) {
1319
+ let residual = '';
1320
+ try {
1321
+ const { rm } = await import('node:fs/promises');
1322
+ await rm(staging, { recursive: true, force: true });
1323
+ } catch (cleanupError) {
1324
+ residual = `(staging残骸: ${staging}: ${String(cleanupError?.message ?? cleanupError)})`;
1325
+ }
1326
+ fail(`artifact rootへのatomic発行に失敗(既存rootの上書きは禁止): ${String(error?.message ?? error)}${residual}`);
1327
+ }
1328
+ return campaignManifest;
1329
+ }
1330
+
1331
+ /**
1332
+ * 8条件scripted campaignを実行し、immutable artifact rootへ発行して
1333
+ * artifact-only verificationまで行う。
1334
+ */
1335
+ export async function runRc3ScriptedCampaign(options = {}) {
1336
+ const { latticeRoot, artifactRoot } = options;
1337
+ if (typeof latticeRoot !== 'string' || latticeRoot.length === 0
1338
+ || typeof artifactRoot !== 'string' || artifactRoot.length === 0) {
1339
+ fail('latticeRoot/artifactRootが必要');
1340
+ }
1341
+
1342
+ const workRoot = await mkdtemp(path.join(tmpdir(), 'lattice-rc3h-'));
1343
+ const scaffold = await scaffoldRc3DogfoodRepo({ latticeRoot, workRoot });
1344
+ const scaffoldVerified = await verifyRc3DogfoodScaffold({
1345
+ latticeRoot, repoRoot: scaffold.repoRoot, expected: scaffold,
1346
+ });
1347
+ if (scaffoldVerified.outcome !== 'verified') {
1348
+ fail(`scaffold検証が失敗: ${JSON.stringify(scaffoldVerified.violations)}`);
1349
+ }
1350
+
1351
+ // cost観測: 各条件の実wall-clockを保存する(未実測を0へ丸めない)。
1352
+ const costs = [];
1353
+ const timed = async (label, thunk) => {
1354
+ const started = performance.now();
1355
+ const value = await thunk();
1356
+ costs.push({ condition: label, elapsed_ms: Math.round((performance.now() - started) * 1000) / 1000 });
1357
+ return value;
1358
+ };
1359
+ const clean = await timed('clean_parallel', () => runCleanParallel({ scaffold }));
1360
+ const late = await timed('late_path_conflict', () => runLateConflict({ scaffold }));
1361
+ // 条件名`scope_violation`はRC3 manifestのdirectory名として凍結されているので動かさない。
1362
+ // 中では単独の予測超過を観測し、conflictへ昇格しない現契約を検証する。
1363
+ const scope = await timed('scope_violation', () => runScopeViolation({ scaffold }));
1364
+ const semantic = await timed('semantic_unknown', () => runSemanticUnknown({ scaffold }));
1365
+ const stale = await timed('stale_receipt', () => runStaleReceipt({ scaffold }));
1366
+ const serial = await timed('irreducible_conflict', () => runIrreducibleConflict({ scaffold }));
1367
+ const seam = await timed('accepted_seam', () => runAcceptedSeam({ latticeRoot }));
1368
+ const corruption = await timed('event_corruption', () => runEventCorruption({ cleanResult: clean }));
1369
+
1370
+ const results = [clean, late, scope, semantic, stale, serial, seam, corruption];
1371
+ const campaignManifest = await writeArtifacts({ artifactRoot, scaffold, results, costs });
1372
+ const verification = await verifyRc3CampaignArtifactsOnDisk({ artifactRoot });
1373
+ if (!verification.valid) {
1374
+ fail(`artifact-only verificationが失敗: ${JSON.stringify(verification.failed_conditions)}`);
1375
+ }
1376
+ return {
1377
+ scaffold,
1378
+ campaignManifest,
1379
+ verification,
1380
+ conditions: results.map(({ record }) => record.condition),
1381
+ scope_expanded: results.flatMap((result) => result.scope_expanded ?? []),
1382
+ };
1383
+ }