@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.
Files changed (58) hide show
  1. package/README.ja.md +11 -1
  2. package/README.md +11 -2
  3. package/bin/lattice-scripted-worker.mjs +59 -0
  4. package/bin/lattice.mjs +12 -1
  5. package/package.json +1 -1
  6. package/sensor/dist/bin/lattice-sensor.js +72 -4
  7. package/sensor/dist/db/migrations.d.ts +1 -1
  8. package/sensor/dist/db/migrations.d.ts.map +1 -1
  9. package/sensor/dist/db/migrations.js +32 -2
  10. package/sensor/dist/db/migrations.js.map +1 -1
  11. package/sensor/dist/db/queries.d.ts +10 -0
  12. package/sensor/dist/db/queries.d.ts.map +1 -1
  13. package/sensor/dist/db/queries.js +53 -7
  14. package/sensor/dist/db/queries.js.map +1 -1
  15. package/sensor/dist/db/schema.sql +6 -1
  16. package/sensor/dist/extraction/tree-sitter.d.ts.map +1 -1
  17. package/sensor/dist/extraction/tree-sitter.js +89 -16
  18. package/sensor/dist/extraction/tree-sitter.js.map +1 -1
  19. package/sensor/dist/index.d.ts +8 -1
  20. package/sensor/dist/index.d.ts.map +1 -1
  21. package/sensor/dist/index.js +9 -0
  22. package/sensor/dist/index.js.map +1 -1
  23. package/sensor/dist/resolution/import-resolver.d.ts +11 -0
  24. package/sensor/dist/resolution/import-resolver.d.ts.map +1 -1
  25. package/sensor/dist/resolution/import-resolver.js +13 -5
  26. package/sensor/dist/resolution/import-resolver.js.map +1 -1
  27. package/sensor/dist/resolution/index.d.ts.map +1 -1
  28. package/sensor/dist/resolution/index.js +11 -0
  29. package/sensor/dist/resolution/index.js.map +1 -1
  30. package/sensor/dist/resolution/types.d.ts +4 -0
  31. package/sensor/dist/resolution/types.d.ts.map +1 -1
  32. package/sensor/dist/types.d.ts +16 -0
  33. package/sensor/dist/types.d.ts.map +1 -1
  34. package/src/artifact-contracts.mjs +3 -0
  35. package/src/cli-help.mjs +6 -0
  36. package/src/rc3-scripted-campaign.mjs +3 -1
  37. package/src/runtime-cli.mjs +454 -97
  38. package/src/runtime-contracts.mjs +1 -1
  39. package/src/runtime-control-store.mjs +24 -1
  40. package/src/runtime-controller-protocol.mjs +21 -3
  41. package/src/runtime-decision-verifier.mjs +1 -1
  42. package/src/runtime-diff-observer.mjs +2 -2
  43. package/src/runtime-front-end.mjs +47 -13
  44. package/src/runtime-hold-recompile.mjs +31 -2
  45. package/src/runtime-io-sentinel.mjs +16 -9
  46. package/src/runtime-managed-supervisor.mjs +9 -1
  47. package/src/runtime-multi-epoch-store.mjs +2 -2
  48. package/src/runtime-projection.mjs +27 -0
  49. package/src/runtime-scripted-adapter-controller.mjs +139 -25
  50. package/src/runtime-seam-resolve.mjs +156 -13
  51. package/src/runtime-seam-treatment.mjs +2 -1
  52. package/src/seam-apply.mjs +101 -4
  53. package/src/seam-cost.mjs +322 -0
  54. package/src/seam-gate.mjs +146 -0
  55. package/src/seam-rewrite.mjs +102 -16
  56. package/src/seam-verification.mjs +26 -3
  57. package/src/sensor-adapter.mjs +6 -1
  58. 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
- 'scope_violation',
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', 'io_scope_warning'].includes(value.warning_kind)
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.v1',
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: 'scope_violation', todo_ids: [todoId], path });
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
- * - scope_violation: 宣言write scope外へのwrite(offender=当該TODO)。
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: 'scope_violation', todo_ids: [todoId], path: entry.path });
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
- const observed = Array.isArray(payload?.affectedTests)
498
- ? [...payload.affectedTests].sort(compareText)
499
- : null;
500
- const declared = [...witness.affected_tests].sort(compareText);
501
- if (observed === null
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', 'io_scope_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の外へ書いた → `io_scope_warning`
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: 'io_scope_warning', todo_ids: [todoId], path: relativePath });
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。probeを通った警報だけがこの写像に乗る。
301
+ * 警報kind → finding kind。**重なりだけがhold経路へ乗る。**
302
302
  *
303
- * 述語が同一(`coveredBy`)なので、写像は1対1に決まる。ここで新しい種類を発明しない——
304
- * 発明すると、警報経由のfindingだけ既存の処置(請求項7の直列化、請求項8のseam変換)が
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 === 'io_scope_warning') return true;
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: 'lattice-runtime-supervisor', recordedAt: new Date().toISOString(),
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', 'scope_violation', 'stale_context'].includes(finding.kind);
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', 'scope_violation', 'stale_context']
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
  }