@quolu/lattice 0.52.3 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/LICENSE +147 -147
  2. package/README.ja.md +355 -355
  3. package/README.md +258 -258
  4. package/bin/lattice-bridge.mjs +25 -0
  5. package/bin/lattice-hub.mjs +67 -0
  6. package/bin/lattice-mcp.mjs +0 -0
  7. package/bin/lattice-scripted-adapter.mjs +0 -0
  8. package/bin/lattice-scripted-worker.mjs +0 -0
  9. package/bin/lattice-work-order-adapter.mjs +0 -0
  10. package/bin/lattice.mjs +0 -0
  11. package/docs/bridge-setup.md +132 -132
  12. package/docs/schemas/lattice.executor_packet.v1.schema.json +57 -57
  13. package/docs/schemas/lattice.executor_receipt.v1.schema.json +66 -66
  14. package/docs/schemas/lattice.phase_todo_revision.v3.schema.json +360 -360
  15. package/docs/schemas/lattice.plan_create_input.v1.schema.json +56 -56
  16. package/docs/schemas/lattice.plan_create_input.v2.schema.json +72 -72
  17. package/docs/schemas/lattice.plan_create_input.v3.schema.json +81 -81
  18. package/docs/schemas/lattice.plan_create_input.v4.schema.json +85 -85
  19. package/docs/schemas/lattice.run_request.v1.schema.json +238 -238
  20. package/docs/schemas/lattice.runtime_adapter_capabilities.v2.schema.json +55 -55
  21. package/docs/schemas/lattice.runtime_adapter_registration_input.v1.schema.json +78 -78
  22. package/docs/schemas/lattice.runtime_adapter_registration_input.v2.schema.json +86 -86
  23. package/docs/schemas/lattice.todo_extraction.v2.schema.json +298 -298
  24. package/docs/schemas/lattice.todo_extraction.v3.schema.json +146 -146
  25. package/docs/schemas/lattice.todo_revision.v2.schema.json +260 -260
  26. package/docs/schemas/lattice.todo_revision_set.v3.schema.json +363 -363
  27. package/package.json +103 -103
  28. package/sensor/LICENSE +21 -21
  29. package/sensor/NOTICE +19 -19
  30. package/sensor/dist/bin/lattice-sensor.js +9 -9
  31. package/sensor/dist/db/index.js +24 -24
  32. package/sensor/dist/db/migrations.js +41 -41
  33. package/sensor/dist/db/queries.js +164 -164
  34. package/sensor/dist/db/schema.sql +205 -205
  35. package/sensor/dist/directory.js +5 -5
  36. package/sensor/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
  37. package/sensor/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
  38. package/sensor/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
  39. package/sensor/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
  40. package/sensor/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
  41. package/sensor/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
  42. package/sensor/dist/extraction/wasm/tree-sitter-go.wasm +0 -0
  43. package/sensor/dist/extraction/wasm/tree-sitter-java.wasm +0 -0
  44. package/sensor/dist/extraction/wasm/tree-sitter-javascript.wasm +0 -0
  45. package/sensor/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
  46. package/sensor/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
  47. package/sensor/dist/extraction/wasm/tree-sitter-python.wasm +0 -0
  48. package/sensor/dist/extraction/wasm/tree-sitter-tsx.wasm +0 -0
  49. package/sensor/dist/extraction/wasm/tree-sitter-typescript.wasm +0 -0
  50. package/sensor/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
  51. package/sensor/dist/mcp/liveness-watchdog.js +53 -53
  52. package/sensor/dist/mcp/server-instructions.js +95 -95
  53. package/sensor/package.json +56 -56
  54. package/src/artifact-contracts-v2.mjs +325 -325
  55. package/src/artifact-contracts.mjs +895 -895
  56. package/src/boundary-compiler.mjs +712 -712
  57. package/src/boundary-observation-compiler-v2.mjs +344 -344
  58. package/src/bounded-seam.mjs +230 -230
  59. package/src/bridge-address.mjs +107 -107
  60. package/src/bridge-cli.mjs +303 -297
  61. package/src/bridge-config.mjs +382 -346
  62. package/src/bridge-daemon.mjs +378 -378
  63. package/src/bridge-hub-heartbeat.mjs +170 -0
  64. package/src/bridge-hub-protocol.mjs +198 -0
  65. package/src/bridge-hub-server.mjs +544 -0
  66. package/src/bridge-launch-agent.mjs +323 -323
  67. package/src/bridge-registrar.mjs +102 -102
  68. package/src/bridge-server.mjs +376 -376
  69. package/src/cli-help.mjs +308 -308
  70. package/src/cli-stdio.mjs +40 -40
  71. package/src/control-compiler.mjs +532 -532
  72. package/src/dag-chain.mjs +261 -261
  73. package/src/factory-diagnostics.mjs +188 -188
  74. package/src/git-process.mjs +72 -0
  75. package/src/hash-chain.mjs +76 -76
  76. package/src/hooks-cli.mjs +1057 -1057
  77. package/src/isolation-runner.mjs +409 -409
  78. package/src/node-version-guard.mjs +44 -44
  79. package/src/project-cli.mjs +3 -3
  80. package/src/project-identity.mjs +130 -130
  81. package/src/rc1-black-box-oracle.mjs +906 -906
  82. package/src/rc1-comparison.mjs +154 -154
  83. package/src/rc1-evidence-bundle.mjs +456 -456
  84. package/src/rc1-v4-campaign.mjs +708 -708
  85. package/src/rc1-v4-transform.mjs +467 -467
  86. package/src/rc1-v5-artifact-set.mjs +855 -855
  87. package/src/rc1-v5-behavior-evidence.mjs +594 -594
  88. package/src/rc1-v5-campaign.mjs +797 -797
  89. package/src/rc1-v5-transform.mjs +421 -421
  90. package/src/rc1-v6-artifact-set.mjs +807 -807
  91. package/src/rc1-v6-behavior-evidence.mjs +273 -273
  92. package/src/rc1-v6-campaign.mjs +623 -623
  93. package/src/rc1-v6-causal-binding.mjs +473 -473
  94. package/src/rc1-v6-measurement.mjs +313 -313
  95. package/src/rc2-artifact-set.mjs +1584 -1584
  96. package/src/rc2-campaign.mjs +1506 -1506
  97. package/src/rc2-delivery-policy-front-end.mjs +1079 -1079
  98. package/src/rc2-delivery-policy-oracle.mjs +134 -134
  99. package/src/rc2-delivery-policy-transform.mjs +1127 -1127
  100. package/src/rc2-rc1-transfer-front-end.mjs +511 -511
  101. package/src/rc3-actual-dogfood.mjs +652 -652
  102. package/src/rc3-dogfood-scaffold.mjs +315 -315
  103. package/src/rc3-scripted-campaign.mjs +1383 -1383
  104. package/src/rc4-stage1-dogfood.mjs +673 -673
  105. package/src/runtime-adapter-registry.mjs +524 -524
  106. package/src/runtime-cli.mjs +4588 -4588
  107. package/src/runtime-contracts.mjs +824 -824
  108. package/src/runtime-control-store.mjs +604 -604
  109. package/src/runtime-controller-protocol.mjs +587 -587
  110. package/src/runtime-decision-verifier.mjs +701 -701
  111. package/src/runtime-diff-observer.mjs +361 -361
  112. package/src/runtime-direct-os-observer.mjs +301 -301
  113. package/src/runtime-driver-state.mjs +166 -166
  114. package/src/runtime-engine.mjs +779 -779
  115. package/src/runtime-errors.mjs +356 -356
  116. package/src/runtime-event-store.mjs +189 -189
  117. package/src/runtime-front-end.mjs +925 -925
  118. package/src/runtime-gate-store.mjs +481 -481
  119. package/src/runtime-hold-recompile.mjs +916 -916
  120. package/src/runtime-io-sentinel.mjs +391 -391
  121. package/src/runtime-lifecycle-lock.mjs +294 -294
  122. package/src/runtime-managed-supervisor.mjs +1490 -1490
  123. package/src/runtime-multi-epoch-store.mjs +838 -838
  124. package/src/runtime-projection.mjs +269 -269
  125. package/src/runtime-pull-intake.mjs +1192 -1192
  126. package/src/runtime-scripted-adapter-controller.mjs +1160 -1160
  127. package/src/runtime-scripted-executor.mjs +163 -163
  128. package/src/runtime-scripted-worktree.mjs +104 -104
  129. package/src/runtime-seam-resolve.mjs +428 -428
  130. package/src/runtime-seam-treatment.mjs +173 -173
  131. package/src/runtime-socket-owner.mjs +125 -125
  132. package/src/runtime-work-order-contracts.mjs +91 -91
  133. package/src/runtime-work-order-controller.mjs +1171 -1171
  134. package/src/runtime-worktree-executor.mjs +199 -199
  135. package/src/schedulability-compiler-v2.mjs +303 -303
  136. package/src/schedulability-verifier-v2.mjs +317 -317
  137. package/src/seam-apply.mjs +549 -549
  138. package/src/seam-commit-shared.mjs +22 -22
  139. package/src/seam-commit-transform.mjs +81 -81
  140. package/src/seam-commit.mjs +18 -18
  141. package/src/seam-cost.mjs +322 -322
  142. package/src/seam-derivation.mjs +188 -188
  143. package/src/seam-gate.mjs +146 -146
  144. package/src/seam-proposal-contracts.mjs +446 -446
  145. package/src/seam-proposal-queries.mjs +521 -521
  146. package/src/seam-proposal.mjs +2011 -2011
  147. package/src/seam-ref.mjs +33 -33
  148. package/src/seam-rewrite.mjs +286 -286
  149. package/src/seam-transform.mjs +554 -554
  150. package/src/seam-verification.mjs +260 -260
  151. package/src/sensor-adapter.mjs +432 -432
  152. package/src/sensor-cli.mjs +139 -139
  153. package/src/sensor-diff.mjs +661 -661
  154. package/src/sensor-node-runtime.mjs +53 -53
  155. package/src/sensor-runtime.mjs +52 -52
  156. package/src/timestamp-contract.mjs +8 -8
  157. package/src/todo-audit-pending.mjs +91 -91
  158. package/src/todo-chain.mjs +178 -178
  159. package/src/todo-cli.mjs +8 -8
  160. package/src/todo-contracts.mjs +728 -728
  161. package/src/todo-dashboard-registry.mjs +573 -573
  162. package/src/todo-dispatch-shape.mjs +190 -190
  163. package/src/todo-gantt-html-independence.mjs +239 -239
  164. package/src/todo-gantt-html-shared.mjs +226 -226
  165. package/src/todo-gantt-html-style.mjs +131 -131
  166. package/src/todo-gantt-html.mjs +248 -248
  167. package/src/todo-gantt-layout.mjs +974 -974
  168. package/src/todo-gantt-live.mjs +361 -361
  169. package/src/todo-gantt-nested.mjs +263 -263
  170. package/src/todo-gantt-presentation.mjs +217 -217
  171. package/src/todo-gantt-scope.mjs +123 -123
  172. package/src/todo-gantt-svg.mjs +353 -353
  173. package/src/todo-independence-contracts.mjs +595 -595
  174. package/src/todo-independence-guidance.mjs +322 -322
  175. package/src/todo-independence.mjs +640 -640
  176. package/src/todo-markdown-renderer.mjs +260 -260
  177. package/src/todo-migration.mjs +448 -448
  178. package/src/todo-narrative-anchor.mjs +130 -130
  179. package/src/todo-note-store.mjs +629 -629
  180. package/src/todo-parallel-candidates.mjs +114 -114
  181. package/src/todo-revision.mjs +995 -995
  182. package/src/todo-split.mjs +472 -472
  183. package/src/todo-status.mjs +690 -690
  184. package/src/todo-store-git-transaction.mjs +418 -418
  185. package/src/todo-store.mjs +42 -21
  186. package/src/treatment-compiler.mjs +728 -728
  187. package/src/treatment-runner.mjs +656 -656
  188. package/src/witness-scaffold.mjs +180 -180
