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