@takagaki/cortex-decisions-viewer 0.12.33 → 0.12.34

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/dist/build.js CHANGED
@@ -297,6 +297,16 @@ async function findProjectIcon(knowledgeRoot) {
297
297
  * ——「朝は正しいのに、開いて数秒すると表示が変わる」という一番気づきにくい食い違いになる。
298
298
  */
299
299
  const SECRET_SPACE_STATES = ["new", "legacy", "false"];
300
+ /**
301
+ * 使っているツールの種別と状態の語彙(engine の `project-tools.mjs` の
302
+ * TOOL_KINDS / TOOL_STATES と**同じ並び・同じ語**)。
303
+ *
304
+ * 🔴 **知らない語の行は落とす。** 画面は kind でアイコンを、state で表示語を引くので、
305
+ * 知らない語を通すと「アイコンも状態も無い行」が並ぶ——engine が語を増やした日に、
306
+ * 顧客の画面が壊れて見える。落とせばその行が出ないだけで、他の行は今までどおり出る。
307
+ */
308
+ const TOOL_KINDS = ["backlog", "github", "slack", "google-chat", "figma", "google-drive", "google-meet"];
309
+ const TOOL_STATES = ["reading", "waiting", "stopped", "unknown"];
300
310
  /**
301
311
  * リポジトリルートの fleet-status.json から externalSources / internalSources / pipelines を読む。
302
312
  * ファイルが無い・パース不能・当該フィールドが無い場合は undefined を返し、
@@ -476,8 +486,56 @@ async function readFleetSources(repoRoot) {
476
486
  applicable: typeof p.applicable === "boolean" ? p.applicable : undefined,
477
487
  }))
478
488
  : undefined;
479
- // **宣言(tools)は読まない(#125 ⑤)。** engine が出力から落としたので、
480
- // 残っているのは再生成前の古いスナップショットだけ。読まずに捨てる。
489
+ // 案件が使っているツール(engine が登録簿 7 種から算出・#275)。
490
+ //
491
+ // **同じ `tools` という名前で、昔は能力→ツールの「宣言マップ」が入っていた**(#125 ⑤ で
492
+ // engine が出力から落とした)。艦隊には再生成前の古いスナップショットが残っていて、
493
+ // そこには `{ 開発: "github" }` のような**版を持たない別の形**が入っている。
494
+ // だから **version で見分ける**——`version: 1` でなければ、宣言マップも未知の新形式も
495
+ // 同じく「読まない」に倒れる。
496
+ //
497
+ // **知らないフィールドは黙って捨てる(明示の許可リスト)。** この JSON は engine が
498
+ // 単独で版を上げるので、画面が知らないキーは必ず先に入る。素通しにすると、
499
+ // 認証情報の伏せ字処理を通していない値が顧客の画面へ黙って流れる。
500
+ let tools;
501
+ if (obj.tools != null) {
502
+ const t = typeof obj.tools === "object" && !Array.isArray(obj.tools) ? obj.tools : null;
503
+ if (!t || t.version !== 1) {
504
+ // **1 行だけ残す。** 静かに消えると「engine が出していない」のか
505
+ // 「出しているが画面が読めていない」のかが切り分けられない
506
+ console.warn(`fleet-status.json の tools が未対応の形です(使用ツールの一覧は出しません): version=${String(t ? t.version : typeof obj.tools)}`);
507
+ }
508
+ else if (!Array.isArray(t.items)) {
509
+ console.warn(`fleet-status.json の tools.items が配列ではありません(使用ツールの一覧は出しません): ${typeof t.items}`);
510
+ }
511
+ else {
512
+ const items = t.items
513
+ .filter((x) => x != null && typeof x === "object" && !Array.isArray(x))
514
+ .map((x) => ({
515
+ // **短縮記法にしないこと。** fleet-passthrough のテストが「画面が読むフィールドを
516
+ // build.ts が受け渡しているか」を `名前:` の字面で走査している
517
+ kind: String(x.kind ?? ""),
518
+ label: String(x.label ?? ""),
519
+ name: String(x.name ?? ""),
520
+ // 🔴 **url だけは null を保つ。** 「開ける先が無いと分かっている」(Google Meet)を
521
+ // 空文字に潰すと、画面が「名前もURLも空の行」と区別できなくなる
522
+ // **https?:// 以外は null に落とす。** fleet-status.json は案件リポにコミットされる
523
+ // ファイルなので、engine の導出以外(手編集・古い生成物)でも中身を書ける。
524
+ // `javascript:` のような値をそのまま href に載せない(el() の注意書きと同じ作法)
525
+ url: typeof x.url === "string" && /^https?:\/\//.test(x.url) ? x.url : null,
526
+ note: String(x.note ?? ""),
527
+ cap: String(x.cap ?? ""),
528
+ question: String(x.question ?? ""),
529
+ ref: String(x.ref ?? ""),
530
+ workspace: String(x.workspace ?? ""),
531
+ state: String(x.state ?? ""),
532
+ }))
533
+ .filter((x) => TOOL_KINDS.indexOf(x.kind) >= 0 && TOOL_STATES.indexOf(x.state) >= 0);
534
+ // **items が 0 件でも器は通す。** 「まだ 1 つも接続が無い」(接続の導線を出す)と
535
+ // 「この項目を知らない古い生成物」(何も出さない)は別の状態
536
+ tools = { version: 1, items: items, paragraph: String(t.paragraph ?? "") };
537
+ }
538
+ }
481
539
  // 能力ごとの導出済みの状態(#125①が載せ、#125②で画面の正本になった)。
482
540
  //
483
541
  // **列挙し忘れは静かに消える。** このマッピングは素通しではなく明示の許可リストで、
@@ -585,7 +643,7 @@ async function readFleetSources(repoRoot) {
585
643
  if (Object.keys(out).length)
586
644
  secretSpaces = out;
587
645
  }
588
- return { externalSources, slackWorkspaces, internalSources, pipelines, capabilities, fleetGeneratedAt, secretSources, secretSpaces, checks, githubOwners, backlogTarget };
646
+ return { externalSources, slackWorkspaces, internalSources, pipelines, capabilities, fleetGeneratedAt, secretSources, secretSpaces, checks, githubOwners, backlogTarget, tools };
589
647
  }
590
648
  /**
591
649
  * AIからの更新提案(`.cortex/gold-proposals.json`)を読む。
@@ -731,7 +789,10 @@ function isUnwritten(body) {
731
789
  *
732
790
  * @returns sections … 最上部に出す節(新形式・旧形式のどちらでも拾う)
733
791
  * unfilled … 案件の概要・ゴールが未記入か(警告カードを出すか)
734
- * restHtml … 自己記述と旧説明を除いた残りの本文(使用ツール等)
792
+ * restHtml … 自己記述と旧説明を除いた残りの本文(案件ごとに書き足された節)。
793
+ * 手書きの「使用ツール」節もここに残る——**落とすのは画面側**(#275)。
794
+ * engine が `tools` を載せている案件でだけ落とす判断なので、
795
+ * `SITE.tools` を知らないここでは決められない
735
796
  */
736
797
  async function buildHomeGrounding(rawMarkdown) {
737
798
  const content = (0, gray_matter_1.default)(rawMarkdown).content;
@@ -1837,6 +1898,9 @@ async function build(opts) {
1837
1898
  checks: fleet.checks,
1838
1899
  githubOwners: fleet.githubOwners,
1839
1900
  backlogTarget: fleet.backlogTarget,
1901
+ // 使っているツール(engine が登録簿から算出・#275)。**短縮記法にしないこと**
1902
+ // ——fleet-passthrough のテストが `名前:` の字面で受け渡しを走査している
1903
+ tools: fleet.tools,
1840
1904
  // **undefined ならキーごと出さない**——画面は
1841
1905
  // 「アイコンがある/無い」をキーの有無で見る(空の器を通さない)
1842
1906
  projectIcon: projectIcon ? { file: projectIcon.file, repoPath: projectIcon.repoPath } : undefined,
package/dist/render.js CHANGED
@@ -9,6 +9,8 @@ exports.isRepositorySlug = isRepositorySlug;
9
9
  exports.devRepositoryRefs = devRepositoryRefs;
10
10
  exports.claudeCodeRepositories = claudeCodeRepositories;
11
11
  exports.claudeCodeUrl = claudeCodeUrl;
12
+ exports.toolStateLabel = toolStateLabel;
13
+ exports.toolStateWithLive = toolStateWithLive;
12
14
  exports.isAlreadyRegistered = isAlreadyRegistered;
13
15
  exports.sanitizeRegisteredNow = sanitizeRegisteredNow;
14
16
  exports.sanitizeStateNow = sanitizeStateNow;
@@ -61,7 +63,9 @@ function renderSite(site, assets) {
61
63
  : "";
62
64
  // 組み立ての正本は claudeCodeUrl 1か所(#404)。ここは薄い包みに留める。
63
65
  // `?? []` は必須——build.ts は fleet-status.json が無い案件で externalSources を持たない
64
- const codeUrl = (prompt) => claudeCodeUrl(prompt, repoSlug, site.externalSources ?? []);
66
+ // 第4引数は使っているツールの段落(#275)。**7導線すべてに同じ段落を付ける**ため、
67
+ // 渡す場所も codeUrl 1か所に寄せる(クライアント側の codeUrl と対)
68
+ const codeUrl = (prompt) => claudeCodeUrl(prompt, repoSlug, site.externalSources ?? [], site.tools && site.tools.paragraph);
65
69
  /** ヘッダー「AIに聞く」の指示文。data-ai-prompt に同じ値を残し、接続状況が届いたら href を組み直す */
66
70
  const ASK_PROMPT = "このプロジェクトの概要について教えてください。";
67
71
  const askUrl = codeUrl(ASK_PROMPT);
@@ -452,12 +456,103 @@ function claudeCodeRepositories(projectRepo, devRefs) {
452
456
  *
453
457
  * repositories はカンマ区切りで**素のまま**載せる(公式の例がその形で、スラッシュも
454
458
  * エンコードしない)。指示文だけ encodeURIComponent する。
459
+ *
460
+ * `toolsParagraph`(#275)は「この案件が使っているツール: …」の 1 段落。非空なら
461
+ * **指示文の先頭に付ける**。7 導線すべてがこの 1 関数を通るので、ここに置くだけで
462
+ * どのボタンから開いても AI が最初に案件の道具立てを知る——**呼び出し側で足さないこと**
463
+ * (足すと、足し忘れたボタンだけ文脈の無い起動になる)。
464
+ * `data-ai-prompt` には元の指示文を残す(貼り替えもこの関数を通るので段落は保たれる)。
455
465
  */
456
- function claudeCodeUrl(prompt, projectRepo, externals) {
466
+ function claudeCodeUrl(prompt, projectRepo, externals, toolsParagraph) {
457
467
  var repos = claudeCodeRepositories(projectRepo, devRepositoryRefs(externals));
458
468
  if (!repos.length)
459
469
  return CLAUDE_CODE_HOME;
460
- return "https://claude.ai/code?repositories=" + repos.join(",") + "&prompt=" + encodeURIComponent(prompt);
470
+ var text = toolsParagraph ? String(toolsParagraph) + "\n\n" + prompt : prompt;
471
+ return "https://claude.ai/code?repositories=" + repos.join(",") + "&prompt=" + encodeURIComponent(text);
472
+ }
473
+ // ---------------------------------------------------------------------------
474
+ // Home の「使用ツール」(engine #275)— 一覧は engine が登録簿から算出した `SITE.tools`
475
+ //
476
+ // **ビューアは登録簿(channels.json 等)を読まない。** 読むと「使っていると言う条件」が
477
+ // engine と画面で 2 枚になり、片方だけ直った日から一覧と AI の起動文が食い違う
478
+ // (viewer/test/no-registry-read.test.mjs が見張っている)。
479
+ // ---------------------------------------------------------------------------
480
+ /**
481
+ * 種別のアイコン。**DS_TYPE(データソース一覧)と同じ絵柄にそろえる**——同じツールが
482
+ * 2 つの画面で違う顔をしていると、同じものだと分からない。
483
+ */
484
+ const TOOL_KIND_ICONS = {
485
+ "backlog": "📋",
486
+ "github": "🐙",
487
+ "slack": "💬",
488
+ "google-chat": "💬",
489
+ "figma": "🎨",
490
+ "google-drive": "📁",
491
+ "google-meet": "🎥",
492
+ };
493
+ /**
494
+ * 状態の表示語と色。**4 語しか出さない**(読み取り中/承認待ち/停止中/未確認)。
495
+ *
496
+ * 色は既存の `ds-gate-*` を流用する——同じ画面で「緑=読めている・橙=人の操作待ち・
497
+ * 灰=分からない」の意味を 2 通りに増やさない。
498
+ *
499
+ * **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す)。
500
+ */
501
+ function toolStateLabel(state) {
502
+ if (state === "reading")
503
+ return { text: "読み取り中", cls: "ds-gate-ok" };
504
+ if (state === "waiting")
505
+ return { text: "承認待ち", cls: "ds-gate-warn" };
506
+ // 止めているものは警告色にしない(直す必要が無いので、橙にすると直しに行かせてしまう)
507
+ if (state === "stopped")
508
+ return { text: "停止中", cls: "ds-gate-muted" };
509
+ return { text: "未確認", cls: "ds-gate-muted" };
510
+ }
511
+ /**
512
+ * ビルド時の状態に、接続状態ストアの実測(`stateNow`)を重ねた state を返す。
513
+ *
514
+ * 🔴 **`stopped` は上書きしない。** 止めているのは登録簿から決まる事実で、
515
+ * ストアが「届く」と答えても読み取っていない。**止めたものを「読み取り中」と言わない**。
516
+ *
517
+ * 🔴 **突き合わせの鍵は行が自分で持っている**(cap + question + ref、Slack だけ workspace)。
518
+ * ここで種別ごとの if 連鎖を書き直すと、engine の書き手(connection-state-write-*)と
519
+ * 鍵の作り方が 2 枚になる。**github だけ大小を無視する**——登録簿の owner/repo と
520
+ * ストアの ref は、人が貼った URL 由来で大小が揺れる。
521
+ *
522
+ * 行が無ければビルド時の値のまま(**「不明」に化けさせない**——夜間が測った結果を
523
+ * ライブが答えなかっただけで消すと、朝には出ていたものが消える)。
524
+ *
525
+ * **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す)。
526
+ */
527
+ function toolStateWithLive(item, rows) {
528
+ if (!item)
529
+ return "unknown";
530
+ if (item.state === "stopped")
531
+ return "stopped";
532
+ var hit = null;
533
+ (rows || []).forEach(function (r) {
534
+ if (hit || !r)
535
+ return;
536
+ if (r.cap !== item.cap || r.question !== item.question)
537
+ return;
538
+ var a = String(r.ref == null ? "" : r.ref);
539
+ var b = String(item.ref == null ? "" : item.ref);
540
+ if (item.cap === "dev" ? a.toLowerCase() !== b.toLowerCase() : a !== b)
541
+ return;
542
+ // チャンネルIDはワークスペースの中でしか一意でない(overlayStateOnFleet と同じ鍵)
543
+ if (item.cap === "chat" && String(r.workspace || "") !== String(item.workspace || ""))
544
+ return;
545
+ hit = r;
546
+ });
547
+ if (!hit)
548
+ return String(item.state || "unknown");
549
+ if (hit.status === "ok")
550
+ return "reading";
551
+ // **「測ったが分からなかった」は「承認待ち」に寄せない**(人が押す操作が無い)
552
+ if (hit.status === "unknown")
553
+ return "unknown";
554
+ // failed / skipped / not_in_channel / missing_scope / no_token … は全部「人の操作待ち」
555
+ return "waiting";
461
556
  }
462
557
  /**
463
558
  * 外部ソースの行が「この能力・このアダプタのタイル」に属するか。
@@ -2702,6 +2797,21 @@ button.site-icon[aria-busy="true"] .site-icon-plus { opacity: 0; }
2702
2797
  padding-bottom: 6px; border-bottom: 1px solid var(--border);
2703
2798
  }
2704
2799
  .home-grounding > .home-grounding-title:first-child { margin-top: 0; }
2800
+ /* ---- Home の「使用ツール」(engine が登録簿から算出・#275)----
2801
+ 自己記述の直後に出る一覧。**本文より目立たせない**——案件の説明を押しのけない。
2802
+ 状態チップは操作タブと同じ .ds-gate 系を使う(同じ意味の色を2通りに増やさない) */
2803
+ .home-tools { margin: 0 0 8px; }
2804
+ .home-tools-list { list-style: none; margin: 8px 0 0; padding: 0; }
2805
+ .home-tools-item {
2806
+ display: flex; flex-wrap: wrap; gap: 6px 12px; align-items: center;
2807
+ padding: 7px 0; border-bottom: 1px solid var(--border); font-size: 13.5px;
2808
+ }
2809
+ .home-tools-item:last-child { border-bottom: none; }
2810
+ /* 種別は幅をそろえて縦に読めるようにする(名前の頭が段違いだと一覧に見えない) */
2811
+ .home-tools-kind { flex: 0 0 auto; min-width: 9.5em; font-weight: 600; color: var(--muted); }
2812
+ .home-tools-name { flex: 1 1 220px; min-width: 0; word-break: break-all; }
2813
+ .home-tools-state { flex: 0 0 auto; }
2814
+ .home-tools-empty, .home-tools-note { font-size: 12.5px; color: var(--muted); line-height: 1.7; margin: 8px 0 0; }
2705
2815
  /* ---- レコード詳細の「AIによる最近の変更」(リポジトリの git 履歴 由来)----
2706
2816
  カードは詳細ページの提案(.proposals)と同系色にする——どちらも
2707
2817
  「AIがやったこと・言っていることを読む」もので種類が同じ。
@@ -3689,6 +3799,12 @@ var STATE_STATUSES = ${JSON.stringify(STATE_STATUSES)};
3689
3799
  var STATE_ROW_FIELDS = ${JSON.stringify(STATE_ROW_FIELDS)};
3690
3800
  ${sanitizeStateNow.toString()}
3691
3801
  ${overlayStateOnFleet.toString()}
3802
+ // Home の「使用ツール」(#275)。**判定と表示語の実体はビルド時の定義**——
3803
+ // 画面側に書き写すと、状態の言い換えが 2 枚になって片方だけ直る。
3804
+ // 🔴 この定数行を落とすと、流し込んだ描画がアイコンを引けずに素の kind を出す
3805
+ var TOOL_KIND_ICONS = ${JSON.stringify(TOOL_KIND_ICONS)};
3806
+ ${toolStateLabel.toString()}
3807
+ ${toolStateWithLive.toString()}
3692
3808
  ${nextStateDelay.toString()}
3693
3809
  // 工程1.5: 正本を直接読む(secretsNow / capabilitiesNow / githubOwners)。
3694
3810
  // **判定の実体はビルド時の定義**——画面側に書き写すと許可リストが2枚になる
@@ -4090,8 +4206,10 @@ ${deriveCapTile.toString()}
4090
4206
  // 🔴 **var MERGE_KEYS の宣言より上に codeUrl を呼ぶトップレベル文を足さないこと。**
4091
4207
  // fleetVal は MERGE_KEYS を読むが、var の初期化は宣言行に達するまで走らない
4092
4208
  // (関数宣言の巻き上げでは undefined を読む)。呼び出しは全て描画(route)以降に置く
4209
+ // 第4引数は使っているツールの段落(#275)。**焼き込んだ SITE から取る**——この段落は
4210
+ // engine が毎朝 fleet-status.json に載せるもので、ライブ(op=state)は運んでこない
4093
4211
  function codeUrl(prompt) {
4094
- return claudeCodeUrl(prompt, REPO_SLUG, fleetVal("externalSources") || []);
4212
+ return claudeCodeUrl(prompt, REPO_SLUG, fleetVal("externalSources") || [], SITE.tools && SITE.tools.paragraph);
4095
4213
  }
4096
4214
  /**
4097
4215
  * 🔴 **「1件も登録されていない」と「確かめられなかった」を分ける(#409)。**
@@ -5089,6 +5207,10 @@ ${deriveCapTile.toString()}
5089
5207
  var g = SITE.homeGrounding;
5090
5208
  if (g && g.unfilled) app.appendChild(homeUnfilledCard());
5091
5209
  if (g && g.sections.length) app.appendChild(homeGroundingSections(g.sections));
5210
+ // **使っているツールは自己記述の直後・本文の前**(#275)。案件の説明を読んだ流れで
5211
+ // 「どこに情報があるか」を見せる。中身は engine が登録簿から算出したもの
5212
+ var toolsSec = homeToolsSection();
5213
+ if (toolsSec) app.appendChild(toolsSec);
5092
5214
 
5093
5215
  var homeBody = el("div", { class: "body" });
5094
5216
  app.appendChild(homeBody);
@@ -5113,6 +5235,74 @@ ${deriveCapTile.toString()}
5113
5235
  return box;
5114
5236
  }
5115
5237
 
5238
+ /**
5239
+ * Home の「使用ツール」(#275)。engine が登録簿 7 種から算出した SITE.tools を描く。
5240
+ *
5241
+ * 🔴 **SITE.tools が無ければ何も出さない。** 生成が古い案件(この項目を知らない
5242
+ * fleet-status.json)では、手書きの節が本文に残っているので、そちらがそのまま出る
5243
+ * ——ここで「まだ接続していません」と出すと、繋いである案件に嘘を出すことになる。
5244
+ *
5245
+ * **0 件は 1 行で言い切る。** 空の一覧を出すと「読み込み中」に見える。
5246
+ */
5247
+ function homeToolsSection() {
5248
+ var t = SITE.tools;
5249
+ // build を通った値は必ず items を持つ。持たない形(renderSite 直渡し・壊れた埋め込み)は
5250
+ // 「この項目を知らない」と同じ扱いにして節ごと出さない
5251
+ if (!t || !Array.isArray(t.items)) return null;
5252
+ var box = el("section", { class: "home-tools" });
5253
+ box.appendChild(el("h2", { class: "home-grounding-title", text: "使用ツール" }));
5254
+ var items = t.items || [];
5255
+ if (!items.length) {
5256
+ box.appendChild(el("p", { class: "home-tools-empty", text: "まだツールを接続していません。操作タブから接続できます。" }));
5257
+ return box;
5258
+ }
5259
+ var ul = el("ul", { class: "home-tools-list" });
5260
+ items.forEach(function (it) {
5261
+ var li = el("li", { class: "home-tools-item" });
5262
+ // 種別は**ツール名**(能力名でも単位語でもない)。DS_TYPE の行の約束と同じ
5263
+ li.appendChild(el("span", {
5264
+ class: "home-tools-kind",
5265
+ text: (TOOL_KIND_ICONS[it.kind] || "🔗") + " " + (it.label || it.kind),
5266
+ }));
5267
+ var name = it.name || it.ref || "";
5268
+ // **開ける先が無い行(Google Meet)はリンクにしない。** 押せないリンクを出すより
5269
+ // 素のテキストのほうが正直(url は engine が null で「無い」と言い切っている)
5270
+ // href に載せる前に isHttp を通す(build 側の許可リストと二重の守り。el() の注意書きのとおり)
5271
+ if (it.url && isHttp(it.url)) li.appendChild(el("a", { class: "home-tools-name", href: it.url, target: "_blank", rel: "noopener", text: name }));
5272
+ else li.appendChild(el("span", { class: "home-tools-name", text: name }));
5273
+ // ライブの実測が届いていれば重ねる(stopped は上書きしない・toolStateWithLive)
5274
+ var st = toolStateWithLive(it, LIVE_STATE);
5275
+ var lab = toolStateLabel(st);
5276
+ var chip = el("span", { class: "home-tools-state ds-gate " + lab.cls, text: lab.text });
5277
+ // 状態そのものを属性に残す(表示語を読み替えずに検査・スタイルできるように)
5278
+ chip.setAttribute("data-state", st);
5279
+ li.appendChild(chip);
5280
+ ul.appendChild(li);
5281
+ });
5282
+ box.appendChild(ul);
5283
+ // **どこで直せるか・いつの情報かを 1 行で言う。** 操作タブで繋いだ直後は
5284
+ // この一覧に出ない(engine が毎朝作り直すまで)ので、黙っていると「効いていない」と読まれる
5285
+ box.appendChild(el("p", {
5286
+ class: "home-tools-note",
5287
+ text: "接続の追加・変更は操作タブで行います(この一覧は毎朝更新されます)。",
5288
+ }));
5289
+ return box;
5290
+ }
5291
+
5292
+ /**
5293
+ * 接続状況(op=state)が届いたときに「使用ツール」を描き直す(#275)。
5294
+ *
5295
+ * 🔴 **document を素で引かない(app.ownerDocument)。** 応答は画面が畳まれた後に
5296
+ * 届くことがある(syncAiLinks と同じ理由)。Home 以外のタブを開いていれば節そのものが
5297
+ * 無いので、何もせずに戻る
5298
+ */
5299
+ function syncHomeTools() {
5300
+ var old = app.ownerDocument.querySelector(".home-tools");
5301
+ if (!old || !old.parentNode) return;
5302
+ var next = homeToolsSection();
5303
+ if (next) old.parentNode.replaceChild(next, old);
5304
+ }
5305
+
5116
5306
  /**
5117
5307
  * 未記入の警告。**「書かれていないと何が起きるか」まで書く。**
5118
5308
  *
@@ -5160,7 +5350,13 @@ ${deriveCapTile.toString()}
5160
5350
  var kids = Array.prototype.slice.call(mount.children);
5161
5351
  var dropping = false;
5162
5352
  kids.forEach(function (n) {
5163
- if (n.tagName === "H2") dropping = !!HOME_DROP_HEADINGS[(n.textContent || "").trim()];
5353
+ // 🔴 **「使用ツール」は HOME_DROP_HEADINGS に入れない。** 落としてよいのは
5354
+ // 自動の一覧を出す案件(SITE.tools がある)だけ——engine が算出を載せていない
5355
+ // 案件で手書きの節まで消すと、**画面から使用ツールが丸ごと消える**(#275)
5356
+ if (n.tagName === "H2") {
5357
+ var h2 = (n.textContent || "").trim();
5358
+ dropping = !!HOME_DROP_HEADINGS[h2] || (h2 === "使用ツール" && !!SITE.tools);
5359
+ }
5164
5360
  // H2 より上位の見出しが来たら、そこで打ち切る(節の外まで消さない)
5165
5361
  else if (n.tagName === "H1") dropping = false;
5166
5362
  if (dropping && n.parentNode) n.parentNode.removeChild(n);
@@ -11257,6 +11453,8 @@ ${deriveCapTile.toString()}
11257
11453
  // 🔴 **後段の仕事より後ろに置く。** ここで投げると、図の復帰・取り直しタイマーが
11258
11454
  // 丸ごと落ちる(href を貼り替えるだけの処理に、そこまで巻き込ませない)
11259
11455
  syncAiLinks();
11456
+ // Home を開いていれば「使用ツール」の状態を実測で描き直す(#275)
11457
+ syncHomeTools();
11260
11458
  // 取得中に保存が効いていたら、迂回付きでもう一度取りに行く
11261
11459
  if (freshWanted) loadLiveFleet();
11262
11460
  })
@@ -11285,6 +11483,9 @@ ${deriveCapTile.toString()}
11285
11483
  // つまり**立ち上げ直後の案件=開発リポジトリを登録したばかりの案件**では、
11286
11484
  // この行だけがヘッダーの導線を追いつかせる。**後段の仕事より後ろに置く**(上と同じ理由)
11287
11485
  syncAiLinks();
11486
+ // 🔴 **ここでも「使用ツール」を描き直す(#275)。** 実測(stateNow)は fleet より
11487
+ // 先に読んでいるので、まとめ表を持たない案件でもこの経路で状態が確定する
11488
+ syncHomeTools();
11288
11489
  // 取得中に保存が効いていたら、迂回付きでもう一度取りに行く
11289
11490
  if (freshWanted) loadLiveFleet();
11290
11491
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.33",
3
+ "version": "0.12.34",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,