@quolu/lattice 0.50.1 → 0.52.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 (42) hide show
  1. package/bin/lattice-work-order-adapter.mjs +20 -0
  2. package/docs/schemas/lattice.runtime_adapter_capabilities.v2.schema.json +55 -0
  3. package/docs/schemas/lattice.runtime_adapter_registration_input.v2.schema.json +86 -0
  4. package/package.json +5 -2
  5. package/src/boundary-observation-compiler-v2.mjs +1 -1
  6. package/src/cli-help.mjs +33 -2
  7. package/src/rc3-actual-dogfood.mjs +6 -2
  8. package/src/rc3-scripted-campaign.mjs +37 -10
  9. package/src/rc4-stage1-dogfood.mjs +6 -2
  10. package/src/runtime-adapter-registry.mjs +21 -7
  11. package/src/runtime-cli.mjs +476 -34
  12. package/src/runtime-contracts.mjs +59 -13
  13. package/src/runtime-controller-protocol.mjs +48 -3
  14. package/src/runtime-decision-verifier.mjs +70 -0
  15. package/src/runtime-diff-observer.mjs +66 -4
  16. package/src/runtime-direct-os-observer.mjs +25 -8
  17. package/src/runtime-driver-state.mjs +162 -0
  18. package/src/runtime-engine.mjs +37 -6
  19. package/src/runtime-front-end.mjs +39 -1
  20. package/src/runtime-managed-supervisor.mjs +80 -14
  21. package/src/runtime-multi-epoch-store.mjs +87 -14
  22. package/src/runtime-pull-intake.mjs +1188 -0
  23. package/src/runtime-work-order-contracts.mjs +91 -0
  24. package/src/runtime-work-order-controller.mjs +1167 -0
  25. package/src/seam-proposal-queries.mjs +1 -1
  26. package/src/todo-cli.mjs +273 -7
  27. package/src/todo-contracts.mjs +19 -2
  28. package/src/todo-gantt-html-independence.mjs +3 -2
  29. package/src/todo-gantt-html-shared.mjs +1 -2
  30. package/src/todo-gantt-html-style.mjs +13 -0
  31. package/src/todo-gantt-html.mjs +15 -2
  32. package/src/todo-gantt-layout.mjs +75 -1
  33. package/src/todo-gantt-nested.mjs +263 -0
  34. package/src/todo-gantt-svg.mjs +80 -5
  35. package/src/todo-independence-contracts.mjs +73 -7
  36. package/src/todo-independence-guidance.mjs +30 -1
  37. package/src/todo-independence.mjs +89 -7
  38. package/src/todo-revision.mjs +1 -1
  39. package/src/todo-split.mjs +472 -0
  40. package/src/todo-status.mjs +10 -1
  41. package/src/todo-store-git-transaction.mjs +418 -0
  42. package/src/todo-store.mjs +144 -4
@@ -5,7 +5,7 @@ import { SENSOR_QUERY_OPERATIONS } from './runtime-contracts.mjs';
5
5
  import { collectSensorEvidence, portableSensorOutcome } from './sensor-adapter.mjs';
6
6
  import { todoSelfDigest } from './todo-contracts.mjs';
7
7
 
8
- const CONFLICT_KINDS = new Set(['symbol', 'path', 'state', 'effect']);
8
+ const CONFLICT_KINDS = new Set(['symbol', 'path', 'state', 'effect', 'line']);
9
9
  const QUERYABLE_KINDS = new Set(['symbol', 'path']);
10
10
  const SYMBOL_OPERATIONS = Object.freeze(['query', 'callers', 'callees', 'impact']);
11
11
  const SENSOR_OPERATIONS = new Set(SENSOR_QUERY_OPERATIONS);
