@quolu/lattice 0.66.1 → 0.67.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quolu/lattice",
3
- "version": "0.66.1",
3
+ "version": "0.67.0",
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
  evidence promote --plan <key> --task <id> --evidence <file>
111
111
  # done状態と完了時刻を維持し、最新doneへ追記eventで証拠を再束縛する
112
112
  dependency connect --from-plan <key> --from-task <id> --to-plan <key> --to-task <id> --reason <text>
113
+ dependency disconnect --from-plan <key> --from-task <id> --to-plan <key> --to-task <id> --reason <text>
113
114
  # 開発中に発見した依存を明示接続する(同一plan内・plan跨ぎとも)。依存の自動推定は行わない
114
115
  independence compile --plan <key> --input <file> # witness setとsensorから並列可否を記録する
115
116
  independence witness migrate --plan <key> # revision後の宣言をtask migrationで写す
@@ -281,6 +282,7 @@ const SUBCOMMAND_USAGE = Object.freeze({
281
282
  'todo evidence promote': 'todo evidence promote --plan <key> --task <id> --evidence <file>',
282
283
  'todo dependency': 'todo dependency connect --from-plan <key> --from-task <id> --to-plan <key> --to-task <id> --reason <text>',
283
284
  'todo dependency connect': 'todo dependency connect --from-plan <key> --from-task <id> --to-plan <key> --to-task <id> --reason <text>',
285
+ 'todo dependency disconnect': 'todo dependency disconnect --from-plan <key> --from-task <id> --to-plan <key> --to-task <id> --reason <text>',
284
286
  'todo split': 'todo split --plan <key> --input <file>',
285
287
  'todo revise': 'todo revise --plan <key> --input <file> | --schema --json',
286
288
  'todo revise-phase': 'todo revise-phase --plan <key> --input <file> | --schema --json',
package/src/todo-cli.mjs CHANGED
@@ -1047,6 +1047,44 @@ async function dependencyConnect({
1047
1047
  return result;
1048
1048
  }
1049
1049
 
1050
+ // connectの対。誤って張った線の除去(tombstone)。端点で一意に生きた接続を特定し、
1051
+ // その接続eventが積まれているplanのplan-scoped chainへ除去eventを積む。
1052
+ async function dependencyDisconnect({
1053
+ repoRoot, env, fromPlanKey, fromTaskId, toPlanKey, toTaskId, reason,
1054
+ }) {
1055
+ const store = await readTodoStore({ repoRoot });
1056
+ const live = projectTodoCrossPlanDependencies(store.members).filter((dependency) => (
1057
+ dependency.from.plan_key === fromPlanKey && dependency.from.task_id === fromTaskId
1058
+ && dependency.to.plan_key === toPlanKey && dependency.to.task_id === toTaskId
1059
+ ));
1060
+ if (live.length === 0) {
1061
+ throw new TodoStoreError('DEPENDENCY_INVALID', 'cross_plan_dependency_not_found', undefined, {
1062
+ from: { plan_key: fromPlanKey, task_id: fromTaskId },
1063
+ to: { plan_key: toPlanKey, task_id: toTaskId },
1064
+ });
1065
+ }
1066
+ const [target] = live;
1067
+ const owner = store.members.find((member) => (member.plan_scoped?.events ?? [])
1068
+ .some((event) => event.event_digest === target.event_digest));
1069
+ const { event } = await appendTodoEvent({
1070
+ repoRoot, writer: createTodoStoreWriter({ caller: 'g5-authoring' }), planKey: owner.plan.plan_key,
1071
+ event: {
1072
+ kind: 'cross_plan_dependency_removed', actor: mutationActor(env),
1073
+ payload: { target_event_digest: target.event_digest, reason },
1074
+ },
1075
+ });
1076
+ const result = {
1077
+ schema: 'lattice.todo_dependency_disconnect_result.v1', project_id: store.project_id,
1078
+ from: target.from, to: target.to, reason,
1079
+ removed_event_digest: target.event_digest,
1080
+ disconnected_by: event.actor, disconnected_at: event.recorded_at,
1081
+ event_digest: event.event_digest,
1082
+ result_digest: '',
1083
+ };
1084
+ result.result_digest = todoSelfDigest(result, 'result_digest');
1085
+ return result;
1086
+ }
1087
+
1050
1088
  async function phaseStatus({ repoRoot, planKey }) {
1051
1089
  const store = await readTodoStore({ repoRoot });
1052
1090
  const [member] = selectMembers(store, planKey);
@@ -3434,7 +3472,7 @@ function writesTodoStore(argv) {
3434
3472
  || (second === 'input' && !argv.includes('--dry-run'));
3435
3473
  case 'seam-proposal': return ['compile', 'apply', 'land'].includes(second);
3436
3474
  case 'evidence': return second === 'promote';
3437
- case 'dependency': return second === 'connect';
3475
+ case 'dependency': return ['connect', 'disconnect'].includes(second);
3438
3476
  case 'phase': return second !== 'status';
3439
3477
  case 'start': case 'retract': case 'block': case 'unblock': case 'retire': case 'done':
3440
3478
  case 'reopen': case 'split': case 'revise': case 'revise-phase': case 'revise-set':
@@ -3713,6 +3751,16 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
3713
3751
  repoRoot, env, fromPlanKey: argv[3], fromTaskId: argv[5],
3714
3752
  toPlanKey: argv[7], toTaskId: argv[9], reason: argv[11],
3715
3753
  });
3754
+ } else if (argv.length === 12 && argv[0] === 'dependency' && argv[1] === 'disconnect'
3755
+ && argv[2] === '--from-plan' && isTodoIdentifier(argv[3])
3756
+ && argv[4] === '--from-task' && isTodoIdentifier(argv[5])
3757
+ && argv[6] === '--to-plan' && isTodoIdentifier(argv[7])
3758
+ && argv[8] === '--to-task' && isTodoIdentifier(argv[9])
3759
+ && argv[10] === '--reason' && argv[11].length > 0) {
3760
+ action = (repoRoot) => dependencyDisconnect({
3761
+ repoRoot, env, fromPlanKey: argv[3], fromTaskId: argv[5],
3762
+ toPlanKey: argv[7], toTaskId: argv[9], reason: argv[11],
3763
+ });
3716
3764
  } else if (argv[0] === 'independence' && argv[1] === 'compile') {
3717
3765
  const flags = matchFlagCommand(argv, ['independence', 'compile'], {
3718
3766
  known: ['plan', 'input'], required: ['plan', 'input'],
@@ -16,11 +16,14 @@ export const TODO_EVENT_KINDS = Object.freeze([
16
16
  // 開発中に発見したplan跨ぎの依存を、active plan topologyの追記改変ではなく
17
17
  // version-boundなplan-scoped eventとして接続する。推定はせず、AIが発見した時だけ積む。
18
18
  'cross_plan_dependency',
19
+ // 誤って張った接続の除去(connectの対・2026-08-29オーナー裁定系)。対象eventのdigestを指す
20
+ // tombstoneとして積み、projectionが両方を読んで線を消す。歴史は書き換えない。
21
+ 'cross_plan_dependency_removed',
19
22
  ]);
20
23
 
21
24
  /** planへ帰属し、taskにもPhaseにも属さないevent kind。 */
22
25
  export const TODO_PLAN_SCOPED_EVENT_KINDS = Object.freeze([
23
- 'coordination_mode', 'cross_plan_dependency',
26
+ 'coordination_mode', 'cross_plan_dependency', 'cross_plan_dependency_removed',
24
27
  ]);
25
28
 
26
29
  /** 調整方式。witness=独立性を宣言し検証して並列する/conversation=会話で調整する。 */
@@ -524,6 +527,11 @@ function validPayload(event) {
524
527
  && dependencyRef(payload.from) && dependencyRef(payload.to)
525
528
  && nullableText(payload.reason) && payload.reason !== null;
526
529
  }
530
+ if (event.kind === 'cross_plan_dependency_removed') {
531
+ return exactRecord(payload, ['target_event_digest', 'reason'])
532
+ && isTodoDigest(payload.target_event_digest)
533
+ && nullableText(payload.reason) && payload.reason !== null;
534
+ }
527
535
  if (event.kind === 'start') return exactRecord(payload, ['override_reason']) && nullableText(payload.override_reason);
528
536
  if (event.kind === 'start_retracted') return exactRecord(payload, ['reason', 'target_start_digest'])
529
537
  && nullableText(payload.reason) && payload.reason !== null
@@ -407,9 +407,15 @@ export function projectTodoCoordination(events) {
407
407
  * 記録された線だけを返す。順序はtask identityで固定し、status/Ganttが同じ入力を見る。
408
408
  */
409
409
  export function projectTodoCrossPlanDependencies(members) {
410
+ // 除去tombstoneを先に集め、指された接続を線として数えない(歴史は両eventとも残る)
411
+ const removed = new Set();
412
+ for (const member of members) for (const event of member.plan_scoped?.events ?? []) {
413
+ if (event.kind === 'cross_plan_dependency_removed') removed.add(event.payload.target_event_digest);
414
+ }
410
415
  const dependencies = [];
411
416
  for (const member of members) for (const event of member.plan_scoped?.events ?? []) {
412
417
  if (event.kind !== 'cross_plan_dependency') continue;
418
+ if (removed.has(event.event_digest)) continue;
413
419
  dependencies.push({
414
420
  from: event.payload.from,
415
421
  to: event.payload.to,
@@ -2040,6 +2046,19 @@ export async function appendTodoEvent(options = {}) {
2040
2046
  if (options.event.kind === 'cross_plan_dependency') {
2041
2047
  validateCrossPlanDependencyTransition(store, member, options.event);
2042
2048
  }
2049
+ if (options.event.kind === 'cross_plan_dependency_removed') {
2050
+ const digest = options.event.payload.target_event_digest;
2051
+ const events = member.plan_scoped?.events ?? [];
2052
+ const target = events.find((entry) => entry.kind === 'cross_plan_dependency'
2053
+ && entry.event_digest === digest);
2054
+ if (target === undefined) {
2055
+ fail('DEPENDENCY_INVALID', 'cross_plan_dependency_not_found', { target_event_digest: digest });
2056
+ }
2057
+ if (events.some((entry) => entry.kind === 'cross_plan_dependency_removed'
2058
+ && entry.payload.target_event_digest === digest)) {
2059
+ fail('DEPENDENCY_INVALID', 'cross_plan_dependency_already_removed', { target_event_digest: digest });
2060
+ }
2061
+ }
2043
2062
  return appendPlanScopedEvent({
2044
2063
  repoRoot, member,
2045
2064
  input: { ...options.event, recorded_at: options.event.recorded_at ?? new Date().toISOString() },