@quolu/lattice 0.31.0 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +11 -1
- package/README.md +11 -2
- package/bin/lattice-scripted-worker.mjs +59 -0
- package/bin/lattice.mjs +12 -1
- package/package.json +1 -1
- package/sensor/dist/bin/lattice-sensor.js +72 -4
- package/sensor/dist/db/migrations.d.ts +1 -1
- package/sensor/dist/db/migrations.d.ts.map +1 -1
- package/sensor/dist/db/migrations.js +32 -2
- package/sensor/dist/db/migrations.js.map +1 -1
- package/sensor/dist/db/queries.d.ts +10 -0
- package/sensor/dist/db/queries.d.ts.map +1 -1
- package/sensor/dist/db/queries.js +53 -7
- package/sensor/dist/db/queries.js.map +1 -1
- package/sensor/dist/db/schema.sql +6 -1
- package/sensor/dist/extraction/tree-sitter.d.ts.map +1 -1
- package/sensor/dist/extraction/tree-sitter.js +89 -16
- package/sensor/dist/extraction/tree-sitter.js.map +1 -1
- package/sensor/dist/index.d.ts +8 -1
- package/sensor/dist/index.d.ts.map +1 -1
- package/sensor/dist/index.js +9 -0
- package/sensor/dist/index.js.map +1 -1
- package/sensor/dist/resolution/import-resolver.d.ts +11 -0
- package/sensor/dist/resolution/import-resolver.d.ts.map +1 -1
- package/sensor/dist/resolution/import-resolver.js +13 -5
- package/sensor/dist/resolution/import-resolver.js.map +1 -1
- package/sensor/dist/resolution/index.d.ts.map +1 -1
- package/sensor/dist/resolution/index.js +11 -0
- package/sensor/dist/resolution/index.js.map +1 -1
- package/sensor/dist/resolution/types.d.ts +4 -0
- package/sensor/dist/resolution/types.d.ts.map +1 -1
- package/sensor/dist/types.d.ts +16 -0
- package/sensor/dist/types.d.ts.map +1 -1
- package/src/artifact-contracts.mjs +3 -0
- package/src/cli-help.mjs +6 -0
- package/src/rc3-scripted-campaign.mjs +3 -1
- package/src/runtime-cli.mjs +454 -97
- package/src/runtime-contracts.mjs +1 -1
- package/src/runtime-control-store.mjs +24 -1
- package/src/runtime-controller-protocol.mjs +21 -3
- package/src/runtime-decision-verifier.mjs +1 -1
- package/src/runtime-diff-observer.mjs +2 -2
- package/src/runtime-front-end.mjs +47 -13
- package/src/runtime-hold-recompile.mjs +31 -2
- package/src/runtime-io-sentinel.mjs +16 -9
- package/src/runtime-managed-supervisor.mjs +9 -1
- package/src/runtime-multi-epoch-store.mjs +2 -2
- package/src/runtime-projection.mjs +27 -0
- package/src/runtime-scripted-adapter-controller.mjs +139 -25
- package/src/runtime-seam-resolve.mjs +156 -13
- package/src/runtime-seam-treatment.mjs +2 -1
- package/src/seam-apply.mjs +101 -4
- package/src/seam-cost.mjs +322 -0
- package/src/seam-gate.mjs +146 -0
- package/src/seam-rewrite.mjs +102 -16
- package/src/seam-verification.mjs +26 -3
- package/src/sensor-adapter.mjs +6 -1
- package/src/todo-cli.mjs +55 -0
|
@@ -53,7 +53,7 @@ export const RUNTIME_CONFLICT_KINDS = Object.freeze([
|
|
|
53
53
|
'observed_write_conflict',
|
|
54
54
|
'semantic_conflict_unknown',
|
|
55
55
|
'effect_conflict_unknown',
|
|
56
|
-
'
|
|
56
|
+
'undeclared_write',
|
|
57
57
|
'stale_context',
|
|
58
58
|
]);
|
|
59
59
|
export const HOLD_REASON_KINDS = Object.freeze([
|
|
@@ -161,7 +161,7 @@ export function validateRuntimeControlEventPayload(kind, value) {
|
|
|
161
161
|
// 機械が何かに気づいたのに黙っている状態を残さない。
|
|
162
162
|
if (kind === 'io_warning_observed') {
|
|
163
163
|
return exact(value, ['warning_kind', 'todo_ids', 'path', 'probe_outcome', 'warning_digest'])
|
|
164
|
-
&& ['io_overlap_warning', '
|
|
164
|
+
&& ['io_overlap_warning', 'io_undeclared_write_warning'].includes(value.warning_kind)
|
|
165
165
|
&& Array.isArray(value.todo_ids) && value.todo_ids.length >= 1 && value.todo_ids.length <= 256
|
|
166
166
|
&& value.todo_ids.every(identifier)
|
|
167
167
|
&& value.todo_ids.every((id, index) => index === 0 || value.todo_ids[index - 1] < id)
|
|
@@ -171,6 +171,29 @@ export function validateRuntimeControlEventPayload(kind, value) {
|
|
|
171
171
|
warning_kind: value.warning_kind, todo_ids: value.todo_ids, path: value.path,
|
|
172
172
|
});
|
|
173
173
|
}
|
|
174
|
+
// hold裁定のcontinue_setをprocessへ反映した記録(請求項7・8の再開側)。
|
|
175
|
+
// barrierは全workerを止めるので、続けてよいと判定した作業を再開しなければ、
|
|
176
|
+
// 判定と実行が食い違ったままになる。
|
|
177
|
+
if (kind === 'workers_resumed') {
|
|
178
|
+
return exact(value, ['resumed_todo_ids', 'skipped_count'])
|
|
179
|
+
&& Array.isArray(value.resumed_todo_ids) && value.resumed_todo_ids.length <= 4096
|
|
180
|
+
&& value.resumed_todo_ids.every(identifier)
|
|
181
|
+
&& value.resumed_todo_ids.every((id, index) => index === 0
|
|
182
|
+
|| value.resumed_todo_ids[index - 1] < id)
|
|
183
|
+
&& Number.isSafeInteger(value.skipped_count) && value.skipped_count >= 0;
|
|
184
|
+
}
|
|
185
|
+
// 破棄の決定としてworker processを終了した記録。**cleanupではない。**
|
|
186
|
+
// 停止したworkerの行き先は「再開」か「破棄」の二択であり、破棄を選んだなら
|
|
187
|
+
// 誰を止めたかが残っていなければ、後から何が捨てられたか説明できない。
|
|
188
|
+
if (kind === 'worker_processes_terminated') {
|
|
189
|
+
return exact(value, ['reason', 'terminated_pids', 'skipped_count'])
|
|
190
|
+
&& typeof value.reason === 'string' && value.reason.length > 0
|
|
191
|
+
&& Array.isArray(value.terminated_pids) && value.terminated_pids.length <= 4096
|
|
192
|
+
&& value.terminated_pids.every((pid) => Number.isSafeInteger(pid) && pid > 0)
|
|
193
|
+
&& value.terminated_pids.every((pid, index) => index === 0
|
|
194
|
+
|| value.terminated_pids[index - 1] < pid)
|
|
195
|
+
&& Number.isSafeInteger(value.skipped_count) && value.skipped_count >= 0;
|
|
196
|
+
}
|
|
174
197
|
// probeを通った警報を既存hold経路へ入れた顛末(ADR 0143の三段目)。
|
|
175
198
|
// 成否どちらも残す。自動escalationが黙って失敗する状態を作らない——失敗の理由は
|
|
176
199
|
// `detail`が持ち、これが無いと「警報は出たのにholdが掛かっていない」を後から説明できない。
|
|
@@ -31,8 +31,8 @@ const WIRES = Object.freeze({
|
|
|
31
31
|
dispatch: {
|
|
32
32
|
request: ['schema', 'request_id', 'registration_digest', 'packet', 'write_lease', 'request_digest'],
|
|
33
33
|
requestSchema: 'lattice.adapter_dispatch_request.v1',
|
|
34
|
-
response: ['schema', 'request_id', 'executor_handle', 'worktree_id', 'packet_digest', 'lease_digest', 'response_digest'],
|
|
35
|
-
responseSchema: 'lattice.adapter_dispatch_response.
|
|
34
|
+
response: ['schema', 'request_id', 'executor_handle', 'worktree_id', 'packet_digest', 'lease_digest', 'worker_process', 'response_digest'],
|
|
35
|
+
responseSchema: 'lattice.adapter_dispatch_response.v2',
|
|
36
36
|
},
|
|
37
37
|
observe: {
|
|
38
38
|
request: ['schema', 'request_id', 'registration_digest', 'executor_handle', 'expected_epoch', 'expected_lease_digest', 'request_digest'],
|
|
@@ -338,8 +338,12 @@ export function validateControllerResponse(operation, value, expectedRequestId =
|
|
|
338
338
|
if (!(Boolean(wire) && exact(value, wire.response) && value.schema === wire.responseSchema
|
|
339
339
|
&& identifier(value.request_id) && (expectedRequestId === null || value.request_id === expectedRequestId)
|
|
340
340
|
&& selfValid(value, 'response_digest'))) return false;
|
|
341
|
+
// v2で`worker_process`を必須にした。executorがcontroller自身のprocessだと、holdが
|
|
342
|
+
// 要求する静止を証明できない(止めれば応答できず、止めなければ証明できない)。
|
|
343
|
+
// 誰を止めればよいかをdispatchの時点で名指しさせる。
|
|
341
344
|
if (operation === 'dispatch') return identifier(value.executor_handle) && identifier(value.worktree_id)
|
|
342
|
-
&& digest(value.packet_digest) && digest(value.lease_digest)
|
|
345
|
+
&& digest(value.packet_digest) && digest(value.lease_digest)
|
|
346
|
+
&& validateExpectedWorkerProcess(value.worker_process);
|
|
343
347
|
if (operation === 'observe') return exact(value.observation, ['schema', 'state', 'executor_handle', 'plan_epoch', 'lease_digest', 'payload_digest', 'observation_digest'])
|
|
344
348
|
&& value.observation.schema === 'lattice.adapter_observation.v1'
|
|
345
349
|
&& ['running', 'checkpoint_ready', 'terminal', 'held'].includes(value.observation.state)
|
|
@@ -397,6 +401,20 @@ function validateProtocolRunningBinding(value) {
|
|
|
397
401
|
&& identifier(value.write_lease_id) && digest(value.controller_registration_digest);
|
|
398
402
|
}
|
|
399
403
|
|
|
404
|
+
/**
|
|
405
|
+
* dispatchが名指しするworker process。直接OS観測が期待するchild processと同じ形にする
|
|
406
|
+
* ——照合先の形が分かれると、supervisorとcontrollerが別のものを見ていても気づけない。
|
|
407
|
+
*/
|
|
408
|
+
export function validateExpectedWorkerProcess(value) {
|
|
409
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
410
|
+
&& Object.keys(value).sort().join('\0')
|
|
411
|
+
=== ['pid', 'process_group_id', 'process_start_identity'].sort().join('\0')
|
|
412
|
+
&& Number.isSafeInteger(value.pid) && value.pid > 0
|
|
413
|
+
&& Number.isSafeInteger(value.process_group_id) && value.process_group_id > 0
|
|
414
|
+
&& validateProcessStartIdentity(value.process_start_identity)
|
|
415
|
+
&& value.process_start_identity.pid === value.pid;
|
|
416
|
+
}
|
|
417
|
+
|
|
400
418
|
export function validateQuiescenceAck(value) {
|
|
401
419
|
return exact(value, ['schema', 'ack_id', 'run_id', 'todo_id', 'executor_handle', 'worktree_id', 'plan_epoch', 'packet_digest', 'write_lease_id', 'barrier_control_digest', 'final_checkpoint_digest', 'process_observation_digest', 'worktree_fingerprint_digest', 'supervisor_session_nonce_digest', 'ack_digest'])
|
|
402
420
|
&& value.schema === 'lattice.executor_quiescence_ack.v1'
|
|
@@ -150,7 +150,7 @@ export function classifyObservedDiff(options = {}) {
|
|
|
150
150
|
const declaredWrites = manifests[todoId].writes ?? [];
|
|
151
151
|
for (const path of paths) {
|
|
152
152
|
if (!declaredWriteCovers(declaredWrites, path)) {
|
|
153
|
-
findings.push({ kind: '
|
|
153
|
+
findings.push({ kind: 'undeclared_write', todo_ids: [todoId], path });
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
}
|
|
@@ -257,7 +257,7 @@ export function coveredBy(declaredWrites, observedPath) {
|
|
|
257
257
|
* cross-bindし、closed conflict分類のfindingを返す(producer側の検出。独立再計算は
|
|
258
258
|
* runtime-decision-verifierの`classifyObservedDiff`が行う)。
|
|
259
259
|
*
|
|
260
|
-
* -
|
|
260
|
+
* - undeclared_write: 宣言write scope外へのwrite(offender=当該TODO)。
|
|
261
261
|
* - observed_write_conflict: 他のrunning TODOのdeclared writeとのpath overlap。
|
|
262
262
|
*/
|
|
263
263
|
export function detectCheckpointFindings(options = {}) {
|
|
@@ -276,7 +276,7 @@ export function detectCheckpointFindings(options = {}) {
|
|
|
276
276
|
const findings = [];
|
|
277
277
|
for (const entry of [...checkpoint.diff.entries].sort((l, r) => (l.path < r.path ? -1 : 1))) {
|
|
278
278
|
if (!coveredBy(packet.scope.writes, entry.path)) {
|
|
279
|
-
findings.push({ kind: '
|
|
279
|
+
findings.push({ kind: 'undeclared_write', todo_ids: [todoId], path: entry.path });
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
282
|
for (const otherId of [...runningTodoIds].sort()) {
|
|
@@ -247,6 +247,25 @@ function affectedPayload(raw, expectPath) {
|
|
|
247
247
|
return plainRecord(payload) ? payload : null;
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
/**
|
|
251
|
+
* 収集済みsensor evidenceから、あるpathのaffected testを読む。
|
|
252
|
+
*
|
|
253
|
+
* 実行時の翻訳段(宣言を観測へ合わせる)が同じ観測を必要とする。payloadの形の解釈をあちらへ
|
|
254
|
+
* 書き直すと、driftを判定する規則と、driftを起こさない宣言を作る規則が別々に育つ。
|
|
255
|
+
*
|
|
256
|
+
* @returns {string[]|null} 観測できなければnull(空配列と区別する)
|
|
257
|
+
*/
|
|
258
|
+
export function affectedTestsFromEvidence({ sensorEvidence, querySet, path: target } = {}) {
|
|
259
|
+
const queries = (querySet?.queries ?? [])
|
|
260
|
+
.filter((query) => query.operation === 'affected' && query.target === target);
|
|
261
|
+
if (queries.length !== 1) return null;
|
|
262
|
+
const outcome = (sensorEvidence?.outcomes ?? [])
|
|
263
|
+
.find((entry) => entry.query_id === queries[0].id);
|
|
264
|
+
if (!plainRecord(outcome) || outcome.status !== 'ready') return null;
|
|
265
|
+
const payload = affectedPayload(outcome.raw, target);
|
|
266
|
+
return Array.isArray(payload?.affectedTests) ? [...payload.affectedTests].sort(compareText) : null;
|
|
267
|
+
}
|
|
268
|
+
|
|
250
269
|
function affectedTarget(raw, expectPath) {
|
|
251
270
|
if (!plainRecord(raw) || !Array.isArray(raw.targets)) return null;
|
|
252
271
|
const entry = raw.targets.find((candidate) => (
|
|
@@ -486,28 +505,43 @@ export function compileRuntimePlanV1(options = {}) {
|
|
|
486
505
|
}
|
|
487
506
|
|
|
488
507
|
// affected test drift検査(witness宣言とfresh affected観測のexact比較)。
|
|
508
|
+
//
|
|
509
|
+
// 比較はTODO単位で、そのTODOが持つ**全affected束縛の観測の和**に対して行う。1面しか持たない
|
|
510
|
+
// TODOでは束縛ごとの比較と同じ結果になり、複数面を所有するTODOでも表現できる——束縛ごとに
|
|
511
|
+
// 宣言全体とexact比較すると、affectedの異なる2 pathを所有するTODOは原理的に成立せず、
|
|
512
|
+
// 実行時に所有が広がった宣言を受け取れなくなる。和なので緩みは入らない(余分な宣言も
|
|
513
|
+
// 足りない宣言もdriftのまま)。
|
|
489
514
|
const affectedDrift = [];
|
|
490
515
|
for (const todoId of todoIds) {
|
|
491
516
|
const witness = request.manual_witness[todoId];
|
|
517
|
+
const declared = [...witness.affected_tests].sort(compareText);
|
|
518
|
+
const queryIds = [];
|
|
519
|
+
const observedUnion = new Set();
|
|
520
|
+
let unreadable = false;
|
|
492
521
|
for (const binding of bindingsByTodo.get(todoId)) {
|
|
493
522
|
if (binding.expect.kind !== 'affected') continue;
|
|
494
523
|
const outcome = outcomeByQueryId.get(binding.query_id);
|
|
495
524
|
if (resolveBindingStatus(binding, outcome) !== 'ready') continue;
|
|
525
|
+
queryIds.push(binding.query_id);
|
|
496
526
|
const payload = affectedPayload(outcome.raw, binding.expect.path);
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
: null;
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|| observed.length !== declared.length
|
|
503
|
-
|| observed.some((test, index) => test !== declared[index])) {
|
|
504
|
-
affectedDrift.push({
|
|
505
|
-
todo_id: todoId,
|
|
506
|
-
query_id: binding.query_id,
|
|
507
|
-
declared,
|
|
508
|
-
observed,
|
|
509
|
-
});
|
|
527
|
+
if (!Array.isArray(payload?.affectedTests)) {
|
|
528
|
+
// 観測が読めないのは「一致しない」とは別の事象である。丸めずそのまま残す。
|
|
529
|
+
affectedDrift.push({ todo_id: todoId, query_id: binding.query_id, declared, observed: null });
|
|
530
|
+
unreadable = true;
|
|
531
|
+
continue;
|
|
510
532
|
}
|
|
533
|
+
for (const test of payload.affectedTests) observedUnion.add(test);
|
|
534
|
+
}
|
|
535
|
+
if (unreadable || queryIds.length === 0) continue;
|
|
536
|
+
const observed = [...observedUnion].sort(compareText);
|
|
537
|
+
if (observed.length !== declared.length
|
|
538
|
+
|| observed.some((test, index) => test !== declared[index])) {
|
|
539
|
+
affectedDrift.push({
|
|
540
|
+
todo_id: todoId,
|
|
541
|
+
query_id: queryIds.sort(compareText).join(','),
|
|
542
|
+
declared,
|
|
543
|
+
observed,
|
|
544
|
+
});
|
|
511
545
|
}
|
|
512
546
|
}
|
|
513
547
|
if (affectedDrift.length > 0) {
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { canonicalizeArtifact, digestArtifact } from './artifact-contracts.mjs';
|
|
2
|
+
// 切断可能性の写像はtodo側と同じ正本を使う。計画時と実行時で「切れる種類」の定義が
|
|
3
|
+
// 分かれると、同じ資源が段によって別の答えを持つ。
|
|
4
|
+
import { severabilityOfConflictKind } from './todo-independence-contracts.mjs';
|
|
2
5
|
import { buildNextRunEvent, buildExecutorPackets } from './runtime-engine.mjs';
|
|
3
6
|
import { projectRuntimeState } from './runtime-projection.mjs';
|
|
4
7
|
import {
|
|
@@ -594,20 +597,46 @@ export function detectNonConvergentConflicts(options = {}) {
|
|
|
594
597
|
));
|
|
595
598
|
}
|
|
596
599
|
|
|
600
|
+
/**
|
|
601
|
+
* 係争資源が構造的に切れる種類かを返す(ADR 0144の続き、請求項8の入口判定)。
|
|
602
|
+
*
|
|
603
|
+
* pathやsymbolは面を分ければ別々に所有できる。共有stateや外部effectは分けられない——
|
|
604
|
+
* 変換をどれだけ工夫しても同じ資源に触り続ける。**これは「切れる」ではなく「切れる種類だ」で
|
|
605
|
+
* あって、実際に切れるかは試して初めて分かる。**
|
|
606
|
+
*/
|
|
607
|
+
export function severabilityOfRuntimeFinding(finding) {
|
|
608
|
+
return severabilityOfConflictKind(typeof finding?.path === 'string' ? 'path' : 'state');
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* 競合をどちらの処置へ運ぶか(請求項7=直列化/請求項8=変換)。
|
|
613
|
+
*
|
|
614
|
+
* `seam_transform`を返すのは事前宣言済みtreatmentが係争pathを覆っている時だけである。
|
|
615
|
+
* それ以外の既定は直列化だが、**それは「変換が不可能」という意味ではない**。実行時に初めて
|
|
616
|
+
* 見つかった競合には事前宣言が無いので、既定だけを見ると請求項8へ行く道が無いように見える。
|
|
617
|
+
*
|
|
618
|
+
* そこで`severability`と`transform_attemptable`を併せて返す。装置が言えるのは「切れる種類の
|
|
619
|
+
* 資源か」までで、**実際の難しさは変換を試して五条件で測る**(`run seam resolve`)。試すかどうかは
|
|
620
|
+
* 費用のかかる判断なので装置が決めない——隔離worktreeでの変換、focused test、再indexを毎回
|
|
621
|
+
* 走らせるかは、操作するAIが持つ文脈で決める(AGENTS.md「装置の境界にAIを含める」)。
|
|
622
|
+
*/
|
|
597
623
|
export function routeConflictTreatment(options = {}) {
|
|
598
624
|
if (!exactRecord(options, ['finding', 'predeclaredTreatments'])) {
|
|
599
625
|
fail('routeConflictTreatment optionsがexact shapeでない');
|
|
600
626
|
}
|
|
601
627
|
const { finding, predeclaredTreatments } = options;
|
|
602
628
|
if (!plainRecord(finding) || typeof finding.kind !== 'string') fail('findingが不正');
|
|
629
|
+
const severability = severabilityOfRuntimeFinding(finding);
|
|
603
630
|
if (finding.kind === 'observed_write_conflict' && typeof finding.path === 'string') {
|
|
604
631
|
for (const treatment of predeclaredTreatments) {
|
|
605
632
|
if (Array.isArray(treatment.covered_paths) && treatment.covered_paths.includes(finding.path)) {
|
|
606
|
-
return { lane: 'seam_transform', treatment: structuredClone(treatment)
|
|
633
|
+
return { lane: 'seam_transform', treatment: structuredClone(treatment),
|
|
634
|
+
severability, transform_attemptable: true };
|
|
607
635
|
}
|
|
608
636
|
}
|
|
609
637
|
}
|
|
610
|
-
return { lane: 'intentional_serial', treatment: null
|
|
638
|
+
return { lane: 'intentional_serial', treatment: null,
|
|
639
|
+
severability, transform_attemptable: severability === 'code_seam' };
|
|
611
640
|
}
|
|
612
641
|
|
|
613
642
|
/**
|
|
@@ -28,7 +28,7 @@ import { selfDigest } from './runtime-contracts.mjs';
|
|
|
28
28
|
import { coveredBy } from './runtime-diff-observer.mjs';
|
|
29
29
|
|
|
30
30
|
/** 警報の種別。findingのkindとは別空間にする——findingへ昇格するのはprobeを通った後だけである。 */
|
|
31
|
-
export const IO_WARNING_KINDS = Object.freeze(['io_overlap_warning', '
|
|
31
|
+
export const IO_WARNING_KINDS = Object.freeze(['io_overlap_warning', 'io_undeclared_write_warning']);
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* 監視から外すrepo相対prefix。
|
|
@@ -65,7 +65,7 @@ export function isExcludedPath(relativePath, excludes = DEFAULT_IO_EXCLUDES) {
|
|
|
65
65
|
*
|
|
66
66
|
* checkpoint findingの2述語をそのまま1 pathへ適用する:
|
|
67
67
|
* - 他のrunning TODOの宣言scopeに入るpathへ書いた → `io_overlap_warning`
|
|
68
|
-
* - 自分の宣言scopeの外へ書いた → `
|
|
68
|
+
* - 自分の宣言scopeの外へ書いた → `io_undeclared_write_warning`
|
|
69
69
|
*
|
|
70
70
|
* @returns {{warnings: Array<{kind: string, todo_ids: string[], path: string}>}}
|
|
71
71
|
*/
|
|
@@ -84,7 +84,7 @@ export function classifyIoObservation(options = {}) {
|
|
|
84
84
|
|
|
85
85
|
const warnings = [];
|
|
86
86
|
if (!coveredBy(packet.scope.writes, relativePath)) {
|
|
87
|
-
warnings.push({ kind: '
|
|
87
|
+
warnings.push({ kind: 'io_undeclared_write_warning', todo_ids: [todoId], path: relativePath });
|
|
88
88
|
}
|
|
89
89
|
for (const otherId of [...runningTodoIds].sort(compareText)) {
|
|
90
90
|
if (otherId === todoId) continue;
|
|
@@ -298,15 +298,22 @@ export function probeIoWarning({ warning, checkpointsByTodo } = {}) {
|
|
|
298
298
|
}
|
|
299
299
|
|
|
300
300
|
/**
|
|
301
|
-
* 警報kind → finding kind
|
|
301
|
+
* 警報kind → finding kind。**重なりだけがhold経路へ乗る。**
|
|
302
302
|
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
303
|
+
* 宣言境界は計画時の**予測**であって、workerを閉じ込める制約ではない。範囲内へ無理に
|
|
304
|
+
* 押し込めるとworkerの自由度が落ち、成果の品質が下がる。だから自由に書かせ、**実際の足跡が
|
|
305
|
+
* 他の走行中TODOとぶつかった時にだけ**止めて処置する——請求項7(片方を停止し他方を確定して
|
|
306
|
+
* 再開する)と請求項8(限定的な変換を施して双方再開する)はそのための構成である。
|
|
307
|
+
*
|
|
308
|
+
* よって単独のscope警報——誰の領分とも重なっていない宣言外の書き込み——はhold経路へ運ばない。
|
|
309
|
+
* それは競合ではなく、**予測が実態より狭かったという情報**であり、止める理由が無い。記録は
|
|
310
|
+
* 残る(`io_warning_observed`)ので、再計画の材料としては失われない。
|
|
311
|
+
*
|
|
312
|
+
* 止めるべきでないものを止めると、処置の当てようが無い停止が生まれる。scope違反に処置が
|
|
313
|
+
* 無いのは欠落ではなく、処置すべき事象ではないことの現れである。
|
|
306
314
|
*/
|
|
307
315
|
const WARNING_FINDING_KIND = Object.freeze({
|
|
308
316
|
io_overlap_warning: 'observed_write_conflict',
|
|
309
|
-
io_scope_warning: 'scope_violation',
|
|
310
317
|
});
|
|
311
318
|
|
|
312
319
|
/**
|
|
@@ -320,7 +327,7 @@ function selectEscalationAnchor({ warning, writers, packets }) {
|
|
|
320
327
|
const wrote = new Set(writers);
|
|
321
328
|
const qualified = [...warning.todo_ids].sort(compareText).filter((todoId) => {
|
|
322
329
|
if (!wrote.has(todoId)) return false;
|
|
323
|
-
if (warning.kind === '
|
|
330
|
+
if (warning.kind === 'io_undeclared_write_warning') return true;
|
|
324
331
|
return warning.todo_ids.some((otherId) => {
|
|
325
332
|
if (otherId === todoId) return false;
|
|
326
333
|
const other = packets[otherId];
|
|
@@ -1340,7 +1340,15 @@ if (isDirectDaemon) {
|
|
|
1340
1340
|
commitPhaseRevision: (revision) => applyPhaseTodoRevision({
|
|
1341
1341
|
repoRoot: path.resolve(runDir, '..', '..', '..'),
|
|
1342
1342
|
writer: createTodoStoreWriter({ caller: 'g5-authoring' }), revision,
|
|
1343
|
-
actor
|
|
1343
|
+
// actorは`{host, session, agent}`のexact recordである(`lattice.todo_event.v4`)。
|
|
1344
|
+
// 文字列を渡していた間、seam_splitの再計画はphase revisionをcommitできなかった
|
|
1345
|
+
// ——工程storeへ書く直前で必ず落ちるので、請求項8の再開まで一度も届いていない。
|
|
1346
|
+
actor: {
|
|
1347
|
+
host: 'lattice-runtime',
|
|
1348
|
+
session: sessionNonce.slice(0, 32),
|
|
1349
|
+
agent: 'lattice-runtime-supervisor',
|
|
1350
|
+
},
|
|
1351
|
+
recordedAt: new Date().toISOString(),
|
|
1344
1352
|
}),
|
|
1345
1353
|
});
|
|
1346
1354
|
})
|
|
@@ -83,7 +83,7 @@ export function validateRuntimeFindingRecord(value) {
|
|
|
83
83
|
|| new Set(finding.evidence_digests).size !== finding.evidence_digests.length
|
|
84
84
|
|| finding.evidence_digests.some((entry, index) => index > 0 && finding.evidence_digests[index - 1] >= entry)
|
|
85
85
|
|| !selfDigestValid(finding, 'finding_digest')) return false;
|
|
86
|
-
const pathKind = ['observed_write_conflict', '
|
|
86
|
+
const pathKind = ['observed_write_conflict', 'undeclared_write', 'stale_context'].includes(finding.kind);
|
|
87
87
|
if (pathKind !== (finding.path !== null) || pathKind === (finding.resource_id !== null)) return false;
|
|
88
88
|
const observer = value.recorded_by;
|
|
89
89
|
return exactRecord(observer, ['schema', 'kind', 'controller_registration_digest',
|
|
@@ -109,7 +109,7 @@ export function validateRuntimeFindingCandidate(value) {
|
|
|
109
109
|
|| value.evidence_digests.some((digest, index) => index > 0
|
|
110
110
|
&& value.evidence_digests[index - 1] >= digest)
|
|
111
111
|
|| !selfDigestValid(value, 'candidate_digest')) return false;
|
|
112
|
-
const pathKind = ['observed_write_conflict', '
|
|
112
|
+
const pathKind = ['observed_write_conflict', 'undeclared_write', 'stale_context']
|
|
113
113
|
.includes(value.proposed_kind);
|
|
114
114
|
return pathKind
|
|
115
115
|
? typeof value.path === 'string' && value.path.length > 0 && value.resource_id === null
|
|
@@ -238,5 +238,32 @@ export function projectRuntimeStatusOverlays(options = {}) {
|
|
|
238
238
|
carry_over: members('carry_over'),
|
|
239
239
|
redispatch: members('redispatch'),
|
|
240
240
|
intake_frozen: runtimeState.freeze !== null,
|
|
241
|
+
treatment_advice: treatmentAdvice(events),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* 止まっているrunに対して、請求項8(変換)を試せるかを助言する。
|
|
247
|
+
*
|
|
248
|
+
* 拒否ではなく助言である(ADR 0128と同じ規律)。既定の再計画modeが`intentional_serial`だから
|
|
249
|
+
* といって「変換が不可能」ではない——事前宣言が無いだけである。実行時に初めて見つかった競合には
|
|
250
|
+
* 事前宣言が無いので、既定だけを見ると請求項8へ行く道が無いように見える。
|
|
251
|
+
*
|
|
252
|
+
* 装置が言えるのは**係争資源が切れる種類か**までとする。共有stateや外部effectは面を分けても
|
|
253
|
+
* 同じ資源に触り続けるので切れない。pathは分けられる**かもしれない**——実際に切れるかと、
|
|
254
|
+
* それが割に合うかは、隔離worktreeで変換して五条件で測って初めて分かる(`run seam resolve`)。
|
|
255
|
+
* 試すかどうかは費用のかかる判断なので、装置は決めずに材料だけ渡す。
|
|
256
|
+
*/
|
|
257
|
+
function treatmentAdvice(events) {
|
|
258
|
+
const conflict = [...events].reverse()
|
|
259
|
+
.find((event) => event.kind === 'conflict_found'
|
|
260
|
+
&& event.payload !== null && typeof event.payload === 'object');
|
|
261
|
+
if (conflict === undefined) return null;
|
|
262
|
+
const severability = typeof conflict.payload.path === 'string' ? 'code_seam' : 'serial';
|
|
263
|
+
return {
|
|
264
|
+
finding_digest: typeof conflict.payload.finding_digest === 'string'
|
|
265
|
+
? conflict.payload.finding_digest : null,
|
|
266
|
+
severability,
|
|
267
|
+
transform_attemptable: severability === 'code_seam',
|
|
241
268
|
};
|
|
242
269
|
}
|