@quolu/lattice 0.52.3 → 0.52.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/artifact-contracts-v2.mjs +325 -325
- package/src/artifact-contracts.mjs +895 -895
- package/src/boundary-compiler.mjs +712 -712
- package/src/boundary-observation-compiler-v2.mjs +344 -344
- package/src/bounded-seam.mjs +230 -230
- package/src/bridge-address.mjs +107 -107
- package/src/bridge-cli.mjs +297 -297
- package/src/bridge-config.mjs +346 -346
- package/src/bridge-daemon.mjs +378 -378
- package/src/bridge-hub-protocol.mjs +198 -0
- package/src/bridge-launch-agent.mjs +323 -323
- package/src/bridge-registrar.mjs +102 -102
- package/src/bridge-server.mjs +376 -376
- package/src/cli-help.mjs +308 -308
- package/src/cli-stdio.mjs +40 -40
- package/src/control-compiler.mjs +532 -532
- package/src/dag-chain.mjs +261 -261
- package/src/factory-diagnostics.mjs +188 -188
- package/src/git-process.mjs +72 -0
- package/src/hash-chain.mjs +76 -76
- package/src/hooks-cli.mjs +1057 -1057
- package/src/isolation-runner.mjs +409 -409
- package/src/node-version-guard.mjs +44 -44
- package/src/project-cli.mjs +3 -3
- package/src/project-identity.mjs +130 -130
- package/src/rc1-black-box-oracle.mjs +906 -906
- package/src/rc1-comparison.mjs +154 -154
- package/src/rc1-evidence-bundle.mjs +456 -456
- package/src/rc1-v4-campaign.mjs +708 -708
- package/src/rc1-v4-transform.mjs +467 -467
- package/src/rc1-v5-artifact-set.mjs +855 -855
- package/src/rc1-v5-behavior-evidence.mjs +594 -594
- package/src/rc1-v5-campaign.mjs +797 -797
- package/src/rc1-v5-transform.mjs +421 -421
- package/src/rc1-v6-artifact-set.mjs +807 -807
- package/src/rc1-v6-behavior-evidence.mjs +273 -273
- package/src/rc1-v6-campaign.mjs +623 -623
- package/src/rc1-v6-causal-binding.mjs +473 -473
- package/src/rc1-v6-measurement.mjs +313 -313
- package/src/rc2-artifact-set.mjs +1584 -1584
- package/src/rc2-campaign.mjs +1506 -1506
- package/src/rc2-delivery-policy-front-end.mjs +1079 -1079
- package/src/rc2-delivery-policy-oracle.mjs +134 -134
- package/src/rc2-delivery-policy-transform.mjs +1127 -1127
- package/src/rc2-rc1-transfer-front-end.mjs +511 -511
- package/src/rc3-actual-dogfood.mjs +652 -652
- package/src/rc3-dogfood-scaffold.mjs +315 -315
- package/src/rc3-scripted-campaign.mjs +1383 -1383
- package/src/rc4-stage1-dogfood.mjs +673 -673
- package/src/runtime-adapter-registry.mjs +524 -524
- package/src/runtime-cli.mjs +4588 -4588
- package/src/runtime-contracts.mjs +824 -824
- package/src/runtime-control-store.mjs +604 -604
- package/src/runtime-controller-protocol.mjs +587 -587
- package/src/runtime-decision-verifier.mjs +701 -701
- package/src/runtime-diff-observer.mjs +361 -361
- package/src/runtime-direct-os-observer.mjs +301 -301
- package/src/runtime-driver-state.mjs +166 -166
- package/src/runtime-engine.mjs +779 -779
- package/src/runtime-errors.mjs +356 -356
- package/src/runtime-event-store.mjs +189 -189
- package/src/runtime-front-end.mjs +925 -925
- package/src/runtime-gate-store.mjs +481 -481
- package/src/runtime-hold-recompile.mjs +916 -916
- package/src/runtime-io-sentinel.mjs +391 -391
- package/src/runtime-lifecycle-lock.mjs +294 -294
- package/src/runtime-managed-supervisor.mjs +1490 -1490
- package/src/runtime-multi-epoch-store.mjs +838 -838
- package/src/runtime-projection.mjs +269 -269
- package/src/runtime-pull-intake.mjs +1192 -1192
- package/src/runtime-scripted-adapter-controller.mjs +1160 -1160
- package/src/runtime-scripted-executor.mjs +163 -163
- package/src/runtime-scripted-worktree.mjs +104 -104
- package/src/runtime-seam-resolve.mjs +428 -428
- package/src/runtime-seam-treatment.mjs +173 -173
- package/src/runtime-socket-owner.mjs +125 -125
- package/src/runtime-work-order-contracts.mjs +91 -91
- package/src/runtime-work-order-controller.mjs +1171 -1171
- package/src/runtime-worktree-executor.mjs +199 -199
- package/src/schedulability-compiler-v2.mjs +303 -303
- package/src/schedulability-verifier-v2.mjs +317 -317
- package/src/seam-apply.mjs +549 -549
- package/src/seam-commit-shared.mjs +22 -22
- package/src/seam-commit-transform.mjs +81 -81
- package/src/seam-commit.mjs +18 -18
- package/src/seam-cost.mjs +322 -322
- package/src/seam-derivation.mjs +188 -188
- package/src/seam-gate.mjs +146 -146
- package/src/seam-proposal-contracts.mjs +446 -446
- package/src/seam-proposal-queries.mjs +521 -521
- package/src/seam-proposal.mjs +2011 -2011
- package/src/seam-ref.mjs +33 -33
- package/src/seam-rewrite.mjs +286 -286
- package/src/seam-transform.mjs +554 -554
- package/src/seam-verification.mjs +260 -260
- package/src/sensor-adapter.mjs +432 -432
- package/src/sensor-cli.mjs +139 -139
- package/src/sensor-diff.mjs +661 -661
- package/src/sensor-node-runtime.mjs +53 -53
- package/src/sensor-runtime.mjs +52 -52
- package/src/timestamp-contract.mjs +8 -8
- package/src/todo-audit-pending.mjs +91 -91
- package/src/todo-chain.mjs +178 -178
- package/src/todo-cli.mjs +8 -8
- package/src/todo-contracts.mjs +728 -728
- package/src/todo-dashboard-registry.mjs +573 -573
- package/src/todo-dispatch-shape.mjs +190 -190
- package/src/todo-gantt-html-independence.mjs +239 -239
- package/src/todo-gantt-html-shared.mjs +226 -226
- package/src/todo-gantt-html-style.mjs +131 -131
- package/src/todo-gantt-html.mjs +248 -248
- package/src/todo-gantt-layout.mjs +974 -974
- package/src/todo-gantt-live.mjs +361 -361
- package/src/todo-gantt-nested.mjs +263 -263
- package/src/todo-gantt-presentation.mjs +217 -217
- package/src/todo-gantt-scope.mjs +123 -123
- package/src/todo-gantt-svg.mjs +353 -353
- package/src/todo-independence-contracts.mjs +595 -595
- package/src/todo-independence-guidance.mjs +322 -322
- package/src/todo-independence.mjs +640 -640
- package/src/todo-markdown-renderer.mjs +260 -260
- package/src/todo-migration.mjs +448 -448
- package/src/todo-narrative-anchor.mjs +130 -130
- package/src/todo-note-store.mjs +629 -629
- package/src/todo-parallel-candidates.mjs +114 -114
- package/src/todo-revision.mjs +995 -995
- package/src/todo-split.mjs +472 -472
- package/src/todo-status.mjs +690 -690
- package/src/todo-store-git-transaction.mjs +418 -418
- package/src/todo-store.mjs +42 -21
- package/src/treatment-compiler.mjs +728 -728
- package/src/treatment-runner.mjs +656 -656
- package/src/witness-scaffold.mjs +180 -180
|
@@ -1,824 +1,824 @@
|
|
|
1
|
-
import { canonicalizeArtifact, digestArtifact } from './artifact-contracts.mjs';
|
|
2
|
-
import { isCanonicalUtcTimestamp } from './timestamp-contract.mjs';
|
|
3
|
-
|
|
4
|
-
// RC3 runtime契約(ADR 0044 Decision 2・3・5・7)。
|
|
5
|
-
// RC2公開済みschemaを変更しない加算moduleであり、全schemaはexact key、
|
|
6
|
-
// bounded collection、canonical serialization、自己digest規則をfail closedで検査する。
|
|
7
|
-
// field追加・意味変更はschema versionを上げ、新しいADRで裁定する。
|
|
8
|
-
|
|
9
|
-
const SHA256 = /^[0-9a-f]{64}$/;
|
|
10
|
-
const SHA1 = /^[0-9a-f]{40}$/;
|
|
11
|
-
const IDENTIFIER = /^[0-9A-Za-z](?:[0-9A-Za-z._-]{0,127})$/;
|
|
12
|
-
const MAX_COLLECTION = 256;
|
|
13
|
-
const MAX_NODES_PER_PLAN = 8;
|
|
14
|
-
|
|
15
|
-
// 人間向けaudit reasonは表示を偽装できる制御文字を拒否しつつ、通常のUnicode説明を保持する。
|
|
16
|
-
const AUDIT_BIDI_CONTROLS = /[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/u;
|
|
17
|
-
|
|
18
|
-
export function validRuntimeAbandonReason(value) {
|
|
19
|
-
return typeof value === 'string'
|
|
20
|
-
&& value === value.trim()
|
|
21
|
-
&& [...value].length >= 1
|
|
22
|
-
&& [...value].length <= 256
|
|
23
|
-
&& !/\p{Cc}/u.test(value)
|
|
24
|
-
&& !/[\u2028\u2029]/u.test(value)
|
|
25
|
-
&& !AUDIT_BIDI_CONTROLS.test(value);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// ADR 0044 Decision 3.2のclosed event kind set。拡張はrun_event.v2+新ADRでだけ行う。
|
|
29
|
-
export const RUN_EVENT_KINDS = Object.freeze([
|
|
30
|
-
'run_initialized',
|
|
31
|
-
'plan_compiled',
|
|
32
|
-
'plan_verified',
|
|
33
|
-
'dispatch_decided',
|
|
34
|
-
'executor_dispatched',
|
|
35
|
-
'checkpoint_observed',
|
|
36
|
-
'receipt_recorded',
|
|
37
|
-
'conflict_found',
|
|
38
|
-
'intake_frozen',
|
|
39
|
-
'hold_decided',
|
|
40
|
-
'carry_over_witnessed',
|
|
41
|
-
'epoch_rebound',
|
|
42
|
-
'context_invalidated',
|
|
43
|
-
'plan_recompiled',
|
|
44
|
-
'intake_resumed',
|
|
45
|
-
'receipt_accepted',
|
|
46
|
-
'receipt_rejected',
|
|
47
|
-
'executor_terminal',
|
|
48
|
-
'run_closed',
|
|
49
|
-
]);
|
|
50
|
-
|
|
51
|
-
// ADR 0044 Decision 5のclosed conflict分類と、Decision 7.5のhold理由kind。
|
|
52
|
-
export const RUNTIME_CONFLICT_KINDS = Object.freeze([
|
|
53
|
-
'observed_write_conflict',
|
|
54
|
-
'observed_line_change',
|
|
55
|
-
'semantic_conflict_unknown',
|
|
56
|
-
'effect_conflict_unknown',
|
|
57
|
-
'undeclared_write',
|
|
58
|
-
'stale_context',
|
|
59
|
-
]);
|
|
60
|
-
export const HOLD_REASON_KINDS = Object.freeze([
|
|
61
|
-
...RUNTIME_CONFLICT_KINDS,
|
|
62
|
-
'carry_over_unprovable',
|
|
63
|
-
]);
|
|
64
|
-
|
|
65
|
-
function exactRecord(value, keys) {
|
|
66
|
-
if (value === null || typeof value !== 'object' || Array.isArray(value)
|
|
67
|
-
|| Object.getPrototypeOf(value) !== Object.prototype) {
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
const actual = Object.keys(value).sort();
|
|
71
|
-
const expected = [...keys].sort();
|
|
72
|
-
return actual.length === expected.length
|
|
73
|
-
&& actual.every((key, index) => key === expected[index]);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function plainObject(value) {
|
|
77
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
78
|
-
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function digest(value) {
|
|
82
|
-
return typeof value === 'string' && SHA256.test(value);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function gitSha(value) {
|
|
86
|
-
return typeof value === 'string' && SHA1.test(value);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function identifier(value) {
|
|
90
|
-
return typeof value === 'string' && IDENTIFIER.test(value);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function timestamp(value) {
|
|
94
|
-
return isCanonicalUtcTimestamp(value);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function nonNegativeInteger(value) {
|
|
98
|
-
return Number.isSafeInteger(value) && value >= 0;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function positiveInteger(value) {
|
|
102
|
-
return Number.isSafeInteger(value) && value >= 1;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const CONTROL_CHARACTER = /[\u0000-\u001f\u007f]/;
|
|
106
|
-
const MAX_PATH_BYTES = 1024;
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* repo相対pathだけを受理する(不正pathのfail closed、ADR 0044 Decision 2)。
|
|
110
|
-
* 絶対path、`..`遡上、空segment、制御文字、`\`区切りを拒否する。
|
|
111
|
-
* 末尾`/`はdeclared write prefix(directory宣言)としてのみ許す。
|
|
112
|
-
*/
|
|
113
|
-
function repoRelativePath(value, { allowPrefix = false } = {}) {
|
|
114
|
-
if (typeof value !== 'string'
|
|
115
|
-
|| value.length === 0
|
|
116
|
-
|| Buffer.byteLength(value, 'utf8') > MAX_PATH_BYTES
|
|
117
|
-
|| CONTROL_CHARACTER.test(value)
|
|
118
|
-
|| value.includes('\\')
|
|
119
|
-
|| value.startsWith('/')) {
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
122
|
-
const isPrefix = value.endsWith('/');
|
|
123
|
-
if (isPrefix && !allowPrefix) return false;
|
|
124
|
-
const body = isPrefix ? value.slice(0, -1) : value;
|
|
125
|
-
const segments = body.split('/');
|
|
126
|
-
return segments.every((segment) => (
|
|
127
|
-
segment.length > 0 && segment !== '.' && segment !== '..'
|
|
128
|
-
));
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function repoPathArray(value, options) {
|
|
132
|
-
return boundedArray(value, (entry) => repoRelativePath(entry, options));
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const OWN_KINDS = Object.freeze(['symbol', 'path']);
|
|
136
|
-
const STATE_EFFECT_KINDS = Object.freeze([
|
|
137
|
-
'state',
|
|
138
|
-
'schema',
|
|
139
|
-
'invariant',
|
|
140
|
-
'effect',
|
|
141
|
-
'external_effect',
|
|
142
|
-
]);
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* 所有宣言。`creates: true`は「このpathはまだ存在せず、このTODOが作る」という創作の意思である
|
|
146
|
-
* (ADR 0135 Decision 3・ADR 0136)。
|
|
147
|
-
*
|
|
148
|
-
* `kind`は`path`のまま据え置く。存在の有無は資源の種類ではなく資源の状態であり、kindを分けると
|
|
149
|
-
* 既存のpath判定(write交差の免除、conflict resourceのkind)が全部この宣言を取りこぼす。
|
|
150
|
-
*
|
|
151
|
-
* 値は`true`だけを受理する。`false`は「存在するpath」と同義で、同じ事実に2つの書き方を
|
|
152
|
-
* 与えることになる。省略が既定である。
|
|
153
|
-
*/
|
|
154
|
-
function ownEntry(value, { allowCreates = false } = {}) {
|
|
155
|
-
if (!plainObject(value)) return false;
|
|
156
|
-
const creates = Object.hasOwn(value, 'creates');
|
|
157
|
-
if (creates && !allowCreates) return false;
|
|
158
|
-
return exactRecord(value, creates ? ['kind', 'target', 'creates'] : ['kind', 'target'])
|
|
159
|
-
&& OWN_KINDS.includes(value.kind)
|
|
160
|
-
&& (!creates || (value.creates === true && value.kind === 'path'))
|
|
161
|
-
&& typeof value.target === 'string'
|
|
162
|
-
&& value.target.length > 0
|
|
163
|
-
&& Buffer.byteLength(value.target, 'utf8') <= MAX_PATH_BYTES;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function stateEffectEntry(value) {
|
|
167
|
-
return plainObject(value)
|
|
168
|
-
&& exactRecord(value, ['resource_id', 'kind'])
|
|
169
|
-
&& identifier(value.resource_id)
|
|
170
|
-
&& STATE_EFFECT_KINDS.includes(value.kind);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function unknownEntry(value) {
|
|
174
|
-
return plainObject(value)
|
|
175
|
-
&& exactRecord(value, ['kind', 'ref'])
|
|
176
|
-
&& identifier(value.kind)
|
|
177
|
-
&& typeof value.ref === 'string'
|
|
178
|
-
&& value.ref.length > 0;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export const MANUAL_WITNESS_FIELDS = Object.freeze([
|
|
182
|
-
'owns',
|
|
183
|
-
'reads',
|
|
184
|
-
'writes',
|
|
185
|
-
'resources',
|
|
186
|
-
'state_effects',
|
|
187
|
-
'sensor_provenance',
|
|
188
|
-
'affected_tests',
|
|
189
|
-
'unknowns',
|
|
190
|
-
]);
|
|
191
|
-
export const MANUAL_WITNESS_OPTIONAL_FIELDS = Object.freeze(['lines']);
|
|
192
|
-
export const LINE_ROLES = Object.freeze(['reads', 'writes']);
|
|
193
|
-
|
|
194
|
-
function lineAnchor(value) {
|
|
195
|
-
if (!plainObject(value)) return false;
|
|
196
|
-
if (value.kind === 'path') {
|
|
197
|
-
return exactRecord(value, ['kind', 'path']) && repoRelativePath(value.path);
|
|
198
|
-
}
|
|
199
|
-
return value.kind === 'symbol'
|
|
200
|
-
&& exactRecord(value, ['kind', 'name', 'path'])
|
|
201
|
-
&& typeof value.name === 'string' && value.name.length > 0
|
|
202
|
-
&& Buffer.byteLength(value.name, 'utf8') <= MAX_PATH_BYTES
|
|
203
|
-
&& repoRelativePath(value.path);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function lineEntry(value) {
|
|
207
|
-
return plainObject(value)
|
|
208
|
-
&& exactRecord(value, ['line_id', 'role', 'anchors'])
|
|
209
|
-
&& identifier(value.line_id)
|
|
210
|
-
&& LINE_ROLES.includes(value.role)
|
|
211
|
-
&& boundedArray(value.anchors, lineAnchor, { min: 1 });
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function lineEntries(value) {
|
|
215
|
-
return boundedArray(value, lineEntry)
|
|
216
|
-
&& new Set(value.map(({ line_id: lineId }) => lineId)).size === value.length;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// manual witness entryの判定は`explainRunRequest`だけが所有する。
|
|
220
|
-
// 同じ規則をbooleanと診断の二箇所へ持たないための単一正本化(ADR 0123)。
|
|
221
|
-
|
|
222
|
-
function boundedArray(value, predicate, { min = 0, max = MAX_COLLECTION } = {}) {
|
|
223
|
-
return Array.isArray(value)
|
|
224
|
-
&& value.length >= min
|
|
225
|
-
&& value.length <= max
|
|
226
|
-
&& value.every((entry) => predicate(entry));
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function uniqueIdentifierArray(value, options) {
|
|
230
|
-
return boundedArray(value, identifier, options)
|
|
231
|
-
&& new Set(value).size === value.length;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function validateSafely(value, validator) {
|
|
235
|
-
try {
|
|
236
|
-
canonicalizeArtifact(value);
|
|
237
|
-
return validator(value) === true;
|
|
238
|
-
} catch {
|
|
239
|
-
return false;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* 自己digest規則: `digestField`自身を除いた残り全fieldのcanonical JSON SHA-256。
|
|
245
|
-
* `run_request.v1`の`request_digest`(RC2 `admission_digest`と同型)を全schemaへ適用する。
|
|
246
|
-
*/
|
|
247
|
-
export function selfDigest(value, digestField) {
|
|
248
|
-
const projection = {};
|
|
249
|
-
for (const key of Object.keys(value)) {
|
|
250
|
-
if (key !== digestField) projection[key] = value[key];
|
|
251
|
-
}
|
|
252
|
-
return digestArtifact(projection);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function selfDigestValid(value, digestField) {
|
|
256
|
-
return digest(value[digestField]) && value[digestField] === selfDigest(value, digestField);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const WITNESS_PROVENANCE = Object.freeze([
|
|
260
|
-
'sensor',
|
|
261
|
-
'manual_candidate_spec',
|
|
262
|
-
'manual_state_effect',
|
|
263
|
-
]);
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* 現行のrun request契約。v5はpathを跨ぐ意味的な線を任意宣言できる。v4は同じ予測契約の
|
|
267
|
-
* lines無し版として残し、v3以前の厳密なcompile契約も既存requestの意味を変えずに読む。
|
|
268
|
-
*
|
|
269
|
-
* v2はこの系列ではない。ADR 0064のepoch後継request(`predecessor_request_digest`と
|
|
270
|
-
* `task_migration_digest`を持つ別shape)が既に使っている番号なので、飛ばして採番する。
|
|
271
|
-
*/
|
|
272
|
-
export const RUN_REQUEST_SCHEMA = 'lattice.run_request.v5';
|
|
273
|
-
export const RUN_REQUEST_PREDICTION_SCHEMA = 'lattice.run_request.v4';
|
|
274
|
-
export const RUN_REQUEST_DECLARATIVE_SCHEMA = 'lattice.run_request.v3';
|
|
275
|
-
export const RUN_REQUEST_LEGACY_SCHEMAS = Object.freeze([
|
|
276
|
-
RUN_REQUEST_PREDICTION_SCHEMA,
|
|
277
|
-
RUN_REQUEST_DECLARATIVE_SCHEMA,
|
|
278
|
-
'lattice.run_request.v1',
|
|
279
|
-
]);
|
|
280
|
-
export const RUN_REQUEST_SCHEMAS = Object.freeze([
|
|
281
|
-
RUN_REQUEST_SCHEMA,
|
|
282
|
-
...RUN_REQUEST_LEGACY_SCHEMAS,
|
|
283
|
-
]);
|
|
284
|
-
|
|
285
|
-
export const RUN_REQUEST_FIELDS = Object.freeze([
|
|
286
|
-
'schema',
|
|
287
|
-
'request_id',
|
|
288
|
-
'repo',
|
|
289
|
-
'capacity',
|
|
290
|
-
'todos',
|
|
291
|
-
'manual_witness',
|
|
292
|
-
'sensor_query_set',
|
|
293
|
-
'executor_capability',
|
|
294
|
-
'claim_mode',
|
|
295
|
-
'request_digest',
|
|
296
|
-
]);
|
|
297
|
-
|
|
298
|
-
export const RUN_REQUEST_CLAIM_MODE = 'exact_minimum';
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* `sensor_query_set.queries[].operation`のclosed set。
|
|
302
|
-
* runtime front-endはこの定数を輸入して使う(同じ閉集合を二箇所へ持たない)。
|
|
303
|
-
*/
|
|
304
|
-
export const SENSOR_QUERY_OPERATIONS = Object.freeze([
|
|
305
|
-
'status',
|
|
306
|
-
'query',
|
|
307
|
-
'callers',
|
|
308
|
-
'callees',
|
|
309
|
-
'impact',
|
|
310
|
-
'affected',
|
|
311
|
-
]);
|
|
312
|
-
|
|
313
|
-
/** `manual_witness[].sensor_provenance.queries[].expect.kind`のclosed set。 */
|
|
314
|
-
export const SENSOR_EXPECT_KINDS = Object.freeze(['symbol', 'path', 'affected']);
|
|
315
|
-
|
|
316
|
-
/** `sensor_provenance` entryのexpectを検査する(front-end adapter契約と同一規則)。 */
|
|
317
|
-
function sensorExpect(value) {
|
|
318
|
-
if (!plainObject(value) || !SENSOR_EXPECT_KINDS.includes(value.kind)) return false;
|
|
319
|
-
if (value.kind === 'symbol') {
|
|
320
|
-
return exactRecord(value, ['kind', 'name', 'path'])
|
|
321
|
-
&& typeof value.name === 'string' && value.name.length > 0
|
|
322
|
-
&& repoRelativePath(value.path);
|
|
323
|
-
}
|
|
324
|
-
return exactRecord(value, ['kind', 'path']) && repoRelativePath(value.path);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* `lattice.run_request.v1`の唯一の判定正本(ADR 0123)。
|
|
329
|
-
* 受理は`{ valid: true }`、拒否は最初の違反の`reason`と`path`を返す。
|
|
330
|
-
* `validateRunRequest`は本関数へ委譲するため、boolean判定と診断が乖離しない。
|
|
331
|
-
*/
|
|
332
|
-
export function explainRunRequest(value) {
|
|
333
|
-
const reject = (reason, at) => ({ valid: false, reason, path: at });
|
|
334
|
-
try {
|
|
335
|
-
canonicalizeArtifact(value);
|
|
336
|
-
} catch {
|
|
337
|
-
return reject('non_canonical_request_bytes', '');
|
|
338
|
-
}
|
|
339
|
-
if (!exactRecord(value, RUN_REQUEST_FIELDS)) return reject('unexpected_or_missing_top_level_keys', '');
|
|
340
|
-
if (!RUN_REQUEST_SCHEMAS.includes(value.schema)) return reject('schema_mismatch', '/schema');
|
|
341
|
-
// 創作宣言はv2から。v1のclosed shapeは余分fieldを拒否するので加算互換が成立しない。
|
|
342
|
-
const allowCreates = [
|
|
343
|
-
RUN_REQUEST_SCHEMA, RUN_REQUEST_PREDICTION_SCHEMA, RUN_REQUEST_DECLARATIVE_SCHEMA,
|
|
344
|
-
].includes(value.schema);
|
|
345
|
-
const allowLines = value.schema === RUN_REQUEST_SCHEMA;
|
|
346
|
-
if (!identifier(value.request_id)) return reject('invalid_identifier', '/request_id');
|
|
347
|
-
if (!exactRecord(value.repo, ['base_sha', 'root_kind'])) return reject('unexpected_or_missing_keys', '/repo');
|
|
348
|
-
if (!gitSha(value.repo.base_sha)) return reject('invalid_git_sha', '/repo/base_sha');
|
|
349
|
-
if (!identifier(value.repo.root_kind)) return reject('invalid_identifier', '/repo/root_kind');
|
|
350
|
-
if (!exactRecord(value.capacity, ['executors'])) return reject('unexpected_or_missing_keys', '/capacity');
|
|
351
|
-
if (!positiveInteger(value.capacity.executors)) return reject('not_a_positive_integer', '/capacity/executors');
|
|
352
|
-
if (!Array.isArray(value.todos) || value.todos.length < 1 || value.todos.length > MAX_COLLECTION) {
|
|
353
|
-
return reject('bounded_collection_violation', '/todos');
|
|
354
|
-
}
|
|
355
|
-
for (const [index, todo] of value.todos.entries()) {
|
|
356
|
-
if (!exactRecord(todo, ['todo_id'])) return reject('unexpected_or_missing_keys', `/todos/${index}`);
|
|
357
|
-
if (!identifier(todo.todo_id)) return reject('invalid_identifier', `/todos/${index}/todo_id`);
|
|
358
|
-
}
|
|
359
|
-
const todoIds = value.todos.map((todo) => todo.todo_id);
|
|
360
|
-
if (new Set(todoIds).size !== todoIds.length) return reject('duplicate_todo_id', '/todos');
|
|
361
|
-
if (!plainObject(value.manual_witness)) return reject('not_an_object', '/manual_witness');
|
|
362
|
-
if (!exactRecord(value.manual_witness, todoIds)) {
|
|
363
|
-
return reject('manual_witness_keys_must_equal_todo_ids', '/manual_witness');
|
|
364
|
-
}
|
|
365
|
-
for (const todoId of todoIds) {
|
|
366
|
-
const witness = value.manual_witness[todoId];
|
|
367
|
-
const at = `/manual_witness/${todoId}`;
|
|
368
|
-
if (!plainObject(witness)) return reject('not_an_object', at);
|
|
369
|
-
const hasLines = Object.hasOwn(witness, 'lines');
|
|
370
|
-
const witnessFields = hasLines
|
|
371
|
-
? [...MANUAL_WITNESS_FIELDS, ...MANUAL_WITNESS_OPTIONAL_FIELDS]
|
|
372
|
-
: MANUAL_WITNESS_FIELDS;
|
|
373
|
-
if (!exactRecord(witness, witnessFields)) return reject('unexpected_or_missing_keys', at);
|
|
374
|
-
if (hasLines && !allowLines) return reject('lines_require_run_request_v5', `${at}/lines`);
|
|
375
|
-
if (hasLines && !lineEntries(witness.lines)) return reject('invalid_line_entries', `${at}/lines`);
|
|
376
|
-
if (!boundedArray(witness.owns, (own) => ownEntry(own, { allowCreates }))) {
|
|
377
|
-
return reject('invalid_own_entries', `${at}/owns`);
|
|
378
|
-
}
|
|
379
|
-
if (!repoPathArray(witness.reads)) return reject('invalid_repo_relative_paths', `${at}/reads`);
|
|
380
|
-
if (!repoPathArray(witness.writes, { allowPrefix: true })) return reject('invalid_repo_relative_paths', `${at}/writes`);
|
|
381
|
-
if (!boundedArray(witness.resources, identifier)) return reject('invalid_identifier', `${at}/resources`);
|
|
382
|
-
if (!boundedArray(witness.state_effects, stateEffectEntry)) return reject('invalid_state_effect_entries', `${at}/state_effects`);
|
|
383
|
-
// sensor_provenanceもfront-end adapter契約のshapeまで検査する(ADR 0123)。
|
|
384
|
-
const provenanceAt = `${at}/sensor_provenance`;
|
|
385
|
-
if (!exactRecord(witness.sensor_provenance, ['queries'])) return reject('unexpected_or_missing_keys', provenanceAt);
|
|
386
|
-
if (!Array.isArray(witness.sensor_provenance.queries)
|
|
387
|
-
|| witness.sensor_provenance.queries.length > MAX_COLLECTION) {
|
|
388
|
-
return reject('bounded_collection_violation', `${provenanceAt}/queries`);
|
|
389
|
-
}
|
|
390
|
-
for (const [index, entry] of witness.sensor_provenance.queries.entries()) {
|
|
391
|
-
const entryAt = `${provenanceAt}/queries/${index}`;
|
|
392
|
-
if (!exactRecord(entry, ['query_id', 'expect'])) return reject('unexpected_or_missing_keys', entryAt);
|
|
393
|
-
if (!identifier(entry.query_id)) return reject('invalid_identifier', `${entryAt}/query_id`);
|
|
394
|
-
if (!sensorExpect(entry.expect)) return reject('invalid_sensor_expect', `${entryAt}/expect`);
|
|
395
|
-
}
|
|
396
|
-
if (!repoPathArray(witness.affected_tests)) return reject('invalid_repo_relative_paths', `${at}/affected_tests`);
|
|
397
|
-
if (!boundedArray(witness.unknowns, unknownEntry)) return reject('invalid_unknown_entries', `${at}/unknowns`);
|
|
398
|
-
}
|
|
399
|
-
// sensor_query_set/executor_capabilityは、runtime front-endとrun startが実際に要求する
|
|
400
|
-
// shapeまで検査する。schemaで通ってから後段で落ちる契約分裂を残さない(ADR 0123)。
|
|
401
|
-
if (!exactRecord(value.sensor_query_set, ['queries'])) return reject('unexpected_or_missing_keys', '/sensor_query_set');
|
|
402
|
-
if (!Array.isArray(value.sensor_query_set.queries) || value.sensor_query_set.queries.length > MAX_COLLECTION) {
|
|
403
|
-
return reject('bounded_collection_violation', '/sensor_query_set/queries');
|
|
404
|
-
}
|
|
405
|
-
const queryIds = new Set();
|
|
406
|
-
for (const [index, query] of value.sensor_query_set.queries.entries()) {
|
|
407
|
-
const at = `/sensor_query_set/queries/${index}`;
|
|
408
|
-
const keys = plainObject(query) && Object.hasOwn(query, 'target')
|
|
409
|
-
? ['id', 'operation', 'target'] : ['id', 'operation'];
|
|
410
|
-
if (!exactRecord(query, keys)) return reject('unexpected_or_missing_keys', at);
|
|
411
|
-
if (!identifier(query.id)) return reject('invalid_identifier', `${at}/id`);
|
|
412
|
-
if (!SENSOR_QUERY_OPERATIONS.includes(query.operation)) return reject('unknown_sensor_query_operation', `${at}/operation`);
|
|
413
|
-
if (keys.includes('target') && (typeof query.target !== 'string' || query.target.length === 0)) {
|
|
414
|
-
return reject('empty_query_target', `${at}/target`);
|
|
415
|
-
}
|
|
416
|
-
if (queryIds.has(query.id)) return reject('duplicate_query_id', `${at}/id`);
|
|
417
|
-
queryIds.add(query.id);
|
|
418
|
-
}
|
|
419
|
-
if (!exactRecord(value.executor_capability, ['adapters'])) return reject('unexpected_or_missing_keys', '/executor_capability');
|
|
420
|
-
if (!uniqueIdentifierArray(value.executor_capability.adapters, { min: 1 })) {
|
|
421
|
-
return reject('invalid_adapter_identifiers', '/executor_capability/adapters');
|
|
422
|
-
}
|
|
423
|
-
if (value.claim_mode !== RUN_REQUEST_CLAIM_MODE) return reject('claim_mode_must_be_exact_minimum', '/claim_mode');
|
|
424
|
-
if (!selfDigestValid(value, 'request_digest')) return reject('request_digest_mismatch', '/request_digest');
|
|
425
|
-
return { valid: true };
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
/** `lattice.run_request.v1`。manual witnessはTODOごとに完備でなければならない。 */
|
|
429
|
-
export function validateRunRequest(value) {
|
|
430
|
-
return explainRunRequest(value).valid;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
/**
|
|
434
|
-
* runtime planをrun requestへcross-bindする(ADR 0044 Decision 2)。
|
|
435
|
-
* 同一TODO集合を別base/別requestへ再包装したplanをtyped rejectするための検証で、
|
|
436
|
-
* schema単体のvalidatorとは別に、request bytesとの一致を要求する。
|
|
437
|
-
*/
|
|
438
|
-
export function verifyRuntimePlanBinding(options = {}) {
|
|
439
|
-
if (options === null || typeof options !== 'object' || Array.isArray(options)) return false;
|
|
440
|
-
const { plan, request } = options;
|
|
441
|
-
if (!validateRuntimePlan(plan) || !validateRunRequest(request)) return false;
|
|
442
|
-
if (plan.request_digest !== request.request_digest) return false;
|
|
443
|
-
if (plan.base_sha !== request.repo.base_sha) return false;
|
|
444
|
-
const requestTodoIds = new Set(request.todos.map((todo) => todo.todo_id));
|
|
445
|
-
return plan.nodes.every((node) => requestTodoIds.has(node.todo_id));
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
/**
|
|
449
|
-
* `lattice.boundary_manifest.v4`。v4はpathを跨ぐ意味的な線を任意記録できる。
|
|
450
|
-
*
|
|
451
|
-
* v3は`owns[].creates`だけがv2との差である。宣言が持っていた「このpathはまだ無い」を
|
|
452
|
-
* 記録側でも保つ——落とすと、manifestだけを読む消費者が既存fileと同じ扱いをする。
|
|
453
|
-
* 旧v2/v3 manifestはrun storeに残るので読み口として受理し、linesを要求しない。
|
|
454
|
-
*/
|
|
455
|
-
export const BOUNDARY_MANIFEST_SCHEMA = 'lattice.boundary_manifest.v4';
|
|
456
|
-
export const BOUNDARY_MANIFEST_SCHEMAS = Object.freeze([
|
|
457
|
-
BOUNDARY_MANIFEST_SCHEMA,
|
|
458
|
-
'lattice.boundary_manifest.v3',
|
|
459
|
-
'lattice.boundary_manifest.v2',
|
|
460
|
-
]);
|
|
461
|
-
export function validateRuntimeBoundaryManifest(value) {
|
|
462
|
-
return validateSafely(value, (manifest) => {
|
|
463
|
-
const hasLines = Object.hasOwn(manifest, 'lines');
|
|
464
|
-
return (
|
|
465
|
-
exactRecord(manifest, [
|
|
466
|
-
'schema',
|
|
467
|
-
'todo_id',
|
|
468
|
-
'owns',
|
|
469
|
-
'reads',
|
|
470
|
-
'writes',
|
|
471
|
-
'resources',
|
|
472
|
-
'state_effects',
|
|
473
|
-
'unknowns',
|
|
474
|
-
'affected_tests',
|
|
475
|
-
'graph_evidence',
|
|
476
|
-
'witness_provenance',
|
|
477
|
-
...(hasLines ? ['lines'] : []),
|
|
478
|
-
'manifest_digest',
|
|
479
|
-
])
|
|
480
|
-
&& BOUNDARY_MANIFEST_SCHEMAS.includes(manifest.schema)
|
|
481
|
-
&& (!hasLines || manifest.schema === BOUNDARY_MANIFEST_SCHEMA)
|
|
482
|
-
&& identifier(manifest.todo_id)
|
|
483
|
-
&& boundedArray(manifest.owns, (own) => ownEntry(own, {
|
|
484
|
-
allowCreates: [BOUNDARY_MANIFEST_SCHEMA, 'lattice.boundary_manifest.v3'].includes(manifest.schema),
|
|
485
|
-
}))
|
|
486
|
-
&& repoPathArray(manifest.reads)
|
|
487
|
-
&& repoPathArray(manifest.writes, { allowPrefix: true })
|
|
488
|
-
&& boundedArray(manifest.resources, identifier)
|
|
489
|
-
&& boundedArray(manifest.state_effects, stateEffectEntry)
|
|
490
|
-
&& boundedArray(manifest.unknowns, unknownEntry)
|
|
491
|
-
&& repoPathArray(manifest.affected_tests)
|
|
492
|
-
&& boundedArray(manifest.graph_evidence, plainObject)
|
|
493
|
-
&& plainObject(manifest.witness_provenance)
|
|
494
|
-
&& Object.values(manifest.witness_provenance).every((entry) => (
|
|
495
|
-
WITNESS_PROVENANCE.includes(entry)
|
|
496
|
-
))
|
|
497
|
-
&& (!hasLines || lineEntries(manifest.lines))
|
|
498
|
-
&& selfDigestValid(manifest, 'manifest_digest')
|
|
499
|
-
);
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
/** `lattice.runtime_plan.v1`。exact_minimum claimは1〜8 nodeだけを受理する(Decision 1)。 */
|
|
504
|
-
export function validateRuntimePlan(value) {
|
|
505
|
-
return validateSafely(value, (plan) => {
|
|
506
|
-
if (!exactRecord(plan, [
|
|
507
|
-
'schema',
|
|
508
|
-
'plan_ref',
|
|
509
|
-
'plan_epoch',
|
|
510
|
-
'request_digest',
|
|
511
|
-
'base_sha',
|
|
512
|
-
'nodes',
|
|
513
|
-
'precedence',
|
|
514
|
-
'conflicts',
|
|
515
|
-
'capacity',
|
|
516
|
-
'manifest_digests',
|
|
517
|
-
'claim',
|
|
518
|
-
'predecessor_refs',
|
|
519
|
-
'plan_digest',
|
|
520
|
-
])
|
|
521
|
-
|| plan.schema !== 'lattice.runtime_plan.v1'
|
|
522
|
-
|| !identifier(plan.plan_ref)
|
|
523
|
-
|| !nonNegativeInteger(plan.plan_epoch)
|
|
524
|
-
|| !digest(plan.request_digest)
|
|
525
|
-
|| !gitSha(plan.base_sha)
|
|
526
|
-
|| !boundedArray(plan.nodes, (node) => (
|
|
527
|
-
exactRecord(node, ['todo_id']) && identifier(node.todo_id)
|
|
528
|
-
), { min: 1, max: MAX_NODES_PER_PLAN })
|
|
529
|
-
|| !exactRecord(plan.claim, ['mode'])
|
|
530
|
-
|| plan.claim.mode !== 'exact_minimum'
|
|
531
|
-
|| !exactRecord(plan.capacity, ['executors'])
|
|
532
|
-
|| !positiveInteger(plan.capacity.executors)
|
|
533
|
-
|| !boundedArray(plan.predecessor_refs, (entry) => typeof entry === 'string')) {
|
|
534
|
-
return false;
|
|
535
|
-
}
|
|
536
|
-
const nodeIds = plan.nodes.map((node) => node.todo_id);
|
|
537
|
-
const nodeSet = new Set(nodeIds);
|
|
538
|
-
if (nodeSet.size !== nodeIds.length) return false;
|
|
539
|
-
if (!boundedArray(plan.precedence, (edge) => (
|
|
540
|
-
exactRecord(edge, ['from_todo_id', 'to_todo_id'])
|
|
541
|
-
&& nodeSet.has(edge.from_todo_id)
|
|
542
|
-
&& nodeSet.has(edge.to_todo_id)
|
|
543
|
-
&& edge.from_todo_id !== edge.to_todo_id
|
|
544
|
-
))) {
|
|
545
|
-
return false;
|
|
546
|
-
}
|
|
547
|
-
if (!boundedArray(plan.conflicts, (conflict) => (
|
|
548
|
-
exactRecord(conflict, ['todo_ids', 'resource_id'])
|
|
549
|
-
&& Array.isArray(conflict.todo_ids)
|
|
550
|
-
&& conflict.todo_ids.length === 2
|
|
551
|
-
&& conflict.todo_ids.every((todoId) => nodeSet.has(todoId))
|
|
552
|
-
&& conflict.todo_ids[0] !== conflict.todo_ids[1]
|
|
553
|
-
&& identifier(conflict.resource_id)
|
|
554
|
-
))) {
|
|
555
|
-
return false;
|
|
556
|
-
}
|
|
557
|
-
return plainObject(plan.manifest_digests)
|
|
558
|
-
&& exactRecord(plan.manifest_digests, nodeIds)
|
|
559
|
-
&& Object.values(plan.manifest_digests).every(digest)
|
|
560
|
-
&& selfDigestValid(plan, 'plan_digest');
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
/** `lattice.run_event.v1`のevent単体shape。chain規則はruntime-event-store.mjsが検査する。 */
|
|
565
|
-
export function validateRunEvent(value) {
|
|
566
|
-
return validateSafely(value, (event) => (
|
|
567
|
-
exactRecord(event, [
|
|
568
|
-
'schema',
|
|
569
|
-
'run_id',
|
|
570
|
-
'sequence',
|
|
571
|
-
'previous_digest',
|
|
572
|
-
'kind',
|
|
573
|
-
'actor',
|
|
574
|
-
'plan_epoch',
|
|
575
|
-
'subject',
|
|
576
|
-
'payload',
|
|
577
|
-
'recorded_at',
|
|
578
|
-
'event_digest',
|
|
579
|
-
])
|
|
580
|
-
&& event.schema === 'lattice.run_event.v1'
|
|
581
|
-
&& identifier(event.run_id)
|
|
582
|
-
&& nonNegativeInteger(event.sequence)
|
|
583
|
-
&& (event.previous_digest === null || digest(event.previous_digest))
|
|
584
|
-
&& (event.sequence === 0) === (event.previous_digest === null)
|
|
585
|
-
&& RUN_EVENT_KINDS.includes(event.kind)
|
|
586
|
-
&& identifier(event.actor)
|
|
587
|
-
&& nonNegativeInteger(event.plan_epoch)
|
|
588
|
-
&& exactRecord(event.subject, ['kind', 'ref'])
|
|
589
|
-
&& identifier(event.subject.kind)
|
|
590
|
-
&& typeof event.subject.ref === 'string'
|
|
591
|
-
&& plainObject(event.payload)
|
|
592
|
-
&& timestamp(event.recorded_at)
|
|
593
|
-
&& selfDigestValid(event, 'event_digest')
|
|
594
|
-
));
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
const CONTEXT_CONTENT_FIELDS = Object.freeze([
|
|
598
|
-
'todo_id',
|
|
599
|
-
'task_ref',
|
|
600
|
-
'scope',
|
|
601
|
-
'base_sha',
|
|
602
|
-
'verifier_refs',
|
|
603
|
-
'forbidden_operations',
|
|
604
|
-
]);
|
|
605
|
-
|
|
606
|
-
/**
|
|
607
|
-
* ADR 0044 Decision 7.1のcontent projection digest。plan帰属field
|
|
608
|
-
* (packet_id・plan_ref・plan_epoch・packet_digest)を除いた6 fieldだけを対象にし、
|
|
609
|
-
* epoch rebindが「content不変・epochだけ更新」であることを機械検査可能にする。
|
|
610
|
-
*/
|
|
611
|
-
export function computeContextContentDigest(packet) {
|
|
612
|
-
const projection = {};
|
|
613
|
-
for (const field of CONTEXT_CONTENT_FIELDS) {
|
|
614
|
-
projection[field] = packet[field];
|
|
615
|
-
}
|
|
616
|
-
return digestArtifact(projection);
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
/** `lattice.executor_packet.v1`。dispatch時に発行する唯一のcontext packet。 */
|
|
620
|
-
export function validateExecutorPacket(value) {
|
|
621
|
-
return validateSafely(value, (packet) => (
|
|
622
|
-
exactRecord(packet, [
|
|
623
|
-
'schema',
|
|
624
|
-
'packet_id',
|
|
625
|
-
'todo_id',
|
|
626
|
-
'task_ref',
|
|
627
|
-
'scope',
|
|
628
|
-
'base_sha',
|
|
629
|
-
'plan_ref',
|
|
630
|
-
'plan_epoch',
|
|
631
|
-
'verifier_refs',
|
|
632
|
-
'forbidden_operations',
|
|
633
|
-
'context_content_digest',
|
|
634
|
-
'packet_digest',
|
|
635
|
-
])
|
|
636
|
-
&& packet.schema === 'lattice.executor_packet.v1'
|
|
637
|
-
&& identifier(packet.packet_id)
|
|
638
|
-
&& identifier(packet.todo_id)
|
|
639
|
-
&& identifier(packet.task_ref)
|
|
640
|
-
&& plainObject(packet.scope)
|
|
641
|
-
&& gitSha(packet.base_sha)
|
|
642
|
-
&& identifier(packet.plan_ref)
|
|
643
|
-
&& nonNegativeInteger(packet.plan_epoch)
|
|
644
|
-
&& boundedArray(packet.verifier_refs, (entry) => typeof entry === 'string')
|
|
645
|
-
&& boundedArray(packet.forbidden_operations, (entry) => typeof entry === 'string', { min: 1 })
|
|
646
|
-
&& packet.context_content_digest === computeContextContentDigest(packet)
|
|
647
|
-
&& selfDigestValid(packet, 'packet_digest')
|
|
648
|
-
));
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
/** `lattice.executor_receipt.v1`。binding欠落はtyped rejectの対象(Decision 7.4)。 */
|
|
652
|
-
export function validateExecutorReceipt(value) {
|
|
653
|
-
return validateSafely(value, (receipt) => (
|
|
654
|
-
exactRecord(receipt, [
|
|
655
|
-
'schema',
|
|
656
|
-
'receipt_id',
|
|
657
|
-
'executor_handle',
|
|
658
|
-
'worktree_id',
|
|
659
|
-
'base_sha',
|
|
660
|
-
'plan_epoch',
|
|
661
|
-
'packet_digest',
|
|
662
|
-
'todo_id',
|
|
663
|
-
'checkpoint_digest',
|
|
664
|
-
'observed_diff',
|
|
665
|
-
'receipt_digest',
|
|
666
|
-
])
|
|
667
|
-
&& receipt.schema === 'lattice.executor_receipt.v1'
|
|
668
|
-
&& identifier(receipt.receipt_id)
|
|
669
|
-
&& identifier(receipt.executor_handle)
|
|
670
|
-
&& identifier(receipt.worktree_id)
|
|
671
|
-
&& gitSha(receipt.base_sha)
|
|
672
|
-
&& nonNegativeInteger(receipt.plan_epoch)
|
|
673
|
-
&& digest(receipt.packet_digest)
|
|
674
|
-
&& identifier(receipt.todo_id)
|
|
675
|
-
&& digest(receipt.checkpoint_digest)
|
|
676
|
-
&& boundedArray(receipt.observed_diff, (entry) => (
|
|
677
|
-
exactRecord(entry, ['path', 'change'])
|
|
678
|
-
&& repoRelativePath(entry.path)
|
|
679
|
-
&& ['added', 'modified', 'deleted'].includes(entry.change)
|
|
680
|
-
))
|
|
681
|
-
&& selfDigestValid(receipt, 'receipt_digest')
|
|
682
|
-
));
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
/** `lattice.hold_decision.v1`。hold/continueは交差しない。 */
|
|
686
|
-
export function validateHoldDecision(value) {
|
|
687
|
-
return validateSafely(value, (decision) => {
|
|
688
|
-
if (!exactRecord(decision, [
|
|
689
|
-
'schema',
|
|
690
|
-
'decision_id',
|
|
691
|
-
'finding',
|
|
692
|
-
'frozen_prefix_digest',
|
|
693
|
-
'affected_closure',
|
|
694
|
-
'hold_set',
|
|
695
|
-
'continue_set',
|
|
696
|
-
'evidence_digests',
|
|
697
|
-
'decision_digest',
|
|
698
|
-
])
|
|
699
|
-
|| decision.schema !== 'lattice.hold_decision.v1'
|
|
700
|
-
|| !identifier(decision.decision_id)
|
|
701
|
-
|| !plainObject(decision.finding)
|
|
702
|
-
|| !HOLD_REASON_KINDS.includes(decision.finding.kind)
|
|
703
|
-
|| !digest(decision.frozen_prefix_digest)
|
|
704
|
-
|| !uniqueIdentifierArray(decision.affected_closure)
|
|
705
|
-
|| !uniqueIdentifierArray(decision.hold_set)
|
|
706
|
-
|| !uniqueIdentifierArray(decision.continue_set)
|
|
707
|
-
|| !boundedArray(decision.evidence_digests, digest)) {
|
|
708
|
-
return false;
|
|
709
|
-
}
|
|
710
|
-
const holdSet = new Set(decision.hold_set);
|
|
711
|
-
return decision.continue_set.every((todoId) => !holdSet.has(todoId))
|
|
712
|
-
&& selfDigestValid(decision, 'decision_digest');
|
|
713
|
-
});
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
const INVARIANT_DIGEST_FIELDS = Object.freeze([
|
|
717
|
-
'todo_input',
|
|
718
|
-
'boundary_manifest',
|
|
719
|
-
'validator',
|
|
720
|
-
'context_content',
|
|
721
|
-
]);
|
|
722
|
-
|
|
723
|
-
/** `lattice.carry_over_witness.v1`。invariant digestは保存bytesから再計算できる。 */
|
|
724
|
-
export function validateCarryOverWitness(value) {
|
|
725
|
-
return validateSafely(value, (witness) => (
|
|
726
|
-
exactRecord(witness, [
|
|
727
|
-
'schema',
|
|
728
|
-
'witness_id',
|
|
729
|
-
'todo_id',
|
|
730
|
-
'predecessor_epoch',
|
|
731
|
-
'successor_epoch',
|
|
732
|
-
'invariant_digests',
|
|
733
|
-
'non_overlap_evidence',
|
|
734
|
-
'receipt_bindings',
|
|
735
|
-
'witness_digest',
|
|
736
|
-
])
|
|
737
|
-
&& witness.schema === 'lattice.carry_over_witness.v1'
|
|
738
|
-
&& identifier(witness.witness_id)
|
|
739
|
-
&& identifier(witness.todo_id)
|
|
740
|
-
&& nonNegativeInteger(witness.predecessor_epoch)
|
|
741
|
-
&& nonNegativeInteger(witness.successor_epoch)
|
|
742
|
-
&& witness.successor_epoch > witness.predecessor_epoch
|
|
743
|
-
&& exactRecord(witness.invariant_digests, INVARIANT_DIGEST_FIELDS)
|
|
744
|
-
&& INVARIANT_DIGEST_FIELDS.every((field) => digest(witness.invariant_digests[field]))
|
|
745
|
-
&& boundedArray(witness.non_overlap_evidence, (entry) => typeof entry === 'string')
|
|
746
|
-
&& boundedArray(witness.receipt_bindings, (binding) => (
|
|
747
|
-
exactRecord(binding, [
|
|
748
|
-
'receipt_id',
|
|
749
|
-
'checkpoint_digest',
|
|
750
|
-
'recorded_sequence',
|
|
751
|
-
'within_frozen_prefix',
|
|
752
|
-
])
|
|
753
|
-
&& identifier(binding.receipt_id)
|
|
754
|
-
&& digest(binding.checkpoint_digest)
|
|
755
|
-
&& nonNegativeInteger(binding.recorded_sequence)
|
|
756
|
-
&& typeof binding.within_frozen_prefix === 'boolean'
|
|
757
|
-
))
|
|
758
|
-
&& selfDigestValid(witness, 'witness_digest')
|
|
759
|
-
));
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
/** `lattice.epoch_rebind_packet.v1`。content不変・plan帰属だけを更新する(Decision 7.3)。 */
|
|
763
|
-
export function validateEpochRebindPacket(value) {
|
|
764
|
-
return validateSafely(value, (packet) => (
|
|
765
|
-
exactRecord(packet, [
|
|
766
|
-
'schema',
|
|
767
|
-
'packet_id',
|
|
768
|
-
'todo_id',
|
|
769
|
-
'executor_handle',
|
|
770
|
-
'worktree_id',
|
|
771
|
-
'witness_digest',
|
|
772
|
-
'context_content_digest',
|
|
773
|
-
'authorized_checkpoint_digest',
|
|
774
|
-
'old_plan_ref',
|
|
775
|
-
'new_plan_ref',
|
|
776
|
-
'new_plan_epoch',
|
|
777
|
-
'packet_digest',
|
|
778
|
-
])
|
|
779
|
-
&& packet.schema === 'lattice.epoch_rebind_packet.v1'
|
|
780
|
-
&& identifier(packet.packet_id)
|
|
781
|
-
&& identifier(packet.todo_id)
|
|
782
|
-
&& identifier(packet.executor_handle)
|
|
783
|
-
&& identifier(packet.worktree_id)
|
|
784
|
-
&& digest(packet.witness_digest)
|
|
785
|
-
&& digest(packet.context_content_digest)
|
|
786
|
-
&& digest(packet.authorized_checkpoint_digest)
|
|
787
|
-
&& identifier(packet.old_plan_ref)
|
|
788
|
-
&& identifier(packet.new_plan_ref)
|
|
789
|
-
&& packet.old_plan_ref !== packet.new_plan_ref
|
|
790
|
-
&& positiveInteger(packet.new_plan_epoch)
|
|
791
|
-
&& selfDigestValid(packet, 'packet_digest')
|
|
792
|
-
));
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
/** `lattice.runtime_plan_diff.v1`。carry-overとredispatchは交差しない。 */
|
|
796
|
-
export function validateRuntimePlanDiff(value) {
|
|
797
|
-
return validateSafely(value, (diff) => {
|
|
798
|
-
if (!exactRecord(diff, [
|
|
799
|
-
'schema',
|
|
800
|
-
'old_plan_ref',
|
|
801
|
-
'new_plan_ref',
|
|
802
|
-
'accepted_checkpoints',
|
|
803
|
-
'invalidated_contexts',
|
|
804
|
-
'carried_over',
|
|
805
|
-
'redispatched',
|
|
806
|
-
'node_edge_diff',
|
|
807
|
-
'diff_digest',
|
|
808
|
-
])
|
|
809
|
-
|| diff.schema !== 'lattice.runtime_plan_diff.v1'
|
|
810
|
-
|| !identifier(diff.old_plan_ref)
|
|
811
|
-
|| !identifier(diff.new_plan_ref)
|
|
812
|
-
|| diff.old_plan_ref === diff.new_plan_ref
|
|
813
|
-
|| !boundedArray(diff.accepted_checkpoints, digest)
|
|
814
|
-
|| !uniqueIdentifierArray(diff.invalidated_contexts)
|
|
815
|
-
|| !uniqueIdentifierArray(diff.carried_over)
|
|
816
|
-
|| !uniqueIdentifierArray(diff.redispatched)
|
|
817
|
-
|| !plainObject(diff.node_edge_diff)) {
|
|
818
|
-
return false;
|
|
819
|
-
}
|
|
820
|
-
const carried = new Set(diff.carried_over);
|
|
821
|
-
return diff.redispatched.every((todoId) => !carried.has(todoId))
|
|
822
|
-
&& selfDigestValid(diff, 'diff_digest');
|
|
823
|
-
});
|
|
824
|
-
}
|
|
1
|
+
import { canonicalizeArtifact, digestArtifact } from './artifact-contracts.mjs';
|
|
2
|
+
import { isCanonicalUtcTimestamp } from './timestamp-contract.mjs';
|
|
3
|
+
|
|
4
|
+
// RC3 runtime契約(ADR 0044 Decision 2・3・5・7)。
|
|
5
|
+
// RC2公開済みschemaを変更しない加算moduleであり、全schemaはexact key、
|
|
6
|
+
// bounded collection、canonical serialization、自己digest規則をfail closedで検査する。
|
|
7
|
+
// field追加・意味変更はschema versionを上げ、新しいADRで裁定する。
|
|
8
|
+
|
|
9
|
+
const SHA256 = /^[0-9a-f]{64}$/;
|
|
10
|
+
const SHA1 = /^[0-9a-f]{40}$/;
|
|
11
|
+
const IDENTIFIER = /^[0-9A-Za-z](?:[0-9A-Za-z._-]{0,127})$/;
|
|
12
|
+
const MAX_COLLECTION = 256;
|
|
13
|
+
const MAX_NODES_PER_PLAN = 8;
|
|
14
|
+
|
|
15
|
+
// 人間向けaudit reasonは表示を偽装できる制御文字を拒否しつつ、通常のUnicode説明を保持する。
|
|
16
|
+
const AUDIT_BIDI_CONTROLS = /[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/u;
|
|
17
|
+
|
|
18
|
+
export function validRuntimeAbandonReason(value) {
|
|
19
|
+
return typeof value === 'string'
|
|
20
|
+
&& value === value.trim()
|
|
21
|
+
&& [...value].length >= 1
|
|
22
|
+
&& [...value].length <= 256
|
|
23
|
+
&& !/\p{Cc}/u.test(value)
|
|
24
|
+
&& !/[\u2028\u2029]/u.test(value)
|
|
25
|
+
&& !AUDIT_BIDI_CONTROLS.test(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ADR 0044 Decision 3.2のclosed event kind set。拡張はrun_event.v2+新ADRでだけ行う。
|
|
29
|
+
export const RUN_EVENT_KINDS = Object.freeze([
|
|
30
|
+
'run_initialized',
|
|
31
|
+
'plan_compiled',
|
|
32
|
+
'plan_verified',
|
|
33
|
+
'dispatch_decided',
|
|
34
|
+
'executor_dispatched',
|
|
35
|
+
'checkpoint_observed',
|
|
36
|
+
'receipt_recorded',
|
|
37
|
+
'conflict_found',
|
|
38
|
+
'intake_frozen',
|
|
39
|
+
'hold_decided',
|
|
40
|
+
'carry_over_witnessed',
|
|
41
|
+
'epoch_rebound',
|
|
42
|
+
'context_invalidated',
|
|
43
|
+
'plan_recompiled',
|
|
44
|
+
'intake_resumed',
|
|
45
|
+
'receipt_accepted',
|
|
46
|
+
'receipt_rejected',
|
|
47
|
+
'executor_terminal',
|
|
48
|
+
'run_closed',
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
// ADR 0044 Decision 5のclosed conflict分類と、Decision 7.5のhold理由kind。
|
|
52
|
+
export const RUNTIME_CONFLICT_KINDS = Object.freeze([
|
|
53
|
+
'observed_write_conflict',
|
|
54
|
+
'observed_line_change',
|
|
55
|
+
'semantic_conflict_unknown',
|
|
56
|
+
'effect_conflict_unknown',
|
|
57
|
+
'undeclared_write',
|
|
58
|
+
'stale_context',
|
|
59
|
+
]);
|
|
60
|
+
export const HOLD_REASON_KINDS = Object.freeze([
|
|
61
|
+
...RUNTIME_CONFLICT_KINDS,
|
|
62
|
+
'carry_over_unprovable',
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
function exactRecord(value, keys) {
|
|
66
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)
|
|
67
|
+
|| Object.getPrototypeOf(value) !== Object.prototype) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
const actual = Object.keys(value).sort();
|
|
71
|
+
const expected = [...keys].sort();
|
|
72
|
+
return actual.length === expected.length
|
|
73
|
+
&& actual.every((key, index) => key === expected[index]);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function plainObject(value) {
|
|
77
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
78
|
+
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function digest(value) {
|
|
82
|
+
return typeof value === 'string' && SHA256.test(value);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function gitSha(value) {
|
|
86
|
+
return typeof value === 'string' && SHA1.test(value);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function identifier(value) {
|
|
90
|
+
return typeof value === 'string' && IDENTIFIER.test(value);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function timestamp(value) {
|
|
94
|
+
return isCanonicalUtcTimestamp(value);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function nonNegativeInteger(value) {
|
|
98
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function positiveInteger(value) {
|
|
102
|
+
return Number.isSafeInteger(value) && value >= 1;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const CONTROL_CHARACTER = /[\u0000-\u001f\u007f]/;
|
|
106
|
+
const MAX_PATH_BYTES = 1024;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* repo相対pathだけを受理する(不正pathのfail closed、ADR 0044 Decision 2)。
|
|
110
|
+
* 絶対path、`..`遡上、空segment、制御文字、`\`区切りを拒否する。
|
|
111
|
+
* 末尾`/`はdeclared write prefix(directory宣言)としてのみ許す。
|
|
112
|
+
*/
|
|
113
|
+
function repoRelativePath(value, { allowPrefix = false } = {}) {
|
|
114
|
+
if (typeof value !== 'string'
|
|
115
|
+
|| value.length === 0
|
|
116
|
+
|| Buffer.byteLength(value, 'utf8') > MAX_PATH_BYTES
|
|
117
|
+
|| CONTROL_CHARACTER.test(value)
|
|
118
|
+
|| value.includes('\\')
|
|
119
|
+
|| value.startsWith('/')) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
const isPrefix = value.endsWith('/');
|
|
123
|
+
if (isPrefix && !allowPrefix) return false;
|
|
124
|
+
const body = isPrefix ? value.slice(0, -1) : value;
|
|
125
|
+
const segments = body.split('/');
|
|
126
|
+
return segments.every((segment) => (
|
|
127
|
+
segment.length > 0 && segment !== '.' && segment !== '..'
|
|
128
|
+
));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function repoPathArray(value, options) {
|
|
132
|
+
return boundedArray(value, (entry) => repoRelativePath(entry, options));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const OWN_KINDS = Object.freeze(['symbol', 'path']);
|
|
136
|
+
const STATE_EFFECT_KINDS = Object.freeze([
|
|
137
|
+
'state',
|
|
138
|
+
'schema',
|
|
139
|
+
'invariant',
|
|
140
|
+
'effect',
|
|
141
|
+
'external_effect',
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 所有宣言。`creates: true`は「このpathはまだ存在せず、このTODOが作る」という創作の意思である
|
|
146
|
+
* (ADR 0135 Decision 3・ADR 0136)。
|
|
147
|
+
*
|
|
148
|
+
* `kind`は`path`のまま据え置く。存在の有無は資源の種類ではなく資源の状態であり、kindを分けると
|
|
149
|
+
* 既存のpath判定(write交差の免除、conflict resourceのkind)が全部この宣言を取りこぼす。
|
|
150
|
+
*
|
|
151
|
+
* 値は`true`だけを受理する。`false`は「存在するpath」と同義で、同じ事実に2つの書き方を
|
|
152
|
+
* 与えることになる。省略が既定である。
|
|
153
|
+
*/
|
|
154
|
+
function ownEntry(value, { allowCreates = false } = {}) {
|
|
155
|
+
if (!plainObject(value)) return false;
|
|
156
|
+
const creates = Object.hasOwn(value, 'creates');
|
|
157
|
+
if (creates && !allowCreates) return false;
|
|
158
|
+
return exactRecord(value, creates ? ['kind', 'target', 'creates'] : ['kind', 'target'])
|
|
159
|
+
&& OWN_KINDS.includes(value.kind)
|
|
160
|
+
&& (!creates || (value.creates === true && value.kind === 'path'))
|
|
161
|
+
&& typeof value.target === 'string'
|
|
162
|
+
&& value.target.length > 0
|
|
163
|
+
&& Buffer.byteLength(value.target, 'utf8') <= MAX_PATH_BYTES;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function stateEffectEntry(value) {
|
|
167
|
+
return plainObject(value)
|
|
168
|
+
&& exactRecord(value, ['resource_id', 'kind'])
|
|
169
|
+
&& identifier(value.resource_id)
|
|
170
|
+
&& STATE_EFFECT_KINDS.includes(value.kind);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function unknownEntry(value) {
|
|
174
|
+
return plainObject(value)
|
|
175
|
+
&& exactRecord(value, ['kind', 'ref'])
|
|
176
|
+
&& identifier(value.kind)
|
|
177
|
+
&& typeof value.ref === 'string'
|
|
178
|
+
&& value.ref.length > 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export const MANUAL_WITNESS_FIELDS = Object.freeze([
|
|
182
|
+
'owns',
|
|
183
|
+
'reads',
|
|
184
|
+
'writes',
|
|
185
|
+
'resources',
|
|
186
|
+
'state_effects',
|
|
187
|
+
'sensor_provenance',
|
|
188
|
+
'affected_tests',
|
|
189
|
+
'unknowns',
|
|
190
|
+
]);
|
|
191
|
+
export const MANUAL_WITNESS_OPTIONAL_FIELDS = Object.freeze(['lines']);
|
|
192
|
+
export const LINE_ROLES = Object.freeze(['reads', 'writes']);
|
|
193
|
+
|
|
194
|
+
function lineAnchor(value) {
|
|
195
|
+
if (!plainObject(value)) return false;
|
|
196
|
+
if (value.kind === 'path') {
|
|
197
|
+
return exactRecord(value, ['kind', 'path']) && repoRelativePath(value.path);
|
|
198
|
+
}
|
|
199
|
+
return value.kind === 'symbol'
|
|
200
|
+
&& exactRecord(value, ['kind', 'name', 'path'])
|
|
201
|
+
&& typeof value.name === 'string' && value.name.length > 0
|
|
202
|
+
&& Buffer.byteLength(value.name, 'utf8') <= MAX_PATH_BYTES
|
|
203
|
+
&& repoRelativePath(value.path);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function lineEntry(value) {
|
|
207
|
+
return plainObject(value)
|
|
208
|
+
&& exactRecord(value, ['line_id', 'role', 'anchors'])
|
|
209
|
+
&& identifier(value.line_id)
|
|
210
|
+
&& LINE_ROLES.includes(value.role)
|
|
211
|
+
&& boundedArray(value.anchors, lineAnchor, { min: 1 });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function lineEntries(value) {
|
|
215
|
+
return boundedArray(value, lineEntry)
|
|
216
|
+
&& new Set(value.map(({ line_id: lineId }) => lineId)).size === value.length;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// manual witness entryの判定は`explainRunRequest`だけが所有する。
|
|
220
|
+
// 同じ規則をbooleanと診断の二箇所へ持たないための単一正本化(ADR 0123)。
|
|
221
|
+
|
|
222
|
+
function boundedArray(value, predicate, { min = 0, max = MAX_COLLECTION } = {}) {
|
|
223
|
+
return Array.isArray(value)
|
|
224
|
+
&& value.length >= min
|
|
225
|
+
&& value.length <= max
|
|
226
|
+
&& value.every((entry) => predicate(entry));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function uniqueIdentifierArray(value, options) {
|
|
230
|
+
return boundedArray(value, identifier, options)
|
|
231
|
+
&& new Set(value).size === value.length;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function validateSafely(value, validator) {
|
|
235
|
+
try {
|
|
236
|
+
canonicalizeArtifact(value);
|
|
237
|
+
return validator(value) === true;
|
|
238
|
+
} catch {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* 自己digest規則: `digestField`自身を除いた残り全fieldのcanonical JSON SHA-256。
|
|
245
|
+
* `run_request.v1`の`request_digest`(RC2 `admission_digest`と同型)を全schemaへ適用する。
|
|
246
|
+
*/
|
|
247
|
+
export function selfDigest(value, digestField) {
|
|
248
|
+
const projection = {};
|
|
249
|
+
for (const key of Object.keys(value)) {
|
|
250
|
+
if (key !== digestField) projection[key] = value[key];
|
|
251
|
+
}
|
|
252
|
+
return digestArtifact(projection);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function selfDigestValid(value, digestField) {
|
|
256
|
+
return digest(value[digestField]) && value[digestField] === selfDigest(value, digestField);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const WITNESS_PROVENANCE = Object.freeze([
|
|
260
|
+
'sensor',
|
|
261
|
+
'manual_candidate_spec',
|
|
262
|
+
'manual_state_effect',
|
|
263
|
+
]);
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* 現行のrun request契約。v5はpathを跨ぐ意味的な線を任意宣言できる。v4は同じ予測契約の
|
|
267
|
+
* lines無し版として残し、v3以前の厳密なcompile契約も既存requestの意味を変えずに読む。
|
|
268
|
+
*
|
|
269
|
+
* v2はこの系列ではない。ADR 0064のepoch後継request(`predecessor_request_digest`と
|
|
270
|
+
* `task_migration_digest`を持つ別shape)が既に使っている番号なので、飛ばして採番する。
|
|
271
|
+
*/
|
|
272
|
+
export const RUN_REQUEST_SCHEMA = 'lattice.run_request.v5';
|
|
273
|
+
export const RUN_REQUEST_PREDICTION_SCHEMA = 'lattice.run_request.v4';
|
|
274
|
+
export const RUN_REQUEST_DECLARATIVE_SCHEMA = 'lattice.run_request.v3';
|
|
275
|
+
export const RUN_REQUEST_LEGACY_SCHEMAS = Object.freeze([
|
|
276
|
+
RUN_REQUEST_PREDICTION_SCHEMA,
|
|
277
|
+
RUN_REQUEST_DECLARATIVE_SCHEMA,
|
|
278
|
+
'lattice.run_request.v1',
|
|
279
|
+
]);
|
|
280
|
+
export const RUN_REQUEST_SCHEMAS = Object.freeze([
|
|
281
|
+
RUN_REQUEST_SCHEMA,
|
|
282
|
+
...RUN_REQUEST_LEGACY_SCHEMAS,
|
|
283
|
+
]);
|
|
284
|
+
|
|
285
|
+
export const RUN_REQUEST_FIELDS = Object.freeze([
|
|
286
|
+
'schema',
|
|
287
|
+
'request_id',
|
|
288
|
+
'repo',
|
|
289
|
+
'capacity',
|
|
290
|
+
'todos',
|
|
291
|
+
'manual_witness',
|
|
292
|
+
'sensor_query_set',
|
|
293
|
+
'executor_capability',
|
|
294
|
+
'claim_mode',
|
|
295
|
+
'request_digest',
|
|
296
|
+
]);
|
|
297
|
+
|
|
298
|
+
export const RUN_REQUEST_CLAIM_MODE = 'exact_minimum';
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* `sensor_query_set.queries[].operation`のclosed set。
|
|
302
|
+
* runtime front-endはこの定数を輸入して使う(同じ閉集合を二箇所へ持たない)。
|
|
303
|
+
*/
|
|
304
|
+
export const SENSOR_QUERY_OPERATIONS = Object.freeze([
|
|
305
|
+
'status',
|
|
306
|
+
'query',
|
|
307
|
+
'callers',
|
|
308
|
+
'callees',
|
|
309
|
+
'impact',
|
|
310
|
+
'affected',
|
|
311
|
+
]);
|
|
312
|
+
|
|
313
|
+
/** `manual_witness[].sensor_provenance.queries[].expect.kind`のclosed set。 */
|
|
314
|
+
export const SENSOR_EXPECT_KINDS = Object.freeze(['symbol', 'path', 'affected']);
|
|
315
|
+
|
|
316
|
+
/** `sensor_provenance` entryのexpectを検査する(front-end adapter契約と同一規則)。 */
|
|
317
|
+
function sensorExpect(value) {
|
|
318
|
+
if (!plainObject(value) || !SENSOR_EXPECT_KINDS.includes(value.kind)) return false;
|
|
319
|
+
if (value.kind === 'symbol') {
|
|
320
|
+
return exactRecord(value, ['kind', 'name', 'path'])
|
|
321
|
+
&& typeof value.name === 'string' && value.name.length > 0
|
|
322
|
+
&& repoRelativePath(value.path);
|
|
323
|
+
}
|
|
324
|
+
return exactRecord(value, ['kind', 'path']) && repoRelativePath(value.path);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* `lattice.run_request.v1`の唯一の判定正本(ADR 0123)。
|
|
329
|
+
* 受理は`{ valid: true }`、拒否は最初の違反の`reason`と`path`を返す。
|
|
330
|
+
* `validateRunRequest`は本関数へ委譲するため、boolean判定と診断が乖離しない。
|
|
331
|
+
*/
|
|
332
|
+
export function explainRunRequest(value) {
|
|
333
|
+
const reject = (reason, at) => ({ valid: false, reason, path: at });
|
|
334
|
+
try {
|
|
335
|
+
canonicalizeArtifact(value);
|
|
336
|
+
} catch {
|
|
337
|
+
return reject('non_canonical_request_bytes', '');
|
|
338
|
+
}
|
|
339
|
+
if (!exactRecord(value, RUN_REQUEST_FIELDS)) return reject('unexpected_or_missing_top_level_keys', '');
|
|
340
|
+
if (!RUN_REQUEST_SCHEMAS.includes(value.schema)) return reject('schema_mismatch', '/schema');
|
|
341
|
+
// 創作宣言はv2から。v1のclosed shapeは余分fieldを拒否するので加算互換が成立しない。
|
|
342
|
+
const allowCreates = [
|
|
343
|
+
RUN_REQUEST_SCHEMA, RUN_REQUEST_PREDICTION_SCHEMA, RUN_REQUEST_DECLARATIVE_SCHEMA,
|
|
344
|
+
].includes(value.schema);
|
|
345
|
+
const allowLines = value.schema === RUN_REQUEST_SCHEMA;
|
|
346
|
+
if (!identifier(value.request_id)) return reject('invalid_identifier', '/request_id');
|
|
347
|
+
if (!exactRecord(value.repo, ['base_sha', 'root_kind'])) return reject('unexpected_or_missing_keys', '/repo');
|
|
348
|
+
if (!gitSha(value.repo.base_sha)) return reject('invalid_git_sha', '/repo/base_sha');
|
|
349
|
+
if (!identifier(value.repo.root_kind)) return reject('invalid_identifier', '/repo/root_kind');
|
|
350
|
+
if (!exactRecord(value.capacity, ['executors'])) return reject('unexpected_or_missing_keys', '/capacity');
|
|
351
|
+
if (!positiveInteger(value.capacity.executors)) return reject('not_a_positive_integer', '/capacity/executors');
|
|
352
|
+
if (!Array.isArray(value.todos) || value.todos.length < 1 || value.todos.length > MAX_COLLECTION) {
|
|
353
|
+
return reject('bounded_collection_violation', '/todos');
|
|
354
|
+
}
|
|
355
|
+
for (const [index, todo] of value.todos.entries()) {
|
|
356
|
+
if (!exactRecord(todo, ['todo_id'])) return reject('unexpected_or_missing_keys', `/todos/${index}`);
|
|
357
|
+
if (!identifier(todo.todo_id)) return reject('invalid_identifier', `/todos/${index}/todo_id`);
|
|
358
|
+
}
|
|
359
|
+
const todoIds = value.todos.map((todo) => todo.todo_id);
|
|
360
|
+
if (new Set(todoIds).size !== todoIds.length) return reject('duplicate_todo_id', '/todos');
|
|
361
|
+
if (!plainObject(value.manual_witness)) return reject('not_an_object', '/manual_witness');
|
|
362
|
+
if (!exactRecord(value.manual_witness, todoIds)) {
|
|
363
|
+
return reject('manual_witness_keys_must_equal_todo_ids', '/manual_witness');
|
|
364
|
+
}
|
|
365
|
+
for (const todoId of todoIds) {
|
|
366
|
+
const witness = value.manual_witness[todoId];
|
|
367
|
+
const at = `/manual_witness/${todoId}`;
|
|
368
|
+
if (!plainObject(witness)) return reject('not_an_object', at);
|
|
369
|
+
const hasLines = Object.hasOwn(witness, 'lines');
|
|
370
|
+
const witnessFields = hasLines
|
|
371
|
+
? [...MANUAL_WITNESS_FIELDS, ...MANUAL_WITNESS_OPTIONAL_FIELDS]
|
|
372
|
+
: MANUAL_WITNESS_FIELDS;
|
|
373
|
+
if (!exactRecord(witness, witnessFields)) return reject('unexpected_or_missing_keys', at);
|
|
374
|
+
if (hasLines && !allowLines) return reject('lines_require_run_request_v5', `${at}/lines`);
|
|
375
|
+
if (hasLines && !lineEntries(witness.lines)) return reject('invalid_line_entries', `${at}/lines`);
|
|
376
|
+
if (!boundedArray(witness.owns, (own) => ownEntry(own, { allowCreates }))) {
|
|
377
|
+
return reject('invalid_own_entries', `${at}/owns`);
|
|
378
|
+
}
|
|
379
|
+
if (!repoPathArray(witness.reads)) return reject('invalid_repo_relative_paths', `${at}/reads`);
|
|
380
|
+
if (!repoPathArray(witness.writes, { allowPrefix: true })) return reject('invalid_repo_relative_paths', `${at}/writes`);
|
|
381
|
+
if (!boundedArray(witness.resources, identifier)) return reject('invalid_identifier', `${at}/resources`);
|
|
382
|
+
if (!boundedArray(witness.state_effects, stateEffectEntry)) return reject('invalid_state_effect_entries', `${at}/state_effects`);
|
|
383
|
+
// sensor_provenanceもfront-end adapter契約のshapeまで検査する(ADR 0123)。
|
|
384
|
+
const provenanceAt = `${at}/sensor_provenance`;
|
|
385
|
+
if (!exactRecord(witness.sensor_provenance, ['queries'])) return reject('unexpected_or_missing_keys', provenanceAt);
|
|
386
|
+
if (!Array.isArray(witness.sensor_provenance.queries)
|
|
387
|
+
|| witness.sensor_provenance.queries.length > MAX_COLLECTION) {
|
|
388
|
+
return reject('bounded_collection_violation', `${provenanceAt}/queries`);
|
|
389
|
+
}
|
|
390
|
+
for (const [index, entry] of witness.sensor_provenance.queries.entries()) {
|
|
391
|
+
const entryAt = `${provenanceAt}/queries/${index}`;
|
|
392
|
+
if (!exactRecord(entry, ['query_id', 'expect'])) return reject('unexpected_or_missing_keys', entryAt);
|
|
393
|
+
if (!identifier(entry.query_id)) return reject('invalid_identifier', `${entryAt}/query_id`);
|
|
394
|
+
if (!sensorExpect(entry.expect)) return reject('invalid_sensor_expect', `${entryAt}/expect`);
|
|
395
|
+
}
|
|
396
|
+
if (!repoPathArray(witness.affected_tests)) return reject('invalid_repo_relative_paths', `${at}/affected_tests`);
|
|
397
|
+
if (!boundedArray(witness.unknowns, unknownEntry)) return reject('invalid_unknown_entries', `${at}/unknowns`);
|
|
398
|
+
}
|
|
399
|
+
// sensor_query_set/executor_capabilityは、runtime front-endとrun startが実際に要求する
|
|
400
|
+
// shapeまで検査する。schemaで通ってから後段で落ちる契約分裂を残さない(ADR 0123)。
|
|
401
|
+
if (!exactRecord(value.sensor_query_set, ['queries'])) return reject('unexpected_or_missing_keys', '/sensor_query_set');
|
|
402
|
+
if (!Array.isArray(value.sensor_query_set.queries) || value.sensor_query_set.queries.length > MAX_COLLECTION) {
|
|
403
|
+
return reject('bounded_collection_violation', '/sensor_query_set/queries');
|
|
404
|
+
}
|
|
405
|
+
const queryIds = new Set();
|
|
406
|
+
for (const [index, query] of value.sensor_query_set.queries.entries()) {
|
|
407
|
+
const at = `/sensor_query_set/queries/${index}`;
|
|
408
|
+
const keys = plainObject(query) && Object.hasOwn(query, 'target')
|
|
409
|
+
? ['id', 'operation', 'target'] : ['id', 'operation'];
|
|
410
|
+
if (!exactRecord(query, keys)) return reject('unexpected_or_missing_keys', at);
|
|
411
|
+
if (!identifier(query.id)) return reject('invalid_identifier', `${at}/id`);
|
|
412
|
+
if (!SENSOR_QUERY_OPERATIONS.includes(query.operation)) return reject('unknown_sensor_query_operation', `${at}/operation`);
|
|
413
|
+
if (keys.includes('target') && (typeof query.target !== 'string' || query.target.length === 0)) {
|
|
414
|
+
return reject('empty_query_target', `${at}/target`);
|
|
415
|
+
}
|
|
416
|
+
if (queryIds.has(query.id)) return reject('duplicate_query_id', `${at}/id`);
|
|
417
|
+
queryIds.add(query.id);
|
|
418
|
+
}
|
|
419
|
+
if (!exactRecord(value.executor_capability, ['adapters'])) return reject('unexpected_or_missing_keys', '/executor_capability');
|
|
420
|
+
if (!uniqueIdentifierArray(value.executor_capability.adapters, { min: 1 })) {
|
|
421
|
+
return reject('invalid_adapter_identifiers', '/executor_capability/adapters');
|
|
422
|
+
}
|
|
423
|
+
if (value.claim_mode !== RUN_REQUEST_CLAIM_MODE) return reject('claim_mode_must_be_exact_minimum', '/claim_mode');
|
|
424
|
+
if (!selfDigestValid(value, 'request_digest')) return reject('request_digest_mismatch', '/request_digest');
|
|
425
|
+
return { valid: true };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** `lattice.run_request.v1`。manual witnessはTODOごとに完備でなければならない。 */
|
|
429
|
+
export function validateRunRequest(value) {
|
|
430
|
+
return explainRunRequest(value).valid;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* runtime planをrun requestへcross-bindする(ADR 0044 Decision 2)。
|
|
435
|
+
* 同一TODO集合を別base/別requestへ再包装したplanをtyped rejectするための検証で、
|
|
436
|
+
* schema単体のvalidatorとは別に、request bytesとの一致を要求する。
|
|
437
|
+
*/
|
|
438
|
+
export function verifyRuntimePlanBinding(options = {}) {
|
|
439
|
+
if (options === null || typeof options !== 'object' || Array.isArray(options)) return false;
|
|
440
|
+
const { plan, request } = options;
|
|
441
|
+
if (!validateRuntimePlan(plan) || !validateRunRequest(request)) return false;
|
|
442
|
+
if (plan.request_digest !== request.request_digest) return false;
|
|
443
|
+
if (plan.base_sha !== request.repo.base_sha) return false;
|
|
444
|
+
const requestTodoIds = new Set(request.todos.map((todo) => todo.todo_id));
|
|
445
|
+
return plan.nodes.every((node) => requestTodoIds.has(node.todo_id));
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* `lattice.boundary_manifest.v4`。v4はpathを跨ぐ意味的な線を任意記録できる。
|
|
450
|
+
*
|
|
451
|
+
* v3は`owns[].creates`だけがv2との差である。宣言が持っていた「このpathはまだ無い」を
|
|
452
|
+
* 記録側でも保つ——落とすと、manifestだけを読む消費者が既存fileと同じ扱いをする。
|
|
453
|
+
* 旧v2/v3 manifestはrun storeに残るので読み口として受理し、linesを要求しない。
|
|
454
|
+
*/
|
|
455
|
+
export const BOUNDARY_MANIFEST_SCHEMA = 'lattice.boundary_manifest.v4';
|
|
456
|
+
export const BOUNDARY_MANIFEST_SCHEMAS = Object.freeze([
|
|
457
|
+
BOUNDARY_MANIFEST_SCHEMA,
|
|
458
|
+
'lattice.boundary_manifest.v3',
|
|
459
|
+
'lattice.boundary_manifest.v2',
|
|
460
|
+
]);
|
|
461
|
+
export function validateRuntimeBoundaryManifest(value) {
|
|
462
|
+
return validateSafely(value, (manifest) => {
|
|
463
|
+
const hasLines = Object.hasOwn(manifest, 'lines');
|
|
464
|
+
return (
|
|
465
|
+
exactRecord(manifest, [
|
|
466
|
+
'schema',
|
|
467
|
+
'todo_id',
|
|
468
|
+
'owns',
|
|
469
|
+
'reads',
|
|
470
|
+
'writes',
|
|
471
|
+
'resources',
|
|
472
|
+
'state_effects',
|
|
473
|
+
'unknowns',
|
|
474
|
+
'affected_tests',
|
|
475
|
+
'graph_evidence',
|
|
476
|
+
'witness_provenance',
|
|
477
|
+
...(hasLines ? ['lines'] : []),
|
|
478
|
+
'manifest_digest',
|
|
479
|
+
])
|
|
480
|
+
&& BOUNDARY_MANIFEST_SCHEMAS.includes(manifest.schema)
|
|
481
|
+
&& (!hasLines || manifest.schema === BOUNDARY_MANIFEST_SCHEMA)
|
|
482
|
+
&& identifier(manifest.todo_id)
|
|
483
|
+
&& boundedArray(manifest.owns, (own) => ownEntry(own, {
|
|
484
|
+
allowCreates: [BOUNDARY_MANIFEST_SCHEMA, 'lattice.boundary_manifest.v3'].includes(manifest.schema),
|
|
485
|
+
}))
|
|
486
|
+
&& repoPathArray(manifest.reads)
|
|
487
|
+
&& repoPathArray(manifest.writes, { allowPrefix: true })
|
|
488
|
+
&& boundedArray(manifest.resources, identifier)
|
|
489
|
+
&& boundedArray(manifest.state_effects, stateEffectEntry)
|
|
490
|
+
&& boundedArray(manifest.unknowns, unknownEntry)
|
|
491
|
+
&& repoPathArray(manifest.affected_tests)
|
|
492
|
+
&& boundedArray(manifest.graph_evidence, plainObject)
|
|
493
|
+
&& plainObject(manifest.witness_provenance)
|
|
494
|
+
&& Object.values(manifest.witness_provenance).every((entry) => (
|
|
495
|
+
WITNESS_PROVENANCE.includes(entry)
|
|
496
|
+
))
|
|
497
|
+
&& (!hasLines || lineEntries(manifest.lines))
|
|
498
|
+
&& selfDigestValid(manifest, 'manifest_digest')
|
|
499
|
+
);
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** `lattice.runtime_plan.v1`。exact_minimum claimは1〜8 nodeだけを受理する(Decision 1)。 */
|
|
504
|
+
export function validateRuntimePlan(value) {
|
|
505
|
+
return validateSafely(value, (plan) => {
|
|
506
|
+
if (!exactRecord(plan, [
|
|
507
|
+
'schema',
|
|
508
|
+
'plan_ref',
|
|
509
|
+
'plan_epoch',
|
|
510
|
+
'request_digest',
|
|
511
|
+
'base_sha',
|
|
512
|
+
'nodes',
|
|
513
|
+
'precedence',
|
|
514
|
+
'conflicts',
|
|
515
|
+
'capacity',
|
|
516
|
+
'manifest_digests',
|
|
517
|
+
'claim',
|
|
518
|
+
'predecessor_refs',
|
|
519
|
+
'plan_digest',
|
|
520
|
+
])
|
|
521
|
+
|| plan.schema !== 'lattice.runtime_plan.v1'
|
|
522
|
+
|| !identifier(plan.plan_ref)
|
|
523
|
+
|| !nonNegativeInteger(plan.plan_epoch)
|
|
524
|
+
|| !digest(plan.request_digest)
|
|
525
|
+
|| !gitSha(plan.base_sha)
|
|
526
|
+
|| !boundedArray(plan.nodes, (node) => (
|
|
527
|
+
exactRecord(node, ['todo_id']) && identifier(node.todo_id)
|
|
528
|
+
), { min: 1, max: MAX_NODES_PER_PLAN })
|
|
529
|
+
|| !exactRecord(plan.claim, ['mode'])
|
|
530
|
+
|| plan.claim.mode !== 'exact_minimum'
|
|
531
|
+
|| !exactRecord(plan.capacity, ['executors'])
|
|
532
|
+
|| !positiveInteger(plan.capacity.executors)
|
|
533
|
+
|| !boundedArray(plan.predecessor_refs, (entry) => typeof entry === 'string')) {
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
const nodeIds = plan.nodes.map((node) => node.todo_id);
|
|
537
|
+
const nodeSet = new Set(nodeIds);
|
|
538
|
+
if (nodeSet.size !== nodeIds.length) return false;
|
|
539
|
+
if (!boundedArray(plan.precedence, (edge) => (
|
|
540
|
+
exactRecord(edge, ['from_todo_id', 'to_todo_id'])
|
|
541
|
+
&& nodeSet.has(edge.from_todo_id)
|
|
542
|
+
&& nodeSet.has(edge.to_todo_id)
|
|
543
|
+
&& edge.from_todo_id !== edge.to_todo_id
|
|
544
|
+
))) {
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
if (!boundedArray(plan.conflicts, (conflict) => (
|
|
548
|
+
exactRecord(conflict, ['todo_ids', 'resource_id'])
|
|
549
|
+
&& Array.isArray(conflict.todo_ids)
|
|
550
|
+
&& conflict.todo_ids.length === 2
|
|
551
|
+
&& conflict.todo_ids.every((todoId) => nodeSet.has(todoId))
|
|
552
|
+
&& conflict.todo_ids[0] !== conflict.todo_ids[1]
|
|
553
|
+
&& identifier(conflict.resource_id)
|
|
554
|
+
))) {
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
return plainObject(plan.manifest_digests)
|
|
558
|
+
&& exactRecord(plan.manifest_digests, nodeIds)
|
|
559
|
+
&& Object.values(plan.manifest_digests).every(digest)
|
|
560
|
+
&& selfDigestValid(plan, 'plan_digest');
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/** `lattice.run_event.v1`のevent単体shape。chain規則はruntime-event-store.mjsが検査する。 */
|
|
565
|
+
export function validateRunEvent(value) {
|
|
566
|
+
return validateSafely(value, (event) => (
|
|
567
|
+
exactRecord(event, [
|
|
568
|
+
'schema',
|
|
569
|
+
'run_id',
|
|
570
|
+
'sequence',
|
|
571
|
+
'previous_digest',
|
|
572
|
+
'kind',
|
|
573
|
+
'actor',
|
|
574
|
+
'plan_epoch',
|
|
575
|
+
'subject',
|
|
576
|
+
'payload',
|
|
577
|
+
'recorded_at',
|
|
578
|
+
'event_digest',
|
|
579
|
+
])
|
|
580
|
+
&& event.schema === 'lattice.run_event.v1'
|
|
581
|
+
&& identifier(event.run_id)
|
|
582
|
+
&& nonNegativeInteger(event.sequence)
|
|
583
|
+
&& (event.previous_digest === null || digest(event.previous_digest))
|
|
584
|
+
&& (event.sequence === 0) === (event.previous_digest === null)
|
|
585
|
+
&& RUN_EVENT_KINDS.includes(event.kind)
|
|
586
|
+
&& identifier(event.actor)
|
|
587
|
+
&& nonNegativeInteger(event.plan_epoch)
|
|
588
|
+
&& exactRecord(event.subject, ['kind', 'ref'])
|
|
589
|
+
&& identifier(event.subject.kind)
|
|
590
|
+
&& typeof event.subject.ref === 'string'
|
|
591
|
+
&& plainObject(event.payload)
|
|
592
|
+
&& timestamp(event.recorded_at)
|
|
593
|
+
&& selfDigestValid(event, 'event_digest')
|
|
594
|
+
));
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const CONTEXT_CONTENT_FIELDS = Object.freeze([
|
|
598
|
+
'todo_id',
|
|
599
|
+
'task_ref',
|
|
600
|
+
'scope',
|
|
601
|
+
'base_sha',
|
|
602
|
+
'verifier_refs',
|
|
603
|
+
'forbidden_operations',
|
|
604
|
+
]);
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* ADR 0044 Decision 7.1のcontent projection digest。plan帰属field
|
|
608
|
+
* (packet_id・plan_ref・plan_epoch・packet_digest)を除いた6 fieldだけを対象にし、
|
|
609
|
+
* epoch rebindが「content不変・epochだけ更新」であることを機械検査可能にする。
|
|
610
|
+
*/
|
|
611
|
+
export function computeContextContentDigest(packet) {
|
|
612
|
+
const projection = {};
|
|
613
|
+
for (const field of CONTEXT_CONTENT_FIELDS) {
|
|
614
|
+
projection[field] = packet[field];
|
|
615
|
+
}
|
|
616
|
+
return digestArtifact(projection);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/** `lattice.executor_packet.v1`。dispatch時に発行する唯一のcontext packet。 */
|
|
620
|
+
export function validateExecutorPacket(value) {
|
|
621
|
+
return validateSafely(value, (packet) => (
|
|
622
|
+
exactRecord(packet, [
|
|
623
|
+
'schema',
|
|
624
|
+
'packet_id',
|
|
625
|
+
'todo_id',
|
|
626
|
+
'task_ref',
|
|
627
|
+
'scope',
|
|
628
|
+
'base_sha',
|
|
629
|
+
'plan_ref',
|
|
630
|
+
'plan_epoch',
|
|
631
|
+
'verifier_refs',
|
|
632
|
+
'forbidden_operations',
|
|
633
|
+
'context_content_digest',
|
|
634
|
+
'packet_digest',
|
|
635
|
+
])
|
|
636
|
+
&& packet.schema === 'lattice.executor_packet.v1'
|
|
637
|
+
&& identifier(packet.packet_id)
|
|
638
|
+
&& identifier(packet.todo_id)
|
|
639
|
+
&& identifier(packet.task_ref)
|
|
640
|
+
&& plainObject(packet.scope)
|
|
641
|
+
&& gitSha(packet.base_sha)
|
|
642
|
+
&& identifier(packet.plan_ref)
|
|
643
|
+
&& nonNegativeInteger(packet.plan_epoch)
|
|
644
|
+
&& boundedArray(packet.verifier_refs, (entry) => typeof entry === 'string')
|
|
645
|
+
&& boundedArray(packet.forbidden_operations, (entry) => typeof entry === 'string', { min: 1 })
|
|
646
|
+
&& packet.context_content_digest === computeContextContentDigest(packet)
|
|
647
|
+
&& selfDigestValid(packet, 'packet_digest')
|
|
648
|
+
));
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** `lattice.executor_receipt.v1`。binding欠落はtyped rejectの対象(Decision 7.4)。 */
|
|
652
|
+
export function validateExecutorReceipt(value) {
|
|
653
|
+
return validateSafely(value, (receipt) => (
|
|
654
|
+
exactRecord(receipt, [
|
|
655
|
+
'schema',
|
|
656
|
+
'receipt_id',
|
|
657
|
+
'executor_handle',
|
|
658
|
+
'worktree_id',
|
|
659
|
+
'base_sha',
|
|
660
|
+
'plan_epoch',
|
|
661
|
+
'packet_digest',
|
|
662
|
+
'todo_id',
|
|
663
|
+
'checkpoint_digest',
|
|
664
|
+
'observed_diff',
|
|
665
|
+
'receipt_digest',
|
|
666
|
+
])
|
|
667
|
+
&& receipt.schema === 'lattice.executor_receipt.v1'
|
|
668
|
+
&& identifier(receipt.receipt_id)
|
|
669
|
+
&& identifier(receipt.executor_handle)
|
|
670
|
+
&& identifier(receipt.worktree_id)
|
|
671
|
+
&& gitSha(receipt.base_sha)
|
|
672
|
+
&& nonNegativeInteger(receipt.plan_epoch)
|
|
673
|
+
&& digest(receipt.packet_digest)
|
|
674
|
+
&& identifier(receipt.todo_id)
|
|
675
|
+
&& digest(receipt.checkpoint_digest)
|
|
676
|
+
&& boundedArray(receipt.observed_diff, (entry) => (
|
|
677
|
+
exactRecord(entry, ['path', 'change'])
|
|
678
|
+
&& repoRelativePath(entry.path)
|
|
679
|
+
&& ['added', 'modified', 'deleted'].includes(entry.change)
|
|
680
|
+
))
|
|
681
|
+
&& selfDigestValid(receipt, 'receipt_digest')
|
|
682
|
+
));
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/** `lattice.hold_decision.v1`。hold/continueは交差しない。 */
|
|
686
|
+
export function validateHoldDecision(value) {
|
|
687
|
+
return validateSafely(value, (decision) => {
|
|
688
|
+
if (!exactRecord(decision, [
|
|
689
|
+
'schema',
|
|
690
|
+
'decision_id',
|
|
691
|
+
'finding',
|
|
692
|
+
'frozen_prefix_digest',
|
|
693
|
+
'affected_closure',
|
|
694
|
+
'hold_set',
|
|
695
|
+
'continue_set',
|
|
696
|
+
'evidence_digests',
|
|
697
|
+
'decision_digest',
|
|
698
|
+
])
|
|
699
|
+
|| decision.schema !== 'lattice.hold_decision.v1'
|
|
700
|
+
|| !identifier(decision.decision_id)
|
|
701
|
+
|| !plainObject(decision.finding)
|
|
702
|
+
|| !HOLD_REASON_KINDS.includes(decision.finding.kind)
|
|
703
|
+
|| !digest(decision.frozen_prefix_digest)
|
|
704
|
+
|| !uniqueIdentifierArray(decision.affected_closure)
|
|
705
|
+
|| !uniqueIdentifierArray(decision.hold_set)
|
|
706
|
+
|| !uniqueIdentifierArray(decision.continue_set)
|
|
707
|
+
|| !boundedArray(decision.evidence_digests, digest)) {
|
|
708
|
+
return false;
|
|
709
|
+
}
|
|
710
|
+
const holdSet = new Set(decision.hold_set);
|
|
711
|
+
return decision.continue_set.every((todoId) => !holdSet.has(todoId))
|
|
712
|
+
&& selfDigestValid(decision, 'decision_digest');
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const INVARIANT_DIGEST_FIELDS = Object.freeze([
|
|
717
|
+
'todo_input',
|
|
718
|
+
'boundary_manifest',
|
|
719
|
+
'validator',
|
|
720
|
+
'context_content',
|
|
721
|
+
]);
|
|
722
|
+
|
|
723
|
+
/** `lattice.carry_over_witness.v1`。invariant digestは保存bytesから再計算できる。 */
|
|
724
|
+
export function validateCarryOverWitness(value) {
|
|
725
|
+
return validateSafely(value, (witness) => (
|
|
726
|
+
exactRecord(witness, [
|
|
727
|
+
'schema',
|
|
728
|
+
'witness_id',
|
|
729
|
+
'todo_id',
|
|
730
|
+
'predecessor_epoch',
|
|
731
|
+
'successor_epoch',
|
|
732
|
+
'invariant_digests',
|
|
733
|
+
'non_overlap_evidence',
|
|
734
|
+
'receipt_bindings',
|
|
735
|
+
'witness_digest',
|
|
736
|
+
])
|
|
737
|
+
&& witness.schema === 'lattice.carry_over_witness.v1'
|
|
738
|
+
&& identifier(witness.witness_id)
|
|
739
|
+
&& identifier(witness.todo_id)
|
|
740
|
+
&& nonNegativeInteger(witness.predecessor_epoch)
|
|
741
|
+
&& nonNegativeInteger(witness.successor_epoch)
|
|
742
|
+
&& witness.successor_epoch > witness.predecessor_epoch
|
|
743
|
+
&& exactRecord(witness.invariant_digests, INVARIANT_DIGEST_FIELDS)
|
|
744
|
+
&& INVARIANT_DIGEST_FIELDS.every((field) => digest(witness.invariant_digests[field]))
|
|
745
|
+
&& boundedArray(witness.non_overlap_evidence, (entry) => typeof entry === 'string')
|
|
746
|
+
&& boundedArray(witness.receipt_bindings, (binding) => (
|
|
747
|
+
exactRecord(binding, [
|
|
748
|
+
'receipt_id',
|
|
749
|
+
'checkpoint_digest',
|
|
750
|
+
'recorded_sequence',
|
|
751
|
+
'within_frozen_prefix',
|
|
752
|
+
])
|
|
753
|
+
&& identifier(binding.receipt_id)
|
|
754
|
+
&& digest(binding.checkpoint_digest)
|
|
755
|
+
&& nonNegativeInteger(binding.recorded_sequence)
|
|
756
|
+
&& typeof binding.within_frozen_prefix === 'boolean'
|
|
757
|
+
))
|
|
758
|
+
&& selfDigestValid(witness, 'witness_digest')
|
|
759
|
+
));
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/** `lattice.epoch_rebind_packet.v1`。content不変・plan帰属だけを更新する(Decision 7.3)。 */
|
|
763
|
+
export function validateEpochRebindPacket(value) {
|
|
764
|
+
return validateSafely(value, (packet) => (
|
|
765
|
+
exactRecord(packet, [
|
|
766
|
+
'schema',
|
|
767
|
+
'packet_id',
|
|
768
|
+
'todo_id',
|
|
769
|
+
'executor_handle',
|
|
770
|
+
'worktree_id',
|
|
771
|
+
'witness_digest',
|
|
772
|
+
'context_content_digest',
|
|
773
|
+
'authorized_checkpoint_digest',
|
|
774
|
+
'old_plan_ref',
|
|
775
|
+
'new_plan_ref',
|
|
776
|
+
'new_plan_epoch',
|
|
777
|
+
'packet_digest',
|
|
778
|
+
])
|
|
779
|
+
&& packet.schema === 'lattice.epoch_rebind_packet.v1'
|
|
780
|
+
&& identifier(packet.packet_id)
|
|
781
|
+
&& identifier(packet.todo_id)
|
|
782
|
+
&& identifier(packet.executor_handle)
|
|
783
|
+
&& identifier(packet.worktree_id)
|
|
784
|
+
&& digest(packet.witness_digest)
|
|
785
|
+
&& digest(packet.context_content_digest)
|
|
786
|
+
&& digest(packet.authorized_checkpoint_digest)
|
|
787
|
+
&& identifier(packet.old_plan_ref)
|
|
788
|
+
&& identifier(packet.new_plan_ref)
|
|
789
|
+
&& packet.old_plan_ref !== packet.new_plan_ref
|
|
790
|
+
&& positiveInteger(packet.new_plan_epoch)
|
|
791
|
+
&& selfDigestValid(packet, 'packet_digest')
|
|
792
|
+
));
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/** `lattice.runtime_plan_diff.v1`。carry-overとredispatchは交差しない。 */
|
|
796
|
+
export function validateRuntimePlanDiff(value) {
|
|
797
|
+
return validateSafely(value, (diff) => {
|
|
798
|
+
if (!exactRecord(diff, [
|
|
799
|
+
'schema',
|
|
800
|
+
'old_plan_ref',
|
|
801
|
+
'new_plan_ref',
|
|
802
|
+
'accepted_checkpoints',
|
|
803
|
+
'invalidated_contexts',
|
|
804
|
+
'carried_over',
|
|
805
|
+
'redispatched',
|
|
806
|
+
'node_edge_diff',
|
|
807
|
+
'diff_digest',
|
|
808
|
+
])
|
|
809
|
+
|| diff.schema !== 'lattice.runtime_plan_diff.v1'
|
|
810
|
+
|| !identifier(diff.old_plan_ref)
|
|
811
|
+
|| !identifier(diff.new_plan_ref)
|
|
812
|
+
|| diff.old_plan_ref === diff.new_plan_ref
|
|
813
|
+
|| !boundedArray(diff.accepted_checkpoints, digest)
|
|
814
|
+
|| !uniqueIdentifierArray(diff.invalidated_contexts)
|
|
815
|
+
|| !uniqueIdentifierArray(diff.carried_over)
|
|
816
|
+
|| !uniqueIdentifierArray(diff.redispatched)
|
|
817
|
+
|| !plainObject(diff.node_edge_diff)) {
|
|
818
|
+
return false;
|
|
819
|
+
}
|
|
820
|
+
const carried = new Set(diff.carried_over);
|
|
821
|
+
return diff.redispatched.every((todoId) => !carried.has(todoId))
|
|
822
|
+
&& selfDigestValid(diff, 'diff_digest');
|
|
823
|
+
});
|
|
824
|
+
}
|