@quolu/lattice 0.41.1 → 0.42.1
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.ja.md +3 -2
- package/README.md +5 -3
- package/bin/lattice-dashboard.mjs +2 -2
- package/package.json +1 -1
- package/src/todo-cli.mjs +7 -2
- package/src/todo-gantt-html-independence.mjs +55 -13
- package/src/todo-gantt-html-shared.mjs +4 -1
- package/src/todo-gantt-html-style.mjs +1 -0
- package/src/todo-gantt-html.mjs +6 -0
package/README.ja.md
CHANGED
|
@@ -61,8 +61,9 @@ lattice todo note --plan <key> --task <id> --message "既存parserを使い、fa
|
|
|
61
61
|
lattice todo show --plan <key> --task <id> --json
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
動的工程表は、選択したToDoの右ペインへ初期設計メモと追記noteの本文を表示します。あわせて前提工程・
|
|
65
|
+
後続工程それぞれの状態(未着手/作業中/完了/ブロック中)と、その工程の並列可否を示します。
|
|
66
|
+
note本文を落とすのはrepo外へHTMLを出す公開配信面だけです。project別HTMLの生成や再生成は不要です。
|
|
66
67
|
|
|
67
68
|
### 変換の受入五条件
|
|
68
69
|
|
package/README.md
CHANGED
|
@@ -74,9 +74,11 @@ lattice todo note --plan <key> --task <id> --message "Use the existing parser; d
|
|
|
74
74
|
lattice todo show --plan <key> --task <id> --json
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
The
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
The selected ToDo's detail pane shows the initial design memo and the append-only note bodies, plus the
|
|
78
|
+
status of each prerequisite and dependent ToDo (pending, in progress, done, blocked) and whether the ToDo
|
|
79
|
+
can run in parallel. Only a public distribution surface — HTML served outside the repository — drops note
|
|
80
|
+
bodies. Static per-project HTML is not generated; `lattice todo gantt serve` and the shared dashboard
|
|
81
|
+
read the store dynamically.
|
|
80
82
|
|
|
81
83
|
### The five acceptance conditions
|
|
82
84
|
|
|
@@ -5,7 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
|
|
6
6
|
import { readTodoStoreStable } from '../src/todo-store.mjs';
|
|
7
7
|
import { projectTodoStatus } from '../src/todo-status.mjs';
|
|
8
|
-
import { ganttLiveHeadDigest,
|
|
8
|
+
import { ganttLiveHeadDigest, renderTodoGanttForProject } from '../src/todo-cli.mjs';
|
|
9
9
|
import {
|
|
10
10
|
readVisibleTodoDashboardProjects,
|
|
11
11
|
todoDashboardMemberNeedsVisibility,
|
|
@@ -90,7 +90,7 @@ async function synchronize() {
|
|
|
90
90
|
displayName: entry.display_name,
|
|
91
91
|
render: async ({ displayName }) => {
|
|
92
92
|
const store = await readCachedStore(entry.repo_root);
|
|
93
|
-
const result = await
|
|
93
|
+
const result = await renderTodoGanttForProject({ repoRoot: entry.repo_root,
|
|
94
94
|
stable: true, displayName, readModel: store });
|
|
95
95
|
return {
|
|
96
96
|
html: result.rendered.html,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quolu/lattice",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.1",
|
|
4
4
|
"description": "Schedulability compiler for multi-agent development: observe real code boundaries, refactor the conflicting seam, recompile the plan for parallel execution",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Quo / クオ at kitepon.dev",
|
package/src/todo-cli.mjs
CHANGED
|
@@ -2292,7 +2292,12 @@ export async function renderTodoGanttForProject({
|
|
|
2292
2292
|
return { store, metadata, memberBindings, rendered };
|
|
2293
2293
|
}
|
|
2294
2294
|
|
|
2295
|
-
/**
|
|
2295
|
+
/**
|
|
2296
|
+
* 公開配信面はToDo本体の設計メモを表示し、append-only作業記録は除外する。
|
|
2297
|
+
*
|
|
2298
|
+
* loopbackのdashboardと`gantt serve`は公開配信面ではない。作業者本人が読む面なので
|
|
2299
|
+
* 記録込みで描く。この入口はrepo外へHTMLを出す面が生えた時に使う。
|
|
2300
|
+
*/
|
|
2296
2301
|
export async function renderPublicTodoGanttForProject(options = {}) {
|
|
2297
2302
|
return renderTodoGanttForProject({ ...options, includeNotes: false });
|
|
2298
2303
|
}
|
|
@@ -2306,7 +2311,7 @@ async function serveGantt({ repoRoot, port, stdout, env, scope = DEFAULT_GANTT_S
|
|
|
2306
2311
|
port,
|
|
2307
2312
|
render: async () => {
|
|
2308
2313
|
const store = await readTodoStoreStable({ repoRoot });
|
|
2309
|
-
const { rendered } = await
|
|
2314
|
+
const { rendered } = await renderTodoGanttForProject({
|
|
2310
2315
|
repoRoot, stable: true, displayName: identity.displayName, scope, readModel: store,
|
|
2311
2316
|
});
|
|
2312
2317
|
return { html: rendered.html, head_digest: await ganttLiveHeadDigest({ repoRoot, store }) };
|
|
@@ -14,7 +14,9 @@ function renderNoteContext(context) {
|
|
|
14
14
|
: '<p class="note-warning">安全上表示できない要素を除外しました。</p>';
|
|
15
15
|
const correction = note.correction_state === 'superseded'
|
|
16
16
|
? `訂正済み(後継 ${note.superseded_by.slice(0, 12)}…)` : '現行';
|
|
17
|
-
|
|
17
|
+
// UTCのまま埋め、閲覧者の現地時刻への変換はブラウザ側へ委ねる(title属性に原文を残す)。
|
|
18
|
+
const stamp = `<time datetime="${escapeHtmlAttribute(note.recorded_at)}" title="${escapeHtmlAttribute(note.recorded_at)}" data-utc-stamp>${escapeHtmlText(note.recorded_at)}</time>`;
|
|
19
|
+
return `<article class="work-log-entry"><header>${stamp}<span>${escapeHtmlText(correction)}</span></header><div class="work-log-body">${rendered.html}</div>${filtering}<p class="work-log-origin">来歴: ${escapeHtmlText(note.origin_plan_version)}/${escapeHtmlText(note.origin_task_id)}・記録者 ${escapeHtmlText(`${note.actor.host}/${note.actor.agent}`)}</p></article>`;
|
|
18
20
|
}).join('');
|
|
19
21
|
const overflow = context.overflow_count === 0 ? ''
|
|
20
22
|
: `<p class="note-warning">ほか ${context.overflow_count}件は上限のため省略。全履歴: <code>${escapeHtmlText(context.full_history_command)}</code></p>`;
|
|
@@ -60,20 +62,59 @@ export function dispatchBasis(summary) {
|
|
|
60
62
|
return `${parts.join('、')}。未検査は依存線が無くても並列可の根拠になりません。`;
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
const COVERAGE_REASON = Object.freeze({
|
|
66
|
+
missing: 'このplanには独立性の記録がまだありません。',
|
|
67
|
+
stale: '記録はありますが、その後のdiffで失効しています。',
|
|
68
|
+
superseded: '記録は旧plan versionのもので、現在のtopologyには読めません。',
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 未検査を黙って消さない。記録が無い状態こそ最も警告が要る(ADR 0127)。
|
|
73
|
+
*
|
|
74
|
+
* 枠ごと消すと「競合なし」と読まれる。判定していないことと、次の一手を必ず言う。
|
|
75
|
+
*/
|
|
76
|
+
function unverifiedIndependence(planKey, coverage) {
|
|
77
|
+
const reason = COVERAGE_REASON[coverage] ?? COVERAGE_REASON.missing;
|
|
78
|
+
return `<p class="readiness-note"><strong>並列可否:</strong> 未検査です。競合が無いのではなく、まだ判定していません。${escapeHtmlText(reason)}宣言を書いて <code>lattice todo independence compile --plan ${escapeHtmlText(planKey)} --input <ref></code> を通すと判定できます。</p>`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function conflictItems(pairs) {
|
|
82
|
+
return pairs.map((pair) => `<li>${escapeHtmlText(pair.other)} — ${escapeHtmlText(SEVERABILITY_LABEL[pair.severability] ?? pair.severability)}(資源 ${escapeHtmlText(pair.detail)})</li>`).join('');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 作業中の工程は自分がready frontierに居ないため、conflictの相手側として現れる。
|
|
87
|
+
* 「今これを動かしている間、何が同時に始められないか」がその工程での並列可否である。
|
|
88
|
+
*/
|
|
89
|
+
function activeIndependence(plan, taskId) {
|
|
90
|
+
const blocked = plan.conflicts_with_active
|
|
91
|
+
.filter((entry) => entry.active_task_id === taskId)
|
|
92
|
+
.map((entry) => ({
|
|
93
|
+
other: entry.ready_task_id, severability: entry.severability, detail: entry.detail,
|
|
94
|
+
}));
|
|
95
|
+
if (blocked.length > 0) {
|
|
96
|
+
return `<p class="readiness-note"><strong>並列可否:</strong> この工程が作業中のあいだ、次の着手候補は同時に始められません。</p><ul class="independence-conflicts">${conflictItems(blocked)}</ul>`;
|
|
97
|
+
}
|
|
98
|
+
if (plan.coverage !== 'verified') return unverifiedIndependence(plan.plan_key, plan.coverage);
|
|
99
|
+
return '<p class="readiness-note"><strong>並列可否:</strong> 着手済のため同時着手の判定対象ではありません。現在の着手候補に、この工程と競合するものはありません。</p>';
|
|
100
|
+
}
|
|
101
|
+
|
|
63
102
|
/** 個別ToDoについて、競合相手と切断可能性を言葉で示す。 */
|
|
64
|
-
export function renderIndependenceNote(ref, node, summary) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
103
|
+
export function renderIndependenceNote(ref, node, summary, status) {
|
|
104
|
+
// 完了した工程は同時着手の判断材料ではない。ここだけは黙ってよい。
|
|
105
|
+
if (status === 'done') return '';
|
|
106
|
+
const plan = summary === null ? undefined : summary.byPlan.get(ref.plan_key);
|
|
107
|
+
if (plan === undefined) return unverifiedIndependence(ref.plan_key, 'missing');
|
|
68
108
|
const taskId = ref.task_id;
|
|
69
|
-
const state = node
|
|
70
|
-
if (state === null)
|
|
109
|
+
const state = node?.visibility?.independence ?? null;
|
|
110
|
+
if (state === null) {
|
|
111
|
+
if (status === 'in-progress') return activeIndependence(plan, taskId);
|
|
112
|
+
return '<p class="readiness-note"><strong>並列可否:</strong> まだ着手候補ではないため判定していません。前提工程が片付いて着手候補に入った時点で判定します。</p>';
|
|
113
|
+
}
|
|
71
114
|
if (state === 'verified') {
|
|
72
115
|
return '<p class="readiness-note"><strong>並列可否:</strong> 独立検証済です。記録時点の宣言境界では他のready工程と干渉しません。</p>';
|
|
73
116
|
}
|
|
74
|
-
if (state === 'unknown')
|
|
75
|
-
return '<p class="readiness-note"><strong>並列可否:</strong> 未検査です。競合が無いのではなく、まだ判定していません。</p>';
|
|
76
|
-
}
|
|
117
|
+
if (state === 'unknown') return unverifiedIndependence(plan.plan_key, plan.coverage);
|
|
77
118
|
const pairs = [
|
|
78
119
|
...plan.serialize_pairs
|
|
79
120
|
.filter((pair) => pair.task_ids.includes(taskId))
|
|
@@ -90,8 +131,7 @@ export function renderIndependenceNote(ref, node, summary) {
|
|
|
90
131
|
detail: entry.detail,
|
|
91
132
|
})),
|
|
92
133
|
];
|
|
93
|
-
|
|
94
|
-
return `<p class="readiness-note"><strong>並列可否:</strong> 要直列です。</p><ul class="independence-conflicts">${items}</ul>`;
|
|
134
|
+
return `<p class="readiness-note"><strong>並列可否:</strong> 要直列です。</p><ul class="independence-conflicts">${conflictItems(pairs)}</ul>`;
|
|
95
135
|
}
|
|
96
136
|
|
|
97
137
|
export function renderRightPane(
|
|
@@ -157,7 +197,9 @@ export function renderRightPane(
|
|
|
157
197
|
const readiness = node?.visibility.next_ready
|
|
158
198
|
? `<p class="readiness-note">ready frontierの一員です。${ready.length > 1 ? '他のready工程と同時着手できるかは下の並列可否で判断してください。' : '現在の唯一の着手候補です。'}</p>`
|
|
159
199
|
: incoming.get(key).length === 0 ? '<p class="readiness-note">登録済みの前提工程はありません。図だけではdispatch可否を判定しません。</p>' : '';
|
|
160
|
-
const independenceNote = renderIndependenceNote(
|
|
200
|
+
const independenceNote = renderIndependenceNote(
|
|
201
|
+
section.ref, node, independenceSummary, section.state.status,
|
|
202
|
+
);
|
|
161
203
|
// Say it plainly when the reader will not find this ToDo on the diagram.
|
|
162
204
|
const foldedNote = !folds.has(key) ? ''
|
|
163
205
|
: expandable
|
|
@@ -105,7 +105,10 @@ export function renderRelationList(relations, sectionByKey, lookup, emptyText, f
|
|
|
105
105
|
// Say which ones the diagram does not draw, so the reader stops looking.
|
|
106
106
|
const reference = folds.has(targetKey)
|
|
107
107
|
? `${taskReference(target, lookup)}(図では非表示)` : taskReference(target, lookup);
|
|
108
|
-
|
|
108
|
+
// 前提が終わっているか、後続が動き出せるかは、相手の状態を見ないと判断できない。
|
|
109
|
+
const status = DOCUMENT_STATUS[target.state.status] ?? { mark: '?', label: '状態不明' };
|
|
110
|
+
const badge = `<span class="relation-status status-${escapeHtmlAttribute(target.state.status)}">${escapeHtmlText(`${status.mark} ${status.label}`)}</span>`;
|
|
111
|
+
return `<li><button type="button" data-select-node-key="${escapeHtmlAttribute(targetKey)}"><span class="relation-head">${badge}<strong>${escapeHtmlText(reference)}</strong></span><span>${escapeHtmlText(target.task.title)}</span></button>${join}</li>`;
|
|
109
112
|
}).join('')}</ul>`;
|
|
110
113
|
}
|
|
111
114
|
|
|
@@ -63,6 +63,7 @@ body{display:grid;grid-template-rows:minmax(0,1fr);height:100vh;margin:0;backgro
|
|
|
63
63
|
.work-log-origin,.work-log-head,.note-warning{color:var(--text-secondary);font-size:12px;overflow-wrap:anywhere}.note-warning{color:var(--critical)}
|
|
64
64
|
.gantt-warning{margin:16px 0;padding:12px;border:1px solid var(--critical);background:var(--surface-2)}.gantt-warning h2{margin:0 0 8px}.gantt-warning ul{margin:0;padding-left:20px}
|
|
65
65
|
.relation-list button{display:flex;width:100%;flex-direction:column}.relation-list button span{font-weight:400}.relation-kind{display:block;margin-top:4px;color:var(--text-secondary);font-size:12px}
|
|
66
|
+
.relation-head{display:flex;align-items:baseline;gap:6px;flex-wrap:wrap}.relation-status{color:var(--text-secondary);font-size:12px;font-weight:600;white-space:nowrap}.relation-status.status-in-progress{color:var(--accent)}.relation-status.status-done{color:var(--good)}.relation-status.status-blocked{color:var(--critical)}
|
|
66
67
|
.task-diagnostics{margin:16px 0;color:var(--text-secondary);font-size:12px}.task-diagnostics summary{cursor:pointer;color:var(--text-primary);font-weight:600}.task-diagnostics dl{display:grid;grid-template-columns:auto 1fr;gap:4px 12px}.task-diagnostics dd{margin:0;overflow-wrap:anywhere}
|
|
67
68
|
.task-index>h1{margin:0 0 8px;font-size:19px;font-weight:650;line-height:1.45}.task-index>p{margin:0 0 24px;color:var(--text-secondary)}
|
|
68
69
|
.task-index-plan+.task-index-plan{margin-top:32px}.task-index-plan h2{margin:0 0 12px;font-size:16px;font-weight:600}.task-index-plan h2 code{font-size:inherit}
|
package/src/todo-gantt-html.mjs
CHANGED
|
@@ -150,6 +150,12 @@ const CONTROLLER = `
|
|
|
150
150
|
root.addEventListener('pointermove',event=>{if(event.pointerId===resizePointerId)setSplit(event.clientX);});
|
|
151
151
|
root.addEventListener('pointerup',finishResize);root.addEventListener('pointercancel',finishResize);
|
|
152
152
|
root.addEventListener('keydown',event=>{if(event.key==='Escape'){event.preventDefault();showOverview();applyLane(null);return;}const laneChip=event.target.closest('.summary-lane[data-lane-key]');if(laneChip&&(event.key==='Enter'||event.key===' ')){event.preventDefault();toggleLane(laneChip.dataset.laneKey);return;}const node=event.target.closest('[data-node-key]');if(node&&(event.key==='Enter'||event.key===' ')){event.preventDefault();select(node.dataset.nodeKey);}});
|
|
153
|
+
// 記録時刻はUTCで埋まっている。閲覧者の地域とlocaleを知っているのはブラウザだけなので、
|
|
154
|
+
// ここで現地時刻へ直す。scriptが動かなければ埋め込んだUTCがそのまま読める。
|
|
155
|
+
for(const stamp of root.querySelectorAll('time[data-utc-stamp]')){
|
|
156
|
+
const parsed=new Date(stamp.getAttribute('datetime'));
|
|
157
|
+
if(!Number.isNaN(parsed.getTime()))stamp.textContent=parsed.toLocaleString();
|
|
158
|
+
}
|
|
153
159
|
showPanel('overview');
|
|
154
160
|
})();
|
|
155
161
|
`;
|