@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 +1 -1
- package/src/todo-gantt-html.mjs +18 -52
package/package.json
CHANGED
package/src/todo-gantt-html.mjs
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
|
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) {
|