@quolu/lattice 0.78.0 → 0.78.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quolu/lattice",
3
- "version": "0.78.0",
3
+ "version": "0.78.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",
@@ -42,6 +42,11 @@ export function terminalPublicLabel(terminalId) {
42
42
  return `terminal-${digest.slice(0, LABEL_HEX_LENGTH)}`;
43
43
  }
44
44
 
45
+ /** Whether a recorded host name is a public label — the only shape a published page may show as-is (ADR 0198). */
46
+ export function isTerminalPublicLabel(value) {
47
+ return typeof value === 'string' && /^terminal-[0-9a-f]{12}$/u.test(value);
48
+ }
49
+
45
50
  /**
46
51
  * Synchronous twin of `readOrCreateBridgeHubTerminalId`: same file, same
47
52
  * schema, same no-overwrite publish. The todo CLI resolves its actor
@@ -5,8 +5,39 @@ import {
5
5
  renderTodoStructurePanel,
6
6
  } from './todo-gantt-structure.mjs';
7
7
  import { renderTodoMarkdown } from './todo-markdown-renderer.mjs';
8
+ import { isTerminalPublicLabel } from './terminal-identity.mjs';
8
9
 
9
- function renderNoteContext(context) {
10
+ /**
11
+ * 記録に残る端末名を、ページへ出す名前へ写す(ADR 0198)。
12
+ *
13
+ * journalと作業記録の`actor.host`は、書いた時の自由文である——OSのhost名のことも、席が自分で付けた名前の
14
+ * こともあり、持ち主の名前を含みうる。ページは公開面へ配信されるので、そのまま出さない。出してよいのは
15
+ * 端末の公開label(`terminal-`と16進12桁)だけで、それ以外は同じ名前に同じ番号を振った「端末N」にする。
16
+ * 番号はその名前が最初に記録された時刻の順で、同時刻は名前の順。記録は追記だけなので、後から現れた名前は
17
+ * 後ろの番号になり、既に振った番号は動かない。記録の本体は変えない——変えるのは表示だけである。
18
+ */
19
+ export function recordedHostDisplayNames(sections) {
20
+ const firstSeen = new Map();
21
+ for (const section of sections) {
22
+ for (const note of section.noteContext?.notes ?? []) {
23
+ const host = note.actor.host;
24
+ if (isTerminalPublicLabel(host)) continue;
25
+ const known = firstSeen.get(host);
26
+ if (known === undefined || note.recorded_at < known) firstSeen.set(host, note.recorded_at);
27
+ }
28
+ }
29
+ const ordered = [...firstSeen].sort(([leftHost, leftAt], [rightHost, rightAt]) => (
30
+ leftAt < rightAt ? -1 : leftAt > rightAt ? 1 : leftHost < rightHost ? -1 : leftHost > rightHost ? 1 : 0));
31
+ return new Map(ordered.map(([host], index) => [host, `端末${index + 1}`]));
32
+ }
33
+
34
+ function recordedHostDisplayName(host, displayNames) {
35
+ if (isTerminalPublicLabel(host)) return host;
36
+ // 対応表に無い名前を素通しにしない。表を作る側の漏れは、名前でなく固定の語として現れる。
37
+ return displayNames.get(host) ?? '端末';
38
+ }
39
+
40
+ function renderNoteContext(context, hostDisplayNames) {
10
41
  if (context === null) {
11
42
  return '<section class="work-log"><h2>作業記録</h2><p class="note-warning">作業記録を読み取れません。概要の読取警告を確認してください。</p></section>';
12
43
  }
@@ -21,7 +52,7 @@ function renderNoteContext(context) {
21
52
  ? `訂正済み(後継 ${note.superseded_by.slice(0, 12)}…)` : '現行';
22
53
  // UTCのまま埋め、閲覧者の現地時刻への変換はブラウザ側へ委ねる(title属性に原文を残す)。
23
54
  const stamp = `<time datetime="${escapeHtmlAttribute(note.recorded_at)}" title="${escapeHtmlAttribute(note.recorded_at)}" data-utc-stamp>${escapeHtmlText(note.recorded_at)}</time>`;
24
- 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>`;
55
+ 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(`${recordedHostDisplayName(note.actor.host, hostDisplayNames)}/${note.actor.agent}`)}</p></article>`;
25
56
  }).join('');
26
57
  const overflow = context.overflow_count === 0 ? ''
27
58
  : `<p class="note-warning">ほか ${context.overflow_count}件は上限のため省略。全履歴: <code>${escapeHtmlText(context.full_history_command)}</code></p>`;
@@ -144,6 +175,8 @@ export function renderRightPane(
144
175
  structurePresentation = null,
145
176
  ) {
146
177
  const lookup = presentationLookup(presentation);
178
+ // 番号はproject全体で1つの対応表から振る。工程ごとに振ると、同じ端末が工程によって別の番号になる。
179
+ const hostDisplayNames = recordedHostDisplayNames(sections);
147
180
  const sectionByKey = new Map(sections.map((section) => [refKey(section.ref), section]));
148
181
  const semanticNodes = [...layout.nodes, ...(layout.hierarchy_nodes ?? [])];
149
182
  const nodeByKey = new Map(semanticNodes.map((node) => [refKey(node.ref), node]));
@@ -212,7 +245,7 @@ export function renderRightPane(
212
245
  : expandable
213
246
  ? '<p class="fold-note">完走済みのため既定の図には描いていません。左上の「完走済み」バッジを押すと、このページ内で表示できます。</p>'
214
247
  : '<p class="fold-note">完走済みのため図には描いていません。図に出すには動的dashboardを <code>lattice todo gantt serve --port &lt;port&gt; --scope all</code> で起動してください。</p>';
215
- const workLog = notesEnabled ? renderNoteContext(section.noteContext) : '';
248
+ const workLog = notesEnabled ? renderNoteContext(section.noteContext, hostDisplayNames) : '';
216
249
  const designMemo = renderDesignMemo(section.task);
217
250
  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>`;
218
251
  }).join('');