@takagaki/cortex-decisions-viewer 0.12.57 → 0.12.58

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 +222 -6
  2. package/package.json +1 -1
package/dist/render.js CHANGED
@@ -56,6 +56,7 @@ exports.calendarWaiting = calendarWaiting;
56
56
  exports.calendarTemplateUrl = calendarTemplateUrl;
57
57
  exports.calendarAddUrl = calendarAddUrl;
58
58
  exports.deriveCapTile = deriveCapTile;
59
+ exports.derivePendingTile = derivePendingTile;
59
60
  exports.renderConnectionMap = renderConnectionMap;
60
61
  const node_crypto_1 = require("node:crypto");
61
62
  const google_chat_guide_images_1 = require("./google-chat-guide-images");
@@ -2672,6 +2673,112 @@ function deriveCapTile(cap, ctx) {
2672
2673
  return { rule: 4, state: "idle", text: STATE_CONFIRMING_TEXT, keyMissing: false };
2673
2674
  return { rule: 4, state: "idle", text: meta.noFact, keyMissing: false };
2674
2675
  }
2676
+ /**
2677
+ * 「まとめ表が知らない行がある」能力のタイルに、行と同じ答えを出せるか(#576)。
2678
+ *
2679
+ * ## なぜ要るか
2680
+ *
2681
+ * タイルの段階1(`capPending`)は「まとめ表(fleet-status.json)が知らない行を持つ能力は
2682
+ * 『登録状況を確認中です』」に倒す。まとめ表は案件リポの fleet-status.yml が**毎朝 08:30 JST
2683
+ * の schedule でしか再生成しない**ので、日中に登録した接続先は、行は「接続を確認」で
2684
+ * その場で接続OKになっても、**タイルは翌朝まで確認中のまま**——行とタイルが食い違う
2685
+ * (`ops-ui-must-show-truth`)。まとめ表が `adapters[]` に unset を焼き込んでいると導出
2686
+ * (`deriveCapTile`)も止まる(変種を持たないアダプタは `derivableUnset` を通らない)ので、
2687
+ * ライブの登録一覧+接続状態ストアの行から**行と同じ語**を引く材料をここで作る。
2688
+ *
2689
+ * ## 規則(上から順)
2690
+ *
2691
+ * 1. 登録簿が配列でない・**0 件 → `null`**(確認中のまま)。0 件は「外して 0 件になった」側で、
2692
+ * 夜間の `capabilities` はまだ on。ストアの行が無い(外すときに消してある)ことを
2693
+ * 「未接続」の根拠にしない——導出(規則1: 登録0件→未接続)が答えられる回はそちらが
2694
+ * 先に勝つ(`pend = capPending && !derive`)ので、ここへ来るのは導出が止まっている回だけ
2695
+ * 2. 登録行ごとに、突き合わせ鍵を `registeredRefKeys`(#523・#556 の正本。チャットは
2696
+ * ワークスペース付き/無しの 2 本)で作り、ストアの `cap === regKey && question === "reachable"`
2697
+ * の行のうち `stateRefKey` が当たるものを集める。1 件も当たらない登録行は**未測定**
2698
+ * (測っていないものを OK とも失敗とも言わない)
2699
+ * 3. 当たった行が複数なら `at` が最新の 1 行(読めない `at` は最古扱い・同値は後勝ち)。
2700
+ * `ok` → OK、`BAD_STATE_STATUSES.reachable` の語 → 失敗、それ以外(`unknown`)→ 未測定
2701
+ * 4. **失敗 > 未測定 > OK の順**: 失敗が 1 つでもあれば `{ kind: "bad", rows }`(登録順)。
2702
+ * 未測定が 1 つでもあれば `null`。全部 OK なら `{ kind: "ok", at }`(`at` はストアの最新)
2703
+ *
2704
+ * 語の解決(行と同じ `dsGateOf` / `dsGateForCap`)と仮置き(#388)の扱いは画面側
2705
+ * (`pendingTileVerdict`)が持つ。**純関数に localStorage を見せない。**
2706
+ *
2707
+ * @param regKey `registeredNow` のキー(chat / google-chat / dev / design / materials / issues。
2708
+ * ストアの `cap` と同じ綴り)
2709
+ * @param registered `LIVE_REGISTERED[regKey]`(配列でなければ「読めていない」)
2710
+ * @param rows 接続状態ストアの行(`LIVE_STATE`)
2711
+ * @returns `null`(従来どおり「登録状況を確認中です」)/`{ kind: "ok", at }`/
2712
+ * `{ kind: "bad", rows: [{ status, ref, workspace, type, at }] }`(登録順)
2713
+ *
2714
+ * **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す。
2715
+ * `registeredRefKeys` / `stateRefKey` / `BAD_STATE_STATUSES` はこれより前に流し込んである)。
2716
+ */
2717
+ function derivePendingTile(regKey, registered, rows) {
2718
+ if (!Array.isArray(registered) || registered.length === 0)
2719
+ return null;
2720
+ var state = Array.isArray(rows) ? rows : [];
2721
+ var bad = BAD_STATE_STATUSES.reachable || [];
2722
+ var latestAt = "";
2723
+ var latestMs = -Infinity;
2724
+ var unmeasured = false;
2725
+ var badRows = [];
2726
+ for (var i = 0; i < registered.length; i++) {
2727
+ var reg = registered[i];
2728
+ if (!reg || typeof reg !== "object")
2729
+ 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
+ }
2749
+ // 🔴 **測っていない登録行が 1 つでもあれば「接続OK」と断言しない。** 1 件の OK で能力
2750
+ // 全体を「接続OK」にすると、登録したばかりで一度も測っていないチャンネルまで繋がっている
2751
+ // ことになる(この関数が直そうとしている嘘の裏返し)。ただし**ループは最後まで回す**
2752
+ // ——測定済みの失敗は事実なので、未測定の行が混ざっていても失敗は出す(下の優先順)。
2753
+ // `unknown`(測れなかった)も未測定と同じ扱い: 行は「不明」と出るが、タイルは断言しない
2754
+ if (hit === null || (hit.status !== "ok" && bad.indexOf(hit.status) < 0)) {
2755
+ unmeasured = true;
2756
+ continue;
2757
+ }
2758
+ if (hit.status === "ok") {
2759
+ if (hitMs >= latestMs) {
2760
+ latestMs = hitMs;
2761
+ latestAt = String(hit.at || "");
2762
+ }
2763
+ continue;
2764
+ }
2765
+ badRows.push({
2766
+ status: String(hit.status),
2767
+ ref: String(hit.ref || ""),
2768
+ workspace: String(hit.workspace == null ? (reg.workspace == null ? "" : reg.workspace) : hit.workspace),
2769
+ type: String(reg.type || ""),
2770
+ at: String(hit.at || ""),
2771
+ });
2772
+ }
2773
+ // **優先順: 失敗 > 未測定 > OK。** 失敗は測った事実なので、未測定の行が混ざっていても
2774
+ // 隠さない(行は「Slackで招待してください」と出ているのにタイルが「確認中」では、
2775
+ // 行とタイルが違うことを言う)。「接続OK」だけは全行が測定済みのときに限る
2776
+ if (badRows.length)
2777
+ return { kind: "bad", rows: badRows };
2778
+ if (unmeasured)
2779
+ return null;
2780
+ return { kind: "ok", at: latestAt };
2781
+ }
2675
2782
  function capabilityState(cap, ctx, adapter) {
2676
2783
  // **能力→パイプラインの対応はここに持つ。** 呼び出し側に渡させると、
2677
2784
  // クライアント側(この関数を .toString() で受け取る側)が同じ表を書き写すことになり、
@@ -4706,6 +4813,9 @@ var BAD_STATE_STATUSES = ${JSON.stringify(BAD_STATE_STATUSES)};
4706
4813
  var STATE_CONFIRMING_TEXT = ${JSON.stringify(STATE_CONFIRMING_TEXT)};
4707
4814
  ${keyMissingText.toString()}
4708
4815
  ${deriveCapTile.toString()}
4816
+ // 登録直後のタイル(#576)。**registeredRefKeys / stateRefKey / BAD_STATE_STATUSES より後に置く**
4817
+ //(上の 3 つを引く。前に置くと var の BAD_STATE_STATUSES が undefined で判定が丸ごと死ぬ)
4818
+ ${derivePendingTile.toString()}
4709
4819
  // 案件カレンダー(#428)。**判定の実体はビルド時の定義**——画面側に書き写すと、
4710
4820
  // 「作成済みか」の判定が 2 枚になって片方だけ直る(=2 本目のカレンダーを作らせる)。
4711
4821
  // 🔴 **BAD_STATE_STATUSES より後に置く。** calendarPhase が「悪い」語をそこから引く
@@ -8469,6 +8579,71 @@ ${calendarAddUrl.toString()}
8469
8579
  return !!m[cap];
8470
8580
  }
