@quolu/lattice 0.63.1 → 0.63.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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/todo-store.mjs +52 -21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quolu/lattice",
3
- "version": "0.63.1",
3
+ "version": "0.63.2",
4
4
  "description": "Schedulability compiler for multi-agent development: observe real code boundaries, refactor the conflicting seam, recompile the plan for parallel execution",
5
5
  "author": {
6
6
  "name": "Quo / クオ at kitepon.dev",
@@ -486,6 +486,11 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
486
486
  const doneDigest = new Map();
487
487
  const completion = new Map();
488
488
  const authoredStartBindings = new Map();
489
+ // hard証拠検証は即時でなくreplay完了後、まだ現在状態を支えている記述子だけへ行う。
490
+ // reopen・再doneで差し替えられた古いdoneが指すblobは一度もcommitされないままGCで消え得るし、
491
+ // 履歴の完全性はevent_digest鎖が既に持つ。即時検証だと消えたblobを指す過去eventが
492
+ // storeを恒久的に書き込み不能へ落とす(2026-08-22実被弾: reopen済み旧doneのdangling blob)。
493
+ const pendingVerifications = new Map();
489
494
  const importedGenesis = events[0]?.payload.historical_import === true;
490
495
  let previousTime = null;
491
496
  for (const event of events) {
@@ -528,13 +533,15 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
528
533
  const state = states.get(migration.to_task_id);
529
534
  Object.assign(state, structuredClone(migration.state), { evidence_unverified: false });
530
535
  if (state.evidence !== null) {
536
+ const evidence = state.evidence;
537
+ const context = { plan_key: plan.plan_key, task_id: migration.to_task_id };
531
538
  if (state.imported) {
532
- if (verifyImportSource) verifyImportSource(state.evidence, {
533
- plan_key: plan.plan_key, task_id: migration.to_task_id,
534
- });
535
- } else if (verifyEvidence) verifyEvidence(state.evidence, {
536
- plan_key: plan.plan_key, task_id: migration.to_task_id,
537
- });
539
+ if (verifyImportSource) {
540
+ pendingVerifications.set(`task:${migration.to_task_id}`, () => verifyImportSource(evidence, context));
541
+ }
542
+ } else if (verifyEvidence) {
543
+ pendingVerifications.set(`task:${migration.to_task_id}`, () => verifyEvidence(evidence, context));
544
+ }
538
545
  }
539
546
  if (state.status === 'done') {
540
547
  doneDigest.set(migration.to_task_id, event.event_digest);
@@ -571,6 +578,7 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
571
578
  if (currentStatus !== 'gate_ready') fail('STORE_INCONSISTENT', 'phase_gate_not_ready');
572
579
  state.status = 'reviewing'; state.review_event_digest = event.event_digest;
573
580
  state.decision_event_digest = null; state.decision_evidence = null;
581
+ pendingVerifications.delete(`phase:${event.phase_id}`);
574
582
  } else if (event.kind === 'phase_accept') {
575
583
  const phase = phasesOf(plan).find(({ phase_id }) => phase_id === event.phase_id);
576
584
  const slots = event.payload.evidence_slots.map(({ slot_id }) => slot_id);
@@ -579,8 +587,12 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
579
587
  fail('STORE_INCONSISTENT', 'phase_accept_binding_invalid');
580
588
  }
581
589
  if (verifyEvidence) {
582
- verifyEvidence(event.payload.decision_evidence);
583
- for (const slot of event.payload.evidence_slots) verifyEvidence(slot.evidence);
590
+ const decisionEvidence = event.payload.decision_evidence;
591
+ const slotEvidence = event.payload.evidence_slots.map((slot) => slot.evidence);
592
+ pendingVerifications.set(`phase:${event.phase_id}`, () => {
593
+ verifyEvidence(decisionEvidence);
594
+ for (const evidence of slotEvidence) verifyEvidence(evidence);
595
+ });
584
596
  }
585
597
  state.status = 'accepted'; state.decision_event_digest = event.event_digest;
586
598
  state.decision_evidence = event.payload.decision_evidence;
@@ -588,7 +600,10 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
588
600
  if (currentStatus !== 'reviewing' || state.review_event_digest !== event.payload.review_event_digest) {
589
601
  fail('STORE_INCONSISTENT', 'phase_reject_binding_invalid');
590
602
  }
591
- if (verifyEvidence) verifyEvidence(event.payload.decision_evidence);
603
+ if (verifyEvidence) {
604
+ const decisionEvidence = event.payload.decision_evidence;
605
+ pendingVerifications.set(`phase:${event.phase_id}`, () => verifyEvidence(decisionEvidence));
606
+ }
592
607
  state.status = 'rejected'; state.decision_event_digest = event.event_digest;
593
608
  state.decision_evidence = event.payload.decision_evidence;
594
609
  } else if (event.kind === 'phase_close_unaudited') {
@@ -597,6 +612,7 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
597
612
  if (currentStatus !== 'gate_ready') fail('STORE_INCONSISTENT', 'phase_gate_not_ready');
598
613
  state.status = 'closed_unaudited'; state.decision_event_digest = event.event_digest;
599
614
  state.decision_evidence = null;
615
+ pendingVerifications.delete(`phase:${event.phase_id}`);
600
616
  } else {
601
617
  // phase_reopen。ADR 0148裁定5: closed_unauditedもaccepted/rejectedと同じく
602
618
  // reopenで初期状態へ戻せる——監査せずに閉じた工程を、後から本当に監査したくなった時に
@@ -624,6 +640,7 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
624
640
  fail('STORE_INCONSISTENT', 'phase_reopen_has_started_successor');
625
641
  }
