@quolu/lattice 0.52.0 → 0.52.2

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