@quolu/lattice 0.12.16 → 0.12.18

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/README.md CHANGED
@@ -89,8 +89,16 @@ dashboardはmanifestのfile identityが変わらない間のstable store readを
89
89
  長時間の外部処理中にCLI呼出しが途切れても進行中projectを休眠扱いしません。
90
90
  LANや外部reverse proxyから閲覧するoptional bridgeは既定で無効です。明示したIPにだけbindする初回設定、
91
91
  再設定、停止方法は[bridge setup](docs/bridge-setup.md)を参照してください。
92
+ 工程図の既定表示は、後続に作業中・未着手が残っていない完了工程を図から除きます。まとめnodeも置かないため、
93
+ 完走したplanは図の場所を取りません。除いた工程は凡例の件数、右ペインの「全工程」一覧、各工程の詳細から
94
+ 辿れ、詳細の前提・後続は除外前の依存関係を示します。総数・進捗・最長依存鎖は除外前の全工程で数えます。
95
+ 凡例の件数バッジを押すと全工程を描いた図へ切り替わり、`lattice todo gantt --scope all`は最初から全件を
96
+ 描きます。表示規約は[ADR 0066](docs/adr/0066-gantt-live-scope-drops-finished-work.md)が正です。
97
+
92
98
  静的工程表は`lattice todo gantt status`で`current / stale / missing`を確認でき、HTMLまたは
93
99
  digest付きsidecarの欠落・改ざんはtyped failureになります。
100
+ dashboard daemonは起動時に読み込んだ版数をhealthで名乗り、installされた版と食い違えば`lattice status`の
101
+ たびに新版daemonへ置き換わります。publishしただけで配信面が古いまま残ることはありません。
94
102
  状態を書き込む`start / block / unblock / done / evidence promote / reopen / revise / revise-phase / revise-set`
95
103
  では、監査actorとして次の3環境変数をすべて設定してください。
96
104
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quolu/lattice",
3
- "version": "0.12.16",
3
+ "version": "0.12.18",
4
4
  "description": "Lattice — phase-aware TODO graph compiler and conflict-aware orchestration runtime",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/cli-help.mjs CHANGED
@@ -45,7 +45,7 @@ Read commands:
45
45
  status [--json]
46
46
  verify [--plan <key>] [--json]
47
47
  snapshot --rebuild --plan <key>
48
- gantt [--out <file>] [--scope live|all] # 既定live: 完走した枝を畳む
48
+ gantt [--out <file>] [--scope live|all] # 既定live: 完走した工程を図から除く(一覧には残る)
49
49
  gantt status [--out <file>]
50
50
  gantt serve --port <port> [--scope live|all] # /projects/<project_id>/ をforeground配信
51
51
  phase status --plan <key>
@@ -6,7 +6,7 @@ import {
6
6
  } from './todo-markdown-renderer.mjs';
7
7
  import { renderTodoGanttSvg, TODO_GANTT_STATUS_PRESENTATION } from './todo-gantt-svg.mjs';
8
8
 
9
- export const TODO_GANTT_RENDERER_VERSION = 'lattice.todo_gantt_renderer.v13';
9
+ export const TODO_GANTT_RENDERER_VERSION = 'lattice.todo_gantt_renderer.v14';
10
10
  export const TODO_GANTT_PROSE_MAX_BYTES = 8 * 1024 * 1024;
11
11
  export const TODO_GANTT_HTML_MAX_BYTES = 24 * 1024 * 1024;
12
12
 
@@ -4,6 +4,9 @@ const TASK_LIMIT = 2_000;
4
4
  const EDGE_LIMIT = 8_000;
5
5
  const SWEEP_ROUNDS = 4;
6
6
 