626
642
  Object.assign(state, emptyPhaseState(event.phase_id));
643
+ pendingVerifications.delete(`phase:${event.phase_id}`);
627
644
  }
628
645
  continue;
629
646
  }
@@ -635,9 +652,12 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
635
652
  if (!importedGenesis || state.status !== 'pending') {
636
653
  fail('STORE_INCONSISTENT', 'invalid_historical_import_start_transition');
637
654
  }
638
- if (verifyImportSource) verifyImportSource(event.payload.evidence, {
639
- plan_key: plan.plan_key, task_id: event.task_id,
640
- });
655
+ if (verifyImportSource) {
656
+ const evidence = event.payload.evidence;
657
+ pendingVerifications.set(`task:${event.task_id}`, () => verifyImportSource(evidence, {
658
+ plan_key: plan.plan_key, task_id: event.task_id,
659
+ }));
660
+ }
641
661
  state.status = 'in-progress';
642
662
  state.started_at = event.payload.started_at === 'unknown_requires_evidence'
643
663
  ? null : event.payload.started_at;
@@ -686,18 +706,24 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
686
706
  } else if (event.kind === 'done') {
687
707
  if (event.payload.done_mode === 'authored') {
688
708
  if (state.status !== 'in-progress' || !dependenciesDone) fail('STORE_INCONSISTENT', 'invalid_done_transition');
689
- if (verifyEvidence) verifyEvidence(event.payload.evidence, {
690
- plan_key: plan.plan_key, task_id: event.task_id,
691
- });
709
+ if (verifyEvidence) {
710
+ const evidence = event.payload.evidence;
711
+ pendingVerifications.set(`task:${event.task_id}`, () => verifyEvidence(evidence, {
712
+ plan_key: plan.plan_key, task_id: event.task_id,
713
+ }));
714
+ }
692
715
  state.status = 'done'; state.done_at = event.recorded_at; state.evidence = event.payload.evidence;
693
716
  state.test_result = event.payload.test_result ?? null;
694
717
  state.imported = false;
695
718
  completion.set(event.task_id, { mode: 'authored', completed_at: event.recorded_at });
696
719
  } else if (event.payload.done_mode === 'historical_import') {
697
720
  if (!importedGenesis || state.status !== 'pending') fail('STORE_INCONSISTENT', 'invalid_historical_import_transition');
698
- if (verifyImportSource) verifyImportSource(event.payload.evidence, {
699
- plan_key: plan.plan_key, task_id: event.task_id,
700
- });
721
+ if (verifyImportSource) {
722
+ const evidence = event.payload.evidence;
723
+ pendingVerifications.set(`task:${event.task_id}`, () => verifyImportSource(evidence, {
724
+ plan_key: plan.plan_key, task_id: event.task_id,
725
+ }));
726
+ }
701
727
  state.status = 'done'; state.done_at = event.payload.completed_at === 'unknown_requires_evidence'
702
728
  ? null : event.payload.completed_at;
703
729
  state.evidence = event.payload.evidence; state.imported = true;
@@ -709,9 +735,12 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
709
735
  || doneDigest.get(event.task_id) !== event.payload.target_done_digest) {
710
736
  fail('STORE_INCONSISTENT', 'invalid_evidence_promotion');
711
737
  }
712
- if (verifyEvidence) verifyEvidence(event.payload.evidence, {
713
- plan_key: plan.plan_key, task_id: event.task_id,
714
- });
738
+ if (verifyEvidence) {
739
+ const evidence = event.payload.evidence;
740
+ pendingVerifications.set(`task:${event.task_id}`, () => verifyEvidence(evidence, {
741
+ plan_key: plan.plan_key, task_id: event.task_id,
742
+ }));
743
+ }
715
744
  state.evidence = event.payload.evidence;
716
745
  completion.set(event.task_id, { mode: 'evidence_promotion', completed_at: current.completed_at });
717
746
  }
@@ -739,8 +768,10 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
739
768
  if (startedSuccessor && event.payload.override_reason === null) fail('STORE_INCONSISTENT', 'reopen_has_started_successor');
740
769
  state.status = 'in-progress'; state.done_at = null; state.evidence = null; state.test_result = null;
741
770
  completion.delete(event.task_id);
771
+ pendingVerifications.delete(`task:${event.task_id}`);
742
772
  }
743
773
  }
774
+ for (const verify of pendingVerifications.values()) verify();
744
775
  return [...states.values()].sort((left, right) => left.task_id < right.task_id ? -1 : left.task_id > right.task_id ? 1 : 0);
745
776
  }
746
777