@quolu/lattice 0.19.0 → 0.20.1
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
CHANGED
|
@@ -15,6 +15,11 @@ const PROVENANCE_SOURCES = new Set([
|
|
|
15
15
|
const SENSOR_STATUSES = new Set([
|
|
16
16
|
'ready',
|
|
17
17
|
'symbol_absent',
|
|
18
|
+
// 不存在path関連。宣言の有無で3つに分かれる(ADR 0136)。宣言が無いabsentはpath_absent、
|
|
19
|
+
// 宣言があるのに実在するのはcreates_path_present、fs観測が記録に無いのはcreates_unverified。
|
|
20
|
+
'path_absent',
|
|
21
|
+
'creates_path_present',
|
|
22
|
+
'creates_unverified',
|
|
18
23
|
'empty',
|
|
19
24
|
'unresolved',
|
|
20
25
|
'command_failure',
|
|
@@ -140,10 +140,23 @@ const STATE_EFFECT_KINDS = Object.freeze([
|
|
|
140
140
|
'external_effect',
|
|
141
141
|
]);
|
|
142
142
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
143
|
+
/**
|
|
144
|
+
* 所有宣言。`creates: true`は「このpathはまだ存在せず、このTODOが作る」という創作の意思である
|
|
145
|
+
* (ADR 0135 Decision 3・ADR 0136)。
|
|
146
|
+
*
|
|
147
|
+
* `kind`は`path`のまま据え置く。存在の有無は資源の種類ではなく資源の状態であり、kindを分けると
|
|
148
|
+
* 既存のpath判定(write交差の免除、conflict resourceのkind)が全部この宣言を取りこぼす。
|
|
149
|
+
*
|
|
150
|
+
* 値は`true`だけを受理する。`false`は「存在するpath」と同義で、同じ事実に2つの書き方を
|
|
151
|
+
* 与えることになる。省略が既定である。
|
|
152
|
+
*/
|
|
153
|
+
function ownEntry(value, { allowCreates = false } = {}) {
|
|
154
|
+
if (!plainObject(value)) return false;
|
|
155
|
+
const creates = Object.hasOwn(value, 'creates');
|
|
156
|
+
if (creates && !allowCreates) return false;
|
|
157
|
+
return exactRecord(value, creates ? ['kind', 'target', 'creates'] : ['kind', 'target'])
|
|
146
158
|
&& OWN_KINDS.includes(value.kind)
|
|
159
|
+
&& (!creates || (value.creates === true && value.kind === 'path'))
|
|
147
160
|
&& typeof value.target === 'string'
|
|
148
161
|
&& value.target.length > 0
|
|
149
162
|
&& Buffer.byteLength(value.target, 'utf8') <= MAX_PATH_BYTES;
|
|
@@ -221,6 +234,20 @@ const WITNESS_PROVENANCE = Object.freeze([
|
|
|
221
234
|
'manual_state_effect',
|
|
222
235
|
]);
|
|
223
236
|
|
|
237
|
+
/**
|
|
238
|
+
* 現行のrun request契約。v3は`owns[].creates`だけがv1との差であり、境界宣言としては同値である。
|
|
239
|
+
* 既存requestの書き換えを要求しないため、v1は読み口として残す。
|
|
240
|
+
*
|
|
241
|
+
* v2はこの系列ではない。ADR 0064のepoch後継request(`predecessor_request_digest`と
|
|
242
|
+
* `task_migration_digest`を持つ別shape)が既に使っている番号なので、飛ばして採番する。
|
|
243
|
+
*/
|
|
244
|
+
export const RUN_REQUEST_SCHEMA = 'lattice.run_request.v3';
|
|
245
|
+
export const RUN_REQUEST_LEGACY_SCHEMAS = Object.freeze(['lattice.run_request.v1']);
|
|
246
|
+
export const RUN_REQUEST_SCHEMAS = Object.freeze([
|
|
247
|
+
RUN_REQUEST_SCHEMA,
|
|
248
|
+
...RUN_REQUEST_LEGACY_SCHEMAS,
|
|
249
|
+
]);
|
|
250
|
+
|
|
224
251
|
export const RUN_REQUEST_FIELDS = Object.freeze([
|
|
225
252
|
'schema',
|
|
226
253
|
'request_id',
|
|
@@ -276,7 +303,9 @@ export function explainRunRequest(value) {
|
|
|
276
303
|
return reject('non_canonical_request_bytes', '');
|
|
277
304
|
}
|
|
278
305
|
if (!exactRecord(value, RUN_REQUEST_FIELDS)) return reject('unexpected_or_missing_top_level_keys', '');
|
|
279
|
-
if (value.schema
|
|
306
|
+
if (!RUN_REQUEST_SCHEMAS.includes(value.schema)) return reject('schema_mismatch', '/schema');
|
|
307
|
+
// 創作宣言はv2から。v1のclosed shapeは余分fieldを拒否するので加算互換が成立しない。
|
|
308
|
+
const allowCreates = value.schema === RUN_REQUEST_SCHEMA;
|
|
280
309
|
if (!identifier(value.request_id)) return reject('invalid_identifier', '/request_id');
|
|
281
310
|
if (!exactRecord(value.repo, ['base_sha', 'root_kind'])) return reject('unexpected_or_missing_keys', '/repo');
|
|
282
311
|
if (!gitSha(value.repo.base_sha)) return reject('invalid_git_sha', '/repo/base_sha');
|
|
@@ -301,7 +330,9 @@ export function explainRunRequest(value) {
|
|
|
301
330
|
const at = `/manual_witness/${todoId}`;
|
|
302
331
|
if (!plainObject(witness)) return reject('not_an_object', at);
|
|
303
332
|
if (!exactRecord(witness, MANUAL_WITNESS_FIELDS)) return reject('unexpected_or_missing_keys', at);
|
|
304
|
-
if (!boundedArray(witness.owns,
|
|
333
|
+
if (!boundedArray(witness.owns, (own) => ownEntry(own, { allowCreates }))) {
|
|
334
|
+
return reject('invalid_own_entries', `${at}/owns`);
|
|
335
|
+
}
|
|
305
336
|
if (!repoPathArray(witness.reads)) return reject('invalid_repo_relative_paths', `${at}/reads`);
|
|
306
337
|
if (!repoPathArray(witness.writes, { allowPrefix: true })) return reject('invalid_repo_relative_paths', `${at}/writes`);
|
|
307
338
|
if (!boundedArray(witness.resources, identifier)) return reject('invalid_identifier', `${at}/resources`);
|
|
@@ -371,7 +402,18 @@ export function verifyRuntimePlanBinding(options = {}) {
|
|
|
371
402
|
return plan.nodes.every((node) => requestTodoIds.has(node.todo_id));
|
|
372
403
|
}
|
|
373
404
|
|
|
374
|
-
/**
|
|
405
|
+
/**
|
|
406
|
+
* `lattice.boundary_manifest.v3`。witness_provenanceはresourceごとに区別する。
|
|
407
|
+
*
|
|
408
|
+
* v3は`owns[].creates`だけがv2との差である。宣言が持っていた「このpathはまだ無い」を
|
|
409
|
+
* 記録側でも保つ——落とすと、manifestだけを読む消費者が既存fileと同じ扱いをする。
|
|
410
|
+
* 旧v2 manifestはrun storeに残るので読み口として受理する。
|
|
411
|
+
*/
|
|
412
|
+
export const BOUNDARY_MANIFEST_SCHEMA = 'lattice.boundary_manifest.v3';
|
|
413
|
+
export const BOUNDARY_MANIFEST_SCHEMAS = Object.freeze([
|
|
414
|
+
BOUNDARY_MANIFEST_SCHEMA,
|
|
415
|
+
'lattice.boundary_manifest.v2',
|
|
416
|
+
]);
|
|
375
417
|
export function validateRuntimeBoundaryManifest(value) {
|
|
376
418
|
return validateSafely(value, (manifest) => (
|
|
377
419
|
exactRecord(manifest, [
|
|
@@ -388,9 +430,11 @@ export function validateRuntimeBoundaryManifest(value) {
|
|
|
388
430
|
'witness_provenance',
|
|
389
431
|
'manifest_digest',
|
|
390
432
|
])
|
|
391
|
-
&& manifest.schema
|
|
433
|
+
&& BOUNDARY_MANIFEST_SCHEMAS.includes(manifest.schema)
|
|
392
434
|
&& identifier(manifest.todo_id)
|
|
393
|
-
&& boundedArray(manifest.owns, ownEntry
|
|
435
|
+
&& boundedArray(manifest.owns, (own) => ownEntry(own, {
|
|
436
|
+
allowCreates: manifest.schema === BOUNDARY_MANIFEST_SCHEMA,
|
|
437
|
+
}))
|
|
394
438
|
&& repoPathArray(manifest.reads)
|
|
395
439
|
&& repoPathArray(manifest.writes, { allowPrefix: true })
|
|
396
440
|
&& boundedArray(manifest.resources, identifier)
|
|
@@ -6,6 +6,7 @@ import { portableSensorOutcome } from './sensor-adapter.mjs';
|
|
|
6
6
|
import { compileSchedulabilityGraphV2 } from './schedulability-compiler-v2.mjs';
|
|
7
7
|
import { verifySchedulabilityPlanV2 } from './schedulability-verifier-v2.mjs';
|
|
8
8
|
import {
|
|
9
|
+
BOUNDARY_MANIFEST_SCHEMA,
|
|
9
10
|
SENSOR_EXPECT_KINDS,
|
|
10
11
|
SENSOR_QUERY_OPERATIONS,
|
|
11
12
|
selfDigest,
|
|
@@ -265,11 +266,44 @@ function entryNode(entry) {
|
|
|
265
266
|
return null;
|
|
266
267
|
}
|
|
267
268
|
|
|
269
|
+
/**
|
|
270
|
+
* 創作宣言されたpathの裏付けを決める(ADR 0136)。
|
|
271
|
+
*
|
|
272
|
+
* 宣言と観測が一致する時だけ`ready`にする。fresh absentは推測ではなくfsのlstat結果なので、
|
|
273
|
+
* 「観測できなかった」ではなく「観測して、無かった」である。存在しないfileに依存するものは
|
|
274
|
+
* 構造的に存在しえないため、affected testが空であることまで確かめれば裏付けは閉じる。
|
|
275
|
+
*
|
|
276
|
+
* 既に存在するpathへ創作を宣言していたら`creates_path_present`で止める。実害は無いが、
|
|
277
|
+
* 宣言が実態からずれているのを黙って通すと、宣言と観測が一致しているという前提が崩れる。
|
|
278
|
+
* 宣言していないabsent pathは従来どおり`path_absent`のままにする——綴り違いを
|
|
279
|
+
* 「黙って通る創作境界」にしないための線である。
|
|
280
|
+
*/
|
|
281
|
+
function creationBoundaryStatus(outcome, expectPath) {
|
|
282
|
+
// sensorが答えられていない状況は、この関数の管轄でない。通常のstatus判定へ返す。
|
|
283
|
+
if (!['ready', 'empty'].includes(outcome.status)) return null;
|
|
284
|
+
const target = affectedTarget(outcome.raw, expectPath);
|
|
285
|
+
// fs観測が記録に無いなら、不存在は確かめられていない。宣言だけで裏付けにしない。
|
|
286
|
+
if (target === null || target === undefined) return 'creates_unverified';
|
|
287
|
+
if (target.path_state !== 'absent') return 'creates_path_present';
|
|
288
|
+
const payload = affectedPayload(outcome.raw, expectPath);
|
|
289
|
+
if (payload === null
|
|
290
|
+
|| !Array.isArray(payload.changedFiles)
|
|
291
|
+
|| payload.changedFiles.length !== 1
|
|
292
|
+
|| payload.changedFiles[0] !== expectPath
|
|
293
|
+
|| !Array.isArray(payload.affectedTests)
|
|
294
|
+
|| payload.affectedTests.length !== 0) return 'creates_unverified';
|
|
295
|
+
return 'ready';
|
|
296
|
+
}
|
|
297
|
+
|
|
268
298
|
/**
|
|
269
299
|
* 束縛queryのoutcomeへexpectをexact照合し、sensor statusを決める。
|
|
270
300
|
* fuzzy解決・空結果は依存なしへ丸めず、unknown系statusへ落とす(AGENTS.md)。
|
|
271
301
|
*/
|
|
272
302
|
function resolveBindingStatus(binding, outcome) {
|
|
303
|
+
if (binding.expect.kind === 'affected' && binding.creates === true) {
|
|
304
|
+
const declared = creationBoundaryStatus(outcome, binding.expect.path);
|
|
305
|
+
if (declared !== null) return declared;
|
|
306
|
+
}
|
|
273
307
|
if (outcome.status !== 'ready') return outcome.status;
|
|
274
308
|
const { expect } = binding;
|
|
275
309
|
if (expect.kind === 'affected') {
|
|
@@ -401,7 +435,13 @@ export function compileRuntimePlanV1(options = {}) {
|
|
|
401
435
|
const driftDetails = [];
|
|
402
436
|
for (const todoId of todoIds) {
|
|
403
437
|
const witness = request.manual_witness[todoId];
|
|
404
|
-
|
|
438
|
+
// 創作宣言はowns側にある。裏付けを決めるのはbindingなので、覆っているbindingへ写す。
|
|
439
|
+
const creating = new Set(witness.owns
|
|
440
|
+
.filter((own) => own.creates === true).map((own) => own.target));
|
|
441
|
+
const bindings = normalizeProvenanceQueries(witness, todoId)
|
|
442
|
+
.map((binding) => (creating.size > 0
|
|
443
|
+
&& [...creating].some((target) => bindingCoversOwn(binding, { kind: 'path', target }))
|
|
444
|
+
? { ...binding, creates: true } : binding));
|
|
405
445
|
for (const binding of bindings) {
|
|
406
446
|
const query = queryById.get(binding.query_id);
|
|
407
447
|
if (query === undefined) {
|
|
@@ -697,7 +737,7 @@ export function compileRuntimePlanV1(options = {}) {
|
|
|
697
737
|
guidance: hasFreshAbsentPath
|
|
698
738
|
? {
|
|
699
739
|
code: 'BOOTSTRAP_OWNERSHIP_SEAM',
|
|
700
|
-
message: '
|
|
740
|
+
message: '不存在と観測されたpathは、そのTODOが作るならowns entryへcreates: trueを宣言して再compileする。宣言せずに裏付けを得るなら、親が空の専用seamをbase commitへ先行追加し、sensor sync後に同じrequestを再compileする',
|
|
701
741
|
}
|
|
702
742
|
: {
|
|
703
743
|
code: 'ACQUIRE_OWNERSHIP_EVIDENCE',
|
|
@@ -729,7 +769,7 @@ export function compileRuntimePlanV1(options = {}) {
|
|
|
729
769
|
: 'manual_state_effect';
|
|
730
770
|
}
|
|
731
771
|
const manifest = {
|
|
732
|
-
schema:
|
|
772
|
+
schema: BOUNDARY_MANIFEST_SCHEMA,
|
|
733
773
|
todo_id: todoId,
|
|
734
774
|
owns: witness.owns,
|
|
735
775
|
reads: witness.reads,
|
|
@@ -751,7 +791,7 @@ export function compileRuntimePlanV1(options = {}) {
|
|
|
751
791
|
};
|
|
752
792
|
manifest.manifest_digest = selfDigest(manifest, 'manifest_digest');
|
|
753
793
|
if (!validateRuntimeBoundaryManifest(manifest)) {
|
|
754
|
-
fail(`生成manifestがboundary_manifest
|
|
794
|
+
fail(`生成manifestがboundary_manifest contractを満たさない: ${todoId}`);
|
|
755
795
|
}
|
|
756
796
|
manifests[todoId] = manifest;
|
|
757
797
|
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from './runtime-decision-verifier.mjs';
|
|
8
8
|
import {
|
|
9
9
|
computeContextContentDigest,
|
|
10
|
+
RUN_REQUEST_SCHEMA,
|
|
10
11
|
selfDigest,
|
|
11
12
|
validateCarryOverWitness,
|
|
12
13
|
validateEpochRebindPacket,
|
|
@@ -133,7 +134,9 @@ export function validateRunRequestV2(value) {
|
|
|
133
134
|
|| !HEX_DIGEST.test(value.task_migration_digest ?? '')
|
|
134
135
|
|| !selfDigestValid(value, 'request_digest')) return false;
|
|
135
136
|
const projected = {
|
|
136
|
-
|
|
137
|
+
// 後継requestの本体はbase契約と同じ規律で読む。v1へ固定すると、創作境界を持つ
|
|
138
|
+
// 宣言が再計画を跨げない(ADR 0136)。
|
|
139
|
+
schema: RUN_REQUEST_SCHEMA, request_id: value.request_id, repo: value.repo,
|
|
137
140
|
capacity: value.capacity, todos: value.todos, manual_witness: value.manual_witness,
|
|
138
141
|
sensor_query_set: value.sensor_query_set, executor_capability: value.executor_capability,
|
|
139
142
|
claim_mode: value.claim_mode, request_digest: '',
|
|
@@ -8,21 +8,31 @@ import {
|
|
|
8
8
|
} from './todo-contracts.mjs';
|
|
9
9
|
import {
|
|
10
10
|
RUN_REQUEST_CLAIM_MODE,
|
|
11
|
+
RUN_REQUEST_SCHEMA,
|
|
11
12
|
explainRunRequest,
|
|
12
13
|
selfDigest as runtimeSelfDigest,
|
|
13
14
|
} from './runtime-contracts.mjs';
|
|
14
15
|
import { TODO_INDEPENDENCE_GUIDANCE_CODES } from './todo-independence-guidance.mjs';
|
|
15
16
|
|
|
16
|
-
export const TODO_WITNESS_SET_SCHEMA = 'lattice.todo_witness_set.
|
|
17
|
+
export const TODO_WITNESS_SET_SCHEMA = 'lattice.todo_witness_set.v3';
|
|
17
18
|
/**
|
|
18
|
-
* まだ受理する旧witness set契約。v1はconcern anchor
|
|
19
|
-
*
|
|
19
|
+
* まだ受理する旧witness set契約。v1はconcern anchorを、v2は創作宣言を持てないだけで、
|
|
20
|
+
* 境界宣言としてはv3と同値である。既存宣言を書き換えさせないために読み口を残す。
|
|
20
21
|
*/
|
|
21
|
-
export const TODO_WITNESS_SET_LEGACY_SCHEMAS = Object.freeze([
|
|
22
|
+
export const TODO_WITNESS_SET_LEGACY_SCHEMAS = Object.freeze([
|
|
23
|
+
'lattice.todo_witness_set.v2',
|
|
24
|
+
'lattice.todo_witness_set.v1',
|
|
25
|
+
]);
|
|
22
26
|
export const TODO_WITNESS_SET_SCHEMAS = Object.freeze([
|
|
23
27
|
TODO_WITNESS_SET_SCHEMA,
|
|
24
28
|
...TODO_WITNESS_SET_LEGACY_SCHEMAS,
|
|
25
29
|
]);
|
|
30
|
+
/** 宣言できる欄はversionごとに違う。どの版から使えるかを1箇所で持つ。 */
|
|
31
|
+
const CONCERN_ANCHOR_SCHEMAS = Object.freeze([
|
|
32
|
+
TODO_WITNESS_SET_SCHEMA,
|
|
33
|
+
'lattice.todo_witness_set.v2',
|
|
34
|
+
]);
|
|
35
|
+
const CREATES_SCHEMAS = Object.freeze([TODO_WITNESS_SET_SCHEMA]);
|
|
26
36
|
|
|
27
37
|
/** 1 taskが宣言できるconcern anchorの資源数と、資源あたりのsymbol数の上限。 */
|
|
28
38
|
export const TODO_CONCERN_ANCHOR_LIMIT = 256;
|
|
@@ -117,7 +127,7 @@ function boundedText(value, maximumBytes = 4_096) {
|
|
|
117
127
|
export function synthesizeWitnessRunRequest(witnessSet, { baseSha, requestId }) {
|
|
118
128
|
const taskIds = Object.keys(witnessSet.manual_witness).sort(compareText);
|
|
119
129
|
const request = {
|
|
120
|
-
schema:
|
|
130
|
+
schema: RUN_REQUEST_SCHEMA,
|
|
121
131
|
request_id: requestId,
|
|
122
132
|
repo: { base_sha: baseSha, root_kind: 'git' },
|
|
123
133
|
capacity: witnessSet.capacity,
|
|
@@ -212,13 +222,19 @@ export function explainTodoWitnessSet(value) {
|
|
|
212
222
|
});
|
|
213
223
|
const explained = explainRunRequest(probe);
|
|
214
224
|
if (!explained.valid) return reject(explained.reason, explained.path);
|
|
215
|
-
//
|
|
216
|
-
|
|
225
|
+
// 版ごとの欄。probeはRUN_REQUEST_SCHEMAで合成するので創作宣言はそこを通ってしまう。
|
|
226
|
+
// 旧版の宣言に新しい欄を書けないことは、ここだけが見る。
|
|
217
227
|
for (const taskId of taskIds) {
|
|
218
228
|
const witness = value.manual_witness[taskId];
|
|
229
|
+
if (!CREATES_SCHEMAS.includes(value.schema)
|
|
230
|
+
&& witness.owns.some((own) => Object.hasOwn(own, 'creates'))) {
|
|
231
|
+
return reject('creates_require_witness_set_v3', `/manual_witness/${taskId}/owns`);
|
|
232
|
+
}
|
|
219
233
|
const at = `/manual_witness/${taskId}/concern_anchors`;
|
|
220
234
|
if (!Object.hasOwn(witness, 'concern_anchors')) continue;
|
|
221
|
-
if (
|
|
235
|
+
if (!CONCERN_ANCHOR_SCHEMAS.includes(value.schema)) {
|
|
236
|
+
return reject('concern_anchors_require_witness_set_v2', at);
|
|
237
|
+
}
|
|
222
238
|
const anchors = explainConcernAnchors(witness.concern_anchors, witness.owns, at);
|
|
223
239
|
if (!anchors.valid) return reject(anchors.reason, anchors.path);
|
|
224
240
|
}
|