7
+ /** Columns for a stage made only of ToDos with no registered dependency. */
8
+ const LOOSE_COLUMNS_MINIMUM = 8;
9
+
7
10
  const GEOMETRY = Object.freeze({
8
11
  left: 16,
9
12
  top: 16,
@@ -11,6 +14,7 @@ const GEOMETRY = Object.freeze({
11
14
  lane_gap: 296,
12
15
  node_width: 272,
13
16
  node_height: 68,
17
+ stage_row_gap: 12,
14
18
  route_inset: 8,
15
19
  route_spacing: 12,
16
20
  });
@@ -410,9 +414,27 @@ export function layoutTodoGantt(readModel, chainProjection, options = {}) {
410
414
  const nodesByKey = new Map(nodes.map((node) => [node.key, node]));
411
415
  const { incoming, outgoing, wave } = assignWaves(nodes, nodesByKey, edges);
412
416
  const layers = orderLayers(nodes, wave, incoming, outgoing);
417
+ // A ToDo with no registered dependency has nothing to line up with. Laying
418
+ // every one of them out in a single row makes the canvas as wide as the plan
419
+ // is old — real stores have hundreds — so they wrap into a block underneath
420
+ // the wired ToDos of their stage. Nodes that carry edges keep the top row:
421
+ // edge routing assumes both endpoints of a stage share one baseline.
422
+ const wired = new Set();
423
+ for (const edge of edges) { wired.add(edge.from); wired.add(edge.to); }
413
424
  const transversePosition = new Map();
425
+ const stageRow = new Map();
426
+ const stageRowCount = [];
414
427
  for (const layer of layers) {
415
- for (let index = 0; index < layer.length; index += 1) transversePosition.set(layer[index], index);
428
+ const connected = layer.filter((key) => wired.has(key));
429
+ const loose = layer.filter((key) => !wired.has(key));
430
+ connected.forEach((key, index) => { transversePosition.set(key, index); stageRow.set(key, 0); });
431
+ const columns = Math.max(LOOSE_COLUMNS_MINIMUM, connected.length);
432
+ const firstLooseRow = connected.length === 0 ? 0 : 1;
433
+ loose.forEach((key, index) => {
434
+ transversePosition.set(key, index % columns);
435
+ stageRow.set(key, firstLooseRow + Math.floor(index / columns));
436
+ });
437
+ stageRowCount.push(Math.max(0, ...layer.map((key) => stageRow.get(key))) + 1);
416
438
  }
417
439
  const visibleKeys = new Set(nodes.map(({ key }) => key));
418
440
  const displayBranches = edges.flatMap((edge, semanticIndex) => {
@@ -458,15 +480,25 @@ export function layoutTodoGantt(readModel, chainProjection, options = {}) {
458
480
  const nodeWidth = Math.max(GEOMETRY.node_width,
459
481
  GEOMETRY.route_inset * 2 + (maximumPortTraffic + 1) * GEOMETRY.route_spacing);
460
482
  const laneGap = nodeWidth + 24;
461
- const waveGap = GEOMETRY.node_height + Math.max(36,
462
- (maximumGapOccupancy + 1) * GEOMETRY.route_spacing);
483
+ // Each routing band is sized for the edges that actually cross it. One
484
+ // crowded band used to set the spacing for every stage in the diagram, so a
485
+ // long plan paid the width of its busiest junction on all of its stages.
486
+ const gapBand = (gap) => Math.max(36, ((gapGroups.get(gap)?.length ?? 0)
487
+ + (connectorGroupKeysByGap.get(gap)?.size ?? 0) + 1) * GEOMETRY.route_spacing);
488
+ const stageHeight = (index) => stageRowCount[index] * GEOMETRY.node_height
489
+ + (stageRowCount[index] - 1) * GEOMETRY.stage_row_gap;
490
+ const stageTop = [GEOMETRY.top];
491
+ for (let index = 0; index + 1 < layers.length; index += 1) {
492
+ stageTop.push(stageTop[index] + stageHeight(index) + gapBand(index));
493
+ }
463
494
 
464
495
  const coordinates = new Map();
465
496
  const projectedNodes = nodes.map((node) => {
466
497
  const visible = visibleKeys.has(node.key);
467
498
  const geometry = visible ? {
468
499
  x: GEOMETRY.left + transversePosition.get(node.key) * laneGap,
469
- y: GEOMETRY.top + wave.get(node.key) * waveGap,
500
+ y: stageTop[wave.get(node.key)]
501
+ + stageRow.get(node.key) * (GEOMETRY.node_height + GEOMETRY.stage_row_gap),
470
502
  width: nodeWidth,
471
503
  height: GEOMETRY.node_height,
472
504
  } : null;
@@ -486,10 +518,11 @@ export function layoutTodoGantt(readModel, chainProjection, options = {}) {
486
518
  const gapPosition = (edgeKey, waveIndex) => {
487
519
  const keys = gapGroups.get(waveIndex);
488
520
  const index = keys.indexOf(edgeKey);
489
- return GEOMETRY.top + waveIndex * waveGap + GEOMETRY.node_height
521
+ return stageTop[waveIndex] + stageHeight(waveIndex)
490
522
  + GEOMETRY.route_spacing * (index + 1);
491
523
  };
492
- const maximumLayerWidth = Math.max(...layers.map((layer) => layer.length));
524
+ const maximumLayerWidth = Math.max(1,
525
+ ...[...transversePosition.values()].map((position) => position + 1));
493
526
  const routeRight = GEOMETRY.left + (maximumLayerWidth - 1) * laneGap + nodeWidth + 12;
494
527
  const branchCounts = new Map();
495
528
  for (const branch of displayBranches) {
@@ -551,7 +584,7 @@ export function layoutTodoGantt(readModel, chainProjection, options = {}) {
551
584
  connectorGapRanks.set(targetGap, groupRank + 1);
552
585
  const primaryTargetPortX = group[0].route.at(-1)[0];
553
586
  const junction = [primaryTargetPortX,
554
- GEOMETRY.top + targetGap * waveGap + GEOMETRY.node_height
587
+ stageTop[targetGap] + stageHeight(targetGap)
555
588
  + GEOMETRY.route_spacing * ((gapGroups.get(targetGap)?.length ?? 0) + groupRank + 1)];
556
589
  const contacts = [];
557
590
  for (const [index, edge] of group.entries()) {
@@ -602,8 +635,8 @@ export function layoutTodoGantt(readModel, chainProjection, options = {}) {
602
635
  sweep: { method: 'stable_median', rounds: SWEEP_ROUNDS, tie_break: 'previous_position_then_task_ref' },
603
636
  bounds: {
604
637
  width: nodes.length === 0 ? 0 : routeMaximumX + GEOMETRY.left,
605
- height: nodes.length === 0 ? 0 : GEOMETRY.top * 2
606
- + (layers.length - 1) * waveGap + GEOMETRY.node_height,
638
+ height: nodes.length === 0 ? 0
639
+ : GEOMETRY.top + stageTop.at(-1) + stageHeight(layers.length - 1),
607
640
  wave_count: layers.length,
608
641
  },
609
642
  nodes: projectedNodes,