@quolu/lattice 0.12.18 → 0.12.19

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.18",
3
+ "version": "0.12.19",
4
4
  "description": "Lattice — phase-aware TODO graph compiler and conflict-aware orchestration runtime",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,12 +1,9 @@
1
1
  import { createHash } from 'node:crypto';
2
2
 
3
- import {
4
- renderTodoMarkdownDocument,
5
- serializeJsonForScript,
6
- } from './todo-markdown-renderer.mjs';
3
+ import { serializeJsonForScript } from './todo-markdown-renderer.mjs';
7
4
  import { renderTodoGanttSvg, TODO_GANTT_STATUS_PRESENTATION } from './todo-gantt-svg.mjs';
8
5
 
9
- export const TODO_GANTT_RENDERER_VERSION = 'lattice.todo_gantt_renderer.v14';
6
+ export const TODO_GANTT_RENDERER_VERSION = 'lattice.todo_gantt_renderer.v15';
10
7
  export const TODO_GANTT_PROSE_MAX_BYTES = 8 * 1024 * 1024;
11
8
  export const TODO_GANTT_HTML_MAX_BYTES = 24 * 1024 * 1024;
12
9
 
@@ -50,18 +47,21 @@ function digest(value) {
50
47
  return createHash('sha256').update(value, 'utf8').digest('hex');
51
48
  }
52
49
 
53
- // The Markdown renderer deliberately permits safe web links for its standalone API.
54
- // A self-contained gantt permits fragments only, so its known allow-list output is de-linked here.
55
- function removeNavigation(markup) {
56
- return markup.replace(/<a href="[^"]*"(?: title="[^"]*")?>/gu, '<span class="deactivated-link">')
57
- .replaceAll('</a>', '</span>');
58
- }
59
-
50
+ /**
51
+ * Sections carry one row per ToDo, plus the prose accounting for the narratives
52
+ * behind them.
53
+ *
54
+ * The right pane shows the store, not the Markdown: per the 2026-07-19 ruling
55
+ * the "全工程" list is the plan rendered from the ToDo store, and the original
56
+ * documents are reached through each ToDo's source reference. The narratives are
57
+ * still read, because anchor verification needs them, and their size is still
58
+ * bounded — but they are not rendered into the page.
59
+ */
60
60
  function normalizeSections(readModel, narratives, anchorOutcomes) {
61
61
  const supplied = new Map(narratives.map((entry) => [refKey(entry.ref), entry]));
62
62
  const outcomes = new Map(anchorOutcomes.map((entry) => [refKey(entry.ref), entry]));
63
63
  const result = [];
64
- const documents = new Map();
64
+ const counted = new Set();
65
65
  let proseBytes = 0;
66
66
  for (const member of readModel.members) {
67
67
  const states = new Map(member.tasks.map((state) => [state.task_id, state]));
@@ -70,57 +70,23 @@ function normalizeSections(readModel, narratives, anchorOutcomes) {
70
70
  const narrative = supplied.get(refKey(ref));
71
71
  const markdown = narrative?.markdown ?? '';
72
72
  const narrativeRef = narrative?.narrative_ref ?? task.narrative_ref;
73
+ // ToDos that share one narrative document count its bytes once.
73
74
  const documentKey = narrativeRef === null
74
75
  ? refKey(ref) : JSON.stringify([member.plan.plan_key, narrativeRef, digest(markdown)]);
75
- let document = documents.get(documentKey);
76
- if (document === undefined) {
76
+ if (!counted.has(documentKey)) {
77
+ counted.add(documentKey);
77
78
  proseBytes += Buffer.byteLength(markdown, 'utf8');
78
79
  if (proseBytes > TODO_GANTT_PROSE_MAX_BYTES) {
79
80
  throw new TodoGanttRenderError('TODO_SCALE_EXCEEDED', 'todo gantt embedded prose limit exceeded', {
80
81
  prose_bytes: proseBytes, prose_limit: TODO_GANTT_PROSE_MAX_BYTES,
81
82
  });
82
83
  }
83
- document = { markdown, narrativeRef, tasks: [], rendered: '' };
84
- documents.set(documentKey, document);
85
84
  }
86
85
  const anchorOutcome = outcomes.get(refKey(ref)) ?? {
87
86
  ref, narrative_ref: narrativeRef, anchored: false, reason: 'anchor_missing',
88
87
  origin_line: task.narrative_anchor?.origin_line ?? null,
89
88
  };
90
- const section = {
91
- ref, task, state: states.get(task.task_id), documentKey, narrativeRef, anchorOutcome, document,
92
- };
93
- document.tasks.push(section);
94
- result.push(section);
95
- }
96
- }
97
- for (const document of documents.values()) {
98
- const taskStatesByLine = new Map();
99
- for (const section of document.tasks) {
100
- if (!section.anchorOutcome.anchored) continue;
101
- const status = DOCUMENT_STATUS[section.state.status] ?? { mark: '?', label: '状態不明' };
102
- taskStatesByLine.set(section.anchorOutcome.origin_line, {
103
- status: section.state.status,
104
- mark: status.mark,
105
- label: status.label,
106
- narrativeKey: refKey(section.ref),
107
- blockedReason: section.state.blocked_reason,
108
- });
109
- }
110
- try {
111
- document.rendered = removeNavigation(renderTodoMarkdownDocument(
112
- document.markdown,
113
- { taskStatesByLine },
114
- ).html);
115
- } catch (error) {
116
- if (error?.code === 'TODO_MARKDOWN_SECTION_TOO_LARGE') {
117
- throw new TodoGanttRenderError('TODO_SCALE_EXCEEDED', 'todo gantt narrative section limit exceeded', {
118
- narrative_ref: document.narrativeRef,
119
- prose_section_bytes: error.detail.actual_bytes,
120
- prose_section_limit: error.detail.maximum_bytes,
121
- });
122
- }
123
- throw error;
89
+ result.push({ ref, task, state: states.get(task.task_id), narrativeRef, anchorOutcome });
124
90
  }
125
91
  }
126
92
  return { sections: result, proseBytes };
@@ -282,7 +248,7 @@ function renderRightPane(sections, layout, presentation, readModel) {
282
248
  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>`;
283
249
  }).join('');
284
250
  const taskIndex = renderTaskIndex(sections, lookup, folds);
285
- 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>元Markdown全文</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>`;
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>`;
286
252
  }
287
253
 
288
254
  function renderDiagramLegend(presentation, layout = null, expandable = false) {