@quolu/lattice 0.12.19 → 0.12.20

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.12.19",
3
+ "version": "0.12.20",
4
4
  "description": "Lattice — phase-aware TODO graph compiler and conflict-aware orchestration runtime",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -3,7 +3,7 @@ import { createHash } from 'node:crypto';
3
3
  import { serializeJsonForScript } from './todo-markdown-renderer.mjs';
4
4
  import { renderTodoGanttSvg, TODO_GANTT_STATUS_PRESENTATION } from './todo-gantt-svg.mjs';
5
5
 
6
- export const TODO_GANTT_RENDERER_VERSION = 'lattice.todo_gantt_renderer.v15';
6
+ export const TODO_GANTT_RENDERER_VERSION = 'lattice.todo_gantt_renderer.v16';
7
7
  export const TODO_GANTT_PROSE_MAX_BYTES = 8 * 1024 * 1024;
8
8
  export const TODO_GANTT_HTML_MAX_BYTES = 24 * 1024 * 1024;
9
9
 
@@ -43,6 +43,10 @@ function refKey(ref) {
43
43
  return JSON.stringify([ref.project_id, ref.plan_key, ref.task_id]);
44
44
  }
45
45
 
46
+ function compareText(left, right) {
47
+ return left < right ? -1 : left > right ? 1 : 0;
48
+ }
49
+
46
50
  function digest(value) {
47
51
  return createHash('sha256').update(value, 'utf8').digest('hex');
48
52
  }
@@ -108,16 +112,37 @@ function renderTaskIndexEntry(section, lookup) {
108
112
  return `<li><button type="button" data-select-node-key="${escapeHtmlAttribute(selectKey)}"><span class="task-index-status status-${escapeHtmlAttribute(section.state.status)}" role="img" aria-label="${escapeHtmlAttribute(status.label)}">${escapeHtmlText(status.mark)}</span><span class="task-index-reference">${escapeHtmlText(taskReference(section, lookup))}</span><strong>${escapeHtmlText(section.task.title)}</strong>${blockedReason}</button></li>`;
109
113
  }
110
114
 
