@quolu/lattice 0.52.0 → 0.52.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +147 -147
- package/README.ja.md +355 -355
- package/README.md +258 -258
- package/bin/lattice-mcp.mjs +0 -0
- package/bin/lattice-scripted-adapter.mjs +0 -0
- package/bin/lattice-scripted-worker.mjs +0 -0
- package/bin/lattice-work-order-adapter.mjs +0 -0
- package/bin/lattice.mjs +0 -0
- package/docs/bridge-setup.md +132 -132
- package/docs/schemas/lattice.executor_packet.v1.schema.json +57 -57
- package/docs/schemas/lattice.executor_receipt.v1.schema.json +66 -66
- package/docs/schemas/lattice.phase_todo_revision.v3.schema.json +360 -360
- package/docs/schemas/lattice.plan_create_input.v1.schema.json +56 -56
- package/docs/schemas/lattice.plan_create_input.v2.schema.json +72 -72
- package/docs/schemas/lattice.plan_create_input.v3.schema.json +81 -81
- package/docs/schemas/lattice.plan_create_input.v4.schema.json +85 -85
- package/docs/schemas/lattice.run_request.v1.schema.json +238 -238
- package/docs/schemas/lattice.runtime_adapter_capabilities.v2.schema.json +55 -55
- package/docs/schemas/lattice.runtime_adapter_registration_input.v1.schema.json +78 -78
- package/docs/schemas/lattice.runtime_adapter_registration_input.v2.schema.json +86 -86
- package/docs/schemas/lattice.todo_extraction.v2.schema.json +298 -298
- package/docs/schemas/lattice.todo_extraction.v3.schema.json +146 -146
- package/docs/schemas/lattice.todo_revision.v2.schema.json +260 -260
- package/docs/schemas/lattice.todo_revision_set.v3.schema.json +363 -363
- package/package.json +103 -103
- package/sensor/LICENSE +21 -21
- package/sensor/NOTICE +19 -19
- package/sensor/dist/bin/lattice-sensor.js +9 -9
- package/sensor/dist/db/index.js +24 -24
- package/sensor/dist/db/migrations.js +41 -41
- package/sensor/dist/db/queries.js +164 -164
- package/sensor/dist/db/schema.sql +205 -205
- package/sensor/dist/directory.js +5 -5
- package/sensor/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-go.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-java.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-javascript.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-python.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-tsx.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-typescript.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
- package/sensor/dist/mcp/liveness-watchdog.js +53 -53
- package/sensor/dist/mcp/server-instructions.js +95 -95
- package/sensor/package.json +56 -56
- 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-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/hash-chain.mjs +76 -76
- package/src/hooks-cli.mjs +1057 -1053
- package/src/isolation-runner.mjs +409 -409
- package/src/node-version-guard.mjs +44 -44
- package/src/project-cli.mjs +697 -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 -1502
- 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 -520
- package/src/runtime-cli.mjs +4588 -4584
- package/src/runtime-contracts.mjs +824 -824
- package/src/runtime-control-store.mjs +604 -600
- 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 -162
- 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 -477
- package/src/runtime-hold-recompile.mjs +916 -916
- package/src/runtime-io-sentinel.mjs +391 -391
- package/src/runtime-lifecycle-lock.mjs +294 -290
- package/src/runtime-managed-supervisor.mjs +1490 -1490
- package/src/runtime-multi-epoch-store.mjs +838 -834
- package/src/runtime-projection.mjs +269 -269
- package/src/runtime-pull-intake.mjs +1192 -1188
- package/src/runtime-scripted-adapter-controller.mjs +1160 -1156
- 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 -1167
- 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 +3141 -3126
- 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 -625
- 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 +4322 -4314
- package/src/treatment-compiler.mjs +728 -728
- package/src/treatment-runner.mjs +656 -656
- 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
|
+
}
|