@takagaki/cortex-decisions-viewer 0.12.57 → 0.12.59

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.
Files changed (2) hide show
  1. package/dist/render.js +713 -63
  2. package/package.json +1 -1
package/dist/render.js CHANGED
@@ -10,7 +10,7 @@ exports.devRepositoryRefs = devRepositoryRefs;
10
10
  exports.claudeCodeRepositories = claudeCodeRepositories;
11
11
  exports.claudeCodeUrl = claudeCodeUrl;
12
12
  exports.toolStateLabel = toolStateLabel;
13
- exports.toolStateWithLive = toolStateWithLive;
13
+ exports.regRowOfToolItem = regRowOfToolItem;
14
14
  exports.toolsFoldSummary = toolsFoldSummary;
15
15
  exports.isAlreadyRegistered = isAlreadyRegistered;
16
16
  exports.sanitizeRegisteredNow = sanitizeRegisteredNow;
@@ -56,6 +56,11 @@ exports.calendarWaiting = calendarWaiting;
56
56
  exports.calendarTemplateUrl = calendarTemplateUrl;
57
57
  exports.calendarAddUrl = calendarAddUrl;
58
58
  exports.deriveCapTile = deriveCapTile;
59
+ exports.derivePendingTile = derivePendingTile;
60
+ exports.liveRowVerdict = liveRowVerdict;
61
+ exports.liveRegistryOf = liveRegistryOf;
62
+ exports.liveToolKey = liveToolKey;
63
+ exports.liveToolItems = liveToolItems;
59
64
  exports.renderConnectionMap = renderConnectionMap;
60
65
  const node_crypto_1 = require("node:crypto");
61
66
  const google_chat_guide_images_1 = require("./google-chat-guide-images");
