@quolu/lattice 0.67.3 → 0.67.5

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quolu/lattice",
3
- "version": "0.67.3",
3
+ "version": "0.67.5",
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",
package/src/cli-help.mjs CHANGED
@@ -110,6 +110,7 @@ Write commands:
110
110
  reopen --plan <key> --task <id> --reason <text> [--override-reason <text>]
111
111
  evidence promote --plan <key> --task <id> --evidence <file>
112
112
  # done状態と完了時刻を維持し、最新doneへ追記eventで証拠を再束縛する
113
+ start --plan <key> --task <id> --rebind --reason <text> # 改訂でcarryされたin-progressへ現版のstart束縛を積み直す(ADR 0191)
113
114
  dependency connect --from-plan <key> --from-task <id> --to-plan <key> --to-task <id> --reason <text>
114
115
  dependency disconnect --from-plan <key> --from-task <id> --to-plan <key> --to-task <id> --reason <text>
115
116
  # 開発中に発見した依存を明示接続する(同一plan内・plan跨ぎとも)。依存の自動推定は行わない
package/src/todo-cli.mjs CHANGED
@@ -901,6 +901,47 @@ async function startTask({
901
901
  structureContext });
902
902
  }
903
903
 
904
+ // plan改訂でcarryされたin-progress ToDoの再束縛入口(ADR 0191)。task状態・開始時刻は
905
+ // 変えず、現plan_version・現actorのstart束縛だけをjournalへ積む。runtime intakeは
906
+ // literal startだけを束縛に使いcarryを推定しないため、改訂を跨いだ進行中工程の
907
+ // 再開はこの入口だけが正規経路になる。advisory・independence gateは初回startで
908
+ // 済んでいるため再適用しない。
909
+ async function rebindStart({ repoRoot, env, planKey, taskId, reason }) {
910
+ const store = await readTodoStore({ repoRoot });
911
+ const member = store.members.find(({ descriptor }) => descriptor.plan_key === planKey);
912
+ if (member === undefined) throw new TodoStoreError('STORE_INCONSISTENT', 'plan_not_active');
913
+ const task = member.tasks.find(({ task_id: candidate }) => candidate === taskId);
914
+ if (task === undefined) {
915
+ throw new TodoStoreError('TASK_NOT_FOUND', 'task_not_found', undefined, {
916
+ requested_task_id: taskId,
917
+ });
918
+ }
919
+ if (task.status !== 'in-progress') {
920
+ throw new TodoStoreError('TASK_START_BINDING_UNSUPPORTED', 'rebind_requires_in_progress', undefined, {
921
+ task_id: taskId, status: task.status,
922
+ });
923
+ }
924
+ let noteContext;
925
+ try {
926
+ ({ context: noteContext } = await readTodoNoteContext({ repoRoot, store, planKey, taskId }));
927
+ } catch (error) {
928
+ if (!['NOTE_LOG_CORRUPT', 'NOTE_PROJECTION_INVALID'].includes(error?.code)) throw error;
929
+ noteContext = {
930
+ schema: 'lattice.todo_note_context.unreadable.v1',
931
+ project_id: member.plan.project_id,
932
+ plan_key: planKey,
933
+ task_id: taskId,
934
+ code: error.code,
935
+ reason: error.detail?.reason ?? 'note_context_unreadable',
936
+ next_action: `lattice todo note list --plan ${planKey} --json`,
937
+ };
938
+ }
939
+ return mutate({
940
+ repoRoot, env, planKey, taskId, kind: 'start',
941
+ payload: { start_mode: 'rebind', reason }, evidenceRef: null, noteContext,
942
+ });
943
+ }
944
+
904
945
  async function retractStart({ repoRoot, env, planKey, taskId, reason }) {
905
946
  const actor = mutationActor(env);
906
947
  const store = await readTodoStore({ repoRoot });
@@ -1052,7 +1093,8 @@ async function dependencyConnect({
1052
1093
  async function dependencyDisconnect({
1053
1094
  repoRoot, env, fromPlanKey, fromTaskId, toPlanKey, toTaskId, reason,
1054
1095
  }) {
1055
- const store = await readTodoStore({ repoRoot });
1096
+ // 修理扉: 越境依存の固定digestが陳腐化した store でも disconnect だけは通す(todo-store.mjs 参照)
1097
+ const store = await readTodoStore({ repoRoot, tolerateStaleBindings: true });
1056
1098
  const live = projectTodoCrossPlanDependencies(store.members).filter((dependency) => (
1057
1099
  dependency.from.plan_key === fromPlanKey && dependency.from.task_id === fromTaskId
1058
1100
  && dependency.to.plan_key === toPlanKey && dependency.to.task_id === toTaskId
@@ -1068,6 +1110,7 @@ async function dependencyDisconnect({
1068
1110
  .some((event) => event.event_digest === target.event_digest));
1069
1111
  const { event } = await appendTodoEvent({
1070
1112
  repoRoot, writer: createTodoStoreWriter({ caller: 'g5-authoring' }), planKey: owner.plan.plan_key,
1113
+ tolerateStaleBindings: true,
1071
1114
  event: {
1072
1115
  kind: 'cross_plan_dependency_removed', actor: mutationActor(env),
1073
1116
  payload: { target_event_digest: target.event_digest, reason },
@@ -3968,11 +4011,18 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
3968
4011
  action = (repoRoot) => phaseBaseline({ repoRoot, env, reason: argv[3], exceptPlanKeys });
3969
4012
  } else if (argv[0] === 'start') {
3970
4013
  const flags = matchFlagCommand(argv, ['start'], {
3971
- known: ['plan', 'task', 'parallel-frontier', 'override-reason', 'serial-confirmed'],
4014
+ known: ['plan', 'task', 'parallel-frontier', 'override-reason', 'serial-confirmed', 'rebind', 'reason'],
3972
4015
  required: ['plan', 'task'],
3973
- booleans: ['parallel-frontier', 'serial-confirmed'],
4016
+ booleans: ['parallel-frontier', 'serial-confirmed', 'rebind'],
3974
4017
  });
3975
- if (flags !== null && isTodoIdentifier(flags.plan) && isTodoIdentifier(flags.task)
4018
+ if (flags !== null && flags.rebind === true) {
4019
+ if (isTodoIdentifier(flags.plan) && isTodoIdentifier(flags.task)
4020
+ && typeof flags.reason === 'string' && flags.reason.length > 0) {
4021
+ action = (repoRoot) => rebindStart({
4022
+ repoRoot, env, planKey: flags.plan, taskId: flags.task, reason: flags.reason,
4023
+ });
4024
+ }
4025
+ } else if (flags !== null && isTodoIdentifier(flags.plan) && isTodoIdentifier(flags.task)
3976
4026
  && (flags['override-reason'] === undefined || (typeof flags['override-reason'] === 'string'
3977
4027
  && flags['override-reason'].length > 0))) {
3978
4028
  action = (repoRoot) => startTask({
@@ -532,6 +532,12 @@ function validPayload(event) {
532
532
  && isTodoDigest(payload.target_event_digest)
533
533
  && nullableText(payload.reason) && payload.reason !== null;
534
534
  }
535
+ // rebind: plan改訂でcarryされたin-progress ToDoへ、現plan_versionの明示的start束縛を積み直す
536
+ // (runtime intakeはliteral startだけを束縛に使い、carryを推定しない。ADR 0191)
537
+ if (event.kind === 'start' && payload?.start_mode === 'rebind') {
538
+ return exactRecord(payload, ['start_mode', 'reason'])
539
+ && nullableText(payload.reason) && payload.reason !== null;
540
+ }
535
541
  if (event.kind === 'start') return exactRecord(payload, ['override_reason']) && nullableText(payload.override_reason);
536
542
  if (event.kind === 'start_retracted') return exactRecord(payload, ['reason', 'target_start_digest'])
537
543
  && nullableText(payload.reason) && payload.reason !== null
@@ -678,6 +678,17 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
678
678
  if (state === undefined) fail('STORE_INCONSISTENT', 'event_task_missing');
679
679
  const dependenciesDone = localPredecessors(plan, event.task_id).every((id) => states.get(id)?.status === 'done');
680
680
  if (event.kind === 'start') {
681
+ if (event.payload.start_mode === 'rebind') {
682
+ // 改訂でcarryされたin-progressの再束縛。状態・開始時刻は動かさず、start束縛だけを
683
+ // 現actor・現plan_versionのeventへ更新する(ADR 0191)
684
+ if (state.status !== 'in-progress') {
685
+ fail('STORE_INCONSISTENT', 'invalid_rebind_start_transition');
686
+ }
687
+ authoredStartBindings.set(event.task_id, {
688
+ actor: structuredClone(event.actor), event_digest: event.event_digest,
689
+ });
690
+ continue;
691
+ }
681
692
  if (event.payload.start_mode === 'historical_import') {
682
693
  if (!importedGenesis || state.status !== 'pending') {
683
694
  fail('STORE_INCONSISTENT', 'invalid_historical_import_start_transition');
@@ -879,7 +890,13 @@ function snapshotFor(plan, events, tasks) {
879
890
  return snapshot;
880
891
  }
881
892
 
882
- function validateMergedGraph(members, crossPlanDependencies = []) {
893
+ function validateMergedGraph(members, crossPlanDependencies = [], options = {}) {
894
+ // tolerateStaleBindings は dependency disconnect 専用の修理扉。plan revise が越境依存の
895
+ // 固定digestを陳腐化させると、通常loadは binding_stale で fail closed するが、その修理
896
+ // コマンド自身も load を要するため、扉が無いと修理経路が存在しない(実被弾 2026-08-29:
897
+ // evidence-2 改訂で store 全体が読めなくなり、disconnect も同エラーで詰んだ)。
898
+ // 許容するのは digest 不一致だけで、dangling / missing は修理扉でも fail する。
899
+ const tolerateStale = options.tolerateStaleBindings === true;
883
900
  const tasks = new Map();
884
901
  for (const member of members) for (const task of member.plan.tasks) {
885
902
  tasks.set(`${member.plan.project_id}\0${member.plan.plan_key}\0${task.task_id}`, member.plan.topology_digest);
@@ -900,7 +917,7 @@ function validateMergedGraph(members, crossPlanDependencies = []) {
900
917
  if (topology === undefined) fail('STORE_INCONSISTENT', 'dangling_dependency');
901
918
  if ((ref.project_id !== ownerPlan.project_id || ref.plan_key !== ownerPlan.plan_key)
902
919
  && ref.expected_topology_digest === undefined) fail('STORE_INCONSISTENT', 'cross_plan_binding_missing');
903
- if (ref.expected_topology_digest !== undefined && topology !== ref.expected_topology_digest) {
920
+ if (!tolerateStale && ref.expected_topology_digest !== undefined && topology !== ref.expected_topology_digest) {
904
921
  fail('STORE_INCONSISTENT', 'binding_stale');
905
922
  }
906
923
  return key;
@@ -911,7 +928,7 @@ function validateMergedGraph(members, crossPlanDependencies = []) {
911
928
  if (topology === undefined) fail('STORE_INCONSISTENT', 'dangling_phase_dependency');
912
929
  if ((ref.project_id !== ownerPlan.project_id || ref.plan_key !== ownerPlan.plan_key)
913
930
  && ref.expected_topology_digest === undefined) fail('STORE_INCONSISTENT', 'cross_plan_binding_missing');
914
- if (ref.expected_topology_digest !== undefined && topology !== ref.expected_topology_digest) {
931
+ if (!tolerateStale && ref.expected_topology_digest !== undefined && topology !== ref.expected_topology_digest) {
915
932
  fail('STORE_INCONSISTENT', 'binding_stale');
916
933
  }
917
934
  return key;
@@ -1119,6 +1136,7 @@ function validateMergedTransition(store, member, event) {
1119
1136
  const phaseAcceptReady = mergedPhaseAcceptReady(store, targetKey);
1120
1137
  const dependenciesDone = taskDependenciesDone && phaseAcceptReady;
1121
1138
  if (event.kind === 'start' && event.payload.start_mode !== 'historical_import'
1139
+ && event.payload.start_mode !== 'rebind'
1122
1140
  && (!phaseAcceptReady || (!taskDependenciesDone && event.payload.override_reason === null))) {
1123
1141
  fail('STORE_INCONSISTENT', 'invalid_start_transition');
1124
1142
  }
@@ -1629,7 +1647,9 @@ export async function readTodoStore(options = {}) {
1629
1647
  snapshot: snapshotStale ? expectedSnapshot : snapshot,
1630
1648
  tasks, phases, coordination, snapshot_stale: snapshotStale });
1631
1649
  }
1632
- validateMergedGraph(loaded, projectTodoCrossPlanDependencies(loaded));
1650
+ validateMergedGraph(loaded, projectTodoCrossPlanDependencies(loaded), {
1651
+ tolerateStaleBindings: options.tolerateStaleBindings === true,
1652
+ });
1633
1653
  return {
1634
1654
  schema: 'lattice.todo_store_read.v1', project_id: manifest.project_id, manifest,
1635
1655
  members: loaded, snapshot_stale: loaded.some((member) => member.snapshot_stale),
@@ -2036,6 +2056,10 @@ export async function appendTodoEvent(options = {}) {
2036
2056
  const store = await readTodoStore({
2037
2057
  repoRoot, forWrite: true, now: options.now, evidenceRepair,
2038
2058
  evidenceRepairCapability: EVIDENCE_REPAIR_CAPABILITY,
2059
+ // 修理扉は除去event(tombstone)の書込みだけに開く。それ以外の書込みは従来どおり
2060
+ // binding_stale で fail closed する(validateMergedGraph の注記を参照)
2061
+ tolerateStaleBindings: options.tolerateStaleBindings === true
2062
+ && options.event.kind === 'cross_plan_dependency_removed',
2039
2063
  });
2040
2064
  const member = store.members.find(({ descriptor }) => descriptor.plan_key === options.planKey);
2041
2065
  if (!member) fail('STORE_INCONSISTENT', 'plan_not_active');