@takagaki/cortex-decisions-viewer 0.12.58 → 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 +516 -82
  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;
@@ -57,6 +57,10 @@ exports.calendarTemplateUrl = calendarTemplateUrl;
57
57
  exports.calendarAddUrl = calendarAddUrl;
58
58
  exports.deriveCapTile = deriveCapTile;
59
59
  exports.derivePendingTile = derivePendingTile;
60
+ exports.liveRowVerdict = liveRowVerdict;
61
+ exports.liveRegistryOf = liveRegistryOf;
62
+ exports.liveToolKey = liveToolKey;
63
+ exports.liveToolItems = liveToolItems;
60
64
  exports.renderConnectionMap = renderConnectionMap;
61
65
  const node_crypto_1 = require("node:crypto");
62
66
  const google_chat_guide_images_1 = require("./google-chat-guide-images");
@@ -523,6 +527,11 @@ function claudeCodeUrl(prompt, projectRepo, externals) {
523
527
  // **ビューアは登録簿(channels.json 等)を読まない。** 読むと「使っていると言う条件」が
524
528
  // engine と画面で 2 枚になり、片方だけ直った日から一覧と AI の起動文が食い違う
525
529
  // (viewer/test/no-registry-read.test.mjs が見張っている)。
530
+ //
531
+ // **一覧の行の有無と状態は、ライブの登録一覧(op=state の registeredNow・操作タブが既に
532
+ // 読んでいるもの)と接続状態ストア(stateNow)で差し替える(#581・`liveToolItems`)。**
533
+ // まとめ表は毎朝の schedule でしか作り直されないので、そのまま描くと日中に外した接続先が
534
+ // 翌朝まで残る。行ごとの状態の判定はタイル(`derivePendingTile`)と同じ `liveRowVerdict`。
526
535
  // ---------------------------------------------------------------------------
527
536
  /**
528
537
  * 種別のアイコン。**DS_TYPE(データソース一覧)と同じ絵柄にそろえる**——同じツールが
@@ -538,11 +547,15 @@ const TOOL_KIND_ICONS = {
538
547
  "google-meet": "🎥",
539
548
  };
540
549
  /**
541
- * 状態の表示語と色。**4 語しか出さない**(読み取り中/承認待ち/停止中/未確認)。
550
+ * 状態の表示語と色。**4 語+「登録状況を確認中です」しか出さない**(読み取り中/承認待ち/
551
+ * 停止中/未確認、そして登録簿にしか無くまだ測っていない行の「確認中」・#581)。
542
552
  *
543
553
  * 色は既存の `ds-gate-*` を流用する——同じ画面で「緑=読めている・橙=人の操作待ち・
544
554
  * 灰=分からない」の意味を 2 通りに増やさない。
545
555
  *
556
+ * 🔴 **`pending` の語は操作タブの行(`DS_GATE_PENDING`)と同じ「登録状況を確認中です」。**
557
+ * 新しい言い方を作ると、同じ接続先が Home と操作タブで違う語になる。
558
+ *
546
559
  * **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す)。
547
560
  */
548
561
  function toolStateLabel(state) {
@@ -553,53 +566,72 @@ function toolStateLabel(state) {
553
566
  // 止めているものは警告色にしない(直す必要が無いので、橙にすると直しに行かせてしまう)
554
567
  if (state === "stopped")
555
568
  return { text: "停止中", cls: "ds-gate-muted" };
569
+ // 登録簿にはあるが、まだ誰も測っていない(#581)。**既定分岐(未確認)より前に置く**
570
+ if (state === "pending")
571
+ return { text: "登録状況を確認中です", cls: "ds-gate-muted" };
556
572
  return { text: "未確認", cls: "ds-gate-muted" };
557
573
  }
558
574
  /**
559
- * ビルド時の状態に、接続状態ストアの実測(`stateNow`)を重ねた state を返す。
575
+ * 種別 ライブの登録一覧(`registeredNow`)のキー(#581)。**この表は Home の一覧の
576
+ * 突き合わせにしか使わない**。会議(google-meet)は登録簿を持たない(ref 固定 `transcripts`)
577
+ * ので載せない=Home ではまとめ表の行がそのまま残る。
560
578
  *
561
- * 🔴 **`stopped` は上書きしない。** 止めているのは登録簿から決まる事実で、
562
- * ストアが「届く」と答えても読み取っていない。**止めたものを「読み取り中」と言わない**。
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)。
563
609
  *
564
- * 🔴 **突き合わせの鍵は行が自分で持っている**(cap + question + ref、Slack だけ workspace)。
565
- * ここで種別ごとの if 連鎖を書き直すと、engine の書き手(connection-state-write-*)と
566
- * 鍵の作り方が 2 枚になる。**github だけ大小を無視する**——登録簿の owner/repo と
567
- * ストアの ref は、人が貼った URL 由来で大小が揺れる。
610
+ * **状態の判定を 1 本にするための小さな写し。** 登録簿が読めなかった回(`registered`
611
+ * null)でも、まとめ表の行をこの形にして `liveRowVerdict` に渡す——「登録簿が届いたか
612
+ * どうか」で別の関数から状態が出る形を残さない(設計レビュー🔴5)。
568
613
  *
569
- * 行が無ければビルド時の値のまま(**「不明」に化けさせない**——夜間が測った結果を
570
- * ライブが答えなかっただけで消すと、朝には出ていたものが消える)。
614
+ * 鍵の畳み方は `registeredRefKeys` が持つ。ここは欄の名前を合わせるだけ
615
+ * (issues は `domain/KEY` を割る。割れない値は空の行=鍵が組めず未測定に倒れる)。
571
616
  *
572
617
  * **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す)。
573
618
  */
574
- function toolStateWithLive(item, rows) {
575
- if (!item)
576
- return "unknown";
577
- if (item.state === "stopped")
578
- return "stopped";
579
- var hit = null;
580
- (rows || []).forEach(function (r) {
581
- if (hit || !r)
582
- return;
583
- if (r.cap !== item.cap || r.question !== item.question)
584
- return;
585
- var a = String(r.ref == null ? "" : r.ref);
586
- var b = String(item.ref == null ? "" : item.ref);
587
- if (item.cap === "dev" ? a.toLowerCase() !== b.toLowerCase() : a !== b)
588
- return;
589
- // チャンネルIDはワークスペースの中でしか一意でない(overlayStateOnFleet と同じ鍵)
590
- if (item.cap === "chat" && String(r.workspace || "") !== String(item.workspace || ""))
591
- return;
592
- hit = r;
593
- });
594
- if (!hit)
595
- return String(item.state || "unknown");
596
- if (hit.status === "ok")
597
- return "reading";
598
- // **「測ったが分からなかった」は「承認待ち」に寄せない**(人が押す操作が無い)
599
- if (hit.status === "unknown")
600
- return "unknown";
601
- // failed / skipped / not_in_channel / missing_scope / no_token … は全部「人の操作待ち」
602
- 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 };
603
635
  }
604
636
  /**
605
637
  * 畳んだ種別の 1 行に出す、状態の要約(#424)。
@@ -617,9 +649,9 @@ function toolStateWithLive(item, rows) {
617
649
  * **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す)。
618
650
  */
619
651
  function toolsFoldSummary(states) {
620
- // 表示の順は状態の 4 語と同じ(読み取り中→承認待ち→停止中→未確認)
621
- var order = ["reading", "waiting", "stopped", "unknown"];
622
- 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 };
623
655
  (states || []).forEach(function (s) {
624
656
  // ⚠ 4 語のどれかに当たるかは **order で見る**(素のオブジェクトの添字だと
625
657
  // "constructor" のような語が継承プロパティに当たって、知らない語が素通りする)
@@ -2538,9 +2570,15 @@ function calendarTemplateUrl(id) {
2538
2570
  *
2539
2571
  * ⚠ `calendarTemplateUrl` と同じく**実測は未了**(#425 の 3)。共有されている人が開くと
2540
2572
  * 「カレンダーを追加しますか」が出る想定。
2573
+ *
2574
+ * 🔴 **engine の `projectCalendarUrl`(Home の「使用ツール」の Google Meet 行)と同じ形にする**
2575
+ * (cortex-engine #343)。`u/0` は「1 番目にログインしている Google アカウント」で、
2576
+ * 片方だけに付いていると、業務アカウントが 2 番目の人には**Home のリンクは開けるのに
2577
+ * この接続タブは開けない(あるいはその逆)**という、説明できない差になる。
2578
+ * 外すときは engine 側と同じ回に外す。
2541
2579
  */
2542
2580
  function calendarAddUrl(id) {
2543
- return "https://calendar.google.com/calendar/r?cid=" + encodeURIComponent(id);
2581
+ return "https://calendar.google.com/calendar/u/0/r?cid=" + encodeURIComponent(id);
2544
2582
  }
2545
2583
  /**
2546
2584
  * タイル1行(と接続グラフの1ノード)の状態を、**正本から導出する**(0.12.0)。
@@ -2717,8 +2755,6 @@ function deriveCapTile(cap, ctx) {
2717
2755
  function derivePendingTile(regKey, registered, rows) {
2718
2756
  if (!Array.isArray(registered) || registered.length === 0)
2719
2757
  return null;
2720
- var state = Array.isArray(rows) ? rows : [];
2721
- var bad = BAD_STATE_STATUSES.reachable || [];
2722
2758
  var latestAt = "";
2723
2759
  var latestMs = -Infinity;
2724
2760
  var unmeasured = false;
@@ -2727,37 +2763,25 @@ function derivePendingTile(regKey, registered, rows) {
2727
2763
  var reg = registered[i];
2728
2764
  if (!reg || typeof reg !== "object")
2729
2765
  continue;
2730
- var keys = registeredRefKeys(regKey, [reg]);
2731
- var hit = null;
2732
- var hitMs = -Infinity;
2733
- for (var j = 0; j < state.length; j++) {
2734
- var r = state[j];
2735
- if (!r || r.cap !== regKey || r.question !== "reachable")
2736
- continue;
2737
- if (!keys || !keys[stateRefKey(regKey, r.ref, r.workspace)])
2738
- continue;
2739
- // **最新の 1 行が勝つ。** 読めない at は最古(NaN は比較で常に負けるので -Infinity に畳む)。
2740
- // 同値は後勝ち(`>=`)——サーバが並べた順を崩さない
2741
- var ms = Date.parse(String(r.at || ""));
2742
- if (ms !== ms)
2743
- ms = -Infinity;
2744
- if (hit === null || ms >= hitMs) {
2745
- hit = r;
2746
- hitMs = ms;
2747
- }
2748
- }
2766
+ // 行ごとの判定は Home の「使用ツール」と同じ 1 本(`liveRowVerdict`・#581)。
2767
+ // ここに鍵の突き合わせや語の分類を書き戻すと、タイルと Home が別の判定を持つ
2768
+ var v = liveRowVerdict(regKey, reg, rows, "reachable");
2749
2769
  // 🔴 **測っていない登録行が 1 つでもあれば「接続OK」と断言しない。** 1 件の OK で能力
2750
2770
  // 全体を「接続OK」にすると、登録したばかりで一度も測っていないチャンネルまで繋がっている
2751
2771
  // ことになる(この関数が直そうとしている嘘の裏返し)。ただし**ループは最後まで回す**
2752
2772
  // ——測定済みの失敗は事実なので、未測定の行が混ざっていても失敗は出す(下の優先順)。
2753
2773
  // `unknown`(測れなかった)も未測定と同じ扱い: 行は「不明」と出るが、タイルは断言しない
2754
- if (hit === null || (hit.status !== "ok" && bad.indexOf(hit.status) < 0)) {
2774
+ if (v === null || v.status === "unknown") {
2755
2775
  unmeasured = true;
2756
2776
  continue;
2757
2777
  }
2758
- if (hit.status === "ok") {
2759
- if (hitMs >= latestMs) {
2760
- latestMs = hitMs;
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;
2761
2785
  latestAt = String(hit.at || "");
2762
2786
  }
2763
2787
  continue;
@@ -2779,6 +2803,338 @@ function derivePendingTile(regKey, registered, rows) {
2779
2803
  return null;
2780
2804
  return { kind: "ok", at: latestAt };
2781
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
+ }
2782
3138
  function capabilityState(cap, ctx, adapter) {
2783
3139
  // **能力→パイプラインの対応はここに持つ。** 呼び出し側に渡させると、
2784
3140
  // クライアント側(この関数を .toString() で受け取る側)が同じ表を書き写すことになり、
@@ -4777,7 +5133,6 @@ ${overlayStateOnFleet.toString()}
4777
5133
  // 🔴 この定数行を落とすと、流し込んだ描画がアイコンを引けずに素の kind を出す
4778
5134
  var TOOL_KIND_ICONS = ${JSON.stringify(TOOL_KIND_ICONS)};
4779
5135
  ${toolStateLabel.toString()}
4780
- ${toolStateWithLive.toString()}
4781
5136
  // 畳んだ種別の 1 行に出す状態の要約(#424)。**同じ 4 語を使う**——ここで別の言い方を
4782
5137
  // 作ると、畳んだ 1 行と開いた行が違う語で同じ状態を指す
4783
5138
  ${toolsFoldSummary.toString()}
@@ -4816,6 +5171,18 @@ ${deriveCapTile.toString()}
4816
5171
  // 登録直後のタイル(#576)。**registeredRefKeys / stateRefKey / BAD_STATE_STATUSES より後に置く**
4817
5172
  //(上の 3 つを引く。前に置くと var の BAD_STATE_STATUSES が undefined で判定が丸ごと死ぬ)
4818
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()}
4819
5186
  // 案件カレンダー(#428)。**判定の実体はビルド時の定義**——画面側に書き写すと、
4820
5187
  // 「作成済みか」の判定が 2 枚になって片方だけ直る(=2 本目のカレンダーを作らせる)。
4821
5188
  // 🔴 **BAD_STATE_STATUSES より後に置く。** calendarPhase が「悪い」語をそこから引く
@@ -6315,13 +6682,78 @@ ${calendarAddUrl.toString()}
6315
6682
  }
6316
6683
 
6317
6684
  /**
6318
- * 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)で描く。
6319
6748
  *
6320
6749
  * 🔴 **SITE.tools が無ければ何も出さない。** 生成が古い案件(この項目を知らない
6321
6750
  * fleet-status.json)では、手書きの節が本文に残っているので、そちらがそのまま出る
6322
6751
  * ——ここで「まだ接続していません」と出すと、繋いである案件に嘘を出すことになる。
6752
+ * ライブだけで節を出すことはしない(手書きの節との重複回避 「dropLegacyHomeSections」 と
6753
+ * 衝突する)。効くのは「SITE.tools はあるが古い」案件
6323
6754
  *
6324
- * **0 件は 1 行で言い切る。** 空の一覧を出すと「読み込み中」に見える。
6755
+ * **0 件は 1 行で言い切る。** 空の一覧を出すと「読み込み中」に見える。ライブで全部消えて
6756
+ * 0 件になった場合も同じ 1 行
6325
6757
  */
6326
6758
  function homeToolsSection() {
6327
6759
  var t = SITE.tools;
@@ -6330,7 +6762,7 @@ ${calendarAddUrl.toString()}
6330
6762
  if (!t || !Array.isArray(t.items)) return null;
6331
6763
  var box = el("section", { class: "home-tools" });
6332
6764
  box.appendChild(el("h2", { class: "home-grounding-title", text: "使用ツール" }));
6333
- var items = t.items || [];
6765
+ var items = homeToolsItemsNow();
6334
6766
  if (!items.length) {
6335
6767
  // 「操作タブ」はその場で移れる口にする(文言は変えない・#424)
6336
6768
  box.appendChild(el("p", { class: "home-tools-empty" }, [
@@ -6368,12 +6800,12 @@ ${calendarAddUrl.toString()}
6368
6800
  if (!ul) { ul = el("ul", { class: "home-tools-list" }); box.appendChild(ul); }
6369
6801
  g.items.forEach(function (it) { ul.appendChild(homeToolRow(it)); });
6370
6802
  });
6371
- // **どこで直せるか・いつの情報かを 1 行で言う。** 操作タブで繋いだ直後は
6372
- // この一覧に出ない(engine が毎朝作り直すまで)ので、黙っていると「効いていない」と読まれる
6803
+ // **どこで直せるかを 1 行で言う。** 「(この一覧は毎朝更新されます)」は #581 で落とした
6804
+ // ——一覧はライブの登録一覧で描くので、操作タブで足した・外した行はその場で出る・消える
6373
6805
  box.appendChild(el("p", { class: "home-tools-note" }, [
6374
6806
  document.createTextNode("接続の追加・変更は"),
6375
6807
  opsTabLink("操作タブ"),
6376
- document.createTextNode("で行います(この一覧は毎朝更新されます)。"),
6808
+ document.createTextNode("で行います。"),
6377
6809
  ]));
6378
6810
  return box;
6379
6811
  }
@@ -6389,13 +6821,15 @@ ${calendarAddUrl.toString()}
6389
6821
  text: (TOOL_KIND_ICONS[it.kind] || "🔗") + " " + (it.label || it.kind),
6390
6822
  }));
6391
6823
  var name = it.name || it.ref || "";
6392
- // **開ける先が無い行(Google Meet)はリンクにしない。** 押せないリンクを出すより
6393
- // 素のテキストのほうが正直(url は engine が null で「無い」と言い切っている)
6824
+ // **開ける先が無い行はリンクにしない。** 押せないリンクを出すより素のテキストのほうが
6825
+ // 正直(url は engine が null で「無い」と言い切っている)。Google Meet の行も
6826
+ // 案件カレンダーができていれば url を持つ(cortex-engine #343。カレンダーがまだ無い案件は null)
6394
6827
  // href に載せる前に isHttp を通す(build 側の許可リストと二重の守り。el() の注意書きのとおり)
6395
6828
  if (it.url && isHttp(it.url)) li.appendChild(el("a", { class: "home-tools-name", href: it.url, target: "_blank", rel: "noopener", text: name }));
6396
6829
  else li.appendChild(el("span", { class: "home-tools-name", text: name }));
6397
- // ライブの実測が届いていれば重ねる(stopped は上書きしない・toolStateWithLive)
6398
- var st = toolStateWithLive(it, LIVE_STATE);
6830
+ // 状態は 「liveToolItems」 が決めてある(#581)。**ここで判定し直さない**——タイルと同じ
6831
+ // 行ごとの判定(liveRowVerdict)から出た値を語に変えるだけ
6832
+ var st = String(it.state || "unknown");
6399
6833
  var lab = toolStateLabel(st);
6400
6834
  var chip = el("span", { class: "home-tools-state ds-gate " + lab.cls, text: lab.text });
6401
6835
  // 状態そのものを属性に残す(表示語を読み替えずに検査・スタイルできるように)
@@ -6430,8 +6864,8 @@ ${calendarAddUrl.toString()}
6430
6864
  // ⚠ **id に使うので、種別の字をそのまま持ち込まない。** kind は fleet-status.json 由来で、
6431
6865
  // 空白の入った値が来ると id が2つの idref に読まれ、aria-controls の対応が壊れる
6432
6866
  var key = (String(kind).replace(/[^A-Za-z0-9_-]/g, "") || "other") + "-" + seq;
6433
- // 状態は 1 行ずつ求めてから畳む(行と要約が別の判定を持たない)
6434
- 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"); }));
6435
6869
  var chip = el("span", { class: "home-tools-fold-state ds-gate " + sum.cls, text: sum.text });
6436
6870
  chip.setAttribute("data-state", sum.state);
6437
6871
  // 説明として読ませるので id が要る
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.58",
3
+ "version": "0.12.59",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,