@@ -1,701 +1,701 @@
1
- import path from 'node:path';
2
-
3
- import { digestArtifact } from './artifact-contracts.mjs';
4
- import { validateCarryOverWitness } from './runtime-contracts.mjs';
5
- import { projectRuntimeState } from './runtime-projection.mjs';
6
-
7
- // RC3 runtime decision verifier(ADR 0044 Decision 4〜7)。
8
- // dispatch/hold/continue/invalidate裁定を、保存planとevent prefixだけから
9
- // producer非依存に再計算する。schema envelope/digestの完全検証は
10
- // runtime-contracts.mjsのvalidatorが担い、本moduleはruntime意味論を再計算する。
11
- // 判定はすべてevent sequenceで行い、生成時刻・到着時刻を根拠にしない(Decision 7.4)。
12
-
13
- function invalidVerification(reason) {
14
- throw new TypeError(`runtime decision verifier契約違反: ${reason}`);
15
- }
16
-
17
- function requirePlan(plan) {
18
- if (plan === null || typeof plan !== 'object' || Array.isArray(plan)
19
- || !Array.isArray(plan.nodes)
20
- || plan.nodes.length === 0
21
- || !Array.isArray(plan.precedence)
22
- || !Array.isArray(plan.conflicts)
23
- || plan.capacity === null
24
- || typeof plan.capacity !== 'object'
25
- || !Number.isSafeInteger(plan.capacity.executors)
26
- || plan.capacity.executors < 1
27
- || !Number.isSafeInteger(plan.plan_epoch)) {
28
- invalidVerification('runtime planの意味論的shapeが不正');
29
- }
30
- const nodeIds = plan.nodes.map((node) => node.todo_id);
31
- if (nodeIds.some((todoId) => typeof todoId !== 'string')
32
- || new Set(nodeIds).size !== nodeIds.length) {
33
- invalidVerification('plan nodeが不正');
34
- }
35
- return nodeIds;
36
- }
37
-
38
- function sorted(values) {
39
- return [...values].sort();
40
- }
41
-
42
- /**
43
- * ready frontierを再計算する(ADR 0044 Decision 4)。
44
- * minimum waveを同期barrierとして扱わず、hard predecessor充足、running集合との
45
- * conflict不在、実capacityだけをdispatch可否の根拠にする。intake freeze中は
46
- * fail closedでdispatch 0件とする。dispatchableは辞書順で空きcapacity分に切る。
47
- */
48
- export function computeReadyFrontier(options = {}) {
49
- const { plan, events } = options;
50
- const nodeIds = requirePlan(plan);
51
- const state = projectRuntimeState({ events });
52
-
53
- if (state.freeze !== null || state.closed) {
54
- return { dispatchable: [] };
55
- }
56
-
57
- const acceptedSet = new Set(state.accepted);
58
- const runningSet = new Set(state.running);
59
- // dispatch済み・terminal済み・held済みの除外は現plan epochへscopeする。
60
- // 旧epochのdispatch/hold記録は、recompile時のcontext失効とredispatch契約
61
- //(RC3-G)によって新epochでは再配置可能になる。accepted/runningは
62
- // epoch非依存(物理状態)のまま除外する。
63
- const heldSet = new Set();
64
- const terminalSet = new Set();
65
- const dispatchedSet = new Set();
66
- for (const event of events) {
67
- if (event.plan_epoch !== plan.plan_epoch) continue;
68
- if (event.kind === 'hold_decided') {
69
- for (const todoId of event.payload?.hold_set ?? []) heldSet.add(todoId);
70
- }
71
- if (event.subject?.kind !== 'todo') continue;
72
- if (event.kind === 'executor_dispatched') dispatchedSet.add(event.subject.ref);
73
- if (event.kind === 'executor_terminal') terminalSet.add(event.subject.ref);
74
- }
75
- const slots = plan.capacity.executors - runningSet.size;
76
- if (slots < 1) {
77
- return { dispatchable: [] };
78
- }
79
-
80
- const eligible = nodeIds.filter((todoId) => {
81
- if (acceptedSet.has(todoId) || runningSet.has(todoId) || heldSet.has(todoId)
82
- || terminalSet.has(todoId) || dispatchedSet.has(todoId)) return false;
83
- return plan.precedence.every((edge) => (
84
- edge.to_todo_id !== todoId || acceptedSet.has(edge.from_todo_id)
85
- ));
86
- });
87
-
88
- // running集合だけでなく、同じfrontierで既に選んだnodeとのconflictも検査する。
89
- // conflict pairを同一dispatch decisionで同時実行しないための貪欲選択(辞書順)。
90
- const dispatchable = [];
91
- for (const todoId of sorted(eligible)) {
92
- if (dispatchable.length >= slots) break;
93
- const conflictFree = plan.conflicts.every((conflict) => {
94
- if (!conflict.todo_ids.includes(todoId)) return true;
95
- return conflict.todo_ids.every((member) => (
96
- member === todoId || (!runningSet.has(member) && !dispatchable.includes(member))
97
- ));
98
- });
99
- if (conflictFree) dispatchable.push(todoId);
100
- }
101
-
102
- return { dispatchable };
103
- }
104
-
105
- function declaredWriteCovers(declaredWrites, observedPath) {
106
- return declaredWrites.some((declared) => (
107
- declared === observedPath
108
- || (declared.endsWith('/') && observedPath.startsWith(declared))
109
- ));
110
- }
111
-
112
- function witnessSet(manifest, kinds) {
113
- const resources = new Set(manifest?.resources ?? []);
114
- for (const effect of manifest?.state_effects ?? []) {
115
- if (kinds.includes(effect.kind) && typeof effect.resource_id === 'string') {
116
- resources.add(effect.resource_id);
117
- }
118
- }
119
- return resources;
120
- }
121
-
122
- const LINE_ID = /^[0-9A-Za-z](?:[0-9A-Za-z._-]{0,127})$/;
123
- const LINE_PATH_CONTROL = /[\u0000-\u001f\u007f]/;
124
-
125
- function runtimeLinePath(value) {
126
- return typeof value === 'string' && value.length > 0
127
- && Buffer.byteLength(value, 'utf8') <= 1_024
128
- && !LINE_PATH_CONTROL.test(value) && !value.includes('\\')
129
- && !path.posix.isAbsolute(value) && value === path.posix.normalize(value)
130
- && !value.split('/').includes('..');
131
- }
132
-
133
- function runtimeLineGroups(manifests, todoIds) {
134
- const groups = new Map();
135
- for (const todoId of sorted(new Set(todoIds))) {
136
- const manifest = manifests[todoId];
137
- if (manifest === null || typeof manifest !== 'object' || Array.isArray(manifest)
138
- || !Array.isArray(manifest.lines ?? [])) {
139
- invalidVerification(`boundary manifestのlinesが不正: ${todoId}`);
140
- }
141
- const seen = new Set();
142
- for (const line of manifest.lines ?? []) {
143
- if (line === null || typeof line !== 'object' || Array.isArray(line)
144
- || Object.keys(line).sort().join(',') !== 'anchors,line_id,role'
145
- || !LINE_ID.test(line.line_id ?? '') || !['reads', 'writes'].includes(line.role)
146
- || !Array.isArray(line.anchors) || line.anchors.length === 0
147
- || seen.has(line.line_id)) {
148
- invalidVerification(`line宣言が不正: ${todoId}`);
149
- }
150
- seen.add(line.line_id);
151
- const group = groups.get(line.line_id) ?? {
152
- readers: new Set(), writers: new Set(), anchorPaths: new Set(),
153
- };
154
- group[line.role === 'reads' ? 'readers' : 'writers'].add(todoId);
155
- for (const anchor of line.anchors) {
156
- const keys = anchor !== null && typeof anchor === 'object' && !Array.isArray(anchor)
157
- ? Object.keys(anchor).sort().join(',') : '';
158
- const valid = anchor?.kind === 'path'
159
- ? keys === 'kind,path' && runtimeLinePath(anchor.path)
160
- : anchor?.kind === 'symbol' && keys === 'kind,name,path'
161
- && typeof anchor.name === 'string' && anchor.name.length > 0
162
- && runtimeLinePath(anchor.path);
163
- if (!valid) invalidVerification(`line anchorが不正: ${todoId}/${line.line_id}`);
164
- // producerと共有せず、観測pathだけから独立に近似する。
165
- group.anchorPaths.add(anchor.path);
166
- }
167
- groups.set(line.line_id, group);
168
- }
169
- }
170
- return groups;
171
- }
172
-
173
- /**
174
- * 観測diffをdeclared path/resource witnessへcross-bindし、closed conflict分類の
175
- * findingを返す(ADR 0044 Decision 5)。宣言外writeと運転中overlapは別findingで
176
- * 保存し、silent mergeしない。path非交差でも共有witnessがあれば安全と推測せず
177
- * unknown findingにする。
178
- */
179
- export function classifyObservedDiff(options = {}) {
180
- const { plan, manifests, observations, relevantTodoIds = null } = options;
181
- requirePlan(plan);
182
- if (manifests === null || typeof manifests !== 'object' || Array.isArray(manifests)
183
- || !Array.isArray(observations)
184
- || !(relevantTodoIds === null || Array.isArray(relevantTodoIds))) {
185
- invalidVerification('manifests/observationsが不正');
186
- }
187
- const relevant = new Set(relevantTodoIds ?? Object.keys(manifests));
188
-
189
- const findings = [];
190
- const observedByTodo = new Map();
191
- for (const observation of observations) {
192
- if (observation === null || typeof observation !== 'object'
193
- || typeof observation.todo_id !== 'string'
194
- || !Array.isArray(observation.paths)) {
195
- invalidVerification('observationが不正');
196
- }
197
- const manifest = manifests[observation.todo_id];
198
- if (manifest === undefined) {
199
- invalidVerification(`boundary manifestがないTODOの観測: ${observation.todo_id}`);
200
- }
201
- observedByTodo.set(observation.todo_id, [...new Set(observation.paths)].sort());
202
- }
203
-
204
- for (const [todoId, paths] of observedByTodo) {
205
- const declaredWrites = manifests[todoId].writes ?? [];
206
- for (const path of paths) {
207
- if (!declaredWriteCovers(declaredWrites, path)) {
208
- findings.push({ kind: 'undeclared_write', todo_ids: [todoId], path });
209
- }
210
- }
211
- }
212
-
213
- const lineScope = new Set([...relevant, ...observedByTodo.keys()]);
214
- const lineGroups = runtimeLineGroups(manifests, lineScope);
215
- for (const [todoId, paths] of observedByTodo) {
216
- const observedPaths = new Set(paths);
217
- for (const [lineId, group] of lineGroups) {
218
- if (group.writers.has(todoId)) continue;
219
- const readers = [...group.readers].filter((readerId) => readerId !== todoId).sort();
220
- if (readers.length === 0
221
- || ![...group.anchorPaths].some((anchorPath) => observedPaths.has(anchorPath))) continue;
222
- findings.push({
223
- kind: 'observed_line_change',
224
- todo_ids: [todoId, ...readers].sort(),
225
- resource_id: lineId,
226
- });
227
- }
228
- }
229
-
230
- const writersByPath = new Map();
231
- for (const [todoId, paths] of observedByTodo) {
232
- for (const path of paths) {
233
- const writers = writersByPath.get(path) ?? [];
234
- writers.push(todoId);
235
- writersByPath.set(path, writers);
236
- }
237
- }
238
- const overlapPairs = new Set();
239
- const emittedOverlaps = new Set();
240
- for (const [path, writers] of [...writersByPath.entries()].sort()) {
241
- if (writers.length < 2) continue;
242
- const sortedWriters = sorted(writers);
243
- for (let left = 0; left < sortedWriters.length; left += 1) {
244
- for (let right = left + 1; right < sortedWriters.length; right += 1) {
245
- findings.push({
246
- kind: 'observed_write_conflict',
247
- todo_ids: [sortedWriters[left], sortedWriters[right]],
248
- path,
249
- });
250
- emittedOverlaps.add(`${sortedWriters[left]} ${sortedWriters[right]} ${path}`);
251
- overlapPairs.add(`${sortedWriters[left]} ${sortedWriters[right]}`);
252
- }
253
- }
254
- }
255
-
256
- // 観測pathが、まだ観測を持たない別TODOのdeclared write(prefix宣言含む)へ
257
- // 到達する場合も後発write conflictとして保存する(片側観測だけの
258
- // late conflictを安全と推測しない。RC3-F採用のverifier意味論補完)。
259
- for (const [todoId, paths] of observedByTodo) {
260
- for (const [otherId, manifest] of Object.entries(manifests)) {
261
- if (otherId === todoId) continue;
262
- if (!relevant.has(otherId)) continue;
263
- const otherDeclared = [...(manifest.reads ?? []), ...(manifest.writes ?? [])];
264
- for (const path of paths) {
265
- if (!declaredWriteCovers(otherDeclared, path)) continue;
266
- const pair = sorted([todoId, otherId]);
267
- const key = `${pair[0]} ${pair[1]} ${path}`;
268
- if (emittedOverlaps.has(key)) continue;
269
- emittedOverlaps.add(key);
270
- findings.push({
271
- kind: 'observed_write_conflict',
272
- todo_ids: pair,
273
- path,
274
- });
275
- overlapPairs.add(`${pair[0]} ${pair[1]}`);
276
- }
277
- }
278
- }
279
-
280
- const todoIds = sorted(observedByTodo.keys());
281
- for (let left = 0; left < todoIds.length; left += 1) {
282
- for (let right = left + 1; right < todoIds.length; right += 1) {
283
- const pairKey = `${todoIds[left]} ${todoIds[right]}`;
284
- if (overlapPairs.has(pairKey)) continue;
285
- // dynamic/semantic/effect unknownを持つTODOは、path非交差でも独立性を
286
- // 主張できない(ADR 0044 Decision 1.5・5.1)。unknown由来のpairをevidence
287
- // acquisition対象のsemantic conflict unknownとして保存する。
288
- const declaredUnknowns = [
289
- ...(manifests[todoIds[left]].unknowns ?? []),
290
- ...(manifests[todoIds[right]].unknowns ?? []),
291
- ];
292
- if (declaredUnknowns.length > 0) {
293
- findings.push({
294
- kind: 'semantic_conflict_unknown',
295
- todo_ids: [todoIds[left], todoIds[right]],
296
- resource_id: null,
297
- unknowns: structuredClone(declaredUnknowns),
298
- });
299
- }
300
- for (const [kind, witnessKinds] of [
301
- ['semantic_conflict_unknown', ['state', 'schema', 'invariant']],
302
- ['effect_conflict_unknown', ['effect', 'external_effect']],
303
- ]) {
304
- const leftWitness = kind === 'semantic_conflict_unknown'
305
- ? witnessSet(manifests[todoIds[left]], witnessKinds)
306
- : witnessSet({ state_effects: manifests[todoIds[left]].state_effects }, witnessKinds);
307
- const rightWitness = kind === 'semantic_conflict_unknown'
308
- ? witnessSet(manifests[todoIds[right]], witnessKinds)
309
- : witnessSet({ state_effects: manifests[todoIds[right]].state_effects }, witnessKinds);
310
- for (const resourceId of sorted(leftWitness)) {
311
- if (rightWitness.has(resourceId)) {
312
- findings.push({
313
- kind,
314
- todo_ids: [todoIds[left], todoIds[right]],
315
- resource_id: resourceId,
316
- });
317
- }
318
- }
319
- }
320
- }
321
- }
322
-
323
- return { findings };
324
- }
325
-
326
- function affectedClosure(plan, seedTodoIds, manifests) {
327
- const closure = new Set(seedTodoIds);
328
- // manifestsが与えられた場合、同一resource witnessへ到達するTODOをclosureへ
329
- // 含める(ADR 0044 Decision 6.3の第三要素)。resource→TODOのindexを先に作る。
330
- const byResource = new Map();
331
- if (manifests !== undefined) {
332
- for (const [todoId, manifest] of Object.entries(manifests)) {
333
- for (const resourceId of witnessSet(manifest, [
334
- 'state', 'schema', 'invariant', 'effect', 'external_effect',
335
- ])) {
336
- const members = byResource.get(resourceId) ?? new Set();
337
- members.add(todoId);
338
- byResource.set(resourceId, members);
339
- }
340
- }
341
- }
342
- let changed = true;
343
- while (changed) {
344
- changed = false;
345
- for (const conflict of plan.conflicts) {
346
- const [left, right] = conflict.todo_ids;
347
- if (closure.has(left) !== closure.has(right)) {
348
- closure.add(left);
349
- closure.add(right);
350
- changed = true;
351
- }
352
- }
353
- for (const edge of plan.precedence) {
354
- if (closure.has(edge.from_todo_id) && !closure.has(edge.to_todo_id)) {
355
- closure.add(edge.to_todo_id);
356
- changed = true;
357
- }
358
- }
359
- for (const members of byResource.values()) {
360
- if ([...members].some((todoId) => closure.has(todoId))) {
361
- for (const todoId of members) {
362
- if (!closure.has(todoId)) {
363
- closure.add(todoId);
364
- changed = true;
365
- }
366
- }
367
- }
368
- }
369
- }
370
- return closure;
371
- }
372
-
373
- /**
374
- * carry_over_witnessed eventが埋め込むwitness documentをschema・自己digest・
375
- * 帰属で検証する。event payloadは`{ witness_digest, witness }`を持ち、witnessは
376
- * `lattice.carry_over_witness.v1`の完全documentでなければならない。
377
- * invariant digestと提供bytesの再照合はverifyCarryOverWitnessが行う(RC3-Gで
378
- * 保存artifactと配線する)。ここでは存在・shape・digest整合を実証できない
379
- * witnessをすべて不成立として扱う(fail closed)。
380
- */
381
- function witnessedForContinue(witnessRecord, todoId) {
382
- if (witnessRecord === undefined) return false;
383
- const payload = witnessRecord.payload;
384
- const witness = payload?.witness;
385
- if (witness === null || witness === undefined) return false;
386
- if (!validateCarryOverWitness(witness)) return false;
387
- if (witness.todo_id !== todoId) return false;
388
- if (typeof payload.witness_digest !== 'string'
389
- || payload.witness_digest !== witness.witness_digest) {
390
- return false;
391
- }
392
- return true;
393
- }
394
-
395
- /**
396
- * frozen prefixからaffected closureとhold/continue集合を再計算する
397
- * (ADR 0044 Decision 6・7.2)。closure外のrunning TODOでも、schema・自己digest
398
- * 整合まで実証できたcarry-over witnessが保存bytesに存在しなければcontinueできない
399
- * (fail closed)。manifestsを渡すと同一resource witness到達もclosureへ含める。
400
- */
401
- export function recomputeHoldDecision(options = {}) {
402
- const { plan, events, manifests } = options;
403
- requirePlan(plan);
404
- const state = projectRuntimeState({ events });
405
- if (state.freeze === null) {
406
- invalidVerification('intake_frozen eventのないprefixからhold decisionを再計算できない');
407
- }
408
- const freezeSequence = state.freeze.sequence;
409
-
410
- // seedは現plan epochのconflictだけ(過去epochの処理済みconflictを再seedしない)。
411
- const currentEpochConflictSequences = new Set(events
412
- .filter((event) => event.kind === 'conflict_found' && event.plan_epoch === plan.plan_epoch)
413
- .map((event) => event.sequence));
414
- const frozenConflicts = state.conflicts.filter((conflict) => (
415
- conflict.sequence <= freezeSequence && currentEpochConflictSequences.has(conflict.sequence)
416
- ));
417
- if (frozenConflicts.length === 0) {
418
- invalidVerification('frozen prefix内にconflict findingがない');
419
- }
420
- const finding = frozenConflicts.at(-1);
421
- const seed = frozenConflicts.flatMap((conflict) => conflict.todo_ids ?? []);
422
- const closure = affectedClosure(plan, seed, manifests);
423
-
424
- const frozenRunning = new Set();
425
- for (const event of events) {
426
- if (event.sequence > freezeSequence) continue;
427
- if (event.kind === 'executor_dispatched' && event.subject?.kind === 'todo') {
428
- frozenRunning.add(event.subject.ref);
429
- }
430
- if ((event.kind === 'receipt_accepted' || event.kind === 'executor_terminal')
431
- && event.subject?.kind === 'todo') {
432
- frozenRunning.delete(event.subject.ref);
433
- }
434
- }
435
-
436
- const holdSet = new Set();
437
- const continueSet = new Set();
438
- const reasons = {};
439
- for (const todoId of frozenRunning) {
440
- if (closure.has(todoId)) {
441
- holdSet.add(todoId);
442
- reasons[todoId] = 'affected_closure';
443
- } else if (witnessedForContinue(state.witnesses[todoId], todoId)) {
444
- continueSet.add(todoId);
445
- } else {
446
- holdSet.add(todoId);
447
- reasons[todoId] = 'carry_over_unprovable';
448
- }
449
- }
450
- for (const todoId of closure) {
451
- if (!holdSet.has(todoId) && !continueSet.has(todoId)) {
452
- holdSet.add(todoId);
453
- reasons[todoId] = 'affected_closure';
454
- }
455
- }
456
-
457
- return {
458
- finding: structuredClone(finding),
459
- frozen_prefix_sequence: freezeSequence,
460
- hold_set: sorted(holdSet),
461
- continue_set: sorted(continueSet),
462
- reasons,
463
- };
464
- }
465
-
466
- /**
467
- * receipt受理裁定をevent prefixから再計算する(ADR 0044 Decision 7.4)。
468
- * 「rebind前」「rebind後」は生成時刻でなくevent sequenceで判定する。
469
- * freeze後のvN epoch receiptはwitness bindingの有無によらずstale_contextで
470
- * rejectし、frozen prefix内のpending receiptはwitness bindingがある場合だけ
471
- * 受理する。証明できない受理はすべてrejectへ倒す(fail closed)。
472
- */
473
- const RECEIPT_BINDING_FIELDS = Object.freeze([
474
- 'executor_handle',
475
- 'worktree_id',
476
- 'base_sha',
477
- 'packet_digest',
478
- 'checkpoint_digest',
479
- ]);
480
-
481
- export function recomputeReceiptDecisions(options = {}) {
482
- const { plan, events } = options;
483
- requirePlan(plan);
484
- const state = projectRuntimeState({ events });
485
- // freezeはresumeで消えない永続境界として扱う。resume後もvN epoch receiptの
486
- // stale判定は最初のfreeze境界に対して行う(受理へ反転させない)。
487
- // 境界は検証対象planのepochで発生したfreezeへscopeする: vN+1 epochの
488
- // receiptをvNのfreezeで塞がない(rebind後のvN+1 receipt受理、RC3-G)。
489
- const freezes = events
490
- .filter((event) => event.kind === 'intake_frozen' && event.plan_epoch === plan.plan_epoch)
491
- .map((event) => event.sequence)
492
- .sort((left, right) => left - right);
493
- const freezeBoundary = freezes.length === 0 ? null : freezes[0];
494
-
495
- const seenReceiptIds = new Set();
496
- const decisions = state.receipts.map((receipt) => {
497
- const base = {
498
- receipt_id: receipt.receipt_id,
499
- todo_id: receipt.todo_id,
500
- sequence: receipt.sequence,
501
- };
502
- const reject = (detail) => (
503
- { ...base, decision: 'rejected', reason: 'stale_context', detail }
504
- );
505
-
506
- // receipt_idの再利用は帰属の一意性を壊すためreject(先着だけを裁定対象にする)。
507
- if (seenReceiptIds.has(receipt.receipt_id)) {
508
- return reject('duplicate_receipt_id');
509
- }
510
- seenReceiptIds.add(receipt.receipt_id);
511
-
512
- // 帰属照合の基準はdispatch記録であり、executor自己申告ではない(Decision 7.4)。
513
- const payload = receipt.payload ?? {};
514
- if (RECEIPT_BINDING_FIELDS.some((field) => typeof payload[field] !== 'string')) {
515
- return reject('binding_missing');
516
- }
517
- // dispatch記録は「receipt記録時点までの最後のdispatch」を使う(裁定時点の
518
- // prefix意味論。全投影の最後勝ちにすると、後続redispatchが過去receiptの
519
- // 帰属先を書き換えてproducer裁定とdivergeする。RC3-J P1採用)。
520
- const dispatchEventForReceipt = events.findLast((event) => (
521
- event.kind === 'executor_dispatched'
522
- && event.subject?.kind === 'todo'
523
- && event.subject.ref === receipt.todo_id
524
- && event.sequence < receipt.sequence
525
- ));
526
- const dispatch = dispatchEventForReceipt === undefined
527
- ? undefined
528
- : { sequence: dispatchEventForReceipt.sequence, payload: dispatchEventForReceipt.payload };
529
- const originBindingRetained = (() => {
530
- if (!Number.isSafeInteger(receipt.plan_epoch) || receipt.plan_epoch >= plan.plan_epoch) return false;
531
- for (let epoch = receipt.plan_epoch; epoch < plan.plan_epoch; epoch += 1) {
532
- const witnessed = events.some((event) => event.sequence < receipt.sequence
533
- && event.kind === 'carry_over_witnessed' && event.plan_epoch === epoch
534
- && event.subject?.kind === 'todo' && event.subject.ref === receipt.todo_id);
535
- const continued = events.some((event) => event.sequence < receipt.sequence
536
- && event.kind === 'hold_decided' && event.plan_epoch === epoch
537
- && event.payload?.continue_set?.includes(receipt.todo_id));
538
- const recompiled = events.some((event) => event.sequence < receipt.sequence
539
- && event.kind === 'plan_recompiled' && event.plan_epoch === epoch + 1);
540
- const invalidated = events.some((event) => event.sequence < receipt.sequence
541
- && event.kind === 'context_invalidated' && event.plan_epoch === epoch + 1
542
- && event.subject?.kind === 'todo' && event.subject.ref === receipt.todo_id);
543
- if (!witnessed || !continued || !recompiled || invalidated) return false;
544
- }
545
- return true;
546
- })();
547
- if (dispatch === undefined) {
548
- return reject('not_dispatched');
549
- }
550
- const dispatchPayload = dispatch.payload ?? {};
551
- if (payload.executor_handle !== dispatchPayload.executor_handle
552
- || payload.worktree_id !== dispatchPayload.worktree_id
553
- || payload.packet_digest !== dispatchPayload.packet_digest) {
554
- return reject('binding_mismatch');
555
- }
556
- if (typeof plan.base_sha === 'string' && payload.base_sha !== plan.base_sha) {
557
- return reject('base_mismatch');
558
- }
559
- if (receipt.plan_epoch !== plan.plan_epoch && !originBindingRetained) {
560
- return reject('epoch_mismatch');
561
- }
562
- // dispatchが旧epochのTODOが現epoch receiptを名乗る場合はepoch_rebound必須
563
- // (rebindなしのepoch自称を受理しない。Decision 7.3/7.4)。
564
- if (dispatchEventForReceipt.plan_epoch !== plan.plan_epoch && !originBindingRetained) {
565
- const rebound = state.rebinds[receipt.todo_id];
566
- if (rebound === undefined
567
- || rebound.payload?.new_plan_epoch !== plan.plan_epoch
568
- || rebound.sequence >= receipt.sequence) {
569
- return reject('unrebound_epoch');
570
- }
571
- }
572
- // checkpoint観測が存在するTODOは、receipt以前の最後の観測checkpointとの
573
- // digest一致とobserved_diff一致を要求(executor自己申告をbinding証拠に
574
- // しない。RC3-F)。bindingは同一dispatch attemptへscope=最後のdispatch
575
- // 以降のcheckpointだけが対象(redispatch後のreceiptを旧attemptの
576
- // checkpointで塞がない。RC3-G)。
577
- const observedCheckpoints = state.checkpoints.filter((entry) => (
578
- entry.todo_id === receipt.todo_id
579
- && entry.sequence > dispatch.sequence
580
- && entry.sequence < receipt.sequence
581
- // supervisorがI/O警報を確かめるために撮ったcheckpointはexecutorの申告境界では
582
- // ないので、bindingの基準にしない(ADR 0143。engine側と同一規則)。
583
- && entry.payload?.observed_by !== 'supervisor_probe'
584
- ));
585
- if (observedCheckpoints.length > 0) {
586
- const last = observedCheckpoints[observedCheckpoints.length - 1].payload;
587
- if (typeof last?.checkpoint_digest === 'string'
588
- && payload.checkpoint_digest !== last.checkpoint_digest) {
589
- return reject('checkpoint_mismatch');
590
- }
591
- if (last?.diff?.entries !== undefined && Array.isArray(last.diff.entries)) {
592
- const expected = JSON.stringify(last.diff.entries.map(({ path, change }) => ({ path, change })));
593
- const reported = JSON.stringify((payload.observed_diff ?? []).map(({ path, change }) => ({ path, change })));
594
- if (expected !== reported) {
595
- return reject('checkpoint_mismatch');
596
- }
597
- }
598
- }
599
- if (receipt.rejected_sequence !== null) {
600
- return reject('recorded_rejection');
601
- }
602
- if (freezeBoundary === null) {
603
- return { ...base, decision: 'accepted', reason: null, detail: null };
604
- }
605
- if (receipt.accepted_sequence !== null && receipt.accepted_sequence <= freezeBoundary) {
606
- return { ...base, decision: 'accepted', reason: null, detail: null };
607
- }
608
- if (receipt.sequence > freezeBoundary) {
609
- return reject('post_freeze');
610
- }
611
- // frozen prefix内のpending receiptは、実証済みwitness documentの
612
- // receipt_bindingsが当該receiptをbindする場合だけ有効になる(Decision 7.5)。
613
- const witnessRecord = state.witnesses[receipt.todo_id];
614
- if (!witnessedForContinue(witnessRecord, receipt.todo_id)) {
615
- return reject('witness_unproven');
616
- }
617
- const bound = witnessRecord.payload.witness.receipt_bindings.some((binding) => (
618
- binding.receipt_id === receipt.receipt_id
619
- && binding.recorded_sequence === receipt.sequence
620
- && binding.within_frozen_prefix === true
621
- ));
622
- if (bound) {
623
- return { ...base, decision: 'accepted', reason: null, detail: null };
624
- }
625
- return reject('unbound_receipt');
626
- });
627
-
628
- return { decisions };
629
- }
630
-
631
- const INVARIANT_DIGEST_FIELDS = Object.freeze([
632
- 'todo_input',
633
- 'boundary_manifest',
634
- 'validator',
635
- 'context_content',
636
- ]);
637
-
638
- /**
639
- * carry-over witnessのinvariant digestを提供bytesから再計算する
640
- * (ADR 0044 Decision 7.2・7.5)。1 fieldでも証明不能ならvalid falseとし、
641
- * reasonsへ具体的なfailureと`carry_over_unprovable`を返す。
642
- */
643
- export function verifyCarryOverWitness(options = {}) {
644
- const { witness, sources } = options;
645
- const reasons = [];
646
- if (witness === null || typeof witness !== 'object' || Array.isArray(witness)
647
- || witness.invariant_digests === null
648
- || typeof witness.invariant_digests !== 'object') {
649
- return { valid: false, reasons: ['witness_shape', 'carry_over_unprovable'] };
650
- }
651
- if (sources === null || typeof sources !== 'object' || Array.isArray(sources)) {
652
- return { valid: false, reasons: ['sources_shape', 'carry_over_unprovable'] };
653
- }
654
-
655
- if (!Number.isSafeInteger(witness.predecessor_epoch)
656
- || !Number.isSafeInteger(witness.successor_epoch)
657
- || witness.successor_epoch <= witness.predecessor_epoch) {
658
- reasons.push('epoch_ordering');
659
- }
660
-
661
- for (const field of INVARIANT_DIGEST_FIELDS) {
662
- if (sources[field] === undefined) {
663
- reasons.push(`missing_source:${field}`);
664
- continue;
665
- }
666
- let recomputed = null;
667
- try {
668
- recomputed = digestArtifact(sources[field]);
669
- } catch {
670
- reasons.push(`source_not_canonical:${field}`);
671
- continue;
672
- }
673
- if (witness.invariant_digests[field] !== recomputed) {
674
- reasons.push(`invariant_digest_mismatch:${field}`);
675
- }
676
- }
677
-
678
- if (typeof witness.witness_digest === 'string') {
679
- const projection = {};
680
- for (const key of Object.keys(witness)) {
681
- if (key !== 'witness_digest') projection[key] = witness[key];
682
- }
683
- let selfDigest = null;
684
- try {
685
- selfDigest = digestArtifact(projection);
686
- } catch {
687
- selfDigest = null;
688
- }
689
- if (witness.witness_digest !== selfDigest) {
690
- reasons.push('witness_digest_mismatch');
691
- }
692
- } else {
693
- reasons.push('witness_digest_mismatch');
694
- }
695
-
696
- if (reasons.length > 0) {
697
- reasons.push('carry_over_unprovable');
698
- return { valid: false, reasons };
699
- }
700
- return { valid: true, reasons: [] };
701
- }
1
+ import path from 'node:path';
2
+
3
+ import { digestArtifact } from './artifact-contracts.mjs';
4
+ import { validateCarryOverWitness } from './runtime-contracts.mjs';
5
+ import { projectRuntimeState } from './runtime-projection.mjs';
6
+
7
+ // RC3 runtime decision verifier(ADR 0044 Decision 4〜7)。
8
+ // dispatch/hold/continue/invalidate裁定を、保存planとevent prefixだけから
9
+ // producer非依存に再計算する。schema envelope/digestの完全検証は
10
+ // runtime-contracts.mjsのvalidatorが担い、本moduleはruntime意味論を再計算する。
11
+ // 判定はすべてevent sequenceで行い、生成時刻・到着時刻を根拠にしない(Decision 7.4)。
12
+
13
+ function invalidVerification(reason) {
14
+ throw new TypeError(`runtime decision verifier契約違反: ${reason}`);
15
+ }
16
+
17
+ function requirePlan(plan) {
18
+ if (plan === null || typeof plan !== 'object' || Array.isArray(plan)
19
+ || !Array.isArray(plan.nodes)
20
+ || plan.nodes.length === 0
21
+ || !Array.isArray(plan.precedence)
22
+ || !Array.isArray(plan.conflicts)
23
+ || plan.capacity === null
24
+ || typeof plan.capacity !== 'object'
25
+ || !Number.isSafeInteger(plan.capacity.executors)
26
+ || plan.capacity.executors < 1
27
+ || !Number.isSafeInteger(plan.plan_epoch)) {
28
+ invalidVerification('runtime planの意味論的shapeが不正');
29
+ }
30
+ const nodeIds = plan.nodes.map((node) => node.todo_id);
31
+ if (nodeIds.some((todoId) => typeof todoId !== 'string')
32
+ || new Set(nodeIds).size !== nodeIds.length) {
33
+ invalidVerification('plan nodeが不正');
34
+ }
35
+ return nodeIds;
36
+ }
37
+
38
+ function sorted(values) {
39
+ return [...values].sort();
40
+ }
41
+
42
+ /**
43
+ * ready frontierを再計算する(ADR 0044 Decision 4)。
44
+ * minimum waveを同期barrierとして扱わず、hard predecessor充足、running集合との
45
+ * conflict不在、実capacityだけをdispatch可否の根拠にする。intake freeze中は
46
+ * fail closedでdispatch 0件とする。dispatchableは辞書順で空きcapacity分に切る。
47
+ */
48
+ export function computeReadyFrontier(options = {}) {
49
+ const { plan, events } = options;
50
+ const nodeIds = requirePlan(plan);
51
+ const state = projectRuntimeState({ events });
52
+
53
+ if (state.freeze !== null || state.closed) {
54
+ return { dispatchable: [] };
55
+ }
56
+
57
+ const acceptedSet = new Set(state.accepted);
58
+ const runningSet = new Set(state.running);
59
+ // dispatch済み・terminal済み・held済みの除外は現plan epochへscopeする。
60
+ // 旧epochのdispatch/hold記録は、recompile時のcontext失効とredispatch契約
61
+ //(RC3-G)によって新epochでは再配置可能になる。accepted/runningは
62
+ // epoch非依存(物理状態)のまま除外する。
63
+ const heldSet = new Set();
64
+ const terminalSet = new Set();
65
+ const dispatchedSet = new Set();
66
+ for (const event of events) {
67
+ if (event.plan_epoch !== plan.plan_epoch) continue;
68
+ if (event.kind === 'hold_decided') {
69
+ for (const todoId of event.payload?.hold_set ?? []) heldSet.add(todoId);
70
+ }
71
+ if (event.subject?.kind !== 'todo') continue;
72
+ if (event.kind === 'executor_dispatched') dispatchedSet.add(event.subject.ref);
73
+ if (event.kind === 'executor_terminal') terminalSet.add(event.subject.ref);
74
+ }
75
+ const slots = plan.capacity.executors - runningSet.size;
76
+ if (slots < 1) {
77
+ return { dispatchable: [] };
78
+ }
79
+
80
+ const eligible = nodeIds.filter((todoId) => {
81
+ if (acceptedSet.has(todoId) || runningSet.has(todoId) || heldSet.has(todoId)
82
+ || terminalSet.has(todoId) || dispatchedSet.has(todoId)) return false;
83
+ return plan.precedence.every((edge) => (
84
+ edge.to_todo_id !== todoId || acceptedSet.has(edge.from_todo_id)
85
+ ));
86
+ });
87
+
88
+ // running集合だけでなく、同じfrontierで既に選んだnodeとのconflictも検査する。
89
+ // conflict pairを同一dispatch decisionで同時実行しないための貪欲選択(辞書順)。
90
+ const dispatchable = [];
91
+ for (const todoId of sorted(eligible)) {
92
+ if (dispatchable.length >= slots) break;
93
+ const conflictFree = plan.conflicts.every((conflict) => {
94
+ if (!conflict.todo_ids.includes(todoId)) return true;
95
+ return conflict.todo_ids.every((member) => (
96
+ member === todoId || (!runningSet.has(member) && !dispatchable.includes(member))
97
+ ));
98
+ });
99
+ if (conflictFree) dispatchable.push(todoId);
100
+ }
101
+
102
+ return { dispatchable };
103
+ }
104
+
105
+ function declaredWriteCovers(declaredWrites, observedPath) {
106
+ return declaredWrites.some((declared) => (
107
+ declared === observedPath
108
+ || (declared.endsWith('/') && observedPath.startsWith(declared))
109
+ ));
110
+ }
111
+
112
+ function witnessSet(manifest, kinds) {
113
+ const resources = new Set(manifest?.resources ?? []);
114
+ for (const effect of manifest?.state_effects ?? []) {
115
+ if (kinds.includes(effect.kind) && typeof effect.resource_id === 'string') {
116
+ resources.add(effect.resource_id);
117
+ }
118
+ }
119
+ return resources;
120
+ }
121
+
122
+ const LINE_ID = /^[0-9A-Za-z](?:[0-9A-Za-z._-]{0,127})$/;
123
+ const LINE_PATH_CONTROL = /[\u0000-\u001f\u007f]/;
124
+
125
+ function runtimeLinePath(value) {
126
+ return typeof value === 'string' && value.length > 0
127
+ && Buffer.byteLength(value, 'utf8') <= 1_024
128
+ && !LINE_PATH_CONTROL.test(value) && !value.includes('\\')
129
+ && !path.posix.isAbsolute(value) && value === path.posix.normalize(value)
130
+ && !value.split('/').includes('..');
131
+ }
132
+
133
+ function runtimeLineGroups(manifests, todoIds) {
134
+ const groups = new Map();
135
+ for (const todoId of sorted(new Set(todoIds))) {
136
+ const manifest = manifests[todoId];
137
+ if (manifest === null || typeof manifest !== 'object' || Array.isArray(manifest)
138
+ || !Array.isArray(manifest.lines ?? [])) {
139
+ invalidVerification(`boundary manifestのlinesが不正: ${todoId}`);
140
+ }
141
+ const seen = new Set();
142
+ for (const line of manifest.lines ?? []) {
143
+ if (line === null || typeof line !== 'object' || Array.isArray(line)
144
+ || Object.keys(line).sort().join(',') !== 'anchors,line_id,role'
145
+ || !LINE_ID.test(line.line_id ?? '') || !['reads', 'writes'].includes(line.role)
146
+ || !Array.isArray(line.anchors) || line.anchors.length === 0
147
+ || seen.has(line.line_id)) {
148
+ invalidVerification(`line宣言が不正: ${todoId}`);
149
+ }
150
+ seen.add(line.line_id);
151
+ const group = groups.get(line.line_id) ?? {
152
+ readers: new Set(), writers: new Set(), anchorPaths: new Set(),
153
+ };
154
+ group[line.role === 'reads' ? 'readers' : 'writers'].add(todoId);
155
+ for (const anchor of line.anchors) {
156
+ const keys = anchor !== null && typeof anchor === 'object' && !Array.isArray(anchor)
157
+ ? Object.keys(anchor).sort().join(',') : '';
158
+ const valid = anchor?.kind === 'path'
159
+ ? keys === 'kind,path' && runtimeLinePath(anchor.path)
160
+ : anchor?.kind === 'symbol' && keys === 'kind,name,path'
161
+ && typeof anchor.name === 'string' && anchor.name.length > 0
162
+ && runtimeLinePath(anchor.path);
163
+ if (!valid) invalidVerification(`line anchorが不正: ${todoId}/${line.line_id}`);
164
+ // producerと共有せず、観測pathだけから独立に近似する。
165
+ group.anchorPaths.add(anchor.path);
166
+ }
167
+ groups.set(line.line_id, group);
168
+ }
169
+ }
170
+ return groups;
171
+ }
172
+
173
+ /**
174
+ * 観測diffをdeclared path/resource witnessへcross-bindし、closed conflict分類の
175
+ * findingを返す(ADR 0044 Decision 5)。宣言外writeと運転中overlapは別findingで
176
+ * 保存し、silent mergeしない。path非交差でも共有witnessがあれば安全と推測せず
177
+ * unknown findingにする。
178
+ */
179
+ export function classifyObservedDiff(options = {}) {
180
+ const { plan, manifests, observations, relevantTodoIds = null } = options;
181
+ requirePlan(plan);
182
+ if (manifests === null || typeof manifests !== 'object' || Array.isArray(manifests)
183
+ || !Array.isArray(observations)
184
+ || !(relevantTodoIds === null || Array.isArray(relevantTodoIds))) {
185
+ invalidVerification('manifests/observationsが不正');
186
+ }
187
+ const relevant = new Set(relevantTodoIds ?? Object.keys(manifests));
188
+
189
+ const findings = [];
190
+ const observedByTodo = new Map();
191
+ for (const observation of observations) {
192
+ if (observation === null || typeof observation !== 'object'
193
+ || typeof observation.todo_id !== 'string'
194
+ || !Array.isArray(observation.paths)) {
195
+ invalidVerification('observationが不正');
196
+ }
197
+ const manifest = manifests[observation.todo_id];
198
+ if (manifest === undefined) {
199
+ invalidVerification(`boundary manifestがないTODOの観測: ${observation.todo_id}`);
200
+ }
201
+ observedByTodo.set(observation.todo_id, [...new Set(observation.paths)].sort());
202
+ }
203
+
204
+ for (const [todoId, paths] of observedByTodo) {
205
+ const declaredWrites = manifests[todoId].writes ?? [];
206
+ for (const path of paths) {
207
+ if (!declaredWriteCovers(declaredWrites, path)) {
208
+ findings.push({ kind: 'undeclared_write', todo_ids: [todoId], path });
209
+ }
210
+ }
211
+ }
212
+
213
+ const lineScope = new Set([...relevant, ...observedByTodo.keys()]);
214
+ const lineGroups = runtimeLineGroups(manifests, lineScope);
215
+ for (const [todoId, paths] of observedByTodo) {
216
+ const observedPaths = new Set(paths);
217
+ for (const [lineId, group] of lineGroups) {
218
+ if (group.writers.has(todoId)) continue;
219
+ const readers = [...group.readers].filter((readerId) => readerId !== todoId).sort();
220
+ if (readers.length === 0
221
+ || ![...group.anchorPaths].some((anchorPath) => observedPaths.has(anchorPath))) continue;
222
+ findings.push({
223
+ kind: 'observed_line_change',
224
+ todo_ids: [todoId, ...readers].sort(),
225
+ resource_id: lineId,
226
+ });
227
+ }
228
+ }
229
+
230
+ const writersByPath = new Map();
231
+ for (const [todoId, paths] of observedByTodo) {
232
+ for (const path of paths) {
233
+ const writers = writersByPath.get(path) ?? [];
234
+ writers.push(todoId);
235
+ writersByPath.set(path, writers);
236
+ }
237
+ }
238
+ const overlapPairs = new Set();
239
+ const emittedOverlaps = new Set();
240
+ for (const [path, writers] of [...writersByPath.entries()].sort()) {
241
+ if (writers.length < 2) continue;
242
+ const sortedWriters = sorted(writers);
243
+ for (let left = 0; left < sortedWriters.length; left += 1) {
244
+ for (let right = left + 1; right < sortedWriters.length; right += 1) {
245
+ findings.push({
246
+ kind: 'observed_write_conflict',
247
+ todo_ids: [sortedWriters[left], sortedWriters[right]],
248
+ path,
249
+ });
250
+ emittedOverlaps.add(`${sortedWriters[left]} ${sortedWriters[right]} ${path}`);
251
+ overlapPairs.add(`${sortedWriters[left]} ${sortedWriters[right]}`);
252
+ }
253
+ }
254
+ }
255
+
256
+ // 観測pathが、まだ観測を持たない別TODOのdeclared write(prefix宣言含む)へ
257
+ // 到達する場合も後発write conflictとして保存する(片側観測だけの
258
+ // late conflictを安全と推測しない。RC3-F採用のverifier意味論補完)。
259
+ for (const [todoId, paths] of observedByTodo) {
260
+ for (const [otherId, manifest] of Object.entries(manifests)) {
261
+ if (otherId === todoId) continue;
262
+ if (!relevant.has(otherId)) continue;
263
+ const otherDeclared = [...(manifest.reads ?? []), ...(manifest.writes ?? [])];
264
+ for (const path of paths) {
265
+ if (!declaredWriteCovers(otherDeclared, path)) continue;
266
+ const pair = sorted([todoId, otherId]);
267
+ const key = `${pair[0]} ${pair[1]} ${path}`;
268
+ if (emittedOverlaps.has(key)) continue;
269
+ emittedOverlaps.add(key);
270
+ findings.push({
271
+ kind: 'observed_write_conflict',
272
+ todo_ids: pair,
273
+ path,
274
+ });
275
+ overlapPairs.add(`${pair[0]} ${pair[1]}`);
276
+ }
277
+ }
278
+ }
279
+
280
+ const todoIds = sorted(observedByTodo.keys());
281
+ for (let left = 0; left < todoIds.length; left += 1) {
282
+ for (let right = left + 1; right < todoIds.length; right += 1) {
283
+ const pairKey = `${todoIds[left]} ${todoIds[right]}`;
284
+ if (overlapPairs.has(pairKey)) continue;
285
+ // dynamic/semantic/effect unknownを持つTODOは、path非交差でも独立性を
286
+ // 主張できない(ADR 0044 Decision 1.5・5.1)。unknown由来のpairをevidence
287
+ // acquisition対象のsemantic conflict unknownとして保存する。
288
+ const declaredUnknowns = [
289
+ ...(manifests[todoIds[left]].unknowns ?? []),
290
+ ...(manifests[todoIds[right]].unknowns ?? []),
291
+ ];
292
+ if (declaredUnknowns.length > 0) {
293
+ findings.push({
294
+ kind: 'semantic_conflict_unknown',
295
+ todo_ids: [todoIds[left], todoIds[right]],
296
+ resource_id: null,
297
+ unknowns: structuredClone(declaredUnknowns),
298
+ });
299
+ }
300
+ for (const [kind, witnessKinds] of [
301
+ ['semantic_conflict_unknown', ['state', 'schema', 'invariant']],
302
+ ['effect_conflict_unknown', ['effect', 'external_effect']],
303
+ ]) {
304
+ const leftWitness = kind === 'semantic_conflict_unknown'
305
+ ? witnessSet(manifests[todoIds[left]], witnessKinds)
306
+ : witnessSet({ state_effects: manifests[todoIds[left]].state_effects }, witnessKinds);
307
+ const rightWitness = kind === 'semantic_conflict_unknown'
308
+ ? witnessSet(manifests[todoIds[right]], witnessKinds)
309
+ : witnessSet({ state_effects: manifests[todoIds[right]].state_effects }, witnessKinds);
310
+ for (const resourceId of sorted(leftWitness)) {
311
+ if (rightWitness.has(resourceId)) {
312
+ findings.push({
313
+ kind,
314
+ todo_ids: [todoIds[left], todoIds[right]],
315
+ resource_id: resourceId,
316
+ });
317
+ }
318
+ }
319
+ }
320
+ }
321
+ }
322
+
323
+ return { findings };
324
+ }
325
+
326
+ function affectedClosure(plan, seedTodoIds, manifests) {
327
+ const closure = new Set(seedTodoIds);
328
+ // manifestsが与えられた場合、同一resource witnessへ到達するTODOをclosureへ
329
+ // 含める(ADR 0044 Decision 6.3の第三要素)。resource→TODOのindexを先に作る。
330
+ const byResource = new Map();
331
+ if (manifests !== undefined) {
332
+ for (const [todoId, manifest] of Object.entries(manifests)) {
333
+ for (const resourceId of witnessSet(manifest, [
334
+ 'state', 'schema', 'invariant', 'effect', 'external_effect',
335
+ ])) {
336
+ const members = byResource.get(resourceId) ?? new Set();
337
+ members.add(todoId);
338
+ byResource.set(resourceId, members);
339
+ }
340
+ }
341
+ }
342
+ let changed = true;
343
+ while (changed) {
344
+ changed = false;
345
+ for (const conflict of plan.conflicts) {
346
+ const [left, right] = conflict.todo_ids;
347
+ if (closure.has(left) !== closure.has(right)) {
348
+ closure.add(left);
349
+ closure.add(right);
350
+ changed = true;
351
+ }
352
+ }
353
+ for (const edge of plan.precedence) {
354
+ if (closure.has(edge.from_todo_id) && !closure.has(edge.to_todo_id)) {
355
+ closure.add(edge.to_todo_id);
356
+ changed = true;
357
+ }
358
+ }
359
+ for (const members of byResource.values()) {
360
+ if ([...members].some((todoId) => closure.has(todoId))) {
361
+ for (const todoId of members) {
362
+ if (!closure.has(todoId)) {
363
+ closure.add(todoId);
364
+ changed = true;
365
+ }
366
+ }
367
+ }
368
+ }
369
+ }
370
+ return closure;
371
+ }
372
+
373
+ /**
374
+ * carry_over_witnessed eventが埋め込むwitness documentをschema・自己digest・
375
+ * 帰属で検証する。event payloadは`{ witness_digest, witness }`を持ち、witnessは
376
+ * `lattice.carry_over_witness.v1`の完全documentでなければならない。
377
+ * invariant digestと提供bytesの再照合はverifyCarryOverWitnessが行う(RC3-Gで
378
+ * 保存artifactと配線する)。ここでは存在・shape・digest整合を実証できない
379
+ * witnessをすべて不成立として扱う(fail closed)。
380
+ */
381
+ function witnessedForContinue(witnessRecord, todoId) {
382
+ if (witnessRecord === undefined) return false;
383
+ const payload = witnessRecord.payload;
384
+ const witness = payload?.witness;
385
+ if (witness === null || witness === undefined) return false;
386
+ if (!validateCarryOverWitness(witness)) return false;
387
+ if (witness.todo_id !== todoId) return false;
388
+ if (typeof payload.witness_digest !== 'string'
389
+ || payload.witness_digest !== witness.witness_digest) {
390
+ return false;
391
+ }
392
+ return true;
393
+ }
394
+
395
+ /**
396
+ * frozen prefixからaffected closureとhold/continue集合を再計算する
397
+ * (ADR 0044 Decision 6・7.2)。closure外のrunning TODOでも、schema・自己digest
398
+ * 整合まで実証できたcarry-over witnessが保存bytesに存在しなければcontinueできない
399
+ * (fail closed)。manifestsを渡すと同一resource witness到達もclosureへ含める。
400
+ */
401
+ export function recomputeHoldDecision(options = {}) {
402
+ const { plan, events, manifests } = options;
403
+ requirePlan(plan);
404
+ const state = projectRuntimeState({ events });
405
+ if (state.freeze === null) {
406
+ invalidVerification('intake_frozen eventのないprefixからhold decisionを再計算できない');
407
+ }
408
+ const freezeSequence = state.freeze.sequence;
409
+
410
+ // seedは現plan epochのconflictだけ(過去epochの処理済みconflictを再seedしない)。
411
+ const currentEpochConflictSequences = new Set(events
412
+ .filter((event) => event.kind === 'conflict_found' && event.plan_epoch === plan.plan_epoch)
413
+ .map((event) => event.sequence));
414
+ const frozenConflicts = state.conflicts.filter((conflict) => (
415
+ conflict.sequence <= freezeSequence && currentEpochConflictSequences.has(conflict.sequence)
416
+ ));
417
+ if (frozenConflicts.length === 0) {
418
+ invalidVerification('frozen prefix内にconflict findingがない');
419
+ }
420
+ const finding = frozenConflicts.at(-1);
421
+ const seed = frozenConflicts.flatMap((conflict) => conflict.todo_ids ?? []);
422
+ const closure = affectedClosure(plan, seed, manifests);
423
+
424
+ const frozenRunning = new Set();
425
+ for (const event of events) {
426
+ if (event.sequence > freezeSequence) continue;
427
+ if (event.kind === 'executor_dispatched' && event.subject?.kind === 'todo') {
428
+ frozenRunning.add(event.subject.ref);
429
+ }
430
+ if ((event.kind === 'receipt_accepted' || event.kind === 'executor_terminal')
431
+ && event.subject?.kind === 'todo') {
432
+ frozenRunning.delete(event.subject.ref);
433
+ }
434
+ }
435
+
436
+ const holdSet = new Set();
437
+ const continueSet = new Set();
438
+ const reasons = {};
439
+ for (const todoId of frozenRunning) {
440
+ if (closure.has(todoId)) {
441
+ holdSet.add(todoId);
442
+ reasons[todoId] = 'affected_closure';
443
+ } else if (witnessedForContinue(state.witnesses[todoId], todoId)) {
444
+ continueSet.add(todoId);
445
+ } else {
446
+ holdSet.add(todoId);
447
+ reasons[todoId] = 'carry_over_unprovable';
448
+ }
449
+ }
450
+ for (const todoId of closure) {
451
+ if (!holdSet.has(todoId) && !continueSet.has(todoId)) {
452
+ holdSet.add(todoId);
453
+ reasons[todoId] = 'affected_closure';
454
+ }
455
+ }
456
+
457
+ return {
458
+ finding: structuredClone(finding),
459
+ frozen_prefix_sequence: freezeSequence,
460
+ hold_set: sorted(holdSet),
461
+ continue_set: sorted(continueSet),
462
+ reasons,
463
+ };
464
+ }
465
+
466
+ /**
467
+ * receipt受理裁定をevent prefixから再計算する(ADR 0044 Decision 7.4)。
468
+ * 「rebind前」「rebind後」は生成時刻でなくevent sequenceで判定する。
469
+ * freeze後のvN epoch receiptはwitness bindingの有無によらずstale_contextで
470
+ * rejectし、frozen prefix内のpending receiptはwitness bindingがある場合だけ
471
+ * 受理する。証明できない受理はすべてrejectへ倒す(fail closed)。
472
+ */
473
+ const RECEIPT_BINDING_FIELDS = Object.freeze([
474
+ 'executor_handle',
475
+ 'worktree_id',
476
+ 'base_sha',
477
+ 'packet_digest',
478
+ 'checkpoint_digest',
479
+ ]);
480
+
481
+ export function recomputeReceiptDecisions(options = {}) {
482
+ const { plan, events } = options;
483
+ requirePlan(plan);
484
+ const state = projectRuntimeState({ events });
485
+ // freezeはresumeで消えない永続境界として扱う。resume後もvN epoch receiptの
486
+ // stale判定は最初のfreeze境界に対して行う(受理へ反転させない)。
487
+ // 境界は検証対象planのepochで発生したfreezeへscopeする: vN+1 epochの
488
+ // receiptをvNのfreezeで塞がない(rebind後のvN+1 receipt受理、RC3-G)。
489
+ const freezes = events
490
+ .filter((event) => event.kind === 'intake_frozen' && event.plan_epoch === plan.plan_epoch)
491
+ .map((event) => event.sequence)
492
+ .sort((left, right) => left - right);
493
+ const freezeBoundary = freezes.length === 0 ? null : freezes[0];
494
+
495
+ const seenReceiptIds = new Set();
496
+ const decisions = state.receipts.map((receipt) => {
497
+ const base = {
498
+ receipt_id: receipt.receipt_id,
499
+ todo_id: receipt.todo_id,
500
+ sequence: receipt.sequence,
501
+ };
502
+ const reject = (detail) => (
503
+ { ...base, decision: 'rejected', reason: 'stale_context', detail }
504
+ );
505
+
506
+ // receipt_idの再利用は帰属の一意性を壊すためreject(先着だけを裁定対象にする)。
507
+ if (seenReceiptIds.has(receipt.receipt_id)) {
508
+ return reject('duplicate_receipt_id');
509
+ }
510
+ seenReceiptIds.add(receipt.receipt_id);
511
+
512
+ // 帰属照合の基準はdispatch記録であり、executor自己申告ではない(Decision 7.4)。
513
+ const payload = receipt.payload ?? {};
514
+ if (RECEIPT_BINDING_FIELDS.some((field) => typeof payload[field] !== 'string')) {
515
+ return reject('binding_missing');
516
+ }
517
+ // dispatch記録は「receipt記録時点までの最後のdispatch」を使う(裁定時点の
518
+ // prefix意味論。全投影の最後勝ちにすると、後続redispatchが過去receiptの
519
+ // 帰属先を書き換えてproducer裁定とdivergeする。RC3-J P1採用)。
520
+ const dispatchEventForReceipt = events.findLast((event) => (
521
+ event.kind === 'executor_dispatched'
522
+ && event.subject?.kind === 'todo'
523
+ && event.subject.ref === receipt.todo_id
524
+ && event.sequence < receipt.sequence
525
+ ));
526
+ const dispatch = dispatchEventForReceipt === undefined
527
+ ? undefined
528
+ : { sequence: dispatchEventForReceipt.sequence, payload: dispatchEventForReceipt.payload };
529
+ const originBindingRetained = (() => {
530
+ if (!Number.isSafeInteger(receipt.plan_epoch) || receipt.plan_epoch >= plan.plan_epoch) return false;
531
+ for (let epoch = receipt.plan_epoch; epoch < plan.plan_epoch; epoch += 1) {
532
+ const witnessed = events.some((event) => event.sequence < receipt.sequence
533
+ && event.kind === 'carry_over_witnessed' && event.plan_epoch === epoch
534
+ && event.subject?.kind === 'todo' && event.subject.ref === receipt.todo_id);
535
+ const continued = events.some((event) => event.sequence < receipt.sequence
536
+ && event.kind === 'hold_decided' && event.plan_epoch === epoch
537
+ && event.payload?.continue_set?.includes(receipt.todo_id));
538
+ const recompiled = events.some((event) => event.sequence < receipt.sequence
539
+ && event.kind === 'plan_recompiled' && event.plan_epoch === epoch + 1);
540
+ const invalidated = events.some((event) => event.sequence < receipt.sequence
541
+ && event.kind === 'context_invalidated' && event.plan_epoch === epoch + 1
542
+ && event.subject?.kind === 'todo' && event.subject.ref === receipt.todo_id);
543
+ if (!witnessed || !continued || !recompiled || invalidated) return false;
544
+ }
545
+ return true;
546
+ })();
547
+ if (dispatch === undefined) {
548
+ return reject('not_dispatched');
549
+ }
550
+ const dispatchPayload = dispatch.payload ?? {};
551
+ if (payload.executor_handle !== dispatchPayload.executor_handle
552
+ || payload.worktree_id !== dispatchPayload.worktree_id
553
+ || payload.packet_digest !== dispatchPayload.packet_digest) {
554
+ return reject('binding_mismatch');
555
+ }
556
+ if (typeof plan.base_sha === 'string' && payload.base_sha !== plan.base_sha) {
557
+ return reject('base_mismatch');
558
+ }
559
+ if (receipt.plan_epoch !== plan.plan_epoch && !originBindingRetained) {
560
+ return reject('epoch_mismatch');
561
+ }
562
+ // dispatchが旧epochのTODOが現epoch receiptを名乗る場合はepoch_rebound必須
563
+ // (rebindなしのepoch自称を受理しない。Decision 7.3/7.4)。
564
+ if (dispatchEventForReceipt.plan_epoch !== plan.plan_epoch && !originBindingRetained) {
565
+ const rebound = state.rebinds[receipt.todo_id];
566
+ if (rebound === undefined
567
+ || rebound.payload?.new_plan_epoch !== plan.plan_epoch
568
+ || rebound.sequence >= receipt.sequence) {
569
+ return reject('unrebound_epoch');
570
+ }
571
+ }
572
+ // checkpoint観測が存在するTODOは、receipt以前の最後の観測checkpointとの
573
+ // digest一致とobserved_diff一致を要求(executor自己申告をbinding証拠に
574
+ // しない。RC3-F)。bindingは同一dispatch attemptへscope=最後のdispatch
575
+ // 以降のcheckpointだけが対象(redispatch後のreceiptを旧attemptの
576
+ // checkpointで塞がない。RC3-G)。
577
+ const observedCheckpoints = state.checkpoints.filter((entry) => (
578
+ entry.todo_id === receipt.todo_id
579
+ && entry.sequence > dispatch.sequence
580
+ && entry.sequence < receipt.sequence
581
+ // supervisorがI/O警報を確かめるために撮ったcheckpointはexecutorの申告境界では
582
+ // ないので、bindingの基準にしない(ADR 0143。engine側と同一規則)。
583
+ && entry.payload?.observed_by !== 'supervisor_probe'
584
+ ));
585
+ if (observedCheckpoints.length > 0) {
586
+ const last = observedCheckpoints[observedCheckpoints.length - 1].payload;
587
+ if (typeof last?.checkpoint_digest === 'string'
588
+ && payload.checkpoint_digest !== last.checkpoint_digest) {
589
+ return reject('checkpoint_mismatch');
590
+ }
591
+ if (last?.diff?.entries !== undefined && Array.isArray(last.diff.entries)) {
592
+ const expected = JSON.stringify(last.diff.entries.map(({ path, change }) => ({ path, change })));
593
+ const reported = JSON.stringify((payload.observed_diff ?? []).map(({ path, change }) => ({ path, change })));
594
+ if (expected !== reported) {
595
+ return reject('checkpoint_mismatch');
596
+ }
597
+ }
598
+ }
599
+ if (receipt.rejected_sequence !== null) {
600
+ return reject('recorded_rejection');
601
+ }
602
+ if (freezeBoundary === null) {
603
+ return { ...base, decision: 'accepted', reason: null, detail: null };
604
+ }
605
+ if (receipt.accepted_sequence !== null && receipt.accepted_sequence <= freezeBoundary) {
606
+ return { ...base, decision: 'accepted', reason: null, detail: null };
607
+ }
608
+ if (receipt.sequence > freezeBoundary) {
609
+ return reject('post_freeze');
610
+ }
611
+ // frozen prefix内のpending receiptは、実証済みwitness documentの
612
+ // receipt_bindingsが当該receiptをbindする場合だけ有効になる(Decision 7.5)。
613
+ const witnessRecord = state.witnesses[receipt.todo_id];
614
+ if (!witnessedForContinue(witnessRecord, receipt.todo_id)) {
615
+ return reject('witness_unproven');
616
+ }
617
+ const bound = witnessRecord.payload.witness.receipt_bindings.some((binding) => (
618
+ binding.receipt_id === receipt.receipt_id
619
+ && binding.recorded_sequence === receipt.sequence
620
+ && binding.within_frozen_prefix === true
621
+ ));
622
+ if (bound) {
623
+ return { ...base, decision: 'accepted', reason: null, detail: null };
624
+ }
625
+ return reject('unbound_receipt');
626
+ });
627
+
628
+ return { decisions };
629
+ }
630
+
631
+ const INVARIANT_DIGEST_FIELDS = Object.freeze([
632
+ 'todo_input',
633
+ 'boundary_manifest',
634
+ 'validator',
635
+ 'context_content',
636
+ ]);
637
+
638
+ /**
639
+ * carry-over witnessのinvariant digestを提供bytesから再計算する
640
+ * (ADR 0044 Decision 7.2・7.5)。1 fieldでも証明不能ならvalid falseとし、
641
+ * reasonsへ具体的なfailureと`carry_over_unprovable`を返す。
642
+ */
643
+ export function verifyCarryOverWitness(options = {}) {
644
+ const { witness, sources } = options;
645
+ const reasons = [];
646
+ if (witness === null || typeof witness !== 'object' || Array.isArray(witness)
647
+ || witness.invariant_digests === null
648
+ || typeof witness.invariant_digests !== 'object') {
649
+ return { valid: false, reasons: ['witness_shape', 'carry_over_unprovable'] };
650
+ }
651
+ if (sources === null || typeof sources !== 'object' || Array.isArray(sources)) {
652
+ return { valid: false, reasons: ['sources_shape', 'carry_over_unprovable'] };
653
+ }
654
+
655
+ if (!Number.isSafeInteger(witness.predecessor_epoch)
656
+ || !Number.isSafeInteger(witness.successor_epoch)
657
+ || witness.successor_epoch <= witness.predecessor_epoch) {
658
+ reasons.push('epoch_ordering');
659
+ }
660
+
661
+ for (const field of INVARIANT_DIGEST_FIELDS) {
662
+ if (sources[field] === undefined) {
663
+ reasons.push(`missing_source:${field}`);
664
+ continue;
665
+ }
666
+ let recomputed = null;
667
+ try {
668
+ recomputed = digestArtifact(sources[field]);
669
+ } catch {
670
+ reasons.push(`source_not_canonical:${field}`);
671
+ continue;
672
+ }
673
+ if (witness.invariant_digests[field] !== recomputed) {
674
+ reasons.push(`invariant_digest_mismatch:${field}`);
675
+ }
676
+ }
677
+
678
+ if (typeof witness.witness_digest === 'string') {
679
+ const projection = {};
680
+ for (const key of Object.keys(witness)) {
681
+ if (key !== 'witness_digest') projection[key] = witness[key];
682
+ }
683
+ let selfDigest = null;
684
+ try {
685
+ selfDigest = digestArtifact(projection);
686
+ } catch {
687
+ selfDigest = null;
688
+ }
689
+ if (witness.witness_digest !== selfDigest) {
690
+ reasons.push('witness_digest_mismatch');
691
+ }
692
+ } else {
693
+ reasons.push('witness_digest_mismatch');
694
+ }
695
+
696
+ if (reasons.length > 0) {
697
+ reasons.push('carry_over_unprovable');
698
+ return { valid: false, reasons };
699
+ }
700
+ return { valid: true, reasons: [] };
701
+ }