@takagaki/cortex-decisions-viewer 0.12.33 → 0.12.35

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,9 @@ 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;
14
+ exports.toolsFoldSummary = toolsFoldSummary;
12
15
  exports.isAlreadyRegistered = isAlreadyRegistered;
13
16
  exports.sanitizeRegisteredNow = sanitizeRegisteredNow;
14
17
  exports.sanitizeStateNow = sanitizeStateNow;
@@ -61,7 +64,9 @@ function renderSite(site, assets) {
61
64
  : "";
62
65
  // 組み立ての正本は claudeCodeUrl 1か所(#404)。ここは薄い包みに留める。
63
66
  // `?? []` は必須——build.ts は fleet-status.json が無い案件で externalSources を持たない
64
- const codeUrl = (prompt) => claudeCodeUrl(prompt, repoSlug, site.externalSources ?? []);
67
+ // 第4引数は使っているツールの段落(#275)。**7導線すべてに同じ段落を付ける**ため、
68
+ // 渡す場所も codeUrl 1か所に寄せる(クライアント側の codeUrl と対)
69
+ const codeUrl = (prompt) => claudeCodeUrl(prompt, repoSlug, site.externalSources ?? [], site.tools && site.tools.paragraph);
65
70
  /** ヘッダー「AIに聞く」の指示文。data-ai-prompt に同じ値を残し、接続状況が届いたら href を組み直す */
66
71
  const ASK_PROMPT = "このプロジェクトの概要について教えてください。";
67
72
  const askUrl = codeUrl(ASK_PROMPT);
@@ -452,12 +457,138 @@ function claudeCodeRepositories(projectRepo, devRefs) {
452
457
  *
453
458
  * repositories はカンマ区切りで**素のまま**載せる(公式の例がその形で、スラッシュも
454
459
  * エンコードしない)。指示文だけ encodeURIComponent する。
460
+ *
461
+ * `toolsParagraph`(#275)は「この案件が使っているツール: …」の 1 段落。非空なら
462
+ * **指示文の先頭に付ける**。7 導線すべてがこの 1 関数を通るので、ここに置くだけで
463
+ * どのボタンから開いても AI が最初に案件の道具立てを知る——**呼び出し側で足さないこと**
464
+ * (足すと、足し忘れたボタンだけ文脈の無い起動になる)。
465
+ * `data-ai-prompt` には元の指示文を残す(貼り替えもこの関数を通るので段落は保たれる)。
455
466
  */
456
- function claudeCodeUrl(prompt, projectRepo, externals) {
467
+ function claudeCodeUrl(prompt, projectRepo, externals, toolsParagraph) {
457
468
  var repos = claudeCodeRepositories(projectRepo, devRepositoryRefs(externals));
458
469
  if (!repos.length)
459
470
  return CLAUDE_CODE_HOME;
460
- return "https://claude.ai/code?repositories=" + repos.join(",") + "&prompt=" + encodeURIComponent(prompt);
471
+ var text = toolsParagraph ? String(toolsParagraph) + "\n\n" + prompt : prompt;
472
+ return "https://claude.ai/code?repositories=" + repos.join(",") + "&prompt=" + encodeURIComponent(text);
473
+ }
474
+ // ---------------------------------------------------------------------------
475
+ // Home の「使用ツール」(engine #275)— 一覧は engine が登録簿から算出した `SITE.tools`
476
+ //
477
+ // **ビューアは登録簿(channels.json 等)を読まない。** 読むと「使っていると言う条件」が
478
+ // engine と画面で 2 枚になり、片方だけ直った日から一覧と AI の起動文が食い違う
479
+ // (viewer/test/no-registry-read.test.mjs が見張っている)。
480
+ // ---------------------------------------------------------------------------
481
+ /**
482
+ * 種別のアイコン。**DS_TYPE(データソース一覧)と同じ絵柄にそろえる**——同じツールが
483
+ * 2 つの画面で違う顔をしていると、同じものだと分からない。
484
+ */
485
+ const TOOL_KIND_ICONS = {
486
+ "backlog": "📋",
487
+ "github": "🐙",
488
+ "slack": "💬",
489
+ "google-chat": "💬",
490
+ "figma": "🎨",
491
+ "google-drive": "📁",
492
+ "google-meet": "🎥",
493
+ };
494
+ /**
495
+ * 状態の表示語と色。**4 語しか出さない**(読み取り中/承認待ち/停止中/未確認)。
496
+ *
497
+ * 色は既存の `ds-gate-*` を流用する——同じ画面で「緑=読めている・橙=人の操作待ち・
498
+ * 灰=分からない」の意味を 2 通りに増やさない。
499
+ *
500
+ * **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す)。
501
+ */
502
+ function toolStateLabel(state) {
503
+ if (state === "reading")
504
+ return { text: "読み取り中", cls: "ds-gate-ok" };
505
+ if (state === "waiting")
506
+ return { text: "承認待ち", cls: "ds-gate-warn" };
507
+ // 止めているものは警告色にしない(直す必要が無いので、橙にすると直しに行かせてしまう)
508
+ if (state === "stopped")
509
+ return { text: "停止中", cls: "ds-gate-muted" };
510
+ return { text: "未確認", cls: "ds-gate-muted" };
511
+ }
512
+ /**
513
+ * ビルド時の状態に、接続状態ストアの実測(`stateNow`)を重ねた state を返す。
514
+ *
515
+ * 🔴 **`stopped` は上書きしない。** 止めているのは登録簿から決まる事実で、
516
+ * ストアが「届く」と答えても読み取っていない。**止めたものを「読み取り中」と言わない**。
517
+ *
518
+ * 🔴 **突き合わせの鍵は行が自分で持っている**(cap + question + ref、Slack だけ workspace)。
519
+ * ここで種別ごとの if 連鎖を書き直すと、engine の書き手(connection-state-write-*)と
520
+ * 鍵の作り方が 2 枚になる。**github だけ大小を無視する**——登録簿の owner/repo と
521
+ * ストアの ref は、人が貼った URL 由来で大小が揺れる。
522
+ *
523
+ * 行が無ければビルド時の値のまま(**「不明」に化けさせない**——夜間が測った結果を
524
+ * ライブが答えなかっただけで消すと、朝には出ていたものが消える)。
525
+ *
526
+ * **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す)。
527
+ */
528
+ function toolStateWithLive(item, rows) {
529
+ if (!item)
530
+ return "unknown";
531
+ if (item.state === "stopped")
532
+ return "stopped";
533
+ var hit = null;
534
+ (rows || []).forEach(function (r) {
535
+ if (hit || !r)
536
+ return;
537
+ if (r.cap !== item.cap || r.question !== item.question)
538
+ return;
539
+ var a = String(r.ref == null ? "" : r.ref);
540
+ var b = String(item.ref == null ? "" : item.ref);
541
+ if (item.cap === "dev" ? a.toLowerCase() !== b.toLowerCase() : a !== b)
542
+ return;
543
+ // チャンネルIDはワークスペースの中でしか一意でない(overlayStateOnFleet と同じ鍵)
544
+ if (item.cap === "chat" && String(r.workspace || "") !== String(item.workspace || ""))
545
+ return;
546
+ hit = r;
547
+ });
548
+ if (!hit)
549
+ return String(item.state || "unknown");
550
+ if (hit.status === "ok")
551
+ return "reading";
552
+ // **「測ったが分からなかった」は「承認待ち」に寄せない**(人が押す操作が無い)
553
+ if (hit.status === "unknown")
554
+ return "unknown";
555
+ // failed / skipped / not_in_channel / missing_scope / no_token … は全部「人の操作待ち」
556
+ return "waiting";
557
+ }
558
+ /**
559
+ * 畳んだ種別の 1 行に出す、状態の要約(#424)。
560
+ *
561
+ * **全部同じなら、その 1 語だけ**(「GitHub 7 件 ● 読み取り中」)。畳んだ 1 行に
562
+ * 「読み取り中 7」と件数を書いても、左に出ている 7 件と同じ数を 2 回言うだけになる。
563
+ *
564
+ * **混ざっていれば件数で**(「読み取り中 5・承認待ち 2」)。ここで「一部が承認待ち」の
565
+ * ような曖昧な言い方にすると、開くまで何件手を入れるのか分からない。
566
+ *
567
+ * 色は `toolStateLabel` の `ds-gate-*` をそのまま使う。混ざっているときは
568
+ * **承認待ちが 1 件でもあれば橙**——畳んだ 1 行が「人の操作待ちを含む」ことを、
569
+ * 開かずに見つけられる必要がある(畳んだせいで気づかれなくなる、が一番痛い)。
570
+ *
571
+ * **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す)。
572
+ */
573
+ function toolsFoldSummary(states) {
574
+ // 表示の順は状態の 4 語と同じ(読み取り中→承認待ち→停止中→未確認)
575
+ var order = ["reading", "waiting", "stopped", "unknown"];
576
+ var count = { reading: 0, waiting: 0, stopped: 0, unknown: 0 };
577
+ (states || []).forEach(function (s) {
578
+ // ⚠ 4 語のどれかに当たるかは **order で見る**(素のオブジェクトの添字だと
579
+ // "constructor" のような語が継承プロパティに当たって、知らない語が素通りする)
580
+ var k = String(s == null ? "" : s);
581
+ count[order.indexOf(k) < 0 ? "unknown" : k] += 1;
582
+ });
583
+ var used = order.filter(function (k) { return count[k] > 0; });
584
+ if (used.length <= 1) {
585
+ var lab = toolStateLabel(used[0] || "unknown");
586
+ return { text: lab.text, cls: lab.cls, state: used[0] || "unknown" };
587
+ }
588
+ var text = used
589
+ .map(function (k) { return toolStateLabel(k).text + " " + count[k]; })
590
+ .join("・");
591
+ return { text: text, cls: count.waiting > 0 ? "ds-gate-warn" : "ds-gate-muted", state: "mixed" };
461
592
  }
462
593
  /**
463
594
  * 外部ソースの行が「この能力・このアダプタのタイル」に属するか。
@@ -2702,6 +2833,49 @@ button.site-icon[aria-busy="true"] .site-icon-plus { opacity: 0; }
2702
2833
  padding-bottom: 6px; border-bottom: 1px solid var(--border);
2703
2834
  }
2704
2835
  .home-grounding > .home-grounding-title:first-child { margin-top: 0; }
2836
+ /* ---- Home の「使用ツール」(engine が登録簿から算出・#275)----
2837
+ 自己記述の直後に出る一覧。**本文より目立たせない**——案件の説明を押しのけない。
2838
+ 状態チップは操作タブと同じ .ds-gate 系を使う(同じ意味の色を2通りに増やさない) */
2839
+ .home-tools { margin: 0 0 8px; }
2840
+ .home-tools-list { list-style: none; margin: 8px 0 0; padding: 0; }
2841
+ .home-tools-item {
2842
+ display: flex; flex-wrap: wrap; gap: 6px 12px; align-items: center;
2843
+ padding: 7px 0; border-bottom: 1px solid var(--border); font-size: 13.5px;
2844
+ }
2845
+ .home-tools-item:last-child { border-bottom: none; }
2846
+ /* 種別は幅をそろえて縦に読めるようにする(名前の頭が段違いだと一覧に見えない) */
2847
+ .home-tools-kind { flex: 0 0 auto; min-width: 9.5em; font-weight: 600; color: var(--muted); }
2848
+ .home-tools-name { flex: 1 1 220px; min-width: 0; word-break: break-all; }
2849
+ .home-tools-state { flex: 0 0 auto; }
2850
+ .home-tools-empty, .home-tools-note { font-size: 12.5px; color: var(--muted); line-height: 1.7; margin: 8px 0 0; }
2851
+ /* 種別ごとの折りたたみ(3件以上・#424)。**畳んだ1行は、行と同じ高さ・同じ字の大きさ**
2852
+ ——一覧の途中に「見出し」が生えたように見せない。既定は畳む(open を付けない) */
2853
+ .home-tools-fold { margin: 8px 0 0; }
2854
+ .home-tools-fold > summary {
2855
+ display: flex; flex-wrap: wrap; gap: 6px 12px; align-items: center;
2856
+ padding: 7px 0; border-bottom: 1px solid var(--border); font-size: 13.5px;
2857
+ cursor: pointer; list-style: none;
2858
+ }
2859
+ .home-tools-fold > summary::-webkit-details-marker { display: none; }
2860
+ /* アイコンは別の span(読み上げから外すため)。畳まない行の種別と同じ字面にそろえる */
2861
+ .home-tools-fold-icon { flex: 0 0 auto; }
2862
+ .home-tools-fold-label { flex: 0 0 auto; font-weight: 600; color: var(--muted); }
2863
+ /* 状態は行と同じく右端へ寄せる。**縦にそろっていないと一覧に見えない**
2864
+ (畳んだ1行だけ状態が真ん中にあると、同じ列の情報だと分からない) */
2865
+ .home-tools-fold-state { flex: 0 0 auto; margin-left: auto; }
2866
+ /* 開閉の印は CSS の content で出す(.ds-group と同じ形)。**要素にしない**——
2867
+ 読み上げ名を「GitHub 7 件」だけにしたいので、文字として DOM に置かない */
2868
+ .home-tools-fold > summary::after { content: "▸"; margin-left: 8px; color: var(--muted); font-size: 12px; }
2869
+ .home-tools-fold[open] > summary::after { content: "▾"; }
2870
+ /* 開いた中身は少し寄せて、畳んだ1行との親子が見えるようにする */
2871
+ .home-tools-fold > .home-tools-list { margin: 0 0 0 12px; padding-left: 10px; border-left: 2px solid var(--border); }
2872
+ /* 畳んだ1行が後ろに続くとき・開いた中身の後ろに行が続くときは、その手前の行にも
2873
+ 区切り線を引く。.home-tools-item:last-child で線を落としたままだと、**境目だけ
2874
+ 線が抜けて** 2行が1行に見える(畳みが無い案件の見た目は変えない)。
2875
+ :has が効かない環境では、その境目の線が1本消えるだけ(並びも文言も無事) */
2876
+ .home-tools-list:has(+ .home-tools-fold) > .home-tools-item:last-child,
2877
+ .home-tools-fold:has(+ .home-tools-list) > .home-tools-list > .home-tools-item:last-child,
2878
+ .home-tools-fold:has(+ .home-tools-fold) > .home-tools-list > .home-tools-item:last-child { border-bottom: 1px solid var(--border); }
2705
2879
  /* ---- レコード詳細の「AIによる最近の変更」(リポジトリの git 履歴 由来)----
2706
2880
  カードは詳細ページの提案(.proposals)と同系色にする——どちらも
2707
2881
  「AIがやったこと・言っていることを読む」もので種類が同じ。
@@ -3689,6 +3863,18 @@ var STATE_STATUSES = ${JSON.stringify(STATE_STATUSES)};
3689
3863
  var STATE_ROW_FIELDS = ${JSON.stringify(STATE_ROW_FIELDS)};
3690
3864
  ${sanitizeStateNow.toString()}
3691
3865
  ${overlayStateOnFleet.toString()}
3866
+ // Home の「使用ツール」(#275)。**判定と表示語の実体はビルド時の定義**——
3867
+ // 画面側に書き写すと、状態の言い換えが 2 枚になって片方だけ直る。
3868
+ // 🔴 この定数行を落とすと、流し込んだ描画がアイコンを引けずに素の kind を出す
3869
+ var TOOL_KIND_ICONS = ${JSON.stringify(TOOL_KIND_ICONS)};
3870
+ ${toolStateLabel.toString()}
3871
+ ${toolStateWithLive.toString()}
3872
+ // 畳んだ種別の 1 行に出す状態の要約(#424)。**同じ 4 語を使う**——ここで別の言い方を
3873
+ // 作ると、畳んだ 1 行と開いた行が違う語で同じ状態を指す
3874
+ ${toolsFoldSummary.toString()}
3875
+ // 🔴 **同じ種別が何件あったら畳むか。** 1 にすると 1 件の種別まで畳まれて、
3876
+ // Backlog・Drive・Meet が「1 件 ▸」の行に化ける(#424 で明示的に避けた形)
3877
+ var TOOLS_FOLD_MIN = 3;
3692
3878
  ${nextStateDelay.toString()}
3693
3879
  // 工程1.5: 正本を直接読む(secretsNow / capabilitiesNow / githubOwners)。
3694
3880
  // **判定の実体はビルド時の定義**——画面側に書き写すと許可リストが2枚になる
@@ -4090,8 +4276,10 @@ ${deriveCapTile.toString()}
4090
4276
  // 🔴 **var MERGE_KEYS の宣言より上に codeUrl を呼ぶトップレベル文を足さないこと。**
4091
4277
  // fleetVal は MERGE_KEYS を読むが、var の初期化は宣言行に達するまで走らない
4092
4278
  // (関数宣言の巻き上げでは undefined を読む)。呼び出しは全て描画(route)以降に置く
4279
+ // 第4引数は使っているツールの段落(#275)。**焼き込んだ SITE から取る**——この段落は
4280
+ // engine が毎朝 fleet-status.json に載せるもので、ライブ(op=state)は運んでこない
4093
4281
  function codeUrl(prompt) {
4094
- return claudeCodeUrl(prompt, REPO_SLUG, fleetVal("externalSources") || []);
4282
+ return claudeCodeUrl(prompt, REPO_SLUG, fleetVal("externalSources") || [], SITE.tools && SITE.tools.paragraph);
4095
4283
  }
4096
4284
  /**
4097
4285
  * 🔴 **「1件も登録されていない」と「確かめられなかった」を分ける(#409)。**
@@ -5089,6 +5277,10 @@ ${deriveCapTile.toString()}
5089
5277
  var g = SITE.homeGrounding;
5090
5278
  if (g && g.unfilled) app.appendChild(homeUnfilledCard());
5091
5279
  if (g && g.sections.length) app.appendChild(homeGroundingSections(g.sections));
5280
+ // **使っているツールは自己記述の直後・本文の前**(#275)。案件の説明を読んだ流れで
5281
+ // 「どこに情報があるか」を見せる。中身は engine が登録簿から算出したもの
5282
+ var toolsSec = homeToolsSection();
5283
+ if (toolsSec) app.appendChild(toolsSec);
5092
5284
 
5093
5285
  var homeBody = el("div", { class: "body" });
5094
5286
  app.appendChild(homeBody);
@@ -5113,6 +5305,169 @@ ${deriveCapTile.toString()}
5113
5305
  return box;
5114
5306
  }
5115
5307
 
5308
+ /**
5309
+ * Home の「使用ツール」(#275)。engine が登録簿 7 種から算出した SITE.tools を描く。
5310
+ *
5311
+ * 🔴 **SITE.tools が無ければ何も出さない。** 生成が古い案件(この項目を知らない
5312
+ * fleet-status.json)では、手書きの節が本文に残っているので、そちらがそのまま出る
5313
+ * ——ここで「まだ接続していません」と出すと、繋いである案件に嘘を出すことになる。
5314
+ *
5315
+ * **0 件は 1 行で言い切る。** 空の一覧を出すと「読み込み中」に見える。
5316
+ */
5317
+ function homeToolsSection() {
5318
+ var t = SITE.tools;
5319
+ // build を通った値は必ず items を持つ。持たない形(renderSite 直渡し・壊れた埋め込み)は
5320
+ // 「この項目を知らない」と同じ扱いにして節ごと出さない
5321
+ if (!t || !Array.isArray(t.items)) return null;
5322
+ var box = el("section", { class: "home-tools" });
5323
+ box.appendChild(el("h2", { class: "home-grounding-title", text: "使用ツール" }));
5324
+ var items = t.items || [];
5325
+ if (!items.length) {
5326
+ box.appendChild(el("p", { class: "home-tools-empty", text: "まだツールを接続していません。操作タブから接続できます。" }));
5327
+ return box;
5328
+ }
5329
+ // **種別ごとにまとめる(#424)。**
5330
+ //
5331
+ // 🔴 **隣り合う同じ種別だけをまとめる**(前から順に見て、種別が変わったら切る)。
5332
+ // 種別ごとに拾い集める形にすると、engine が種別を飛び飛びに出した日に
5333
+ // **並び順が変わる**——「並び順は変えない」は仕様なので、engine の出力の性質
5334
+ // (いまは種別ごとに連続している)に寄りかからず、構造として変わらない形にする
5335
+ var groups = [];
5336
+ items.forEach(function (it) {
5337
+ var k = String(it && it.kind != null ? it.kind : "");
5338
+ var last = groups[groups.length - 1];
5339
+ if (last && last.kind === k) last.items.push(it);
5340
+ else groups.push({ kind: k, items: [it] });
5341
+ });
5342
+ // 畳まない種別は 1 つの <ul> に続けて入れる(1〜2 件の種別のあいだに区切り線が
5343
+ // 入らない、いまの見た目のまま)。畳みを挟んだら次の種別から新しい <ul> にする
5344
+ var ul = null;
5345
+ var foldSeq = 0;
5346
+ groups.forEach(function (g) {
5347
+ if (g.items.length >= TOOLS_FOLD_MIN) {
5348
+ ul = null;
5349
+ // 連番を渡す。**同じ種別が離れて 2 度出ると、種別だけでは名前がぶつかる**
5350
+ // (id が重複し、開閉の復元も両方に効く)
5351
+ box.appendChild(homeToolsFold(g.items, foldSeq++));
5352
+ return;
5353
+ }
5354
+ if (!ul) { ul = el("ul", { class: "home-tools-list" }); box.appendChild(ul); }
5355
+ g.items.forEach(function (it) { ul.appendChild(homeToolRow(it)); });
5356
+ });
5357
+ // **どこで直せるか・いつの情報かを 1 行で言う。** 操作タブで繋いだ直後は
5358
+ // この一覧に出ない(engine が毎朝作り直すまで)ので、黙っていると「効いていない」と読まれる
5359
+ box.appendChild(el("p", {
5360
+ class: "home-tools-note",
5361
+ text: "接続の追加・変更は操作タブで行います(この一覧は毎朝更新されます)。",
5362
+ }));
5363
+ return box;
5364
+ }
5365
+
5366
+ /**
5367
+ * 使用ツールの 1 行。**畳んだ種別の中でも同じ形**(開いたときに見えるのは今までの行)。
5368
+ */
5369
+ function homeToolRow(it) {
5370
+ var li = el("li", { class: "home-tools-item" });
5371
+ // 種別は**ツール名**(能力名でも単位語でもない)。DS_TYPE の行の約束と同じ
5372
+ li.appendChild(el("span", {
5373
+ class: "home-tools-kind",
5374
+ text: (TOOL_KIND_ICONS[it.kind] || "🔗") + " " + (it.label || it.kind),
5375
+ }));
5376
+ var name = it.name || it.ref || "";
5377
+ // **開ける先が無い行(Google Meet)はリンクにしない。** 押せないリンクを出すより
5378
+ // 素のテキストのほうが正直(url は engine が null で「無い」と言い切っている)
5379
+ // href に載せる前に isHttp を通す(build 側の許可リストと二重の守り。el() の注意書きのとおり)
5380
+ if (it.url && isHttp(it.url)) li.appendChild(el("a", { class: "home-tools-name", href: it.url, target: "_blank", rel: "noopener", text: name }));
5381
+ else li.appendChild(el("span", { class: "home-tools-name", text: name }));
5382
+ // ライブの実測が届いていれば重ねる(stopped は上書きしない・toolStateWithLive)
5383
+ var st = toolStateWithLive(it, LIVE_STATE);
5384
+ var lab = toolStateLabel(st);
5385
+ var chip = el("span", { class: "home-tools-state ds-gate " + lab.cls, text: lab.text });
5386
+ // 状態そのものを属性に残す(表示語を読み替えずに検査・スタイルできるように)
5387
+ chip.setAttribute("data-state", st);
5388
+ li.appendChild(chip);
5389
+ return li;
5390
+ }
5391
+
5392
+ /**
5393
+ * 同じ種別が 3 件以上あるときの、畳んだ 1 行(#424)。
5394
+ *
5395
+ * 🔴 **既定は畳む(open 属性を付けない)。** GitHub 7 件・Slack 11 件が縦に並ぶと、
5396
+ * その下の本文が画面の外へ押し出される。件数と状態の要約は畳んだまま読めるので、
5397
+ * 「何件つながっていて、手を入れる先があるか」は開かなくても分かる。
5398
+ * 開閉は**保存しない**(開き直すと畳んだ状態に戻る・portal #77 と同じ)
5399
+ *
5400
+ * 🔴 **行は畳んでも DOM に残す**(details の中に置くだけ)。描き直し(syncHomeTools)で
5401
+ * 開閉を戻せるのも、検査が行数を数えられるのも、行がそこにあるからで、
5402
+ * **作り直す形にすると開いている最中に中身が消える**
5403
+ */
5404
+ function homeToolsFold(group, seq) {
5405
+ var first = group[0] || {};
5406
+ var kind = String(first.kind == null ? "" : first.kind);
5407
+ // 畳み 1 つを指す鍵。**種別だけにしない**——同じ種別が離れて 2 度出たときに
5408
+ // id が重複し、開閉の復元も両方に効いてしまう
5409
+ var key = (kind || "other") + "-" + seq;
5410
+ // 状態は 1 行ずつ求めてから畳む(行と要約が別の判定を持たない)
5411
+ var sum = toolsFoldSummary(group.map(function (it) { return toolStateWithLive(it, LIVE_STATE); }));
5412
+ var chip = el("span", { class: "home-tools-fold-state ds-gate " + sum.cls, text: sum.text });
5413
+ chip.setAttribute("data-state", sum.state);
5414
+ // 説明として読ませるので id が要る
5415
+ var chipId = "home-tools-fold-state-" + key;
5416
+ chip.setAttribute("id", chipId);
5417
+ // 件数まで含めて 1 つのテキストノードにする(分けると読み上げが途切れ、
5418
+ // 文言の完全一致で拾えなくなる。portal #77 と同じ約束)。
5419
+ // ⚠ **ツール名はまとまりの中から拾う**(先頭だけ見ると、engine が先頭の 1 件の
5420
+ // label を落とした日に「github 3 件」と内部名が出る)
5421
+ var named = null;
5422
+ group.forEach(function (it) { if (!named && it && it.label) named = it; });
5423
+ var label = ((named || first).label || first.kind || "その他") + " " + group.length + " 件";
5424
+ var sums = el("summary", { class: "home-tools-fold-summary" }, [
5425
+ // アイコンは読み上げから外す(「たこ」と読まれても種別は分からない)
5426
+ el("span", { class: "home-tools-fold-icon", "aria-hidden": "true", text: TOOL_KIND_ICONS[first.kind] || "🔗" }),
5427
+ el("span", { class: "home-tools-fold-label", text: label }),
5428
+ chip,
5429
+ ]);
5430
+ // ⚠ **読み上げ名は「GitHub 7 件」**(aria-label で言い切る)。状態の要約は
5431
+ // aria-describedby で**説明として**読ませる——チップを aria-hidden にすると、
5432
+ // 畳んでいるあいだ(中身は支援技術からも見えない)「承認待ちが 2 件ある」ことが
5433
+ // 目で見える人にしか伝わらない。それは畳みを橙にした理由と正面から矛盾する
5434
+ sums.setAttribute("aria-label", label);
5435
+ sums.setAttribute("aria-describedby", chipId);
5436
+ var ul = el("ul", { class: "home-tools-list" });
5437
+ group.forEach(function (it) { ul.appendChild(homeToolRow(it)); });
5438
+ // data-fold-key は「描き直しても開いたままにする」ための目印(syncHomeTools が見る)。
5439
+ // data-kind は種別そのもの(畳んだ 1 行がどのツールのものか、を素直に読めるように)
5440
+ return el("details", { class: "home-tools-fold", "data-kind": kind, "data-fold-key": key }, [sums, ul]);
5441
+ }
5442
+
5443
+ /**
5444
+ * 接続状況(op=state)が届いたときに「使用ツール」を描き直す(#275)。
5445
+ *
5446
+ * 🔴 **document を素で引かない(app.ownerDocument)。** 応答は画面が畳まれた後に
5447
+ * 届くことがある(syncAiLinks と同じ理由)。Home 以外のタブを開いていれば節そのものが
5448
+ * 無いので、何もせずに戻る
5449
+ */
5450
+ function syncHomeTools() {
5451
+ var old = app.ownerDocument.querySelector(".home-tools");
5452
+ if (!old || !old.parentNode) return;
5453
+ var next = homeToolsSection();
5454
+ if (!next) return;
5455
+ // 🔴 **人が開いた折りたたみは、描き直しても開いたままにする(#424)。**
5456
+ // 実測は開いた直後に届くことがあり、そこで畳み直すと「押したのに閉じた」になる
5457
+ // (portal #77 で踏んだのと同じ罠)。**保存はしない**——開き直せば畳んだ状態に戻る
5458
+ var opened = {};
5459
+ var before = old.querySelectorAll(".home-tools-fold");
5460
+ for (var i = 0; i < before.length; i++) {
5461
+ // 素のオブジェクトの添字にしない("constructor" のような鍵が継承側に当たる)
5462
+ if (before[i].open) opened["k:" + (before[i].getAttribute("data-fold-key") || "")] = true;
5463
+ }
5464
+ var after = next.querySelectorAll(".home-tools-fold");
5465
+ for (var j = 0; j < after.length; j++) {
5466
+ if (opened["k:" + (after[j].getAttribute("data-fold-key") || "")]) after[j].open = true;
5467
+ }
5468
+ old.parentNode.replaceChild(next, old);
5469
+ }
5470
+
5116
5471
  /**
5117
5472
  * 未記入の警告。**「書かれていないと何が起きるか」まで書く。**
5118
5473
  *
@@ -5160,7 +5515,13 @@ ${deriveCapTile.toString()}
5160
5515
  var kids = Array.prototype.slice.call(mount.children);
5161
5516
  var dropping = false;
5162
5517
  kids.forEach(function (n) {
5163
- if (n.tagName === "H2") dropping = !!HOME_DROP_HEADINGS[(n.textContent || "").trim()];
5518
+ // 🔴 **「使用ツール」は HOME_DROP_HEADINGS に入れない。** 落としてよいのは
5519
+ // 自動の一覧を出す案件(SITE.tools がある)だけ——engine が算出を載せていない
5520
+ // 案件で手書きの節まで消すと、**画面から使用ツールが丸ごと消える**(#275)
5521
+ if (n.tagName === "H2") {
5522
+ var h2 = (n.textContent || "").trim();
5523
+ dropping = !!HOME_DROP_HEADINGS[h2] || (h2 === "使用ツール" && !!SITE.tools);
5524
+ }
5164
5525
  // H2 より上位の見出しが来たら、そこで打ち切る(節の外まで消さない)
5165
5526
  else if (n.tagName === "H1") dropping = false;
5166
5527
  if (dropping && n.parentNode) n.parentNode.removeChild(n);
@@ -11257,6 +11618,8 @@ ${deriveCapTile.toString()}
11257
11618
  // 🔴 **後段の仕事より後ろに置く。** ここで投げると、図の復帰・取り直しタイマーが
11258
11619
  // 丸ごと落ちる(href を貼り替えるだけの処理に、そこまで巻き込ませない)
11259
11620
  syncAiLinks();
11621
+ // Home を開いていれば「使用ツール」の状態を実測で描き直す(#275)
11622
+ syncHomeTools();
11260
11623
  // 取得中に保存が効いていたら、迂回付きでもう一度取りに行く
11261
11624
  if (freshWanted) loadLiveFleet();
11262
11625
  })
@@ -11285,6 +11648,9 @@ ${deriveCapTile.toString()}
11285
11648
  // つまり**立ち上げ直後の案件=開発リポジトリを登録したばかりの案件**では、
11286
11649
  // この行だけがヘッダーの導線を追いつかせる。**後段の仕事より後ろに置く**(上と同じ理由)
11287
11650
  syncAiLinks();
11651
+ // 🔴 **ここでも「使用ツール」を描き直す(#275)。** 実測(stateNow)は fleet より
11652
+ // 先に読んでいるので、まとめ表を持たない案件でもこの経路で状態が確定する
11653
+ syncHomeTools();
11288
11654
  // 取得中に保存が効いていたら、迂回付きでもう一度取りに行く
11289
11655
  if (freshWanted) loadLiveFleet();
11290
11656
  });
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.35",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,