111
- function renderTaskIndex(sections, lookup, folds = new Set()) {
112
- const plans = [];
115
+ /** plan_key -> 最終活動時刻。journalの末尾eventが、そのplanが最後に動いた時点。 */
116
+ function planActivity(readModel) {
117
+ return new Map((readModel?.members ?? []).map((member) => {
118
+ const events = member.journal?.events ?? [];
119
+ const last = events.at(-1)?.recorded_at ?? events[0]?.recorded_at ?? '';
120
+ return [member.plan.plan_key, last];
121
+ }));
122
+ }
123
+
124
+ function renderTaskIndex(sections, lookup, folds = new Set(), activity = new Map()) {
125
+ const byPlan = new Map();
113
126
  for (const section of sections) {
114
- let plan = plans.at(-1);
115
- if (plan === undefined || plan.planKey !== section.ref.plan_key) {
116
- plan = { planKey: section.ref.plan_key, tasks: [] };
117
- plans.push(plan);
118
- }
119
- plan.tasks.push(section);
127
+ if (!byPlan.has(section.ref.plan_key)) byPlan.set(section.ref.plan_key, []);
128
+ byPlan.get(section.ref.plan_key).push(section);
120
129
  }
130
+ // 全ToDoが図から外れたplanは終わった仕事。読む側が先に見たいのは動いているplanなので、
131
+ // 動いているものを最終活動の新しい順で上へ、終わったものを古い順で下へまとめる。
132
+ // plan内のToDo順は登録順のまま触らない。
133
+ const plans = [...byPlan.entries()].map(([planKey, tasks]) => ({
134
+ planKey,
135
+ tasks,
136
+ settled: tasks.every((section) => folds.has(refKey(section.ref))),
137
+ lastActivity: activity.get(planKey) ?? '',
138
+ }));
139
+ plans.sort((left, right) => {
140
+ if (left.settled !== right.settled) return left.settled ? 1 : -1;
141
+ const order = left.settled
142
+ ? compareText(left.lastActivity, right.lastActivity)
143
+ : compareText(right.lastActivity, left.lastActivity);
144
+ return order !== 0 ? order : compareText(left.planKey, right.planKey);
145
+ });
121
146
  return plans.map((plan) => {
122
147
  const drawn = plan.tasks.filter((section) => !folds.has(refKey(section.ref)));
123
148
  const folded = plan.tasks.filter((section) => folds.has(refKey(section.ref)));
@@ -247,8 +272,8 @@ function renderRightPane(sections, layout, presentation, readModel) {
247
272
  : '<p class="fold-note">完走済みのため図には描いていません。図に出すには <code>lattice todo gantt --scope all</code> を実行してください。</p>';
248
273
  return `<article class="task-detail" data-detail-key="${escapeHtmlAttribute(key)}" hidden><header><span class="detail-status status-${escapeHtmlAttribute(section.state.status)}">${escapeHtmlText(status.mark)} ${escapeHtmlText(status.label)}</span><span class="detail-reference">${escapeHtmlText(taskReference(section, lookup))}</span></header><h1>${escapeHtmlText(section.task.title)}</h1><p class="detail-category"><strong>カテゴリ:</strong> ${escapeHtmlText(category)}</p>${categoryDescription}<p><strong>正規ID:</strong> <code>${escapeHtmlText(`${section.ref.plan_key}/${section.task.task_id}`)}</code></p>${blockedReason}${readiness}${foldedNote}<section><h2>前提工程</h2>${renderRelationList(incoming.get(key), sectionByKey, lookup, '登録済みの前提工程はありません。', folds)}</section><section><h2>後続工程</h2>${renderRelationList(outgoing.get(key), sectionByKey, lookup, '登録済みの後続工程はありません。', folds)}</section><p class="anchor-status">${escapeHtmlText(anchorText)}</p><details class="task-diagnostics"><summary>開発者向け診断</summary><dl><dt>canonical ref</dt><dd><code>${escapeHtmlText(`${section.ref.project_id}/${section.ref.plan_key}/${section.task.task_id}`)}</code></dd><dt>anchor</dt><dd>${escapeHtmlText(section.anchorOutcome.anchored ? 'verified' : section.anchorOutcome.reason)}</dd></dl></details></article>`;
249
274
  }).join('');
250
- const taskIndex = renderTaskIndex(sections, lookup, folds);
251
- return `<div class="right-toolbar"><button type="button" data-show-overview>概要</button><button type="button" data-show-selected hidden>選択工程へ戻る</button><button type="button" data-show-task-index>全工程一覧</button></div><div class="right-content">${overview}<div data-right-panel="details" hidden>${details}</div><section class="task-index" data-right-panel="task-index" hidden><h1>全工程</h1><p>Latticeに登録された全工程を、現在の状態とともに登録順で表示しています。</p>${taskIndex}</section></div>`;
275
+ const taskIndex = renderTaskIndex(sections, lookup, folds, planActivity(readModel));
276
+ return `<div class="right-toolbar"><button type="button" data-show-overview>概要</button><button type="button" data-show-selected hidden>選択工程へ戻る</button><button type="button" data-show-task-index>全工程一覧</button></div><div class="right-content">${overview}<div data-right-panel="details" hidden>${details}</div><section class="task-index" data-right-panel="task-index" hidden><h1>全工程</h1><p>Latticeに登録された全工程を現在の状態とともに表示しています。planは動いているものを最終活動の新しい順で上に、完走したものを古い順で下にまとめ、plan内は登録順です。</p>${taskIndex}</section></div>`;
252
277
  }
253
278
 
254
279
  function renderDiagramLegend(presentation, layout = null, expandable = false) {