8471
8581
 
8582
+ /**
8583
+ * 「登録状況を確認中です」に倒れるタイル(「capPending」 が立ち・導出が止まっている回)に、
8584
+ * **行と同じ語**を出せるか(#576)。出せなければ null(従来どおり確認中)。
8585
+ *
8586
+ * 判定は 「derivePendingTile」(純関数・ライブの登録一覧+接続状態ストア)。ここが持つのは
8587
+ * **仮置き(#388)の扱いと語の解決**だけ:
8588
+ *
8589
+ * - 仮置きの add で live にまだ無い行があれば null(登録簿が追いつくまでは測っていない
8590
+ * 行が 1 つある、と同じ)。remove の仮置きで live に在る行はその行を判定から外す
8591
+ * (「pendingCountDelta」 / 「refKeysOf」 と同じ材料。一覧と同じものを見る)。update
8592
+ * (通知先トグル等)は判定を止めない
8593
+ * - 語は**行と同じ関数**から引く(写経しない): チャット・開発は 「dsGateOf」(社内の
8594
+ * no_token →「管理者の設定が必要です」・GitHub の no_token →「App が未インストール」の
8595
+ * 言い換え込み)、取り込む能力は 「dsGateForCap」(Drive の unreachable →「フォルダを
8596
+ * 開けません」等)。OK は「● 接続OK ◯分前に確認」(行の 「rtAgo」 と同じ語・時刻は
8597
+ * ストアの最新)。失敗が複数で語が違っても**登録順で最初の 1 語**(タイルは 1 行。
8598
+ * 内訳はモーダルの行が持つ)
8599
+ *
8600
+ * regKey は 「TILE_CAPS[derCapOf(cap, adapter)].id」 の 1 本(#153 のアダプタ別の鍵をそのまま
8601
+ * 使う——Slack タイルが Google Chat の登録・実測を借りない、逆も)。混載タイル
8602
+ * (capabilities を持たない旧データ・adapter 無し)に 2 つの regKey を合算する分岐は置かない
8603
+ * ——その形の案件はまとめ表に capabilities が無く 「derived === null」 で導出が答えるので、
8604
+ * ここへ来る回が実質無い(設計レビュー🟡)
8605
+ */
8606
+ function pendingTileVerdict(cap, adapter) {
8607
+ var derCap = derCapOf(cap, adapter);
8608
+ var meta = TILE_CAPS[derCap];
8609
+ // 会議は登録簿を持たない(refKeyed でない)ので答えられない
8610
+ if (!meta || !meta.refKeyed || !LIVE_REGISTERED) return null;
8611
+ var regKey = meta.id;
8612
+ var registered = LIVE_REGISTERED[regKey];
8613
+ if (!Array.isArray(registered)) return null;
8614
+ // ---- 仮置き(#388)を登録簿に重ねる ----
8615
+ var ops = pendingOpsNow();
8616
+ var live = registered;
8617
+ for (var i = 0; i < ops.length; i++) {
8618
+ var e = ops[i];
8619
+ var km = e && PENDING_KINDS[e.kind];
8620
+ if (!km || km.reg !== regKey) continue;
8621
+ var found = null;
8622
+ for (var k = 0; k < live.length; k++) {
8623
+ if (pendingLiveRowMatches(e.kind, live[k], e.targetId)) { found = live[k]; break; }
8624
+ }
8625
+ // add で live にまだ無い行=誰も測っていない登録行が 1 つある → 確認中のまま
8626
+ if (e.action === "add" && !found) return null;
8627
+ if (e.action === "remove" && found) {
8628
+ live = live.filter(function (r) { return r !== found; });
8629
+ }
8630
+ }
8631
+ var v = derivePendingTile(regKey, live, LIVE_STATE);
8632
+ if (!v) return null;
8633
+ if (v.kind === "ok") {
8634
+ var ago = rtAgo(v.at);
8635
+ return { text: CAP_STATE_MARK.ok + "接続OK" + (ago ? " " + ago + "に確認" : ""), cls: "ds-gate-ok" };
8636
+ }
8637
+ var first = v.rows[0];
8638
+ var gate;
8639
+ if (regKey === "chat") gate = dsGateOf({ type: "slack", workspace: first.workspace, gate: first.status });
8640
+ else if (regKey === "google-chat") gate = dsGateOf({ type: "google-chat", gate: first.status });
8641
+ else if (regKey === "dev") gate = dsGateOf({ type: first.type || "github-issues", gate: first.status });
8642
+ else gate = dsGateForCap(regKey, first.status);
8643
+ // 行と同じ見た目: warn の語にだけ ⚠️ を前置(muted の語=「管理者の設定が必要です」等には付けない)
8644
+ return { text: (gate.cls === "ds-gate-warn" ? CAP_STATE_MARK.warn : "") + gate.text, cls: gate.cls };
8645
+ }
8646
+
8472
8647
  /**
8473
8648
  * 能力の状態を決めるときに渡す値。**足した行は混ぜない**(誰も測っていないので状態を持たない)。
8474
8649
  *
@@ -9128,7 +9303,9 @@ ${calendarAddUrl.toString()}
9128
9303
  var asOfText = LIVE_FLEET_FAILED
9129
9304
  ? "最新の接続状況を取得できませんでした。表示しているのは " + asOf + " 時点の情報です。"
9130
9305
  : hasPending
9131
- ? "接続状況は " + asOf + " 時点の自動チェック結果です。いま登録した分も一覧に出していますが、読み取れているかどうかが分かるのは次回の自動チェックの後になります。"
9306
+ // 「接続を確認」で確かめた分はタイルにもその場で出る(#576)ので、「次回の自動チェックの後」
9307
+ // と言い切らない——同じ節でタイルが「接続OK」と言っているのに矛盾する
9308
+ ? "接続状況は " + asOf + " 時点の自動チェック結果です。いま登録した分も一覧に出しています。「接続を確認」で確かめたものはタイルにも出し、残りは次回の自動チェックの後に分かります。"
9132
9309
  : "接続状況は " + asOf + " 時点の自動チェック結果です。設定を変えた場合、ここに反映されるのは次回の実行後になります。";
9133
9310
  sec.appendChild(el("p", {
9134
9311
  class: "datasources-asof" + (LIVE_FLEET_FAILED ? " is-stale" : ""),
@@ -9933,6 +10110,17 @@ ${calendarAddUrl.toString()}
9933
10110
  // ここを逆にすると、まとめ表が追いつく翌朝まで「確認中です」が居座る。
9934
10111
  // 確認中の判定はアダプタで絞る(#153・判断⑭)
9935
10112
  var pend = capPending(cap, adapter) && !derive;
10113
+ // **確認中に倒す回でも、ライブの登録一覧+接続状態ストアが行と同じ答えを持っていれば
10114
+ // それを出す(#576)。** まとめ表は毎朝 08:30 の schedule でしか作り直されないので、
10115
+ // 日中に登録して「接続を確認」で行が接続OKになっても、ここが確認中のままだと
10116
+ // 行とタイルが翌朝まで食い違う。**「st === "warn"」 の回は OK を採らない**——fleet の
10117
+ // 行から出た warn を導出が降格しない保護(上)と同じ理由で、色(⚠️)と語(接続OK)が
10118
+ // 割れる。失敗(行と同じ語)は採る
10119
+ // **答えるのは、導出を止めた理由が unset(エンジンが未導出)のときだけ。** off / broken
10120
+ // (意図的に止めた・設定が壊れている)で導出が止まった回にストアの ok で「接続OK」と
10121
+ // 言うと、表 5 の優先順位(OFF・設定に異常が導出より先)を裏切る(実装レビュー🟡2)
10122
+ var pendNow = pend && (derived == null || derived === "on" || derived === "unset") ? pendingTileVerdict(cap, adapter) : null;
10123
+ if (pendNow && st === "warn" && pendNow.cls === "ds-gate-ok") pendNow = null;
9936
10124
  // 🔴 **初回のライブ確認が答えるまで、状態を断言しない(#256・設計v2 の5)。**
9937
10125
  // この2行目は焼いた capabilities 由来の状態を先に言っていたので、図と同じ瞬き
9938
10126
  // (旧状態を見せて1〜2秒後に訂正)をタイルでも起こしていた。**兄弟機能の同型の
@@ -9943,11 +10131,11 @@ ${calendarAddUrl.toString()}
9943
10131
  // 答える能力だけ(derived が null / "on")=図と同じ規則5の線引き。
9944
10132
  var confirming = liveConfirming() && (derived === null || derived === "on");
9945
10133
  var subEl = el("div", {
9946
- class: "ds-tile-sub " + (confirming || pend ? DS_GATE_PENDING.cls : "ds-gate-" + st),
10134
+ class: "ds-tile-sub " + (confirming || (pend && !pendNow) ? DS_GATE_PENDING.cls : pend ? pendNow.cls : "ds-gate-" + st),
9947
10135
  // **反映待ち(pend)の形は変えない。** あちらは「足した行がまとめ表に載るまで」の
9948
10136
  // 折衷で、件数を添えると足した分を数えたようにも読める。確認中は焼いた件数を残す
9949
10137
  text: pend
9950
- ? DS_GATE_PENDING.text
10138
+ ? (pendNow ? pendNow.text : DS_GATE_PENDING.text)
9951
10139
  : tileSub(cap, st, derived, adapter, internals, sources, derive, confirming),
9952
10140
  });
9953
10141
  var body = el("div", { class: "ds-tile-body" }, [
@@ -10579,6 +10767,14 @@ ${calendarAddUrl.toString()}
10579
10767
  // **GitHub は揃えない。** 行の名前は owner/repo そのもので、書き換える口
10580
10768
  //(op=config の refresh-channel-name)は channels.json 専用
10581
10769
  if (s.type === "slack") autoAlignName(s, j, card);
10770
+ // ストアにも 「button」 の行が書かれている(#576)。**取りに行くのは 1 回だけ**——
10771
+ // 届いた行でモーダルを閉じたときにタイルが引き直され、登録直後のタイルが行と同じ
10772
+ // 「接続OK」になる(「pendingTileVerdict」)。ここで取らないと、画面がストアの行を
10773
+ // 読むのは次の op=state(追加行が未確定の 15 分間は 60 秒ごと・過ぎていれば
10774
+ // 再読み込みまで)で、「接続を確認 → 閉じる → その場で接続OK」にならない。
10775
+ // 据え置きあり=GitHub は叩かない(ストアの行は据え置きの対象外で毎回引く)。
10776
+ // 取り込む能力の即答(appendDispatchProbe)は先に同じ 1 行を持っている
10777
+ reloadLiveState();
10582
10778
  } catch (e) {
10583
10779
  msg.className = "ds-toggle-msg is-error";
10584
10780
  msg.textContent = "✗ 通信に失敗しました。";
@@ -11427,7 +11623,7 @@ ${calendarAddUrl.toString()}
11427
11623
  showProbeVerdict(row, capId, checkBtn, j);
11428
11624
  // ストアにも 「button」 の行が書かれている。**取りに行くのは1回だけ**——
11429
11625
  // 届いた行で一覧が描き直され、この行の札が正本の値に揃う
11430
- loadLiveFleet();
11626
+ reloadLiveState();
11431
11627
  } catch (e) {
11432
11628
  note.textContent = "通信に失敗しました。";
11433
11629
  delete PROBE_WAITS[key];
@@ -13688,6 +13884,21 @@ ${calendarAddUrl.toString()}
13688
13884
  * 戻るので、旗として残し、応答後にもう一度取りに行く
13689
13885
  */
