@quolu/lattice 0.52.0 → 0.52.2

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