package/src/todo-cli.mjs CHANGED
@@ -46,10 +46,12 @@ import {
46
46
  createTodoStoreWriter,
47
47
  TodoStoreError,
48
48
  isPhaselessTodoPlanSchema,
49
+ projectTodoCrossPlanDependencies,
49
50
  TERMINAL_AUDIT_PHASE_ID,
50
51
  readTodoIndependenceArtifact,
51
52
  readTodoSeamProposalArtifact,
52
53
  readTodoStore,
54
+ resolveTodoStartRetractionBinding,
53
55
  readTodoWitnessSet,
54
56
  todoWitnessRef,
55
57
  writeTodoWitnessSet,
@@ -60,6 +62,7 @@ import {
60
62
  verifyEffectivePhaseTodoRevisionSources,
61
63
  verifyTodoRevisionSources,
62
64
  } from './todo-store.mjs';
65
+ import { withStartRetractionGuard } from './runtime-pull-intake.mjs';
63
66
  import {
64
67
  appendTodoExtraction,
65
68
  compileTodoExtraction,
@@ -94,6 +97,7 @@ import {
94
97
  selectIndependenceGuidance,
95
98
  selectWitnessScaffoldGuidance,
96
99
  selectSeamProposalGuidance,
100
+ scopeExpansionRecommendations,
97
101
  } from './todo-independence-guidance.mjs';
98
102
  import {
99
103
  buildSeamProposalQuerySet,
@@ -116,6 +120,10 @@ import {
116
120
  parseTodoSourceRef, todoLegacyReconciliationDigest, validatePhaseTodoRevision,
117
121
  validateTodoRevision, validateTodoRevisionSet,
118
122
  } from './todo-revision.mjs';
123
+ import {
124
+ compileTodoSplit,
125
+ prepareTodoSplitWitnessMigration,
126
+ } from './todo-split.mjs';
119
127
  import {
120
128
  appendTodoNote,
121
129
  readTodoNoteContext,
@@ -124,6 +132,7 @@ import {
124
132
  readTodoPlanNotesForStatus,
125
133
  } from './todo-note-store.mjs';
126
134
  import { readTodoParallelCandidatesForStatus } from './todo-parallel-candidates.mjs';
135
+ import { commitTodoStoreMutation } from './todo-store-git-transaction.mjs';
127
136
 
128
137
  const CLI_ERROR_SCHEMA = 'lattice.cli_error.v2';
129
138
  const DEFAULT_GANTT_SCOPE = 'live';
@@ -177,7 +186,7 @@ function typedFailure(stderr, error) {
177
186
  const TODO_COMMAND_NAMES = Object.freeze([
178
187
  'status', 'show', 'note', 'bindings', 'independence', 'seam-profile', 'seam-proposal',
179
188
  'verify', 'snapshot', 'gantt', 'dashboard', 'phase', 'migrate', 'start', 'block',
180
- 'unblock', 'done', 'reopen', 'evidence', 'revise', 'revise-phase', 'revise-set',
189
+ 'unblock', 'done', 'reopen', 'evidence', 'split', 'revise', 'revise-phase', 'revise-set',
181
190
  ]);
182
191
 
183
192
  function typedArgumentFailure(stderr, code, message, detail) {
@@ -186,6 +195,30 @@ function typedArgumentFailure(stderr, code, message, detail) {
186
195
  return 2;
187
196
  }
188
197
 
198
+ function supportsAtomicStoreCommit(argv) {
199
+ const command = argv[0];
200
+ if (command === 'note') return argv[1] !== 'list';
201
+ if (command === 'independence') {
202
+ return argv[1] === 'mode'
203
+ || (argv[1] === 'witness' && ['migrate', 'scaffold'].includes(argv[2]));
204
+ }
205
+ if (command === 'snapshot') return argv[1] === '--rebuild';
206
+ if (command === 'migrate') return !argv.includes('--dry-run') && !argv.includes('--schema');
207
+ if (['revise', 'split', 'revise-set', 'revise-phase', 'start', 'retract', 'block',
208
+ 'unblock', 'done', 'reopen'].includes(command)) return true;
209
+ if (command === 'evidence') return argv[1] === 'promote';
210
+ if (command === 'phase') return argv[1] !== 'status';
211
+ return false;
212
+ }
213
+
214
+ function atomicStoreCommitUnsupported(stderr, argv) {
215
+ return typedArgumentFailure(stderr, 'STORE_COMMIT_UNSUPPORTED',
216
+ 'todo_command_does_not_mutate_only_the_store', {
217
+ command: argv.slice(0, 3),
218
+ next_action: 'remove_--commit-store_or_use_a_supported_todo_write_command',
219
+ });
220
+ }
221
+
189
222
  function resolveRepoRoot(cwd) {
190
223
  try {
191
224
  return execFileSync('git', ['rev-parse', '--show-toplevel'], {
@@ -249,9 +282,13 @@ function taskRef(plan, taskId) {
249
282
  }
250
283
 
251
284
  function mergedTopology(store) {
285
+ const crossPlanDependencies = projectTodoCrossPlanDependencies(store.members);
252
286
  return {
253
287
  nodes: store.members.flatMap(({ plan }) => plan.tasks.map(({ task_id: taskId }) => taskRef(plan, taskId))),
254
- hard_edges: store.members.flatMap(({ plan }) => plan.hard_dependencies),
288
+ hard_edges: [
289
+ ...store.members.flatMap(({ plan }) => plan.hard_dependencies),
290
+ ...crossPlanDependencies.map(({ from, to }) => ({ from, to })),
291
+ ],
255
292
  joins: store.members.flatMap(({ plan }) => plan.joins),
256
293
  };
257
294
  }
@@ -609,6 +646,7 @@ async function startAdvisory({ repoRoot, store, projection, planKey, taskId }) {
609
646
  coverage: 'missing',
610
647
  drift_intersecting: null,
611
648
  conflicts_with_active: [],
649
+ scope_expansion_recommendations: [],
612
650
  uncovered_active_task_ids: projection.active_set
613
651
  .filter((task) => task.plan_key === planKey).map(({ task_id: id }) => id),
614
652
  self_unknowns: [{ kind: 'witness_missing', ref: 'no_independence_record' }],
@@ -652,6 +690,9 @@ async function startAdvisory({ repoRoot, store, projection, planKey, taskId }) {
652
690
  drift_intersecting: projected.drift === null
653
691
  ? null : projected.drift.intersecting_task_ids.includes(taskId),
654
692
  conflicts_with_active: conflictsWithActive,
693
+ scope_expansion_recommendations: scopeExpansionRecommendations(
694
+ Array.isArray(artifact.scope_expanded) ? artifact.scope_expanded : [],
695
+ ).filter(({ task_id: expandedTaskId }) => expandedTaskId === taskId),
655
696
  uncovered_active_task_ids: projected.uncovered_active_task_ids,
656
697
  self_unknowns: selfUnknowns,
657
698
  guidance: selectIndependenceGuidance({
@@ -786,6 +827,23 @@ async function startTask({
786
827
  payload: { override_reason: overrideReason }, evidenceRef: null, advisory, noteContext });
787
828
  }
788
829
 
830
+ async function retractStart({ repoRoot, env, planKey, taskId, reason }) {
831
+ const actor = mutationActor(env);
832
+ const store = await readTodoStore({ repoRoot });
833
+ const binding = resolveTodoStartRetractionBinding(store, { planKey, taskId, actor });
834
+ return withStartRetractionGuard({
835
+ repoRoot,
836
+ planKey,
837
+ taskId: binding.task_id,
838
+ activationEventDigest: binding.activation_event_digest,
839
+ action: () => mutate({
840
+ repoRoot, env, planKey, taskId: binding.task_id, kind: 'start_retracted',
841
+ payload: { reason, target_start_digest: binding.activation_event_digest },
842
+ evidenceRef: null,
843
+ }),
844
+ });
845
+ }
846
+
789
847
  function validatePhaseDecisionInput(value, outcome) {
790
848
  const keys = outcome === 'accept'
791
849
  ? ['schema', 'review_event_digest', 'decision_evidence', 'evidence_slots', 'input_digest']
@@ -864,6 +922,57 @@ async function independenceMode({ repoRoot, env, planKey, mode, reason }) {
864
922
  return result;
865
923
  }
866
924
 
925
+ /** 開発中に発見したplan跨ぎ依存を、consumer planのplan-scoped chainへ接続する。 */
926
+ async function dependencyConnect({
927
+ repoRoot, env, fromPlanKey, fromTaskId, toPlanKey, toTaskId, reason,
928
+ }) {
929
+ const store = await readTodoStore({ repoRoot });
930
+ const dependencyMember = (planKey) => {
931
+ const member = store.members.find(({ plan }) => plan.plan_key === planKey);
932
+ if (member === undefined) {
933
+ throw new TodoStoreError('DEPENDENCY_INVALID', 'dependency_plan_not_found', undefined, {
934
+ plan_key: planKey,
935
+ });
936
+ }
937
+ return member;
938
+ };
939
+ const source = dependencyMember(fromPlanKey);
940
+ const target = dependencyMember(toPlanKey);
941
+ if (!source.plan.tasks.some(({ task_id: taskId }) => taskId === fromTaskId)) {
942
+ throw new TodoStoreError('DEPENDENCY_INVALID', 'dependency_task_not_found', undefined, {
943
+ plan_key: fromPlanKey, task_id: fromTaskId,
944
+ });
945
+ }
946
+ if (!target.plan.tasks.some(({ task_id: taskId }) => taskId === toTaskId)) {
947
+ throw new TodoStoreError('DEPENDENCY_INVALID', 'dependency_task_not_found', undefined, {
948
+ plan_key: toPlanKey, task_id: toTaskId,
949
+ });
950
+ }
951
+ const from = {
952
+ project_id: store.project_id, plan_key: fromPlanKey, task_id: fromTaskId,
953
+ expected_topology_digest: source.plan.topology_digest,
954
+ };
955
+ const to = {
956
+ project_id: store.project_id, plan_key: toPlanKey, task_id: toTaskId,
957
+ expected_topology_digest: target.plan.topology_digest,
958
+ };
959
+ const { event } = await appendTodoEvent({
960
+ repoRoot, writer: createTodoStoreWriter({ caller: 'g5-authoring' }), planKey: toPlanKey,
961
+ event: {
962
+ kind: 'cross_plan_dependency', actor: mutationActor(env), payload: { from, to, reason },
963
+ },
964
+ });
965
+ const result = {
966
+ schema: 'lattice.todo_dependency_connect_result.v1', project_id: store.project_id,
967
+ from: event.payload.from, to: event.payload.to, reason: event.payload.reason,
968
+ connected_by: event.actor, connected_at: event.recorded_at,
969
+ event_digest: event.event_digest, plan_scoped_head_digest: event.event_digest,
970
+ result_digest: '',
971
+ };
972
+ result.result_digest = todoSelfDigest(result, 'result_digest');
973
+ return result;
974
+ }
975
+
867
976
  async function phaseStatus({ repoRoot, planKey }) {
868
977
  const store = await readTodoStore({ repoRoot });
869
978
  const [member] = selectMembers(store, planKey);
@@ -1322,6 +1431,69 @@ async function revisePhase({ repoRoot, env, planKey, inputRef }) {
1322
1431
  revision, actor: mutationActor(env), recordedAt: new Date().toISOString() });
1323
1432
  }
1324
1433
 
1434
+ async function splitTodo({ repoRoot, env, planKey, inputRef }) {
1435
+ const proposal = await readMigrationInput(repoRoot, inputRef, { requireValid: false });
1436
+ const store = await readTodoStore({ repoRoot });
1437
+ const member = store.members.find(({ descriptor }) => descriptor.plan_key === planKey);
1438
+ if (member === undefined) throw new TodoStoreError('STORE_INCONSISTENT', 'plan_not_active');
1439
+ const witnessSet = await readTodoWitnessSet({ repoRoot, planKey });
1440
+ if (witnessSet === null) {
1441
+ throw new TodoStoreError('WITNESS_MIGRATION_UNAVAILABLE', 'witness_set_absent', undefined, {
1442
+ witness_ref: todoWitnessRef(planKey),
1443
+ next_action: `lattice todo independence witness scaffold --plan ${planKey} --input <draft>`,
1444
+ });
1445
+ }
1446
+ const compiled = await compileTodoSplit({ repoRoot, member, proposal });
1447
+ const actor = mutationActor(env);
1448
+ const recordedAt = new Date().toISOString();
1449
+ // splitのmigrationは既存taskへのidentity写像だけである。宣言を純粋に移行・検査し、
1450
+ // 同じcanonical bytesをwitness先へ書けることまでapply前に確定する。これにより、
1451
+ // witness失敗をrevision適用後に返してplan/sourceだけ進んだ状態を作らない。
1452
+ const preparedWitness = prepareTodoSplitWitnessMigration({
1453
+ witnessSet, revision: compiled.revision,
1454
+ });
1455
+ const { ref: witnessRef } = await writeTodoWitnessSet({
1456
+ repoRoot, witnessSet: preparedWitness.witnessSet,
1457
+ });
1458
+ const witnessMigration = {
1459
+ schema: 'lattice.todo_witness_migrate_result.v1',
1460
+ project_id: store.project_id,
1461
+ plan_key: planKey,
1462
+ plan_version: compiled.revision.desired_plan.plan_version,
1463
+ witness_ref: witnessRef,
1464
+ migrated_count: preparedWitness.migrated_count,
1465
+ removed_count: preparedWitness.removed_count,
1466
+ unchanged_count: preparedWitness.unchanged_count,
1467
+ witness_set_digest: preparedWitness.witnessSet.witness_set_digest,
1468
+ result_digest: '',
1469
+ };
1470
+ witnessMigration.result_digest = todoSelfDigest(witnessMigration, 'result_digest');
1471
+ const receipt = compiled.revision.schema === 'lattice.phase_todo_revision.v3'
1472
+ ? await applyPhaseTodoRevision({
1473
+ repoRoot, writer: createTodoStoreWriter({ caller: 'g5-authoring' }),
1474
+ revision: compiled.revision, actor, recordedAt,
1475
+ })
1476
+ : await applyTodoRevision({
1477
+ repoRoot, writer: createTodoStoreWriter({ caller: 'g5-authoring' }),
1478
+ revision: compiled.revision, actor, recordedAt,
1479
+ });
1480
+ const result = {
1481
+ schema: 'lattice.todo_split_result.v1',
1482
+ project_id: store.project_id,
1483
+ plan_key: planKey,
1484
+ predecessor_task_id: proposal.task_id,
1485
+ residual_task_id: proposal.task_id,
1486
+ extracted_task_ids: compiled.extracted_task_ids,
1487
+ plan_version: compiled.revision.desired_plan.plan_version,
1488
+ revision_digest: compiled.revision.revision_digest,
1489
+ revision_receipt_digest: receipt.receipt_digest ?? receipt.result_digest,
1490
+ witness_migration_result_digest: witnessMigration.result_digest,
1491
+ result_digest: '',
1492
+ };
1493
+ result.result_digest = todoSelfDigest(result, 'result_digest');
1494
+ return result;
1495
+ }
1496
+
1325
1497
  async function status({ repoRoot }) {
1326
1498
  const store = await readTodoStore({ repoRoot });
1327
1499
  return projectTodoStatus(store, {
@@ -1530,17 +1702,23 @@ async function independenceCompile({ repoRoot, planKey, inputRef }) {
1530
1702
  const member = store.members.find(({ descriptor }) => descriptor.plan_key === planKey);
1531
1703
  if (!member) throw new TodoStoreError('STORE_INCONSISTENT', 'plan_not_active', undefined, { plan_key: planKey });
1532
1704
 
1705
+ // 前回artifactを渡して膨張の履歴を継ぐ。**例外を握り潰さない。**
1706
+ // `readTodoIndependenceArtifact` は「欠落だけnull・旧版はlegacy marker・壊れた記録は
1707
+ // INDEPENDENCE_ARTIFACT_INVALIDでtyped fail」を既に区別している。ここでcatchすると
1708
+ // **corrupt/permission/I-Oまで「初回」へ化けて履歴が黙って切れる**(suzune の監査で実測・room [1148])。
1709
+ const previousArtifact = await readTodoIndependenceArtifact({ repoRoot, store, planKey });
1533
1710
  const artifact = compileTodoIndependence({
1534
1711
  witnessSet,
1535
1712
  plan: member.plan,
1536
1713
  baseSha,
1537
1714
  compiledAt: new Date().toISOString(),
1538
1715
  sensorEvidence: await collectWitnessSensorEvidence({ cwd: repoRoot, witnessSet }),
1716
+ previousArtifact,
1539
1717
  });
1540
1718
  const { ref } = await writeTodoIndependenceArtifact({ repoRoot, artifact });
1541
1719
 
1542
1720
  const result = {
1543
- schema: 'lattice.todo_independence_compile_result.v1',
1721
+ schema: 'lattice.todo_independence_compile_result.v2',
1544
1722
  project_id: artifact.project_id,
1545
1723
  plan_key: artifact.plan_key,
1546
1724
  plan_version: artifact.plan_version,
@@ -1550,6 +1728,7 @@ async function independenceCompile({ repoRoot, planKey, inputRef }) {
1550
1728
  task_count: artifact.task_ids.length,
1551
1729
  conflict_count: artifact.conflicts.length,
1552
1730
  unknown_count: artifact.unknowns.length,
1731
+ scope_expansion_recommendations: scopeExpansionRecommendations(artifact.scope_expanded),
1553
1732
  result_digest: '',
1554
1733
  };
1555
1734
  result.result_digest = todoSelfDigest(result, 'result_digest');
@@ -2218,7 +2397,35 @@ async function independenceForGantt({ repoRoot, store }) {
2218
2397
  const projections = [];
2219
2398
  for (const member of store.members) {
2220
2399
  const planKey = member.plan.plan_key;
2221
- const artifact = await readTodoIndependenceArtifact({ repoRoot, store, planKey });
2400
+ let artifact = null;
2401
+ let unreadableReason = null;
2402
+ try {
2403
+ artifact = await readTodoIndependenceArtifact({ repoRoot, store, planKey });
2404
+ } catch (error) {
2405
+ if (!(error instanceof TodoStoreError)) throw error;
2406
+ unreadableReason = `${error.code}:${error.detail?.reason ?? error.message}`;
2407
+ }
2408
+ if (unreadableReason !== null) {
2409
+ if (currentBaseSha === null) currentBaseSha = currentHeadSha(repoRoot);
2410
+ const projected = projectIndependenceFrontier({
2411
+ artifact: null,
2412
+ readyTaskIds: frontier.filter((task) => task.plan_key === planKey)
2413
+ .map(({ task_id: taskId }) => taskId),
2414
+ activeTaskIds: status.active_set.filter((task) => task.plan_key === planKey)
2415
+ .map(({ task_id: taskId }) => taskId),
2416
+ plan: member.plan,
2417
+ currentBaseSha,
2418
+ changedPaths: null,
2419
+ });
2420
+ projections.push({
2421
+ project_id: member.plan.project_id,
2422
+ plan_key: planKey,
2423
+ coverage: 'unreadable',
2424
+ unreadable_reason: unreadableReason,
2425
+ frontier: projected.frontier,
2426
+ });
2427
+ continue;
2428
+ }
2222
2429
  if (artifact === null) continue;
2223
2430
  // 記録があるplanが1つでもあれば鮮度の判定にHEADが要る。
2224
2431
  if (currentBaseSha === null) currentBaseSha = currentHeadSha(repoRoot);
@@ -2238,6 +2445,7 @@ async function independenceForGantt({ repoRoot, store }) {
2238
2445
  project_id: member.plan.project_id,
2239
2446
  plan_key: planKey,
2240
2447
  coverage: projected.coverage,
2448
+ unreadable_reason: null,
2241
2449
  frontier: projected.frontier,
2242
2450
  });
2243
2451
  }
@@ -2253,12 +2461,29 @@ async function seamProposalsForGantt({ repoRoot, store }) {
2253
2461
  const projections = [];
2254
2462
  for (const member of store.members) {
2255
2463
  const planKey = member.plan.plan_key;
2256
- const artifact = await readTodoSeamProposalArtifact({ repoRoot, store, planKey });
2464
+ let artifact = null;
2465
+ try {
2466
+ artifact = await readTodoSeamProposalArtifact({ repoRoot, store, planKey });
2467
+ } catch (error) {
2468
+ if (!(error instanceof TodoStoreError)) throw error;
2469
+ projections.push({
2470
+ project_id: member.plan.project_id,
2471
+ plan_key: planKey,
2472
+ coverage: 'superseded',
2473
+ unreadable_reason: `${error.code}:${error.detail?.reason ?? error.message}`,
2474
+ guidance: selectSeamProposalGuidance({ coverage: 'superseded' }),
2475
+ component_count: null,
2476
+ conflict_resource_count: null,
2477
+ components: [],
2478
+ });
2479
+ continue;
2480
+ }
2257
2481
  if (artifact === null) {
2258
2482
  projections.push({
2259
2483
  project_id: member.plan.project_id,
2260
2484
  plan_key: planKey,
2261
2485
  coverage: 'missing',
2486
+ unreadable_reason: null,
2262
2487
  guidance: selectSeamProposalGuidance({ coverage: 'missing' }),
2263
2488
  component_count: null,
2264
2489
  conflict_resource_count: null,
@@ -2268,7 +2493,14 @@ async function seamProposalsForGantt({ repoRoot, store }) {
2268
2493
  }
2269
2494
 
2270
2495
  if (currentBaseSha === null) currentBaseSha = currentHeadSha(repoRoot);
2271
- const independenceArtifact = await readTodoIndependenceArtifact({ repoRoot, store, planKey });
2496
+ let independenceArtifact = null;
2497
+ let unreadableReason = null;
2498
+ try {
2499
+ independenceArtifact = await readTodoIndependenceArtifact({ repoRoot, store, planKey });
2500
+ } catch (error) {
2501
+ if (!(error instanceof TodoStoreError)) throw error;
2502
+ unreadableReason = `${error.code}:${error.detail?.reason ?? error.message}`;
2503
+ }
2272
2504
  const binding = artifact.source_binding;
2273
2505
  const independenceMatches = independenceArtifact !== null
2274
2506
  && validateTodoIndependence(independenceArtifact)
@@ -2287,6 +2519,7 @@ async function seamProposalsForGantt({ repoRoot, store }) {
2287
2519
  project_id: member.plan.project_id,
2288
2520
  plan_key: planKey,
2289
2521
  coverage,
2522
+ unreadable_reason: unreadableReason,
2290
2523
  guidance: selectSeamProposalGuidance({ coverage }),
2291
2524
  component_count: components.length,
2292
2525
  conflict_resource_count: components
@@ -2538,6 +2771,13 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
2538
2771
  throw new TypeError('runTodoCli optionsが不正');
2539
2772
  }
2540
2773
 
2774
+ const atomicCommit = argv.at(-1) === '--commit-store';
2775
+ if (atomicCommit) argv = argv.slice(0, -1);
2776
+ if (atomicCommit && ((argv[1] === '--schema' && argv[2] === '--json')
2777
+ || (argv[0] === 'dashboard' && argv[1] === 'remove'))) {
2778
+ return atomicStoreCommitUnsupported(stderr, argv);
2779
+ }
2780
+
2541
2781
  if (argv[0] === 'migrate' && argv[1] === '--input'
2542
2782
  && typeof argv[2] === 'string' && path.isAbsolute(argv[2])) {
2543
2783
  return typedArgumentFailure(stderr, 'INPUT_OUTSIDE_REPOSITORY', 'absolute_input_path_rejected', {
@@ -2645,6 +2885,16 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
2645
2885
  action = (repoRoot) => independenceMode({
2646
2886
  repoRoot, env, planKey: argv[3], mode: argv[5], reason: argv[7],
2647
2887
  });
2888
+ } else if (argv.length === 12 && argv[0] === 'dependency' && argv[1] === 'connect'
2889
+ && argv[2] === '--from-plan' && isTodoIdentifier(argv[3])
2890
+ && argv[4] === '--from-task' && isTodoIdentifier(argv[5])
2891
+ && argv[6] === '--to-plan' && isTodoIdentifier(argv[7])
2892
+ && argv[8] === '--to-task' && isTodoIdentifier(argv[9])
2893
+ && argv[10] === '--reason' && argv[11].length > 0) {
2894
+ action = (repoRoot) => dependencyConnect({
2895
+ repoRoot, env, fromPlanKey: argv[3], fromTaskId: argv[5],
2896
+ toPlanKey: argv[7], toTaskId: argv[9], reason: argv[11],
2897
+ });
2648
2898
  } else if (argv.length === 6 && argv[0] === 'independence' && argv[1] === 'compile'
2649
2899
  && argv[2] === '--plan' && isTodoIdentifier(argv[3]) && argv[4] === '--input') {
2650
2900
  action = (repoRoot) => independenceCompile({
@@ -2731,6 +2981,10 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
2731
2981
  && argv[1] === '--plan' && isTodoIdentifier(argv[2])
2732
2982
  && argv[3] === '--input' && isTodoRef(argv[4])) {
2733
2983
  action = (repoRoot) => revise({ repoRoot, env, planKey: argv[2], inputRef: argv[4] });
2984
+ } else if (argv.length === 5 && argv[0] === 'split'
2985
+ && argv[1] === '--plan' && isTodoIdentifier(argv[2])
2986
+ && argv[3] === '--input' && isTodoRef(argv[4])) {
2987
+ action = (repoRoot) => splitTodo({ repoRoot, env, planKey: argv[2], inputRef: argv[4] });
2734
2988
  } else if (argv.length === 3 && argv[0] === 'revise-set'
2735
2989
  && argv[1] === '--input' && isTodoRef(argv[2])) {
2736
2990
  action = (repoRoot) => reviseSet({ repoRoot, env, inputRef: argv[2] });
@@ -2784,6 +3038,13 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
2784
3038
  action = (repoRoot) => startTask({ repoRoot, env, planKey: argv[2], taskId: argv[4],
2785
3039
  overrideReason, parallelFrontier: argv.length === 6,
2786
3040
  serialConfirmed: argv.length === 8 });
3041
+ } else if (argv.length === 7 && argv[0] === 'retract'
3042
+ && argv[1] === '--plan' && isTodoIdentifier(argv[2])
3043
+ && argv[3] === '--task' && isTodoIdentifier(argv[4])
3044
+ && argv[5] === '--reason' && argv[6].length > 0) {
3045
+ action = (repoRoot) => retractStart({
3046
+ repoRoot, env, planKey: argv[2], taskId: argv[4], reason: argv[6],
3047
+ });
2787
3048
  } else if (argv.length === 7 && argv[0] === 'block'
2788
3049
  && argv[1] === '--plan' && isTodoIdentifier(argv[2])
2789
3050
  && argv[3] === '--task' && isTodoIdentifier(argv[4])
@@ -2831,6 +3092,9 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
2831
3092
  command, next_action: argumentHelp,
2832
3093
  });
2833
3094
  }
3095
+ if (atomicCommit && !supportsAtomicStoreCommit(argv)) {
3096
+ return atomicStoreCommitUnsupported(stderr, argv);
3097
+ }
2834
3098
 
2835
3099
  try {
2836
3100
  const repoRoot = resolveRepoRoot(cwd);
@@ -2840,7 +3104,9 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
2840
3104
  if (!ganttCommand && !dashboardAdopt && !migrationDryRun) {
2841
3105
  await ensureActiveProjectDashboard({ repoRoot, env });
2842
3106
  }
2843
- const result = await action(repoRoot);
3107
+ const result = atomicCommit
3108
+ ? await commitTodoStoreMutation({ repoRoot, argv, action, env })
3109
+ : await action(repoRoot);
2844
3110
  if (result !== null) stdout.write(`${JSON.stringify(result)}\n`);
2845
3111
  return 0;
2846
3112
  } catch (error) {
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { isCanonicalUtcTimestamp } from './timestamp-contract.mjs';
3
3
 
4
4
  export const TODO_EVENT_KINDS = Object.freeze([
5
- 'plan_genesis', 'start', 'block', 'unblock', 'done', 'reopen',
5
+ 'plan_genesis', 'start', 'start_retracted', 'block', 'unblock', 'done', 'reopen',
6
6
  'phase_review', 'phase_accept', 'phase_reject', 'phase_reopen',
7
7
  // ADR 0148: 監査していない歴史を「監査なしで閉じた」として明示的に閉じるための専用kind。
8
8
  // phase_review/accept/reject/reopenと同じv3 tail event shape(phase_id持ち)に収め、
@@ -13,10 +13,15 @@ export const TODO_EVENT_KINDS = Object.freeze([
13
13
  // 「誰が選んだか」の帰属を持つ——witnessが全planの暗黙義務だった時に帰属が無く、正確な
14
14
  // 案内が素通りされたことへの是正である(オーナー裁定C①)。
15
15
  'coordination_mode',
16
+ // 開発中に発見したplan跨ぎの依存を、active plan topologyの追記改変ではなく
17
+ // version-boundなplan-scoped eventとして接続する。推定はせず、AIが発見した時だけ積む。
18
+ 'cross_plan_dependency',
16
19
  ]);
17
20
 
18
21
  /** planへ帰属し、taskにもPhaseにも属さないevent kind。 */
19
- export const TODO_PLAN_SCOPED_EVENT_KINDS = Object.freeze(['coordination_mode']);
22
+ export const TODO_PLAN_SCOPED_EVENT_KINDS = Object.freeze([
23
+ 'coordination_mode', 'cross_plan_dependency',
24
+ ]);
20
25
 
21
26
  /** 調整方式。witness=独立性を宣言し検証して並列する/conversation=会話で調整する。 */
22
27
  export const TODO_COORDINATION_MODES = Object.freeze(['witness', 'conversation']);
@@ -506,7 +511,19 @@ function validPayload(event) {
506
511
  && TODO_COORDINATION_MODES.includes(payload.mode)
507
512
  && nullableText(payload.reason) && payload.reason !== null;
508
513
  }
514
+ if (event.kind === 'cross_plan_dependency') {
515
+ const dependencyRef = (value) => exactRecord(value, [
516
+ 'project_id', 'plan_key', 'task_id', 'expected_topology_digest',
517
+ ]) && isTodoIdentifier(value.project_id) && isTodoIdentifier(value.plan_key)
518
+ && isTodoIdentifier(value.task_id) && isTodoDigest(value.expected_topology_digest);
519
+ return exactRecord(payload, ['from', 'to', 'reason'])
520
+ && dependencyRef(payload.from) && dependencyRef(payload.to)
521
+ && nullableText(payload.reason) && payload.reason !== null;
522
+ }
509
523
  if (event.kind === 'start') return exactRecord(payload, ['override_reason']) && nullableText(payload.override_reason);
524
+ if (event.kind === 'start_retracted') return exactRecord(payload, ['reason', 'target_start_digest'])
525
+ && nullableText(payload.reason) && payload.reason !== null
526
+ && isTodoDigest(payload.target_start_digest);
510
527
  if (event.kind === 'block') return exactRecord(payload, ['reason']) && nullableText(payload.reason) && payload.reason !== null;
511
528
  if (event.kind === 'unblock') return exactRecord(payload, []);
512
529
  if (event.kind === 'done' && payload?.done_mode === 'authored') {
@@ -139,7 +139,8 @@ export function renderRightPane(
139
139
  ) {
140
140
  const lookup = presentationLookup(presentation);
141
141
  const sectionByKey = new Map(sections.map((section) => [refKey(section.ref), section]));
142
- const nodeByKey = new Map(layout.nodes.map((node) => [refKey(node.ref), node]));
142
+ const semanticNodes = [...layout.nodes, ...(layout.hierarchy_nodes ?? [])];
143
+ const nodeByKey = new Map(semanticNodes.map((node) => [refKey(node.ref), node]));
143
144
  const folds = foldIndex(layout);
144
145
  const incoming = new Map(sections.map((section) => [refKey(section.ref), []]));
145
146
  const outgoing = new Map(sections.map((section) => [refKey(section.ref), []]));
@@ -163,7 +164,7 @@ export function renderRightPane(
163
164
  const counts = { pending: 0, 'in-progress': 0, blocked: 0, done: 0 };
164
165
  for (const section of sections) counts[section.state.status] += 1;
165
166
  const active = sections.filter((section) => section.state.status === 'in-progress');
166
- const ready = layout.nodes.filter((node) => node.visibility.next_ready);
167
+ const ready = semanticNodes.filter((node) => node.visibility.next_ready);
167
168
  const independenceSummary = summarizeIndependence(layout);
168
169
  const readyHeadline = ready.length > 1
169
170
  ? `<p class="readiness-note"><strong>同時dispatch推奨:</strong> ${ready.length}工程。${escapeHtmlText(dispatchBasis(independenceSummary))}</p>`
@@ -90,8 +90,7 @@ export function presentationLookup(presentation) {
90
90
  }
91
91
 
92
92
  export function taskReference(section, lookup) {
93
- const number = lookup.taskNumbers.get(refKey(section.ref));
94
- return number === undefined ? `ID ${section.task.task_id}` : `工程 ${number.display_number}`;
93
+ return `工程 ${section.task.task_id}`;
95
94
  }
96
95
 
97
96
  export function renderRelationList(relations, sectionByKey, lookup, emptyText, folds = new Set()) {
@@ -116,3 +116,16 @@ button.fold-chip[aria-expanded="true"]{border-color:var(--text-primary)}
116
116
  .lane-dimmed{opacity:.35}
117
117
  @media(max-width:900px){body{display:block;height:auto}.shell{display:block}.pane-divider{display:none}.gantt-pane,.narrative-pane{height:70vh}.gantt-pane{border-bottom:1px solid var(--border)}}
118
118
  `;
119
+
120
+ // 階層を持つplanだけが読み込む。親無しplanのHTML/CSS bytesを変えないため、基底CSSへは混ぜない。
121
+ export const NESTED_CSS = `
122
+ .nested-task-panel{filter:drop-shadow(0 4px 12px rgba(11,11,11,.18))}
123
+ .nested-task-surface{fill:var(--surface-1);stroke:var(--text-secondary);stroke-width:1.5}
124
+ .nested-task-label{fill:var(--text-primary);font-size:12px;font-weight:650}
125
+ .nested-task-link{fill:none;stroke:var(--text-secondary);stroke-width:1.5;stroke-dasharray:4 3}
126
+ .nested-task-diagram{outline:1px solid var(--border);background:var(--surface-1)}
127
+ .nested-task-toggle{cursor:pointer}
128
+ .nested-task-toggle rect{fill:var(--surface-1);stroke:var(--text-secondary);stroke-width:1.5}
129
+ .nested-task-toggle text{fill:var(--text-primary);font-size:14px;font-weight:650}
130
+ .nested-task-toggle:focus rect{stroke:var(--text-primary);stroke-width:2.5}
131
+ `;
@@ -5,7 +5,7 @@ import { serializeJsonForScript } from './todo-markdown-renderer.mjs';
5
5
  import { renderTodoGanttSvg, TODO_GANTT_STATUS_PRESENTATION } from './todo-gantt-svg.mjs';
6
6
  import { renderDiagramLegend, renderRightPane } from './todo-gantt-html-independence.mjs';
7
7
  import { escapeHtmlAttribute, escapeHtmlText, refKey } from './todo-gantt-html-shared.mjs';
8
- import { CSS } from './todo-gantt-html-style.mjs';
8
+ import { CSS, NESTED_CSS } from './todo-gantt-html-style.mjs';
9
9
 
10
10
  export const TODO_GANTT_RENDERER_VERSION = 'lattice.todo_gantt_renderer.v19';
11
11
  export const TODO_GANTT_PROSE_MAX_BYTES = 8 * 1024 * 1024;
@@ -188,6 +188,18 @@ const CONTROLLER = `
188
188
  })();
189
189
  `;
190
190
 
191
+ const NESTED_CONTROLLER = `
192
+ (()=>{
193
+ const root=document.querySelector('[data-gantt-root]');if(!root)return;
194
+ const toggles=[...root.querySelectorAll('[data-nested-toggle-for]')];
195
+ const panels=[...root.querySelectorAll('[data-nested-panel-for]')];
196
+ const panelFor=(key)=>panels.find(panel=>panel.dataset.nestedPanelFor===key);
197
+ const toggle=(control)=>{const key=control.dataset.nestedToggleFor;const panel=panelFor(key);if(!panel)return;const open=panel.hasAttribute('hidden');panel.toggleAttribute('hidden',!open);const link=[...root.querySelectorAll('[data-nested-link-for]')].find(candidate=>candidate.dataset.nestedLinkFor===key);link?.toggleAttribute('hidden',!open);control.setAttribute('aria-expanded',String(open));const mark=control.querySelector('text');if(mark)mark.textContent=open?'−':'+';};
198
+ root.addEventListener('click',event=>{const control=event.target.closest('[data-nested-toggle-for]');if(!control||!root.contains(control))return;event.preventDefault();event.stopPropagation();toggle(control);});
199
+ root.addEventListener('keydown',event=>{const control=event.target.closest('[data-nested-toggle-for]');if(!control||!root.contains(control)||(event.key!=='Enter'&&event.key!==' '))return;event.preventDefault();event.stopPropagation();toggle(control);});
200
+ })();
201
+ `;
202
+
191
203
  export function renderTodoGanttHtml({
192
204
  readModel, layout, narratives = [], anchorOutcomes = [], presentation = null, metadata = {},
193
205
  expandedLayout = null, noteContexts = null, noteWarnings = [],
@@ -208,6 +220,7 @@ export function renderTodoGanttHtml({
208
220
  }
209
221
  const normalized = normalizeSections(readModel, narratives, anchorOutcomes, noteContexts);
210
222
  const displayName = projectDisplayName(readModel, metadata);
223
+ const hasHierarchy = layout?.hierarchy?.schema === 'lattice.todo_gantt_hierarchy.v1';
211
224
  const svg = renderTodoGanttSvg(layout, { presentation });
212
225
  // The expanded diagram travels with the page so the badge can bring the
213
226
  // history back without a round trip. A file:// artifact has nowhere to ask.
@@ -224,7 +237,7 @@ export function renderTodoGanttHtml({
224
237
  metadata,
225
238
  presentation,
226
239
  });
227
- const html = `<!doctype html><html lang="ja"><head><meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Lattice — ${escapeHtmlText(displayName)} 依存工程図</title><style>${CSS}</style></head><body data-gantt-root data-view-state="overview"><main class="shell"><section class="gantt-pane" aria-label="${escapeHtmlAttribute(displayName)} 依存工程図"><div class="diagram-toolbar" role="group" aria-label="図のズーム"><strong class="project-heading">${escapeHtmlText(displayName)} 依存工程図</strong>${renderAuditPendingChip(readModel)}<button type="button" data-zoom-action="out" aria-label="縮小">−</button><button type="button" data-zoom-action="reset">等倍</button><button type="button" data-zoom-action="in" aria-label="拡大">+</button><button type="button" data-zoom-action="fit">全体表示</button><output class="zoom-readout" data-zoom-output aria-live="polite">100%</output><span class="diagram-note">縦=依存段階(時間ではない)</span></div>${renderDiagramLegend(presentation, layout, expandedSvg !== '')}<div class="diagram-scroll" data-diagram-scroll tabindex="0" aria-label="縦方向を主にスクロール可能な依存工程図">${diagrams}</div></section><div class="pane-divider" data-pane-divider aria-hidden="true"></div><aside class="narrative-pane" aria-label="選択工程の詳細と全工程一覧">${rightPane}</aside></main><script type="application/json" id="todo-gantt-data">${staticData}</script><script>${CONTROLLER}</script></body></html>`;
240
+ const html = `<!doctype html><html lang="ja"><head><meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Lattice — ${escapeHtmlText(displayName)} 依存工程図</title><style>${CSS}${hasHierarchy ? NESTED_CSS : ''}</style></head><body data-gantt-root data-view-state="overview"><main class="shell"><section class="gantt-pane" aria-label="${escapeHtmlAttribute(displayName)} 依存工程図"><div class="diagram-toolbar" role="group" aria-label="図のズーム"><strong class="project-heading">${escapeHtmlText(displayName)} 依存工程図</strong>${renderAuditPendingChip(readModel)}<button type="button" data-zoom-action="out" aria-label="縮小">−</button><button type="button" data-zoom-action="reset">等倍</button><button type="button" data-zoom-action="in" aria-label="拡大">+</button><button type="button" data-zoom-action="fit">全体表示</button><output class="zoom-readout" data-zoom-output aria-live="polite">100%</output><span class="diagram-note">縦=依存段階(時間ではない)</span></div>${renderDiagramLegend(presentation, layout, expandedSvg !== '')}<div class="diagram-scroll" data-diagram-scroll tabindex="0" aria-label="縦方向を主にスクロール可能な依存工程図">${diagrams}</div></section><div class="pane-divider" data-pane-divider aria-hidden="true"></div><aside class="narrative-pane" aria-label="選択工程の詳細と全工程一覧">${rightPane}</aside></main><script type="application/json" id="todo-gantt-data">${staticData}</script><script>${CONTROLLER}${hasHierarchy ? NESTED_CONTROLLER : ''}</script></body></html>`;
228
241
  const htmlBytes = Buffer.byteLength(html, 'utf8');
229
242
  if (htmlBytes > TODO_GANTT_HTML_MAX_BYTES) {
230
243
  throw new TodoGanttRenderError('TODO_SCALE_EXCEEDED', 'todo gantt HTML limit exceeded', {