13690
13886
  var freshWanted = false;
13887
+ /**
13888
+ * 「接続を確認」の応答が 2xx だったので、ストアの行をもう一度読みたい(#576)。
13889
+ *
13890
+ * 🔴 **取得が飛行中なら loadLiveFleet は黙って戻る**(liveFleetRequested の項)。追加行が
13891
+ * 未確定の 15 分間は 60 秒ごとの取り直しが走っているので、押した瞬間がそれと重なると、
13892
+ * 押した結果(by:button の行)を画面が次の便まで読まない=タイルが「その場で接続OK」に
13893
+ * ならない。そこで**飛行中なら旗を立て、応答が届いたら 1 回だけ取り直す**(保存直後の
13894
+ * freshWanted と同じ形。ただし据え置きは迂回しない——GitHub の枠を削る理由が無い)。
13895
+ * 失敗(catch)の側では取り直さない(失敗を繰り返す形にしない。期限つきの取り直しに任せる)
13896
+ */
13897
+ var againWanted = false;
13898
+ function reloadLiveState() {
13899
+ if (liveFleetRequested) { againWanted = true; return; }
13900
+ loadLiveFleet();
13901
+ }
13691
13902
  function loadLiveFleet(fresh) {
13692
13903
  if (fresh) freshWanted = true;
13693
13904
  if (liveFleetRequested) return;
@@ -13782,10 +13993,15 @@ ${calendarAddUrl.toString()}
13782
13993
  syncAiLinks();
13783
13994
  // Home を開いていれば「使用ツール」の状態を実測で描き直す(#275)
13784
13995
  syncHomeTools();
13785
- // 取得中に保存が効いていたら、迂回付きでもう一度取りに行く
13786
- if (freshWanted) loadLiveFleet();
13996
+ // 取得中に保存が効いていたら、迂回付きでもう一度取りに行く。
13997
+ // 取得中に「接続を確認」が通っていたら、据え置きのままもう一度(reloadLiveState の項)
13998
+ var again = againWanted;
13999
+ againWanted = false;
14000
+ if (freshWanted || again) loadLiveFleet();
13787
14001
  })
13788
14002
  .catch(function () {
14003
+ // 失敗の側では「もう一度」の旗を捨てる(reloadLiveState の項)
14004
+ againWanted = false;
13789
14005
  clearGiveUp();
13790
14006
  // 静的ビルドの値で描き続ける。ただし「古いかもしれない」ことは画面に出す
13791
14007
  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.58",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,