@quolu/lattice 0.57.2 → 0.58.0
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 +32 -0
- package/README.md +33 -0
- package/docs/schemas/lattice.todo_structure_binding.v1.schema.json +47 -0
- package/docs/schemas/lattice.todo_structure_realization.v1.schema.json +55 -0
- package/docs/schemas/lattice.todo_structure_set.v1.schema.json +264 -0
- package/package.json +4 -1
- package/sensor/dist/bin/exact-traversal.d.ts +20 -0
- package/sensor/dist/bin/exact-traversal.d.ts.map +1 -0
- package/sensor/dist/bin/exact-traversal.js +17 -0
- package/sensor/dist/bin/exact-traversal.js.map +1 -0
- package/sensor/dist/bin/lattice-sensor.js +27 -8
- package/sensor/package.json +1 -1
- package/src/cli-help.mjs +16 -3
- package/src/dag-chain.mjs +37 -0
- package/src/project-cli.mjs +9 -3
- package/src/runtime-pull-intake.mjs +8 -4
- package/src/sensor-adapter.mjs +8 -2
- package/src/todo-chain.mjs +19 -5
- package/src/todo-cli.mjs +612 -7
- package/src/todo-gantt-html-independence.mjs +9 -1
- package/src/todo-gantt-html-style.mjs +8 -0
- package/src/todo-gantt-html.mjs +14 -4
- package/src/todo-gantt-structure.mjs +134 -0
- package/src/todo-status.mjs +32 -6
- package/src/todo-store.mjs +645 -27
- package/src/todo-structure-contracts.mjs +706 -0
- package/src/todo-structure-git-adapter.mjs +452 -0
- package/src/todo-structure-overlay.mjs +599 -0
- package/src/todo-structure-presentation.mjs +163 -0
- package/src/todo-structure-source-adapter.mjs +417 -0
- package/src/todo-structure-store.mjs +475 -0
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { DOCUMENT_STATUS, SEVERABILITY_LABEL, escapeHtmlAttribute, escapeHtmlText, foldIndex, planActivity, presentationLookup, refKey, renderPhaseProgress, renderRelationList, renderSeamProposalOverview, renderTaskIndex, statusMarkup, taskReference } from './todo-gantt-html-shared.mjs';
|
|
2
|
+
import {
|
|
3
|
+
hasTodoStructurePresentation,
|
|
4
|
+
renderTodoStructureNoscript,
|
|
5
|
+
renderTodoStructurePanel,
|
|
6
|
+
} from './todo-gantt-structure.mjs';
|
|
2
7
|
import { renderTodoMarkdown } from './todo-markdown-renderer.mjs';
|
|
3
8
|
|
|
4
9
|
function renderNoteContext(context) {
|
|
@@ -136,6 +141,7 @@ export function renderIndependenceNote(ref, node, summary, status) {
|
|
|
136
141
|
|
|
137
142
|
export function renderRightPane(
|
|
138
143
|
sections, layout, presentation, readModel, notesEnabled = false, noteWarnings = [], expandable = false,
|
|
144
|
+
structurePresentation = null,
|
|
139
145
|
) {
|
|
140
146
|
const lookup = presentationLookup(presentation);
|
|
141
147
|
const sectionByKey = new Map(sections.map((section) => [refKey(section.ref), section]));
|
|
@@ -211,7 +217,9 @@ export function renderRightPane(
|
|
|
211
217
|
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}${independenceNote}${foldedNote}${designMemo}<section><h2>前提工程</h2>${renderRelationList(incoming.get(key), sectionByKey, lookup, '登録済みの前提工程はありません。', folds)}</section><section><h2>後続工程</h2>${renderRelationList(outgoing.get(key), sectionByKey, lookup, '登録済みの後続工程はありません。', folds)}</section>${workLog}<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>`;
|
|
212
218
|
}).join('');
|
|
213
219
|
const taskIndex = renderTaskIndex(sections, lookup, folds, planActivity(readModel));
|
|
214
|
-
|
|
220
|
+
const structureButton = hasTodoStructurePresentation(structurePresentation)
|
|
221
|
+
? '<button type="button" data-show-structure>構造検査</button>' : '';
|
|
222
|
+
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>${structureButton}</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>${renderTodoStructurePanel(structurePresentation)}${renderTodoStructureNoscript(structurePresentation)}</div>`;
|
|
215
223
|
}
|
|
216
224
|
|
|
217
225
|
export function renderDiagramLegend(presentation, layout = null, expandable = false) {
|
|
@@ -74,6 +74,14 @@ body{display:grid;grid-template-rows:minmax(0,1fr);height:100vh;margin:0;backgro
|
|
|
74
74
|
.task-index-list{margin:0;padding:0;list-style:none}.task-index-list li+li{margin-top:8px}.task-index-list button{display:grid;width:100%;grid-template-columns:1.5rem auto minmax(0,1fr);gap:4px 8px;align-items:baseline}
|
|
75
75
|
.task-index-status{grid-row:1 / span 2;color:var(--text-secondary);font-size:13.5px;text-align:center}.task-index-status.status-in-progress{color:var(--accent)}.task-index-status.status-done{color:var(--good)}.task-index-status.status-blocked{color:var(--critical)}
|
|
76
76
|
.task-index-reference{color:var(--text-secondary);font-size:12px;white-space:nowrap}.task-index-list strong{font-size:13.5px;font-weight:600;overflow-wrap:anywhere}.task-index-blocked-reason{grid-column:2 / -1;color:var(--text-secondary);font-size:12px;overflow-wrap:anywhere}
|
|
77
|
+
.structure-inspection>h1,.structure-noscript>h1{margin:0 0 8px;font-size:19px}.structure-inspection>p{color:var(--text-secondary)}
|
|
78
|
+
.structure-plan{margin:18px 0;border:1px solid var(--border);background:var(--surface-1)}.structure-plan>summary{display:flex;flex-wrap:wrap;align-items:center;gap:6px 12px;padding:10px 12px;cursor:pointer;background:var(--surface-2)}.structure-plan>summary>code{font-weight:700}.structure-plan>summary>span{color:var(--text-secondary);font-size:12px}
|
|
79
|
+
.structure-verdict{padding:1px 7px;border:1px solid var(--border);border-radius:9999px}.structure-verdict.verdict-consistent{border-color:var(--good);color:var(--good)}.structure-verdict.verdict-inconsistent,.structure-verdict.verdict-unreadable{border-color:var(--critical);color:var(--critical)}.structure-verdict.verdict-unknown,.structure-verdict.verdict-stale{border-color:var(--accent);color:var(--accent)}
|
|
80
|
+
.structure-findings,.structure-graph,.structure-actions{margin:0;padding:12px;border-top:1px solid var(--border)}.structure-findings h3,.structure-graph h3{margin:0 0 8px;font-size:14px}.structure-findings ol,.structure-edge-list,.structure-actions ul,.structure-node details ul{margin:0;padding:0;list-style:none}
|
|
81
|
+
.structure-finding{padding:9px;border-left:4px solid var(--accent);background:var(--surface-2)}.structure-finding+.structure-finding{margin-top:7px}.structure-finding.severity-error{border-left-color:var(--critical)}.structure-finding.severity-notice{border-left-color:var(--good)}.structure-finding>header{display:flex;justify-content:space-between;gap:10px}.structure-finding>p{margin:5px 0 0;color:var(--text-secondary);font-size:12px}.structure-finding-targets{display:flex;flex-wrap:wrap;align-items:center;gap:4px;margin-top:6px}
|
|
82
|
+
.structure-finding-targets button,.structure-finding-edge-targets button,.structure-edge-list button{max-width:100%;padding:2px 5px;border:1px solid var(--border);background:var(--surface-1);cursor:pointer}.structure-finding-targets code,.structure-finding-edge-targets code,.structure-edge-list code{overflow-wrap:anywhere}.structure-finding-edge-targets{display:grid;gap:4px;margin-top:5px}.structure-finding-edge-targets button{text-align:left}
|
|
83
|
+
.structure-node-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(15rem,1fr));gap:8px}.structure-node{min-width:0;padding:9px;border:1px solid var(--border);border-left:4px solid var(--text-secondary);background:var(--surface-2);scroll-margin:56px}.structure-node.kind-task_transform{border-left-color:var(--accent)}.structure-node.kind-data{border-left-color:var(--critical)}.structure-node.kind-code,.structure-node.kind-source_symbol{border-left-color:var(--text-primary)}.structure-node.kind-external{border-left-color:var(--text-secondary)}.structure-node.kind-changeset{border-left-color:var(--good)}.structure-node.focused{outline:3px solid var(--critical);outline-offset:2px}.structure-node>header{display:flex;flex-wrap:wrap;justify-content:space-between;gap:4px 8px}.structure-node>header>span{font-weight:700}.structure-node>header>code{max-width:100%;overflow-wrap:anywhere;color:var(--text-secondary)}.structure-node>p{margin:5px 0;font-size:12px}.structure-node details{margin-top:6px}.structure-node details summary{cursor:pointer}.structure-node details li{overflow-wrap:anywhere}
|
|
84
|
+
.structure-edge-list{display:grid;gap:5px}.structure-edge-list li{display:grid;grid-template-columns:auto minmax(0,1fr) auto minmax(0,1fr);align-items:center;gap:5px;padding:6px;background:var(--surface-2);scroll-margin:56px}.structure-edge-list li.focused{outline:3px solid var(--critical);outline-offset:2px}.structure-edge-list li>span:first-child{color:var(--text-secondary);font-size:11px}.structure-actions summary{cursor:pointer;font-weight:650}.structure-actions li+li{margin-top:5px}.structure-severity-error{color:var(--critical)}.structure-noscript{margin:16px 24px;padding:12px;border:1px solid var(--critical)}
|
|
77
85
|
.todo-gantt text{font-family:system-ui,-apple-system,"Hiragino Sans","Yu Gothic UI",sans-serif;pointer-events:none}
|
|
78
86
|
.todo-node .node-surface{fill:var(--surface-2);stroke:var(--border);stroke-width:1}
|
|
79
87
|
.todo-node .node-meta{fill:var(--text-secondary);font-size:12px;font-weight:500}
|
package/src/todo-gantt-html.mjs
CHANGED
|
@@ -6,8 +6,9 @@ import { renderTodoGanttSvg, TODO_GANTT_STATUS_PRESENTATION } from './todo-gantt
|
|
|
6
6
|
import { renderDiagramLegend, renderRightPane } from './todo-gantt-html-independence.mjs';
|
|
7
7
|
import { escapeHtmlAttribute, escapeHtmlText, refKey } from './todo-gantt-html-shared.mjs';
|
|
8
8
|
import { CSS, NESTED_CSS } from './todo-gantt-html-style.mjs';
|
|
9
|
+
import { TODO_STRUCTURE_PRESENTATION_SCHEMA } from './todo-structure-presentation.mjs';
|
|
9
10
|
|
|
10
|
-
export const TODO_GANTT_RENDERER_VERSION = 'lattice.todo_gantt_renderer.
|
|
11
|
+
export const TODO_GANTT_RENDERER_VERSION = 'lattice.todo_gantt_renderer.v20';
|
|
11
12
|
export const TODO_GANTT_PROSE_MAX_BYTES = 8 * 1024 * 1024;
|
|
12
13
|
export const TODO_GANTT_HTML_MAX_BYTES = 24 * 1024 * 1024;
|
|
13
14
|
|
|
@@ -135,6 +136,7 @@ const CONTROLLER = `
|
|
|
135
136
|
const overviewPanel=root.querySelector('[data-right-panel="overview"]');
|
|
136
137
|
const detailsPanel=root.querySelector('[data-right-panel="details"]');
|
|
137
138
|
const taskIndexPanel=root.querySelector('[data-right-panel="task-index"]');
|
|
139
|
+
const structurePanel=root.querySelector('[data-right-panel="structure"]');
|
|
138
140
|
const selectedReturnButton=root.querySelector('[data-show-selected]');
|
|
139
141
|
const detailPanels=[...root.querySelectorAll('[data-detail-key]')];
|
|
140
142
|
const nodes=[...root.querySelectorAll('[data-node-key]')];
|
|
@@ -157,7 +159,7 @@ const CONTROLLER = `
|
|
|
157
159
|
const toggleLane=(key)=>applyLane(activeLaneKey===key?null:key);
|
|
158
160
|
// Both diagrams ship in the page; the badge picks which one is on screen.
|
|
159
161
|
const setExpanded=(next)=>{if(diagrams.length<2)return;expanded=next;for(const diagram of diagrams)diagram.hidden=(diagram.dataset.diagram==='expanded')!==expanded;svg=diagrams.find(diagram=>!diagram.hidden)?.querySelector('[data-gantt-svg]')??svg;baseWidth=Number(svg?.dataset.svgWidth??0);baseHeight=Number(svg?.dataset.svgHeight??0);if(expandToggle){expandToggle.setAttribute('aria-expanded',String(expanded));}if(toggleLabel){toggleLabel.textContent=expanded?toggleLabel.dataset.expandedLabel:toggleLabel.dataset.collapsedLabel;}setZoom(zoom);applyLane(activeLaneKey);syncSelection();scroller?.scrollTo(0,0);};
|
|
160
|
-
const showPanel=(name)=>{if(overviewPanel)overviewPanel.hidden=name!=='overview';if(detailsPanel)detailsPanel.hidden=name!=='details';if(taskIndexPanel)taskIndexPanel.hidden=name!=='task-index';if(selectedReturnButton)selectedReturnButton.hidden=name!=='task-index'||selectedKey===null;root.dataset.viewState=name;};
|
|
162
|
+
const showPanel=(name)=>{if(overviewPanel)overviewPanel.hidden=name!=='overview';if(detailsPanel)detailsPanel.hidden=name!=='details';if(taskIndexPanel)taskIndexPanel.hidden=name!=='task-index';if(structurePanel)structurePanel.hidden=name!=='structure';if(selectedReturnButton)selectedReturnButton.hidden=name!=='task-index'||selectedKey===null;root.dataset.viewState=name;};
|
|
161
163
|
const syncSelection=()=>{for(const detail of detailPanels)detail.hidden=detail.dataset.detailKey!==selectedKey;for(const node of nodes){const selected=node.dataset.nodeKey===selectedKey;node.setAttribute('aria-selected',String(selected));node.classList.toggle('selected-node',selected);}for(const edge of edges){const selected=selectedKey!==null;edge.classList.toggle('selected-incident-edge',selected&&(edge.dataset.fromNodeKey===selectedKey||edge.dataset.toNodeKey===selectedKey));}};
|
|
162
164
|
const showOverview=()=>{selectedKey=null;syncSelection();showPanel('overview');};
|
|
163
165
|
const showTaskIndex=()=>showPanel('task-index');
|
|
@@ -167,6 +169,8 @@ const CONTROLLER = `
|
|
|
167
169
|
const overviewButton=event.target.closest('[data-show-overview]');if(overviewButton&&root.contains(overviewButton)){showOverview();return;}
|
|
168
170
|
const selectedButton=event.target.closest('[data-show-selected]');if(selectedButton&&root.contains(selectedButton)){showSelected();return;}
|
|
169
171
|
const taskIndexButton=event.target.closest('[data-show-task-index]');if(taskIndexButton&&root.contains(taskIndexButton)){showTaskIndex();return;}
|
|
172
|
+
const structureButton=event.target.closest('[data-show-structure]');if(structureButton&&root.contains(structureButton)){showPanel('structure');return;}
|
|
173
|
+
const structureTarget=event.target.closest('[data-structure-target-id]');if(structureTarget&&root.contains(structureTarget)){const target=document.getElementById(structureTarget.dataset.structureTargetId);if(target&&root.contains(target)){for(const item of root.querySelectorAll('.structure-node.focused,.structure-edge-list li.focused'))item.classList.remove('focused');target.classList.add('focused');target.scrollIntoView({block:'center'});}return;}
|
|
170
174
|
const expandButton=event.target.closest('[data-toggle-expanded]');if(expandButton&&root.contains(expandButton)){setExpanded(!expanded);return;}
|
|
171
175
|
const selectButton=event.target.closest('[data-select-node-key]');if(selectButton&&root.contains(selectButton)){select(selectButton.dataset.selectNodeKey);return;}
|
|
172
176
|
const zoomButton=event.target.closest('[data-zoom-action]');if(zoomButton&&root.contains(zoomButton)){const action=zoomButton.dataset.zoomAction;if(action==='in')setZoom(zoom<1&&zoom*1.25>=1?1:zoom*1.25,.001);else if(action==='out')setZoom(zoom>1&&zoom/1.25<=1?1:zoom/1.25,.001);else if(action==='reset')setZoom(1);else if(action==='fit'&&scroller){setZoom(Math.min(1,(scroller.clientWidth-16)/baseWidth),.001);scroller.scrollTo(0,0);}return;}
|
|
@@ -202,7 +206,7 @@ const NESTED_CONTROLLER = `
|
|
|
202
206
|
|
|
203
207
|
export function renderTodoGanttHtml({
|
|
204
208
|
readModel, layout, narratives = [], anchorOutcomes = [], presentation = null, metadata = {},
|
|
205
|
-
expandedLayout = null, noteContexts = null, noteWarnings = [],
|
|
209
|
+
expandedLayout = null, noteContexts = null, noteWarnings = [], structurePresentation = null,
|
|
206
210
|
}) {
|
|
207
211
|
if (readModel?.schema !== 'lattice.todo_store_read.v1' || !Array.isArray(readModel.members)) {
|
|
208
212
|
throw new TypeError('readModel must be lattice.todo_store_read.v1');
|
|
@@ -218,6 +222,11 @@ export function renderTodoGanttHtml({
|
|
|
218
222
|
|| presentation.project_id !== readModel.project_id)) {
|
|
219
223
|
throw new TypeError('presentation must be lattice.todo_gantt_presentation_model.v1');
|
|
220
224
|
}
|
|
225
|
+
if (structurePresentation !== null
|
|
226
|
+
&& (structurePresentation?.schema !== TODO_STRUCTURE_PRESENTATION_SCHEMA
|
|
227
|
+
|| structurePresentation.project_id !== readModel.project_id)) {
|
|
228
|
+
throw new TypeError('structurePresentation must be lattice.todo_structure_presentation.v1');
|
|
229
|
+
}
|
|
221
230
|
const normalized = normalizeSections(readModel, narratives, anchorOutcomes, noteContexts);
|
|
222
231
|
const displayName = projectDisplayName(readModel, metadata);
|
|
223
232
|
const hasHierarchy = layout?.hierarchy?.schema === 'lattice.todo_gantt_hierarchy.v1';
|
|
@@ -230,12 +239,13 @@ export function renderTodoGanttHtml({
|
|
|
230
239
|
: `<div data-diagram="live">${svg}</div><div data-diagram="expanded" hidden>${expandedSvg}</div>`;
|
|
231
240
|
const rightPane = renderRightPane(
|
|
232
241
|
normalized.sections, layout, presentation, readModel, noteContexts !== null, noteWarnings,
|
|
233
|
-
expandedSvg !== '',
|
|
242
|
+
expandedSvg !== '', structurePresentation,
|
|
234
243
|
);
|
|
235
244
|
const staticData = serializeJsonForScript({
|
|
236
245
|
renderer_version: TODO_GANTT_RENDERER_VERSION,
|
|
237
246
|
metadata,
|
|
238
247
|
presentation,
|
|
248
|
+
structure_presentation: structurePresentation,
|
|
239
249
|
});
|
|
240
250
|
const html = `<!doctype html><html lang="ja"><head><meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Lattice — ${escapeHtmlText(displayName)} 依存工程図</title><style>${CSS}${hasHierarchy ? NESTED_CSS : ''}</style></head><body data-gantt-root data-view-state="overview"><main class="shell"><section class="gantt-pane" aria-label="${escapeHtmlAttribute(displayName)} 依存工程図"><div class="diagram-toolbar" role="group" aria-label="図のズーム"><strong class="project-heading">${escapeHtmlText(displayName)} 依存工程図</strong>${renderAuditPendingChip(readModel)}<button type="button" data-zoom-action="out" aria-label="縮小">−</button><button type="button" data-zoom-action="reset">等倍</button><button type="button" data-zoom-action="in" aria-label="拡大">+</button><button type="button" data-zoom-action="fit">全体表示</button><output class="zoom-readout" data-zoom-output aria-live="polite">100%</output><span class="diagram-note">縦=依存段階(時間ではない)</span></div>${renderDiagramLegend(presentation, layout, expandedSvg !== '')}<div class="diagram-scroll" data-diagram-scroll tabindex="0" aria-label="縦方向を主にスクロール可能な依存工程図">${diagrams}</div></section><div class="pane-divider" data-pane-divider aria-hidden="true"></div><aside class="narrative-pane" aria-label="選択工程の詳細と全工程一覧">${rightPane}</aside></main><script type="application/json" id="todo-gantt-data">${staticData}</script><script>${CONTROLLER}${hasHierarchy ? NESTED_CONTROLLER : ''}</script></body></html>`;
|
|
241
251
|
const htmlBytes = Buffer.byteLength(html, 'utf8');
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { escapeHtmlAttribute, escapeHtmlText } from './todo-gantt-html-shared.mjs';
|
|
2
|
+
import { TODO_STRUCTURE_PRESENTATION_SCHEMA } from './todo-structure-presentation.mjs';
|
|
3
|
+
|
|
4
|
+
const KIND_LABEL = Object.freeze({
|
|
5
|
+
task_transform: '工程変換', data: 'データ', code: 'コード', source_symbol: '既存コード',
|
|
6
|
+
external: '外部契約', changeset: 'commit', constant: '定数', final_product: '最終成果',
|
|
7
|
+
});
|
|
8
|
+
const EDGE_LABEL = Object.freeze({
|
|
9
|
+
input: '入力', output: '出力', sink: '受渡し', source_edge: '既存コード関係',
|
|
10
|
+
realization: '実装commit',
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
function valid(presentation) {
|
|
14
|
+
return presentation?.schema === TODO_STRUCTURE_PRESENTATION_SCHEMA
|
|
15
|
+
&& Array.isArray(presentation.plans);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function hasTodoStructurePresentation(presentation) {
|
|
19
|
+
return valid(presentation) && presentation.plans.length > 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function nodeId(planIndex, nodeIndex) {
|
|
23
|
+
return `structure-node-${planIndex}-${nodeIndex}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function edgeId(planIndex, edgeIndex) {
|
|
27
|
+
return `structure-edge-${planIndex}-${edgeIndex}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function focusRefs(finding) {
|
|
31
|
+
return [...new Set([
|
|
32
|
+
...(finding.task_ids ?? []).map((taskId) => `task:${taskId}`),
|
|
33
|
+
...(finding.data_refs ?? []).map((ref) => ref.startsWith('external/')
|
|
34
|
+
? `external:${ref.slice('external/'.length)}` : `data:${ref}`),
|
|
35
|
+
...(finding.code_refs ?? []).map((ref) => `code:${ref}`),
|
|
36
|
+
...(finding.commit_oids ?? []).map((oid) => `commit:${oid}`),
|
|
37
|
+
])];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function renderTaskDetail(plan, node) {
|
|
41
|
+
const task = plan.tasks.find(({ task_id: taskId }) => taskId === node.task_id);
|
|
42
|
+
if (task === undefined) return '';
|
|
43
|
+
const changed = task.changed_fields.length === 0
|
|
44
|
+
? 'plannedからの変更なし' : `変更: ${task.changed_fields.join(', ')}`;
|
|
45
|
+
const anchors = task.code_anchors.length === 0 ? ''
|
|
46
|
+
: `<details><summary>code anchor ${task.code_anchors.length}件</summary><ul>${task.code_anchors.map((anchor) => `<li><code>${escapeHtmlText(anchor.effect)}</code> <code>${escapeHtmlText(anchor.path)}</code>${anchor.symbol === null ? '' : ` · ${escapeHtmlText(anchor.symbol)}`}</li>`).join('')}</ul></details>`;
|
|
47
|
+
return `<p><strong>${escapeHtmlText(task.form)}</strong> · ${escapeHtmlText(changed)}</p><p>planned: ${escapeHtmlText(task.planned_outcome)}</p>${task.form === 'realized' ? `<p>effective: ${escapeHtmlText(task.effective_outcome)}</p>` : ''}${anchors}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function renderCommitDetail(plan, node) {
|
|
51
|
+
if (node.kind !== 'changeset') return '';
|
|
52
|
+
const oid = node.ref.slice('commit:'.length);
|
|
53
|
+
const commit = plan.provenance?.commits.find(({ commit_oid: commitOid }) => commitOid === oid);
|
|
54
|
+
if (commit === undefined) return '';
|
|
55
|
+
return `<details><summary>変更path ${commit.changes.length}件</summary><ul>${commit.changes.map((change) => `<li><code>${escapeHtmlText(change.change)}</code> ${escapeHtmlText(change.path)}</li>`).join('')}</ul></details>`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function renderNaturalRef(node) {
|
|
59
|
+
if (node.kind !== 'source_symbol' || node.natural_ref === null) return '';
|
|
60
|
+
const natural = node.natural_ref;
|
|
61
|
+
const label = [natural.path, natural.name].filter((value) => typeof value === 'string').join(' · ');
|
|
62
|
+
return label === '' ? '' : `<p>${escapeHtmlText(label)}</p>`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function renderNodes(plan, planIndex) {
|
|
66
|
+
const ids = new Map();
|
|
67
|
+
const nodes = plan.graph.nodes.map((node, index) => {
|
|
68
|
+
const id = nodeId(planIndex, index);
|
|
69
|
+
ids.set(node.ref, id);
|
|
70
|
+
const form = node.kind === 'task_transform' ? ` · ${node.form}` : '';
|
|
71
|
+
return `<article class="structure-node kind-${escapeHtmlAttribute(node.kind)}" id="${id}" data-structure-node-ref="${escapeHtmlAttribute(node.ref)}"><header><span>${escapeHtmlText(KIND_LABEL[node.kind] ?? node.kind)}</span><code>${escapeHtmlText(node.ref)}</code></header>${node.kind === 'task_transform' ? renderTaskDetail(plan, node) : ''}${renderCommitDetail(plan, node)}${renderNaturalRef(node)}${form === '' ? '' : `<p class="structure-form">${escapeHtmlText(form.slice(3))}</p>`}</article>`;
|
|
72
|
+
}).join('');
|
|
73
|
+
return { ids, markup: nodes };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function targetButton(ref, ids) {
|
|
77
|
+
const id = ids.get(ref);
|
|
78
|
+
return id === undefined
|
|
79
|
+
? `<code>${escapeHtmlText(ref)}</code>`
|
|
80
|
+
: `<button type="button" data-structure-target-id="${escapeHtmlAttribute(id)}"><code>${escapeHtmlText(ref)}</code></button>`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function renderFindings(plan, ids, edgeIds) {
|
|
84
|
+
if (plan.unreadable_reason !== null) {
|
|
85
|
+
return `<section class="structure-findings"><h3>読取不能</h3><p class="structure-severity-error">${escapeHtmlText(plan.unreadable_reason)}</p></section>`;
|
|
86
|
+
}
|
|
87
|
+
if (plan.findings.length === 0) {
|
|
88
|
+
return '<section class="structure-findings"><h3>指摘</h3><p>保存artifactに指摘はありません。</p></section>';
|
|
89
|
+
}
|
|
90
|
+
return `<section class="structure-findings"><h3>指摘 ${plan.findings.length}件</h3><ol>${plan.findings.map((finding) => {
|
|
91
|
+
const refs = focusRefs(finding);
|
|
92
|
+
const relation = refs.length === 0 ? ''
|
|
93
|
+
: `<div class="structure-finding-targets">${refs.map((ref) => targetButton(ref, ids)).join('<span aria-hidden="true">→</span>')}</div>`;
|
|
94
|
+
const implicatedEdges = plan.graph.edges.flatMap((edge, index) => {
|
|
95
|
+
if (!refs.includes(edge.from) || !refs.includes(edge.to)) return [];
|
|
96
|
+
const id = edgeIds.get(index);
|
|
97
|
+
return [`<button type="button" data-structure-target-id="${escapeHtmlAttribute(id)}">edge: ${escapeHtmlText(EDGE_LABEL[edge.kind] ?? edge.kind)} <code>${escapeHtmlText(edge.from)}</code> → <code>${escapeHtmlText(edge.to)}</code></button>`];
|
|
98
|
+
});
|
|
99
|
+
const edgeTargets = implicatedEdges.length === 0 ? ''
|
|
100
|
+
: `<div class="structure-finding-edge-targets">${implicatedEdges.join('')}</div>`;
|
|
101
|
+
return `<li class="structure-finding severity-${escapeHtmlAttribute(finding.severity)}"><header><strong>${escapeHtmlText(finding.code)}</strong><span>${escapeHtmlText(finding.severity)}</span></header>${relation}${edgeTargets}<p>次: <code>${escapeHtmlText(finding.next_action)}</code></p></li>`;
|
|
102
|
+
}).join('')}</ol></section>`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function renderEdges(plan, ids, edgeIds) {
|
|
106
|
+
if (plan.graph.edges.length === 0) return '<p>保存artifactにedgeはありません。</p>';
|
|
107
|
+
return `<ol class="structure-edge-list">${plan.graph.edges.map((edge, index) => `<li id="${edgeIds.get(index)}" data-structure-edge-from="${escapeHtmlAttribute(edge.from)}" data-structure-edge-to="${escapeHtmlAttribute(edge.to)}"><span>${escapeHtmlText(EDGE_LABEL[edge.kind] ?? edge.kind)}</span>${targetButton(edge.from, ids)}<span aria-hidden="true">→</span>${targetButton(edge.to, ids)}</li>`).join('')}</ol>`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function renderPlan(plan, planIndex) {
|
|
111
|
+
const { ids, markup: nodes } = renderNodes(plan, planIndex);
|
|
112
|
+
const edgeIds = new Map(plan.graph.edges.map((_, index) => [index, edgeId(planIndex, index)]));
|
|
113
|
+
const verdict = plan.verdict ?? plan.compiled_verdict ?? plan.coverage;
|
|
114
|
+
const finalization = plan.finalization === null ? ''
|
|
115
|
+
: `<span>finalization: ${escapeHtmlText(plan.finalization.status)}</span>`;
|
|
116
|
+
const actions = plan.next_actions.length === 0 ? ''
|
|
117
|
+
: `<details class="structure-actions"><summary>次の操作</summary><ul>${plan.next_actions.map((action) => `<li><code>${escapeHtmlText(action)}</code></li>`).join('')}</ul></details>`;
|
|
118
|
+
return `<details class="structure-plan" open><summary><code>${escapeHtmlText(plan.plan_key)}</code><span class="structure-verdict verdict-${escapeHtmlAttribute(verdict ?? 'unknown')}">${escapeHtmlText(verdict ?? 'unknown')}</span><span>${escapeHtmlText(plan.freshness)}</span>${finalization}</summary>${renderFindings(plan, ids, edgeIds)}<section class="structure-graph" aria-label="${escapeHtmlAttribute(`${plan.plan_key} 構造グラフ`)}"><h3>node</h3><div class="structure-node-grid">${nodes}</div><h3>edge</h3>${renderEdges(plan, ids, edgeIds)}</section>${actions}</details>`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function renderTodoStructurePanel(presentation) {
|
|
122
|
+
if (!hasTodoStructurePresentation(presentation)) return '';
|
|
123
|
+
return `<section class="structure-inspection" data-right-panel="structure" hidden><h1>構造検査</h1><p>工程依存図とは別の面です。task、data、code、external、commit provenanceを最終的な受渡しとして表示します。</p>${presentation.plans.map(renderPlan).join('')}</section>`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** script無効時にもfinding一覧だけは隠さず読めるfallback。 */
|
|
127
|
+
export function renderTodoStructureNoscript(presentation) {
|
|
128
|
+
if (!hasTodoStructurePresentation(presentation)) return '';
|
|
129
|
+
const plans = presentation.plans.map((plan) => `<section><h2>${escapeHtmlText(plan.plan_key)} — ${escapeHtmlText(plan.verdict ?? plan.compiled_verdict ?? plan.coverage)}</h2>${plan.unreadable_reason === null
|
|
130
|
+
? plan.findings.length === 0 ? '<p>指摘はありません。</p>'
|
|
131
|
+
: `<ol>${plan.findings.map((finding) => `<li><strong>${escapeHtmlText(finding.code)}</strong> (${escapeHtmlText(finding.severity)}) — <code>${escapeHtmlText(finding.next_action)}</code></li>`).join('')}</ol>`
|
|
132
|
+
: `<p>${escapeHtmlText(plan.unreadable_reason)}</p>`}</section>`).join('');
|
|
133
|
+
return `<noscript><section class="structure-noscript"><h1>構造検査の指摘</h1>${plans}</section></noscript>`;
|
|
134
|
+
}
|
package/src/todo-status.mjs
CHANGED
|
@@ -22,12 +22,13 @@ import {
|
|
|
22
22
|
} from './todo-store.mjs';
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
25
|
+
* v7で`structure_finalization_pending`を足す。構造機能が未適用のplanでは空配列になり、
|
|
26
|
+
* dispatch面は変えない。ADR 0054・0063の前例どおり既存versionへのin-place追加はしない。
|
|
26
27
|
*
|
|
27
28
|
* plan単位noteは工程に属する義務で、taskへ着手した人のcontextには届くが、**まだ誰も
|
|
28
29
|
* 着手していない工程の義務は、この欄が無いとどこにも出ない**。
|
|
29
30
|
*/
|
|
30
|
-
export const TODO_STATUS_SCHEMA = 'lattice.todo_status_result.
|
|
31
|
+
export const TODO_STATUS_SCHEMA = 'lattice.todo_status_result.v7';
|
|
31
32
|
export const TODO_DISPATCH_FRONTIER_SCHEMA = 'lattice.todo_dispatch_frontier.v1';
|
|
32
33
|
export const TODO_STATUS_LIST_LIMIT = 2_000;
|
|
33
34
|
export const TODO_STATUS_LABEL_LIMIT = 160;
|
|
@@ -234,6 +235,18 @@ function parallelCandidateEntry(value) {
|
|
|
234
235
|
|| value.serialize_pairs.length > 0);
|
|
235
236
|
}
|
|
236
237
|
|
|
238
|
+
function structureFinalizationPendingEntry(value) {
|
|
239
|
+
return exactRecord(value, ['plan_key', 'status', 'reason', 'stale_reasons', 'next_commands'])
|
|
240
|
+
&& isTodoIdentifier(value.plan_key)
|
|
241
|
+
&& ['missing', 'stale'].includes(value.status)
|
|
242
|
+
&& isTodoStatusBoundedText(value.reason, TODO_STATUS_REASON_LIMIT)
|
|
243
|
+
&& boundedList(value.stale_reasons,
|
|
244
|
+
(reason) => isTodoStatusBoundedText(reason, TODO_STATUS_LABEL_LIMIT))
|
|
245
|
+
&& Array.isArray(value.next_commands) && value.next_commands.length > 0
|
|
246
|
+
&& boundedList(value.next_commands,
|
|
247
|
+
(command) => isTodoStatusBoundedText(command, TODO_STATUS_REASON_LIMIT));
|
|
248
|
+
}
|
|
249
|
+
|
|
237
250
|
function memberHead(value) {
|
|
238
251
|
return exactRecord(value, [
|
|
239
252
|
'plan_key', 'plan_version', 'through_sequence', 'journal_head_digest',
|
|
@@ -287,17 +300,19 @@ function dispatchFrontierEntry(value, projectId, nextReady) {
|
|
|
287
300
|
&& value.frontier_digest === expected.frontier_digest;
|
|
288
301
|
}
|
|
289
302
|
|
|
290
|
-
/** todo status
|
|
303
|
+
/** todo status v7 wire shapeを検証し、digestも再計算する。 */
|
|
291
304
|
export function validateTodoStatusResult(value) {
|
|
292
305
|
try {
|
|
293
306
|
return exactRecord(value, [
|
|
294
307
|
'schema', 'project_id', 'active_set', 'next_ready', 'dispatch_frontier',
|
|
295
|
-
'blocked', 'audit_pending', '
|
|
308
|
+
'blocked', 'audit_pending', 'structure_finalization_pending', 'plan_notes',
|
|
309
|
+
'coordination', 'parallel_candidates',
|
|
296
310
|
'member_heads', 'result_digest',
|
|
297
311
|
]) && value.schema === TODO_STATUS_SCHEMA && isTodoIdentifier(value.project_id)
|
|
298
312
|
&& boundedList(value.active_set, activeTaskEntry) && boundedList(value.next_ready, taskEntry)
|
|
299
313
|
&& dispatchFrontierEntry(value.dispatch_frontier, value.project_id, value.next_ready)
|
|
300
314
|
&& boundedList(value.blocked, blockedEntry) && boundedList(value.audit_pending, auditPendingEntry)
|
|
315
|
+
&& boundedList(value.structure_finalization_pending, structureFinalizationPendingEntry)
|
|
301
316
|
&& boundedList(value.plan_notes, planNoteEntry)
|
|
302
317
|
&& boundedList(value.coordination, coordinationEntry)
|
|
303
318
|
&& boundedList(value.parallel_candidates, parallelCandidateEntry)
|
|
@@ -535,6 +550,7 @@ export function computeReadyFrontier(readModel) {
|
|
|
535
550
|
*/
|
|
536
551
|
export const TODO_STATUS_DISPATCH_ONLY = Object.freeze({
|
|
537
552
|
planNotes: Object.freeze([]), parallelCandidates: Object.freeze([]),
|
|
553
|
+
structureFinalizations: Object.freeze([]),
|
|
538
554
|
});
|
|
539
555
|
|
|
540
556
|
/**
|
|
@@ -547,17 +563,23 @@ export const TODO_STATUS_DISPATCH_ONLY = Object.freeze({
|
|
|
547
563
|
* 呼び出し元だけがそれを渡す。dispatch面しか見ない内部呼び出しは`TODO_STATUS_DISPATCH_ONLY`を使う。
|
|
548
564
|
*/
|
|
549
565
|
export function projectTodoStatus(readModel, options = undefined) {
|
|
550
|
-
if (!exactRecord(options, ['planNotes', 'parallelCandidates'])
|
|
566
|
+
if (!(exactRecord(options, ['planNotes', 'parallelCandidates'])
|
|
567
|
+
|| exactRecord(options, ['planNotes', 'parallelCandidates', 'structureFinalizations']))
|
|
551
568
|
|| !Array.isArray(options.planNotes)) {
|
|
552
569
|
fail('TODO_STATUS_INVALID_INPUT', 'todo_status_plan_notes_missing');
|
|
553
570
|
}
|
|
554
571
|
if (!Array.isArray(options.parallelCandidates)) {
|
|
555
572
|
fail('TODO_STATUS_INVALID_INPUT', 'todo_status_parallel_candidates_missing');
|
|
556
573
|
}
|
|
574
|
+
if (options.structureFinalizations !== undefined
|
|
575
|
+
&& !Array.isArray(options.structureFinalizations)) {
|
|
576
|
+
fail('TODO_STATUS_INVALID_INPUT', 'todo_status_structure_finalizations_invalid');
|
|
577
|
+
}
|
|
557
578
|
const graph = buildTodoGraph(readModel);
|
|
558
579
|
const { nodes, incoming, memberHeads, auditPending, coordination } = graph;
|
|
559
580
|
const planNotes = [...options.planNotes];
|
|
560
581
|
const parallelCandidates = [...options.parallelCandidates];
|
|
582
|
+
const structureFinalizations = [...(options.structureFinalizations ?? [])];
|
|
561
583
|
|
|
562
584
|
const activeSet = [];
|
|
563
585
|
const nextReady = [];
|
|
@@ -590,7 +612,8 @@ export function projectTodoStatus(readModel, options = undefined) {
|
|
|
590
612
|
memberHeads.sort((left, right) => left.plan_key < right.plan_key ? -1 : left.plan_key > right.plan_key ? 1 : 0);
|
|
591
613
|
for (const [name, value] of [
|
|
592
614
|
['active_set', activeSet], ['next_ready', nextReady], ['blocked', blocked],
|
|
593
|
-
['audit_pending', auditPending], ['
|
|
615
|
+
['audit_pending', auditPending], ['structure_finalization_pending', structureFinalizations],
|
|
616
|
+
['plan_notes', planNotes],
|
|
594
617
|
['coordination', coordination], ['parallel_candidates', parallelCandidates],
|
|
595
618
|
['member_heads', memberHeads],
|
|
596
619
|
]) enforceListLimit(name, value);
|
|
@@ -605,6 +628,9 @@ export function projectTodoStatus(readModel, options = undefined) {
|
|
|
605
628
|
// 監査待ちは`member.phases`だけから作った別の列で、dispatch(next_ready/dispatch_frontier)へは
|
|
606
629
|
// 影響しない。監査が進んでもfrontier_digestは動かない。
|
|
607
630
|
audit_pending: auditPending,
|
|
631
|
+
// 全task done後、構造finalizationが無い/古いplanだけを別列へ出す。監査状態と
|
|
632
|
+
// dispatch状態を混ぜず、terminal受理へ進めない理由と正規コマンドを同時に示す。
|
|
633
|
+
structure_finalization_pending: structureFinalizations,
|
|
608
634
|
// plan単位noteも同じく別の列である。noteの有無・件数はdispatchへ影響しないので、
|
|
609
635
|
// next_ready・dispatch_frontier・frontier_digestはnoteを書いても1バイトも動かない。
|
|
610
636
|
plan_notes: planNotes,
|