@@ -522,6 +527,11 @@ function claudeCodeUrl(prompt, projectRepo, externals) {
522
527
  // **ビューアは登録簿(channels.json 等)を読まない。** 読むと「使っていると言う条件」が
523
528
  // engine と画面で 2 枚になり、片方だけ直った日から一覧と AI の起動文が食い違う
524
529
  // (viewer/test/no-registry-read.test.mjs が見張っている)。
530
+ //
531
+ // **一覧の行の有無と状態は、ライブの登録一覧(op=state の registeredNow・操作タブが既に
532
+ // 読んでいるもの)と接続状態ストア(stateNow)で差し替える(#581・`liveToolItems`)。**
533
+ // まとめ表は毎朝の schedule でしか作り直されないので、そのまま描くと日中に外した接続先が
534
+ // 翌朝まで残る。行ごとの状態の判定はタイル(`derivePendingTile`)と同じ `liveRowVerdict`。
525
535
  // ---------------------------------------------------------------------------
526
536
  /**
527
537
  * 種別のアイコン。**DS_TYPE(データソース一覧)と同じ絵柄にそろえる**——同じツールが
@@ -537,11 +547,15 @@ const TOOL_KIND_ICONS = {
537
547
  "google-meet": "🎥",
538
548
  };
539
549
  /**
540
- * 状態の表示語と色。**4 語しか出さない**(読み取り中/承認待ち/停止中/未確認)。
550
+ * 状態の表示語と色。**4 語+「登録状況を確認中です」しか出さない**(読み取り中/承認待ち/
551
+ * 停止中/未確認、そして登録簿にしか無くまだ測っていない行の「確認中」・#581)。
541
552
  *
542
553
  * 色は既存の `ds-gate-*` を流用する——同じ画面で「緑=読めている・橙=人の操作待ち・
543
554
  * 灰=分からない」の意味を 2 通りに増やさない。
544
555
  *
556
+ * 🔴 **`pending` の語は操作タブの行(`DS_GATE_PENDING`)と同じ「登録状況を確認中です」。**
557
+ * 新しい言い方を作ると、同じ接続先が Home と操作タブで違う語になる。
558
+ *
545
559
  * **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す)。
546
560
  */
547
561
  function toolStateLabel(state) {
@@ -552,53 +566,72 @@ function toolStateLabel(state) {
552
566
  // 止めているものは警告色にしない(直す必要が無いので、橙にすると直しに行かせてしまう)
553
567
  if (state === "stopped")
554
568
  return { text: "停止中", cls: "ds-gate-muted" };
569
+ // 登録簿にはあるが、まだ誰も測っていない(#581)。**既定分岐(未確認)より前に置く**
570
+ if (state === "pending")
571
+ return { text: "登録状況を確認中です", cls: "ds-gate-muted" };
555
572
  return { text: "未確認", cls: "ds-gate-muted" };
556
573
  }
557
574
  /**
558
- * ビルド時の状態に、接続状態ストアの実測(`stateNow`)を重ねた state を返す。
575
+ * 種別 ライブの登録一覧(`registeredNow`)のキー(#581)。**この表は Home の一覧の
576
+ * 突き合わせにしか使わない**。会議(google-meet)は登録簿を持たない(ref 固定 `transcripts`)
577
+ * ので載せない=Home ではまとめ表の行がそのまま残る。
559
578
  *
560
- * 🔴 **`stopped` は上書きしない。** 止めているのは登録簿から決まる事実で、
561
- * ストアが「届く」と答えても読み取っていない。**止めたものを「読み取り中」と言わない**。
579
+ * GitHub `dev`(読み取り中)と `devExcluded`(停止中)の 2 キーに分かれて届く。
580
+ * 「停止中」と「登録から消えた」を分ける唯一の材料が `devExcluded` なので、
581
+ * **両方が配列のときだけ登録簿を正本にする**(`liveToolItems` の項)
582
+ */
583
+ const TOOL_KIND_REG = {
584
+ backlog: "issues",
585
+ github: "dev",
586
+ slack: "chat",
587
+ "google-chat": "google-chat",
588
+ figma: "design",
589
+ "google-drive": "materials",
590
+ };
591
+ /**
592
+ * 種別の人向けの名前。**engine `project-tools.mjs` の `TOOL_LABELS` の写し**(一致は
593
+ * `home-tools-live.test.mjs` が契約 fixture と突き合わせる)。まとめ表の行が 1 つでも
594
+ * あればそちらの `label` を優先し、ここを引くのは**登録簿にしか無い種別の行**だけ。
595
+ */
596
+ const TOOL_KIND_LABELS = {
597
+ backlog: "Backlog",
598
+ github: "GitHub",
599
+ slack: "Slack",
600
+ "google-chat": "Google Chat",
601
+ figma: "Figma",
602
+ "google-drive": "Google Drive",
603
+ "google-meet": "Google Meet",
604
+ };
605
+ /** ライブの登録行から組む href に許す字(英数と `/` `.` `_` `-`)。形が外れたら url は null */
606
+ const TOOL_URL_SAFE = /^[A-Za-z0-9._\/-]+$/;
607
+ /**
608
+ * まとめ表(`SITE.tools`)の 1 行を、ライブの登録一覧の行と同じ形に写す(#581)。
562
609
  *
563
- * 🔴 **突き合わせの鍵は行が自分で持っている**(cap + question + ref、Slack だけ workspace)。
564
- * ここで種別ごとの if 連鎖を書き直すと、engine の書き手(connection-state-write-*)と
565
- * 鍵の作り方が 2 枚になる。**github だけ大小を無視する**——登録簿の owner/repo と
566
- * ストアの ref は、人が貼った URL 由来で大小が揺れる。
610
+ * **状態の判定を 1 本にするための小さな写し。** 登録簿が読めなかった回(`registered`
611
+ * null)でも、まとめ表の行をこの形にして `liveRowVerdict` に渡す——「登録簿が届いたか
612
+ * どうか」で別の関数から状態が出る形を残さない(設計レビュー🔴5)。
567
613
  *
568
- * 行が無ければビルド時の値のまま(**「不明」に化けさせない**——夜間が測った結果を
569
- * ライブが答えなかっただけで消すと、朝には出ていたものが消える)。
614
+ * 鍵の畳み方は `registeredRefKeys` が持つ。ここは欄の名前を合わせるだけ
615
+ * (issues は `domain/KEY` を割る。割れない値は空の行=鍵が組めず未測定に倒れる)。
570
616
  *
571
617
  * **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す)。
572
618
  */
573
- function toolStateWithLive(item, rows) {
574
- if (!item)
575
- return "unknown";
576
- if (item.state === "stopped")
577
- return "stopped";
578
- var hit = null;
579
- (rows || []).forEach(function (r) {
580
- if (hit || !r)
581
- return;
582
- if (r.cap !== item.cap || r.question !== item.question)
583
- return;
584
- var a = String(r.ref == null ? "" : r.ref);
585
- var b = String(item.ref == null ? "" : item.ref);
586
- if (item.cap === "dev" ? a.toLowerCase() !== b.toLowerCase() : a !== b)
587
- return;
588
- // チャンネルIDはワークスペースの中でしか一意でない(overlayStateOnFleet と同じ鍵)
589
- if (item.cap === "chat" && String(r.workspace || "") !== String(item.workspace || ""))
590
- return;
591
- hit = r;
592
- });
593
- if (!hit)
594
- return String(item.state || "unknown");
595
- if (hit.status === "ok")
596
- return "reading";
597
- // **「測ったが分からなかった」は「承認待ち」に寄せない**(人が押す操作が無い)
598
- if (hit.status === "unknown")
599
- return "unknown";
600
- // failed / skipped / not_in_channel / missing_scope / no_token … は全部「人の操作待ち」
601
- return "waiting";
619
+ function regRowOfToolItem(item) {
620
+ var kind = String(item && item.kind != null ? item.kind : "");
621
+ var ref = String(item && item.ref != null ? item.ref : "");
622
+ if (kind === "slack")
623
+ return { ref: ref, workspace: String(item.workspace == null ? "" : item.workspace), name: item.name };
624
+ if (kind === "figma")
625
+ return { key: ref, name: item.name };
626
+ if (kind === "google-drive")
627
+ return { id: ref, name: item.name };
628
+ if (kind === "backlog") {
629
+ var at = ref.indexOf("/");
630
+ if (at <= 0 || at === ref.length - 1)
631
+ return {};
632
+ return { domain: ref.slice(0, at), projectKey: ref.slice(at + 1) };
633
+ }
634
+ return { ref: ref, name: item.name };
602
635
  }
603
636
  /**
604
637
  * 畳んだ種別の 1 行に出す、状態の要約(#424)。
@@ -616,9 +649,9 @@ function toolStateWithLive(item, rows) {
616
649
  * **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す)。
617
650
  */
618
651
  function toolsFoldSummary(states) {
619
- // 表示の順は状態の 4 語と同じ(読み取り中→承認待ち→停止中→未確認)
620
- var order = ["reading", "waiting", "stopped", "unknown"];
621
- var count = { reading: 0, waiting: 0, stopped: 0, unknown: 0 };
652
+ // 表示の順は状態の語と同じ(読み取り中→承認待ち→停止中→未確認→登録状況を確認中です)
653
+ var order = ["reading", "waiting", "stopped", "unknown", "pending"];
654
+ var count = { reading: 0, waiting: 0, stopped: 0, unknown: 0, pending: 0 };
622
655
  (states || []).forEach(function (s) {
623
656
  // ⚠ 4 語のどれかに当たるかは **order で見る**(素のオブジェクトの添字だと
624
657
  // "constructor" のような語が継承プロパティに当たって、知らない語が素通りする)
@@ -2537,9 +2570,15 @@ function calendarTemplateUrl(id) {
2537
2570
  *
2538
2571
  * ⚠ `calendarTemplateUrl` と同じく**実測は未了**(#425 の 3)。共有されている人が開くと
2539
2572
  * 「カレンダーを追加しますか」が出る想定。
2573
+ *
2574
+ * 🔴 **engine の `projectCalendarUrl`(Home の「使用ツール」の Google Meet 行)と同じ形にする**
2575
+ * (cortex-engine #343)。`u/0` は「1 番目にログインしている Google アカウント」で、
2576
+ * 片方だけに付いていると、業務アカウントが 2 番目の人には**Home のリンクは開けるのに
2577
+ * この接続タブは開けない(あるいはその逆)**という、説明できない差になる。
2578
+ * 外すときは engine 側と同じ回に外す。
2540
2579
  */
2541
2580
  function calendarAddUrl(id) {
2542
- return "https://calendar.google.com/calendar/r?cid=" + encodeURIComponent(id);
2581
+ return "https://calendar.google.com/calendar/u/0/r?cid=" + encodeURIComponent(id);
2543
2582
  }
2544
2583
  /**
2545
2584
  * タイル1行(と接続グラフの1ノード)の状態を、**正本から導出する**(0.12.0)。
@@ -2672,6 +2711,430 @@ function deriveCapTile(cap, ctx) {
2672
2711
  return { rule: 4, state: "idle", text: STATE_CONFIRMING_TEXT, keyMissing: false };
2673
2712
  return { rule: 4, state: "idle", text: meta.noFact, keyMissing: false };
2674
2713
  }
2714
+ /**
2715
+ * 「まとめ表が知らない行がある」能力のタイルに、行と同じ答えを出せるか(#576)。
2716
+ *
2717
+ * ## なぜ要るか
2718
+ *
2719
+ * タイルの段階1(`capPending`)は「まとめ表(fleet-status.json)が知らない行を持つ能力は
2720
+ * 『登録状況を確認中です』」に倒す。まとめ表は案件リポの fleet-status.yml が**毎朝 08:30 JST
2721
+ * の schedule でしか再生成しない**ので、日中に登録した接続先は、行は「接続を確認」で
2722
+ * その場で接続OKになっても、**タイルは翌朝まで確認中のまま**——行とタイルが食い違う
2723
+ * (`ops-ui-must-show-truth`)。まとめ表が `adapters[]` に unset を焼き込んでいると導出
2724
+ * (`deriveCapTile`)も止まる(変種を持たないアダプタは `derivableUnset` を通らない)ので、
2725
+ * ライブの登録一覧+接続状態ストアの行から**行と同じ語**を引く材料をここで作る。
2726
+ *
2727
+ * ## 規則(上から順)
2728
+ *
2729
+ * 1. 登録簿が配列でない・**0 件 → `null`**(確認中のまま)。0 件は「外して 0 件になった」側で、
2730
+ * 夜間の `capabilities` はまだ on。ストアの行が無い(外すときに消してある)ことを
2731
+ * 「未接続」の根拠にしない——導出(規則1: 登録0件→未接続)が答えられる回はそちらが
2732
+ * 先に勝つ(`pend = capPending && !derive`)ので、ここへ来るのは導出が止まっている回だけ
2733
+ * 2. 登録行ごとに、突き合わせ鍵を `registeredRefKeys`(#523・#556 の正本。チャットは
2734
+ * ワークスペース付き/無しの 2 本)で作り、ストアの `cap === regKey && question === "reachable"`
2735
+ * の行のうち `stateRefKey` が当たるものを集める。1 件も当たらない登録行は**未測定**
2736
+ * (測っていないものを OK とも失敗とも言わない)
2737
+ * 3. 当たった行が複数なら `at` が最新の 1 行(読めない `at` は最古扱い・同値は後勝ち)。
2738
+ * `ok` → OK、`BAD_STATE_STATUSES.reachable` の語 → 失敗、それ以外(`unknown`)→ 未測定
2739
+ * 4. **失敗 > 未測定 > OK の順**: 失敗が 1 つでもあれば `{ kind: "bad", rows }`(登録順)。
2740
+ * 未測定が 1 つでもあれば `null`。全部 OK なら `{ kind: "ok", at }`(`at` はストアの最新)
2741
+ *
2742
+ * 語の解決(行と同じ `dsGateOf` / `dsGateForCap`)と仮置き(#388)の扱いは画面側
2743
+ * (`pendingTileVerdict`)が持つ。**純関数に localStorage を見せない。**
2744
+ *
2745
+ * @param regKey `registeredNow` のキー(chat / google-chat / dev / design / materials / issues。
2746
+ * ストアの `cap` と同じ綴り)
2747
+ * @param registered `LIVE_REGISTERED[regKey]`(配列でなければ「読めていない」)
2748
+ * @param rows 接続状態ストアの行(`LIVE_STATE`)
2749
+ * @returns `null`(従来どおり「登録状況を確認中です」)/`{ kind: "ok", at }`/
2750
+ * `{ kind: "bad", rows: [{ status, ref, workspace, type, at }] }`(登録順)
2751
+ *
2752
+ * **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す。
2753
+ * `registeredRefKeys` / `stateRefKey` / `BAD_STATE_STATUSES` はこれより前に流し込んである)。
2754
+ */
2755
+ function derivePendingTile(regKey, registered, rows) {
2756
+ if (!Array.isArray(registered) || registered.length === 0)
2757
+ return null;
2758
+ var latestAt = "";
2759
+ var latestMs = -Infinity;
2760
+ var unmeasured = false;
2761
+ var badRows = [];
2762
+ for (var i = 0; i < registered.length; i++) {
2763
+ var reg = registered[i];
2764
+ if (!reg || typeof reg !== "object")
2765
+ continue;
2766
+ // 行ごとの判定は Home の「使用ツール」と同じ 1 本(`liveRowVerdict`・#581)。
2767
+ // ここに鍵の突き合わせや語の分類を書き戻すと、タイルと Home が別の判定を持つ
2768
+ var v = liveRowVerdict(regKey, reg, rows, "reachable");
2769
+ // 🔴 **測っていない登録行が 1 つでもあれば「接続OK」と断言しない。** 1 件の OK で能力
2770
+ // 全体を「接続OK」にすると、登録したばかりで一度も測っていないチャンネルまで繋がっている
2771
+ // ことになる(この関数が直そうとしている嘘の裏返し)。ただし**ループは最後まで回す**
2772
+ // ——測定済みの失敗は事実なので、未測定の行が混ざっていても失敗は出す(下の優先順)。
2773
+ // `unknown`(測れなかった)も未測定と同じ扱い: 行は「不明」と出るが、タイルは断言しない
2774
+ if (v === null || v.status === "unknown") {
2775
+ unmeasured = true;
2776
+ continue;
2777
+ }
2778
+ var hit = v.row;
2779
+ if (v.status === "ok") {
2780
+ var ms = Date.parse(String(hit.at || ""));
2781
+ if (ms !== ms)
2782
+ ms = -Infinity;
2783
+ if (ms >= latestMs) {
2784
+ latestMs = ms;
2785
+ latestAt = String(hit.at || "");
2786
+ }
2787
+ continue;
2788
+ }
2789
+ badRows.push({
2790
+ status: String(hit.status),
2791
+ ref: String(hit.ref || ""),
2792
+ workspace: String(hit.workspace == null ? (reg.workspace == null ? "" : reg.workspace) : hit.workspace),
2793
+ type: String(reg.type || ""),
2794
+ at: String(hit.at || ""),
2795
+ });
2796
+ }
2797
+ // **優先順: 失敗 > 未測定 > OK。** 失敗は測った事実なので、未測定の行が混ざっていても
2798
+ // 隠さない(行は「Slackで招待してください」と出ているのにタイルが「確認中」では、
2799
+ // 行とタイルが違うことを言う)。「接続OK」だけは全行が測定済みのときに限る
2800
+ if (badRows.length)
2801
+ return { kind: "bad", rows: badRows };
2802
+ if (unmeasured)
2803
+ return null;
2804
+ return { kind: "ok", at: latestAt };
2805
+ }
2806
+ /**
2807
+ * 登録行 1 つに対する、接続状態ストアの答え(#576 の行ごとの判定を切り出したもの・#581)。
2808
+ *
2809
+ * **タイル(`derivePendingTile`)と Home の「使用ツール」(`liveToolItems`)が同じこの
2810
+ * 関数で行の状態を決める**——鍵の突き合わせ・複数行の勝ち方・語の分類を 2 枚にしない
2811
+ * (`ops-ui-controls-policy.md` R10)。
2812
+ *
2813
+ * - 鍵は `registeredRefKeys`(登録行側)と `stateRefKey`(ストア側)の正本を使う。チャットは
2814
+ * ワークスペース付き/無しの 2 本(#523)。**`dev` だけ両側を小文字に畳む**——登録簿の
2815
+ * owner/repo とストアの ref は人が貼った URL 由来で大小が揺れる(engine の `toolState`・
2816
+ * 撤去前の `toolStateWithLive` と同じ「github だけ大小を無視」)。`registeredRefKeys` /
2817
+ * `stateRefKey` 本体は触らない(`deriveCapTile` の篩=#523 の挙動を変えない)
2818
+ * - 当たった行が複数なら `at` が最新の 1 行(読めない `at` は最古・同値は後勝ち。#576 決定 4)
2819
+ * - 分類は `BAD_STATE_STATUSES[question]` の 1 か所: `ok` → ok/悪い語 → bad/
2820
+ * それ以外(`unknown`=測れなかった)→ unknown。**当たる行が無ければ `null`(未測定)**
2821
+ *
2822
+ * @param regKey `registeredNow` のキー(chat / google-chat / dev / design / materials / issues)
2823
+ * @param reg 登録行(`registeredNow` の 1 行。まとめ表の行は `regRowOfToolItem` で同じ形にする)
2824
+ * @param rows 接続状態ストアの行(`LIVE_STATE`)
2825
+ * @param question 見る問い(タイルは `reachable`、Home は種別の問い=`TILE_CAPS[*].question`)
2826
+ *
2827
+ * **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す。
2828
+ * `registeredRefKeys` / `stateRefKey` / `BAD_STATE_STATUSES` はこれより前に流し込んである)。
2829
+ */
2830
+ function liveRowVerdict(regKey, reg, rows, question) {
2831
+ if (!reg || typeof reg !== "object")
2832
+ return null;
2833
+ var keys = registeredRefKeys(regKey, [reg]);
2834
+ if (!keys)
2835
+ return null;
2836
+ var fold = regKey === "dev";
2837
+ if (fold) {
2838
+ var folded = {};
2839
+ for (var k in keys)
2840
+ if (Object.prototype.hasOwnProperty.call(keys, k))
2841
+ folded[k.toLowerCase()] = true;
2842
+ keys = folded;
2843
+ }
2844
+ var state = Array.isArray(rows) ? rows : [];
2845
+ var hit = null;
2846
+ var hitMs = -Infinity;
2847
+ for (var j = 0; j < state.length; j++) {
2848
+ var r = state[j];
2849
+ if (!r || r.cap !== regKey || r.question !== question)
2850
+ continue;
2851
+ var key = stateRefKey(regKey, r.ref, r.workspace);
2852
+ if (fold)
2853
+ key = key.toLowerCase();
2854
+ if (!key || !keys[key])
2855
+ continue;
2856
+ // **最新の 1 行が勝つ。** 読めない at は最古(NaN は比較で常に負けるので -Infinity に畳む)。
2857
+ // 同値は後勝ち(`>=`)——サーバが並べた順を崩さない
2858
+ var ms = Date.parse(String(r.at || ""));
2859
+ if (ms !== ms)
2860
+ ms = -Infinity;
2861
+ if (hit === null || ms >= hitMs) {
2862
+ hit = r;
2863
+ hitMs = ms;
2864
+ }
2865
+ }
2866
+ if (hit === null)
2867
+ return null;
2868
+ if (hit.status === "ok")
2869
+ return { status: "ok", row: hit };
2870
+ var bad = BAD_STATE_STATUSES[question] || [];
2871
+ if (bad.indexOf(hit.status) >= 0)
2872
+ return { status: "bad", row: hit };
2873
+ return { status: "unknown", row: hit };
2874
+ }
2875
+ /**
2876
+ * ライブの登録一覧から、Home の 1 種別ぶんの登録行を取り出す(#581)。
2877
+ *
2878
+ * 返すのは `{ reg, excluded }` の配列。**読めていなければ null**(配列でない=その種別の
2879
+ * 登録ファイルが読めなかった。「読めない」を「0 件」に化けさせない)。
2880
+ * GitHub は `dev` と `devExcluded` が**両方**配列のときだけ読めたと見なす——片方しか無い
2881
+ * 古い Lambda では、停止中(exclude)の行が `dev` に居ないので「登録から消えた」と誤読して
2882
+ * 消してしまう(`pendingOpLiveAgrees` と同じ倒し方・設計レビュー🔴2)。
2883
+ *
2884
+ * **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す)。
2885
+ */
2886
+ function liveRegistryOf(registered, kind) {
2887
+ var regKey = TOOL_KIND_REG[kind];
2888
+ if (!regKey || !registered || typeof registered !== "object")
2889
+ return null;
2890
+ var out = [];
2891
+ var i;
2892
+ if (kind === "github") {
2893
+ if (!Array.isArray(registered.dev) || !Array.isArray(registered.devExcluded))
2894
+ return null;
2895
+ for (i = 0; i < registered.dev.length; i++)
2896
+ if (registered.dev[i] && typeof registered.dev[i] === "object")
2897
+ out.push({ reg: registered.dev[i], excluded: false });
2898
+ for (i = 0; i < registered.devExcluded.length; i++)
2899
+ if (registered.devExcluded[i] && typeof registered.devExcluded[i] === "object")
2900
+ out.push({ reg: registered.devExcluded[i], excluded: true });
2901
+ return out;
2902
+ }
2903
+ var rows = registered[regKey];
2904
+ if (!Array.isArray(rows))
2905
+ return null;
2906
+ for (i = 0; i < rows.length; i++)
2907
+ if (rows[i] && typeof rows[i] === "object")
2908
+ out.push({ reg: rows[i], excluded: false });
2909
+ return out;
2910
+ }
2911
+ /**
2912
+ * 登録行の突き合わせ鍵(Home の一覧で「同じ接続先か」を決める 1 本・#581)。
2913
+ *
2914
+ * `registeredRefKeys` の**ワークスペース無しの鍵**(チャットは `"\t" + ref`)を使う——
2915
+ * 消す側の突き合わせは ref だけ(`subtractRemovedRegistered` の前例。まとめ表の workspace は
2916
+ * engine が実測で埋めることがあり、宣言側と食い違う。誤削除は嘘・引き損ねは安全側)。
2917
+ * **`dev` は小文字**(`liveRowVerdict` と同じ)。鍵が組めなければ空文字。
2918
+ *
2919
+ * **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す)。
2920
+ */
2921
+ function liveToolKey(kind, reg) {
2922
+ var regKey = TOOL_KIND_REG[kind];
2923
+ if (!regKey || !reg || typeof reg !== "object")
2924
+ return "";
2925
+ var keys = registeredRefKeys(regKey, [reg]);
2926
+ if (!keys)
2927
+ return "";
2928
+ var key = "";
2929
+ if (regKey === "chat")
2930
+ key = "\t" + String(reg.ref == null ? "" : reg.ref).trim();
2931
+ else
2932
+ for (var k in keys)
2933
+ if (Object.prototype.hasOwnProperty.call(keys, k)) {
2934
+ key = k;
2935
+ break;
2936
+ }
2937
+ if (!keys[key])
2938
+ return "";
2939
+ return regKey === "dev" ? key.toLowerCase() : key;
2940
+ }
2941
+ /**
2942
+ * Home の「使用ツール」に出す行を、**操作タブと同じ 3 つの材料**から作る(#581)。
2943
+ *
2944
+ * ## 何を直したか
2945
+ *
2946
+ * まとめ表(`SITE.tools`・fleet-status.json)は案件リポの `fleet-status.yml` が**毎朝 08:30 JST
2947
+ * の schedule でしか再生成しない**ので、日中に操作タブで外した接続先(demo-sushiro)が
2948
+ * 翌朝まで Home に残り、足した接続先は翌朝まで出なかった。**登録簿が読めた種別は登録簿が正**
2949
+ * (消えた行は消す・無い行は足す)、状態はストアの行(`liveRowVerdict`=タイルと同じ判定)。
2950
+ *
2951
+ * ## 規則(上から順)
2952
+ *
2953
+ * 1. `registered` が null(Lambda が古い・失敗・intake 無し)→ まとめ表の行をそのまま。
2954
+ * **消さない・足さない**(読めなかったことを「登録が無い」にしない)。状態だけは
2955
+ * 同じ `liveRowVerdict` で出す(`regRowOfToolItem` で登録行の形に写す)
2956
+ * 2. 登録簿を持たない種別(google-meet)はまとめ表の行をそのまま
2957
+ * 3. 種別の登録簿が読めた(`liveRegistryOf` が配列)→ まとめ表の行は登録簿に当たるものだけ残す
2958
+ * (並びはまとめ表の順)。登録簿にしか無い行はその種別の最後の行の後ろに登録簿の順で足す
2959
+ * (まとめ表にその種別が無ければ末尾)。同じ鍵の行は 1 件に畳む(GitHub の issues /
2960
+ * discussions の 2 行)。読めなかった種別は規則 1 と同じ
2961
+ * 4. 足す行: `label` は同じ種別のまとめ表の行の語、無ければ `TOOL_KIND_LABELS`。`name` は
2962
+ * 登録簿の name(Slack は `#` を補う・engine と同じ)/GitHub は ref/Backlog は projectKey/
2963
+ * Drive は name か「共有資料フォルダ」。`url` は操作タブが既に組んでいる形を再利用できる
2964
+ * 種別だけ(GitHub・Backlog・Figma・Drive)。Slack・Google Chat は null(ワークスペースの
2965
+ * ドメインが登録簿に無い)。`cap` / `question` は `TILE_CAPS`(engine の `TOOL_STORE_KEYS` と同じ表)
2966
+ * 5. 状態: GitHub は `devExcluded` 所属なら `stopped`(まとめ表の `stopped` は引き継がない——
2967
+ * 日中に読み取りを再開した repo を翌朝まで「停止中」と言わない)。Drive はまとめ表の
2968
+ * `stopped`(`enabled: false`)を引き継ぐ(登録簿に enabled が無い)。それ以外は
2969
+ * `liveRowVerdict`: ok → `reading`/bad → `waiting`/unknown → `unknown`。行が無ければ、
2970
+ * まとめ表の行は**ビルド時の state**(「不明」に化けさせない)、登録簿にしか無い行は
2971
+ * `pending`(「登録状況を確認中です」・操作タブの行と同じ語)
2972
+ *
2973
+ * @param tools `SITE.tools`(`{ version, items }`)
2974
+ * @param registered `LIVE_REGISTERED`(仮置きを重ねた後のもの。null なら読めていない)
2975
+ * @param rows `LIVE_STATE`
2976
+ * @returns 描く行(まとめ表の行の形+ `state`。足した行は `live: true`)
2977
+ *
2978
+ * **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す。
2979
+ * `TOOL_KIND_REG` / `TOOL_KIND_LABELS` / `TOOL_URL_SAFE` / `TILE_CAPS` と上の 4 関数は
2980
+ * これより前に流し込む)。
2981
+ */
2982
+ function liveToolItems(tools, registered, rows) {
2983
+ var items = tools && Array.isArray(tools.items) ? tools.items : [];
2984
+ var reg = registered && typeof registered === "object" && !Array.isArray(registered) ? registered : null;
2985
+ var questionOf = function (regKey) {
2986
+ for (var cap in TILE_CAPS) {
2987
+ if (Object.prototype.hasOwnProperty.call(TILE_CAPS, cap) && TILE_CAPS[cap].id === regKey)
2988
+ return TILE_CAPS[cap].question;
2989
+ }
2990
+ return "reachable";
2991
+ };
2992
+ var stateOf = function (regKey, regRow, fallback) {
2993
+ var v = liveRowVerdict(regKey, regRow, rows, questionOf(regKey));
2994
+ if (!v)
2995
+ return fallback;
2996
+ if (v.status === "ok")
2997
+ return "reading";
2998
+ if (v.status === "bad")
2999
+ return "waiting";
3000
+ return "unknown";
3001
+ };
3002
+ var out = [];
3003
+ var kept = {};
3004
+ var labels = {};
3005
+ var i;
3006
+ for (i = 0; i < items.length; i++) {
3007
+ var it = items[i];
3008
+ if (!it)
3009
+ continue;
3010
+ var kind = String(it.kind == null ? "" : it.kind);
3011
+ var regKey = TOOL_KIND_REG[kind] || "";
3012
+ if (it.label && !labels[kind])
3013
+ labels[kind] = String(it.label);
3014
+ var live = reg ? liveRegistryOf(reg, kind) : null;
3015
+ var stopped = it.state === "stopped";
3016
+ var regRow = regRowOfToolItem(it);
3017
+ // ストアに行が無いときの値(ビルド時の state)
3018
+ var fallback = String(it.state || "unknown");
3019
+ var key = live ? liveToolKey(kind, regRow) : "";
3020
+ // **鍵が組めないまとめ表の行は消さない**(分からないものは落とさない・`registeredRefKeys` と同じ倒し方)
3021
+ if (live && key) {
3022
+ // 登録簿が正。当たる行が無ければ消えた(規則 3)
3023
+ var match = null;
3024
+ for (var m = 0; m < live.length; m++) {
3025
+ if (liveToolKey(kind, live[m].reg) === key) {
3026
+ match = live[m];
3027
+ break;
3028
+ }
3029
+ }
3030
+ if (!match)
3031
+ continue;
3032
+ if (!kept[kind])
3033
+ kept[kind] = {};
3034
+ kept[kind][key] = true;
3035
+ // 停止中の正本(規則 5): GitHub は登録簿、Drive はまとめ表、他の種別に停止中は無い。
3036
+ // 🔴 **読み取りを再開した repo(まとめ表は stopped・登録簿は dev)は、ストアに行が無くても
3037
+ // 「停止中」に戻さない**——engine が stopped でない行に置くビルド時の値は unknown(実装レビュー🔴1)
3038
+ if (kind === "github") {
3039
+ stopped = match.excluded;
3040
+ if (!stopped && fallback === "stopped")
3041
+ fallback = "unknown";
3042
+ }
3043
+ regRow = Object.assign({}, match.reg);
3044
+ // まとめ表の Slack 行が持つワークスペースを登録行が持たなければ補う(実装レビュー🟡3)。
3045
+ // 登録簿は宣言に workspace があるときだけ載せるので、無いままだとワークスペース付きの
3046
+ // ストア行に当たらず、ライブが届いた瞬間に「読み取り中」が「未確認」へ退化する
3047
+ if (kind === "slack" && !regRow.workspace && it.workspace)
3048
+ regRow.workspace = String(it.workspace);
3049
+ }
3050
+ // 登録簿を持たない種別(google-meet)も、ストアの行(cap=meetings / ingested)は同じ判定で重ねる
3051
+ var stateKey = regKey || String(it.cap == null ? "" : it.cap);
3052
+ var st = stopped ? "stopped" : !stateKey ? fallback : stateOf(stateKey, regRow, fallback);
3053
+ out.push(Object.assign({}, it, { state: st }));
3054
+ }
3055
+ if (!reg)
3056
+ return out;
3057
+ // 登録簿にしか無い行を、種別ごとにまとめ表の最後の行の後ろへ(規則 3〜4)
3058
+ var kinds = ["backlog", "github", "slack", "google-chat", "figma", "google-drive"];
3059
+ for (var ki = 0; ki < kinds.length; ki++) {
3060
+ var k2 = kinds[ki];
3061
+ var live2 = liveRegistryOf(reg, k2);
3062
+ if (!live2)
3063
+ continue;
3064
+ var regKey2 = TOOL_KIND_REG[k2];
3065
+ var seen = kept[k2] || {};
3066
+ var added = [];
3067
+ for (var a = 0; a < live2.length; a++) {
3068
+ var r2 = live2[a].reg;
3069
+ var key2 = liveToolKey(k2, r2);
3070
+ if (!key2 || seen[key2])
3071
+ continue;
3072
+ seen[key2] = true;
3073
+ var ref = "";
3074
+ var name = "";
3075
+ var url = null;
3076
+ if (k2 === "slack") {
3077
+ ref = String(r2.ref).trim();
3078
+ var raw = String(r2.name || ref).trim();
3079
+ name = raw.charAt(0) === "#" ? raw : "#" + raw;
3080
+ }
3081
+ else if (k2 === "google-chat") {
3082
+ ref = String(r2.ref).trim();
3083
+ name = String(r2.name || ref).trim();
3084
+ }
3085
+ else if (k2 === "github") {
3086
+ ref = String(r2.ref).trim();
3087
+ name = ref;
3088
+ if (TOOL_URL_SAFE.test(ref) && ref.split("/").length === 2)
3089
+ url = "https://github.com/" + ref;
3090
+ }
3091
+ else if (k2 === "figma") {
3092
+ ref = String(r2.key).trim();
3093
+ name = String(r2.name || ref).trim();
3094
+ if (TOOL_URL_SAFE.test(ref) && ref.indexOf("/") < 0)
3095
+ url = "https://www.figma.com/design/" + ref;
3096
+ }
3097
+ else if (k2 === "google-drive") {
3098
+ ref = String(r2.id).trim();
3099
+ name = String(r2.name || "").trim() || "共有資料フォルダ";
3100
+ if (TOOL_URL_SAFE.test(ref) && ref.indexOf("/") < 0)
3101
+ url = "https://drive.google.com/drive/folders/" + ref;
3102
+ }
3103
+ else {
3104
+ var domain = String(r2.domain).trim();
3105
+ var pk = String(r2.projectKey).trim();
3106
+ ref = issuesRefOf(domain, pk);
3107
+ name = pk;
3108
+ if (TOOL_URL_SAFE.test(domain) && domain.indexOf("/") < 0 && TOOL_URL_SAFE.test(pk) && pk.indexOf("/") < 0)
3109
+ url = "https://" + domain + "/projects/" + pk;
3110
+ }
3111
+ added.push({
3112
+ kind: k2,
3113
+ label: labels[k2] || TOOL_KIND_LABELS[k2] || k2,
3114
+ name: name,
3115
+ url: url,
3116
+ note: "",
3117
+ cap: regKey2,
3118
+ question: questionOf(regKey2),
3119
+ ref: ref,
3120
+ workspace: k2 === "slack" ? String(r2.workspace == null ? "" : r2.workspace) : "",
3121
+ state: live2[a].excluded ? "stopped" : stateOf(regKey2, r2, "pending"),
3122
+ live: true,
3123
+ });
3124
+ }
3125
+ if (!added.length)
3126
+ continue;
3127
+ var at = -1;
3128
+ for (var o = 0; o < out.length; o++)
3129
+ if (out[o].kind === k2)
3130
+ at = o;
3131
+ if (at < 0)
3132
+ out = out.concat(added);
3133
+ else
3134
+ out = out.slice(0, at + 1).concat(added, out.slice(at + 1));
3135
+ }
3136
+ return out;
3137
+ }
2675
3138
  function capabilityState(cap, ctx, adapter) {
2676
3139
  // **能力→パイプラインの対応はここに持つ。** 呼び出し側に渡させると、
2677
3140
  // クライアント側(この関数を .toString() で受け取る側)が同じ表を書き写すことになり、
@@ -4670,7 +5133,6 @@ ${overlayStateOnFleet.toString()}
4670
5133
  // 🔴 この定数行を落とすと、流し込んだ描画がアイコンを引けずに素の kind を出す
4671
5134
  var TOOL_KIND_ICONS = ${JSON.stringify(TOOL_KIND_ICONS)};
4672
5135
  ${toolStateLabel.toString()}
4673
- ${toolStateWithLive.toString()}
4674
5136
  // 畳んだ種別の 1 行に出す状態の要約(#424)。**同じ 4 語を使う**——ここで別の言い方を
4675
5137
  // 作ると、畳んだ 1 行と開いた行が違う語で同じ状態を指す
4676
5138
  ${toolsFoldSummary.toString()}
@@ -4706,6 +5168,21 @@ var BAD_STATE_STATUSES = ${JSON.stringify(BAD_STATE_STATUSES)};
4706
5168
  var STATE_CONFIRMING_TEXT = ${JSON.stringify(STATE_CONFIRMING_TEXT)};
4707
5169
  ${keyMissingText.toString()}
4708
5170
  ${deriveCapTile.toString()}
5171
+ // 登録直後のタイル(#576)。**registeredRefKeys / stateRefKey / BAD_STATE_STATUSES より後に置く**
5172
+ //(上の 3 つを引く。前に置くと var の BAD_STATE_STATUSES が undefined で判定が丸ごと死ぬ)
5173
+ ${derivePendingTile.toString()}
5174
+ // Home の「使用ツール」をライブの登録一覧+ストアで描く(#581)。**行ごとの判定
5175
+ // (liveRowVerdict)はタイル(derivePendingTile)と共有**——registeredRefKeys / stateRefKey /
5176
+ // BAD_STATE_STATUSES / TILE_CAPS より後に置く。🔴 下の 3 つの定数行を落とすと、流し込んだ
5177
+ // liveToolItems が未定義を参照して Home の描画が丸ごと死ぬ
5178
+ var TOOL_KIND_REG = ${JSON.stringify(TOOL_KIND_REG)};
5179
+ var TOOL_KIND_LABELS = ${JSON.stringify(TOOL_KIND_LABELS)};
5180
+ var TOOL_URL_SAFE = new RegExp(${JSON.stringify(TOOL_URL_SAFE.source)});
5181
+ ${regRowOfToolItem.toString()}
5182
+ ${liveRowVerdict.toString()}
5183
+ ${liveRegistryOf.toString()}
5184
+ ${liveToolKey.toString()}
5185
+ ${liveToolItems.toString()}
4709
5186
  // 案件カレンダー(#428)。**判定の実体はビルド時の定義**——画面側に書き写すと、
4710
5187
  // 「作成済みか」の判定が 2 枚になって片方だけ直る(=2 本目のカレンダーを作らせる)。
4711
5188
  // 🔴 **BAD_STATE_STATUSES より後に置く。** calendarPhase が「悪い」語をそこから引く
@@ -6205,13 +6682,78 @@ ${calendarAddUrl.toString()}
6205
6682
  }
6206
6683
 
6207
6684
  /**
6208
- * Home の「使用ツール」(#275)。engine が登録簿 7 種から算出した SITE.tools を描く。
6685
+ * Home の「使用ツール」に、ライブの登録一覧(「LIVE_REGISTERED」)へ仮置き(#388)を重ねた
6686
+ * 登録簿を渡す(#581)。「pendingTileVerdict」 と同じ材料を見る:
6687
+ *
6688
+ * - **remove で live に在る行は外す・add で live に無い行は足す。** Lambda の 「registeredNow」
6689
+ * は据え置き(保存直後の 1 回だけ迂回)なので、外した直後に Home を再読み込みすると
6690
+ * 据え置きの古い一覧を読む窓がある——仮置きを重ねないと「操作タブでは消えたのに Home には
6691
+ * まだ在る」が最大 1 分並ぶ
6692
+ * - 足す行は 「pendingExternalRow」(Slack のワークスペースは 「slackOrg」 から展開される)。
6693
+ * figma / drive / backlog は payload から登録行の形に組む
6694
+ * - update(通知先トグル等)は行の有無に関わらないので何もしない
6695
+ * - 「LIVE_REGISTERED」 が null なら null のまま(仮置きだけで一覧を作らない・規則 1)
6696
+ *
6697
+ * 既知の限界(「pendingTileVerdict」 と同じ): GitHub の remove は 「targetId」 が小文字で
6698
+ * 「pendingLiveRowMatches」 は生値一致なので、混在綴りの repo は据え置きの窓のあいだ消えない
6699
+ */
6700
+ function registryForHome() {
6701
+ if (!LIVE_REGISTERED) return null;
6702
+ var ops = pendingOpsNow();
6703
+ if (!ops.length) return LIVE_REGISTERED;
6704
+ var reg = Object.assign({}, LIVE_REGISTERED);
6705
+ for (var i = 0; i < ops.length; i++) {
6706
+ var e = ops[i];
6707
+ var km = e && PENDING_KINDS[e.kind];
6708
+ if (!km || !Array.isArray(reg[km.reg])) continue;
6709
+ var rows = reg[km.reg];
6710
+ var found = null;
6711
+ for (var k = 0; k < rows.length; k++) {
6712
+ if (pendingLiveRowMatches(e.kind, rows[k], e.targetId)) { found = rows[k]; break; }
6713
+ }
6714
+ // 停止中(devExcluded)の GitHub 行を「登録から削除」した直後は、その行は dev ではなく
6715
+ // devExcluded に居る。こちらも外さないと据え置きの窓のあいだ「停止中」が残る(実装レビュー🟡4)
6716
+ if (e.action === "remove" && e.kind === "github" && Array.isArray(reg.devExcluded)) {
6717
+ reg.devExcluded = reg.devExcluded.filter(function (r) { return !pendingLiveRowMatches(e.kind, r, e.targetId); });
6718
+ }
6719
+ if (e.action === "remove" && found) {
6720
+ reg[km.reg] = rows.filter(function (r) { return r !== found; });
6721
+ } else if (e.action === "add" && !found) {
6722
+ var p = e.payload || {};
6723
+ var row;
6724
+ if (e.kind === "figma") row = { key: e.targetId, name: p.name || "" };
6725
+ else if (e.kind === "drive") row = { id: e.targetId, name: p.name || "" };
6726
+ else if (e.kind === "backlog") row = { domain: p.domain || "", projectKey: p.projectKey || "" };
6727
+ else row = pendingExternalRow(e);
6728
+ reg[km.reg] = rows.concat([row]);
6729
+ }
6730
+ }
6731
+ return reg;
6732
+ }
6733
+
6734
+ /**
6735
+ * いま Home に出す「使用ツール」の行(#581)。**判定は 「liveToolItems」 の 1 か所**——
6736
+ * まとめ表(「SITE.tools」)を初期表示に、ライブの登録一覧(仮置き込み)と接続状態ストアが
6737
+ * 届いていれば、消えた行は消し・足した行は足し・状態はタイルと同じ行ごとの判定で出す。
6738
+ * 届く前(先読みが飛行中・intake 無し)は 「LIVE_REGISTERED」 が null なので、まとめ表の
6739
+ * 行がそのまま出る
6740
+ */
6741
+ function homeToolsItemsNow() {
6742
+ return liveToolItems(SITE.tools, registryForHome(), LIVE_STATE);
6743
+ }
6744
+
6745
+ /**
6746
+ * Home の「使用ツール」(#275)。engine が登録簿 7 種から算出した SITE.tools を初期表示に、
6747
+ * ライブの登録一覧+接続状態ストア(「homeToolsItemsNow」・#581)で描く。
6209
6748
  *
6210
6749
  * 🔴 **SITE.tools が無ければ何も出さない。** 生成が古い案件(この項目を知らない
6211
6750
  * fleet-status.json)では、手書きの節が本文に残っているので、そちらがそのまま出る
6212
6751
  * ——ここで「まだ接続していません」と出すと、繋いである案件に嘘を出すことになる。
6752
+ * ライブだけで節を出すことはしない(手書きの節との重複回避 「dropLegacyHomeSections」 と
6753
+ * 衝突する)。効くのは「SITE.tools はあるが古い」案件
6213
6754
  *
6214
- * **0 件は 1 行で言い切る。** 空の一覧を出すと「読み込み中」に見える。
6755
+ * **0 件は 1 行で言い切る。** 空の一覧を出すと「読み込み中」に見える。ライブで全部消えて
6756
+ * 0 件になった場合も同じ 1 行
6215
6757
  */
6216
6758
  function homeToolsSection() {
6217
6759
  var t = SITE.tools;
@@ -6220,7 +6762,7 @@ ${calendarAddUrl.toString()}
6220
6762
  if (!t || !Array.isArray(t.items)) return null;
6221
6763
  var box = el("section", { class: "home-tools" });
6222
6764
  box.appendChild(el("h2", { class: "home-grounding-title", text: "使用ツール" }));
6223
- var items = t.items || [];
6765
+ var items = homeToolsItemsNow();
6224
6766
  if (!items.length) {
6225
6767
  // 「操作タブ」はその場で移れる口にする(文言は変えない・#424)
6226
6768
  box.appendChild(el("p", { class: "home-tools-empty" }, [
@@ -6258,12 +6800,12 @@ ${calendarAddUrl.toString()}
6258
6800
  if (!ul) { ul = el("ul", { class: "home-tools-list" }); box.appendChild(ul); }
6259
6801
  g.items.forEach(function (it) { ul.appendChild(homeToolRow(it)); });
6260
6802
  });
6261
- // **どこで直せるか・いつの情報かを 1 行で言う。** 操作タブで繋いだ直後は
6262
- // この一覧に出ない(engine が毎朝作り直すまで)ので、黙っていると「効いていない」と読まれる
6803
+ // **どこで直せるかを 1 行で言う。** 「(この一覧は毎朝更新されます)」は #581 で落とした
6804
+ // ——一覧はライブの登録一覧で描くので、操作タブで足した・外した行はその場で出る・消える
6263
6805
  box.appendChild(el("p", { class: "home-tools-note" }, [
6264
6806
  document.createTextNode("接続の追加・変更は"),
6265
6807
  opsTabLink("操作タブ"),
6266
- document.createTextNode("で行います(この一覧は毎朝更新されます)。"),
6808
+ document.createTextNode("で行います。"),
6267
6809
  ]));
6268
6810
  return box;
6269
6811
  }
@@ -6279,13 +6821,15 @@ ${calendarAddUrl.toString()}
6279
6821
  text: (TOOL_KIND_ICONS[it.kind] || "🔗") + " " + (it.label || it.kind),
6280
6822
  }));
6281
6823
  var name = it.name || it.ref || "";
6282
- // **開ける先が無い行(Google Meet)はリンクにしない。** 押せないリンクを出すより
6283
- // 素のテキストのほうが正直(url は engine が null で「無い」と言い切っている)
6824
+ // **開ける先が無い行はリンクにしない。** 押せないリンクを出すより素のテキストのほうが
6825
+ // 正直(url は engine が null で「無い」と言い切っている)。Google Meet の行も
6826
+ // 案件カレンダーができていれば url を持つ(cortex-engine #343。カレンダーがまだ無い案件は null)
6284
6827
  // href に載せる前に isHttp を通す(build 側の許可リストと二重の守り。el() の注意書きのとおり)
6285
6828
  if (it.url && isHttp(it.url)) li.appendChild(el("a", { class: "home-tools-name", href: it.url, target: "_blank", rel: "noopener", text: name }));
6286
6829
  else li.appendChild(el("span", { class: "home-tools-name", text: name }));
6287
- // ライブの実測が届いていれば重ねる(stopped は上書きしない・toolStateWithLive)
6288
- var st = toolStateWithLive(it, LIVE_STATE);
6830
+ // 状態は 「liveToolItems」 が決めてある(#581)。**ここで判定し直さない**——タイルと同じ
6831
+ // 行ごとの判定(liveRowVerdict)から出た値を語に変えるだけ
6832
+ var st = String(it.state || "unknown");
6289
6833
  var lab = toolStateLabel(st);
6290
6834
  var chip = el("span", { class: "home-tools-state ds-gate " + lab.cls, text: lab.text });
6291
6835
  // 状態そのものを属性に残す(表示語を読み替えずに検査・スタイルできるように)
@@ -6320,8 +6864,8 @@ ${calendarAddUrl.toString()}
6320
6864
  // ⚠ **id に使うので、種別の字をそのまま持ち込まない。** kind は fleet-status.json 由来で、
6321
6865
  // 空白の入った値が来ると id が2つの idref に読まれ、aria-controls の対応が壊れる
6322
6866
  var key = (String(kind).replace(/[^A-Za-z0-9_-]/g, "") || "other") + "-" + seq;
6323
- // 状態は 1 行ずつ求めてから畳む(行と要約が別の判定を持たない)
6324
- var sum = toolsFoldSummary(group.map(function (it) { return toolStateWithLive(it, LIVE_STATE); }));
6867
+ // 状態は行に決めてあるものを畳む(行と要約が別の判定を持たない・#581)
6868
+ var sum = toolsFoldSummary(group.map(function (it) { return String(it.state || "unknown"); }));
6325
6869
  var chip = el("span", { class: "home-tools-fold-state ds-gate " + sum.cls, text: sum.text });
6326
6870
  chip.setAttribute("data-state", sum.state);
6327
6871
  // 説明として読ませるので id が要る
@@ -8469,6 +9013,71 @@ ${calendarAddUrl.toString()}
8469
9013
  return !!m[cap];
8470
9014
  }
8471
9015
 
9016
+ /**
9017
+ * 「登録状況を確認中です」に倒れるタイル(「capPending」 が立ち・導出が止まっている回)に、
9018
+ * **行と同じ語**を出せるか(#576)。出せなければ null(従来どおり確認中)。
9019
+ *
9020
+ * 判定は 「derivePendingTile」(純関数・ライブの登録一覧+接続状態ストア)。ここが持つのは
9021
+ * **仮置き(#388)の扱いと語の解決**だけ:
9022
+ *
9023
+ * - 仮置きの add で live にまだ無い行があれば null(登録簿が追いつくまでは測っていない
9024
+ * 行が 1 つある、と同じ)。remove の仮置きで live に在る行はその行を判定から外す
9025
+ * (「pendingCountDelta」 / 「refKeysOf」 と同じ材料。一覧と同じものを見る)。update
9026
+ * (通知先トグル等)は判定を止めない
9027
+ * - 語は**行と同じ関数**から引く(写経しない): チャット・開発は 「dsGateOf」(社内の
9028
+ * no_token →「管理者の設定が必要です」・GitHub の no_token →「App が未インストール」の
9029
+ * 言い換え込み)、取り込む能力は 「dsGateForCap」(Drive の unreachable →「フォルダを
9030
+ * 開けません」等)。OK は「● 接続OK ◯分前に確認」(行の 「rtAgo」 と同じ語・時刻は
9031
+ * ストアの最新)。失敗が複数で語が違っても**登録順で最初の 1 語**(タイルは 1 行。
9032
+ * 内訳はモーダルの行が持つ)
9033
+ *
9034
+ * regKey は 「TILE_CAPS[derCapOf(cap, adapter)].id」 の 1 本(#153 のアダプタ別の鍵をそのまま
9035
+ * 使う——Slack タイルが Google Chat の登録・実測を借りない、逆も)。混載タイル
9036
+ * (capabilities を持たない旧データ・adapter 無し)に 2 つの regKey を合算する分岐は置かない
9037
+ * ——その形の案件はまとめ表に capabilities が無く 「derived === null」 で導出が答えるので、
9038
+ * ここへ来る回が実質無い(設計レビュー🟡)
9039
+ */
9040
+ function pendingTileVerdict(cap, adapter) {
9041
+ var derCap = derCapOf(cap, adapter);
9042
+ var meta = TILE_CAPS[derCap];
9043
+ // 会議は登録簿を持たない(refKeyed でない)ので答えられない
9044
+ if (!meta || !meta.refKeyed || !LIVE_REGISTERED) return null;
9045
+ var regKey = meta.id;
9046
+ var registered = LIVE_REGISTERED[regKey];
9047
+ if (!Array.isArray(registered)) return null;
9048
+ // ---- 仮置き(#388)を登録簿に重ねる ----
9049
+ var ops = pendingOpsNow();
9050
+ var live = registered;
9051
+ for (var i = 0; i < ops.length; i++) {
9052
+ var e = ops[i];
9053
+ var km = e && PENDING_KINDS[e.kind];
9054
+ if (!km || km.reg !== regKey) continue;
9055
+ var found = null;
9056
+ for (var k = 0; k < live.length; k++) {
9057
+ if (pendingLiveRowMatches(e.kind, live[k], e.targetId)) { found = live[k]; break; }
9058
+ }
9059
+ // add で live にまだ無い行=誰も測っていない登録行が 1 つある → 確認中のまま
9060
+ if (e.action === "add" && !found) return null;
9061
+ if (e.action === "remove" && found) {
9062
+ live = live.filter(function (r) { return r !== found; });
9063
+ }
9064
+ }
9065
+ var v = derivePendingTile(regKey, live, LIVE_STATE);
9066
+ if (!v) return null;
9067
+ if (v.kind === "ok") {
9068
+ var ago = rtAgo(v.at);
9069
+ return { text: CAP_STATE_MARK.ok + "接続OK" + (ago ? " " + ago + "に確認" : ""), cls: "ds-gate-ok" };
9070
+ }
9071
+ var first = v.rows[0];
9072
+ var gate;
9073
+ if (regKey === "chat") gate = dsGateOf({ type: "slack", workspace: first.workspace, gate: first.status });
9074
+ else if (regKey === "google-chat") gate = dsGateOf({ type: "google-chat", gate: first.status });
9075
+ else if (regKey === "dev") gate = dsGateOf({ type: first.type || "github-issues", gate: first.status });
9076
+ else gate = dsGateForCap(regKey, first.status);
9077
+ // 行と同じ見た目: warn の語にだけ ⚠️ を前置(muted の語=「管理者の設定が必要です」等には付けない)
9078
+ return { text: (gate.cls === "ds-gate-warn" ? CAP_STATE_MARK.warn : "") + gate.text, cls: gate.cls };
9079
+ }
9080
+
8472
9081
  /**
8473
9082
  * 能力の状態を決めるときに渡す値。**足した行は混ぜない**(誰も測っていないので状態を持たない)。
8474
9083
  *
@@ -9128,7 +9737,9 @@ ${calendarAddUrl.toString()}
9128
9737
  var asOfText = LIVE_FLEET_FAILED
9129
9738
  ? "最新の接続状況を取得できませんでした。表示しているのは " + asOf + " 時点の情報です。"
9130
9739
  : hasPending
9131
- ? "接続状況は " + asOf + " 時点の自動チェック結果です。いま登録した分も一覧に出していますが、読み取れているかどうかが分かるのは次回の自動チェックの後になります。"
9740
+ // 「接続を確認」で確かめた分はタイルにもその場で出る(#576)ので、「次回の自動チェックの後」
9741
+ // と言い切らない——同じ節でタイルが「接続OK」と言っているのに矛盾する
9742
+ ? "接続状況は " + asOf + " 時点の自動チェック結果です。いま登録した分も一覧に出しています。「接続を確認」で確かめたものはタイルにも出し、残りは次回の自動チェックの後に分かります。"
9132
9743
  : "接続状況は " + asOf + " 時点の自動チェック結果です。設定を変えた場合、ここに反映されるのは次回の実行後になります。";
9133
9744
  sec.appendChild(el("p", {
9134
9745
  class: "datasources-asof" + (LIVE_FLEET_FAILED ? " is-stale" : ""),
@@ -9933,6 +10544,17 @@ ${calendarAddUrl.toString()}
9933
10544
  // ここを逆にすると、まとめ表が追いつく翌朝まで「確認中です」が居座る。
9934
10545
  // 確認中の判定はアダプタで絞る(#153・判断⑭)
9935
10546
  var pend = capPending(cap, adapter) && !derive;
10547
+ // **確認中に倒す回でも、ライブの登録一覧+接続状態ストアが行と同じ答えを持っていれば
10548
+ // それを出す(#576)。** まとめ表は毎朝 08:30 の schedule でしか作り直されないので、
10549
+ // 日中に登録して「接続を確認」で行が接続OKになっても、ここが確認中のままだと
10550
+ // 行とタイルが翌朝まで食い違う。**「st === "warn"」 の回は OK を採らない**——fleet の
10551
+ // 行から出た warn を導出が降格しない保護(上)と同じ理由で、色(⚠️)と語(接続OK)が
10552
+ // 割れる。失敗(行と同じ語)は採る
10553
+ // **答えるのは、導出を止めた理由が unset(エンジンが未導出)のときだけ。** off / broken
10554
+ // (意図的に止めた・設定が壊れている)で導出が止まった回にストアの ok で「接続OK」と
10555
+ // 言うと、表 5 の優先順位(OFF・設定に異常が導出より先)を裏切る(実装レビュー🟡2)
10556
+ var pendNow = pend && (derived == null || derived === "on" || derived === "unset") ? pendingTileVerdict(cap, adapter) : null;
10557
+ if (pendNow && st === "warn" && pendNow.cls === "ds-gate-ok") pendNow = null;
9936
10558
  // 🔴 **初回のライブ確認が答えるまで、状態を断言しない(#256・設計v2 の5)。**
9937
10559
  // この2行目は焼いた capabilities 由来の状態を先に言っていたので、図と同じ瞬き
9938
10560
  // (旧状態を見せて1〜2秒後に訂正)をタイルでも起こしていた。**兄弟機能の同型の
@@ -9943,11 +10565,11 @@ ${calendarAddUrl.toString()}
9943
10565
  // 答える能力だけ(derived が null / "on")=図と同じ規則5の線引き。
9944
10566
  var confirming = liveConfirming() && (derived === null || derived === "on");
9945
10567
  var subEl = el("div", {
9946
- class: "ds-tile-sub " + (confirming || pend ? DS_GATE_PENDING.cls : "ds-gate-" + st),
10568
+ class: "ds-tile-sub " + (confirming || (pend && !pendNow) ? DS_GATE_PENDING.cls : pend ? pendNow.cls : "ds-gate-" + st),
9947
10569
  // **反映待ち(pend)の形は変えない。** あちらは「足した行がまとめ表に載るまで」の
9948
10570
  // 折衷で、件数を添えると足した分を数えたようにも読める。確認中は焼いた件数を残す
9949
10571
  text: pend
9950
- ? DS_GATE_PENDING.text
10572
+ ? (pendNow ? pendNow.text : DS_GATE_PENDING.text)
9951
10573
  : tileSub(cap, st, derived, adapter, internals, sources, derive, confirming),
9952
10574
  });
9953
10575
  var body = el("div", { class: "ds-tile-body" }, [
@@ -10579,6 +11201,14 @@ ${calendarAddUrl.toString()}
10579
11201
  // **GitHub は揃えない。** 行の名前は owner/repo そのもので、書き換える口
10580
11202
  //(op=config の refresh-channel-name)は channels.json 専用
10581
11203
  if (s.type === "slack") autoAlignName(s, j, card);
11204
+ // ストアにも 「button」 の行が書かれている(#576)。**取りに行くのは 1 回だけ**——
11205
+ // 届いた行でモーダルを閉じたときにタイルが引き直され、登録直後のタイルが行と同じ
11206
+ // 「接続OK」になる(「pendingTileVerdict」)。ここで取らないと、画面がストアの行を
11207
+ // 読むのは次の op=state(追加行が未確定の 15 分間は 60 秒ごと・過ぎていれば
11208
+ // 再読み込みまで)で、「接続を確認 → 閉じる → その場で接続OK」にならない。
11209
+ // 据え置きあり=GitHub は叩かない(ストアの行は据え置きの対象外で毎回引く)。
11210
+ // 取り込む能力の即答(appendDispatchProbe)は先に同じ 1 行を持っている
11211
+ reloadLiveState();
10582
11212
  } catch (e) {
10583
11213
  msg.className = "ds-toggle-msg is-error";
10584
11214
  msg.textContent = "✗ 通信に失敗しました。";
@@ -11427,7 +12057,7 @@ ${calendarAddUrl.toString()}
11427
12057
  showProbeVerdict(row, capId, checkBtn, j);
11428
12058
  // ストアにも 「button」 の行が書かれている。**取りに行くのは1回だけ**——
11429
12059
  // 届いた行で一覧が描き直され、この行の札が正本の値に揃う
11430
- loadLiveFleet();
12060
+ reloadLiveState();
11431
12061
  } catch (e) {
11432
12062
  note.textContent = "通信に失敗しました。";
11433
12063
  delete PROBE_WAITS[key];
@@ -13688,6 +14318,21 @@ ${calendarAddUrl.toString()}
13688
14318
  * 戻るので、旗として残し、応答後にもう一度取りに行く
13689
14319
  */
13690
14320
  var freshWanted = false;
14321
+ /**
14322
+ * 「接続を確認」の応答が 2xx だったので、ストアの行をもう一度読みたい(#576)。
14323
+ *
14324
+ * 🔴 **取得が飛行中なら loadLiveFleet は黙って戻る**(liveFleetRequested の項)。追加行が
14325
+ * 未確定の 15 分間は 60 秒ごとの取り直しが走っているので、押した瞬間がそれと重なると、
14326
+ * 押した結果(by:button の行)を画面が次の便まで読まない=タイルが「その場で接続OK」に
14327
+ * ならない。そこで**飛行中なら旗を立て、応答が届いたら 1 回だけ取り直す**(保存直後の
14328
+ * freshWanted と同じ形。ただし据え置きは迂回しない——GitHub の枠を削る理由が無い)。
14329
+ * 失敗(catch)の側では取り直さない(失敗を繰り返す形にしない。期限つきの取り直しに任せる)
14330
+ */
14331
+ var againWanted = false;
14332
+ function reloadLiveState() {
14333
+ if (liveFleetRequested) { againWanted = true; return; }
14334
+ loadLiveFleet();
14335
+ }
13691
14336
  function loadLiveFleet(fresh) {
13692
14337
  if (fresh) freshWanted = true;
13693
14338
  if (liveFleetRequested) return;
@@ -13782,10 +14427,15 @@ ${calendarAddUrl.toString()}
13782
14427
  syncAiLinks();
13783
14428
  // Home を開いていれば「使用ツール」の状態を実測で描き直す(#275)
13784
14429
  syncHomeTools();
13785
- // 取得中に保存が効いていたら、迂回付きでもう一度取りに行く
13786
- if (freshWanted) loadLiveFleet();
14430
+ // 取得中に保存が効いていたら、迂回付きでもう一度取りに行く。
14431
+ // 取得中に「接続を確認」が通っていたら、据え置きのままもう一度(reloadLiveState の項)
14432
+ var again = againWanted;
14433
+ againWanted = false;
14434
+ if (freshWanted || again) loadLiveFleet();
13787
14435
  })
13788
14436
  .catch(function () {
14437
+ // 失敗の側では「もう一度」の旗を捨てる(reloadLiveState の項)
14438
+ againWanted = false;
13789
14439
  clearGiveUp();
13790
14440
  // 静的ビルドの値で描き続ける。ただし「古いかもしれない」ことは画面に出す
13791
14441
  LIVE_FLEET = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.57",
3
+ "version": "0.12.59",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,