@takagaki/cortex-decisions-viewer 0.12.56 → 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.
@@ -202,9 +202,17 @@ function backlogTargetOf(input) {
202
202
  function pendingIsSlackTeamId(v) {
203
203
  return /^[TE][A-Z0-9]{6,20}$/.test(String(v == null ? "" : v));
204
204
  }
205
- /** Backlog の接続先の鍵(render.ts の issuesRefOf と同形: ドメイン小文字 / キー大文字) */
205
+ /**
206
+ * Backlog の接続先の鍵(render.ts の issuesRefOf と同形: ドメイン小文字 / キー大文字)。
207
+ *
208
+ * 🔴 **規則の正本は `infra/lambda/shared/connection-state-keys.mjs` の `issuesRefOf`**(#556)。
209
+ * ここも render.ts も**写し**で、ずれると仮置きの札が実測の行に重ならない
210
+ * (押した直後の札が消えないまま、別の行に本物が出る)。
211
+ * ずれを捕まえる網は `viewer/test/state-ref-parity.test.mjs`。
212
+ */
206
213
  function pendingIssuesRef(domain, projectKey) {
207
- return String(domain == null ? "" : domain).toLowerCase() + "/" + String(projectKey == null ? "" : projectKey).toUpperCase();
214
+ return String(domain == null ? "" : domain).trim().toLowerCase() + "/" +
215
+ String(projectKey == null ? "" : projectKey).trim().toUpperCase();
208
216
  }
209
217
  // ---- エントリを作る(op → kind / action / targetId の対応表はここ 1 つ) ------------
210
218
  /**
@@ -277,8 +285,13 @@ function pendingOpOf(payload, ctx) {
277
285
  const name = pendingLabel(payload.name);
278
286
  return name ? make("slack", "update", payload.ref, { name: name }) : null;
279
287
  }
280
- case "set-notify":
281
- return typeof payload.value === "boolean" ? make("slack", "update", payload.ref, { notify: payload.value }) : null;
288
+ case "set-notify": {
289
+ if (typeof payload.value !== "boolean")
290
+ return null;
291
+ // op は 1 つ。ref の形(spaces/…)で Google Chat と決める(サーバの notifyRefKind と同じ・#572)
292
+ const kind = pendingIsSpaceRef(payload.ref) ? "google-chat" : "slack";
293
+ return make(kind, "update", payload.ref, { notify: payload.value });
294
+ }
282
295
  case "add-google-chat-space": {
283
296
  const ref = pendingGoogleChatSpaceRef(payload.url);
284
297
  // 名前は画面に欄が無い(サーバが引く)。live が来るまでは ref で出す
@@ -660,6 +673,9 @@ function pendingExternalRow(e) {
660
673
  row.type = "google-chat";
661
674
  row.name = p.name || e.targetId;
662
675
  row.ref = e.targetId;
676
+ // add に畳まれた update(通知先 ON)を落とさない(#572・Slack と同じ)
677
+ if (p.notify === true)
678
+ row.notify = true;
663
679
  }
664
680
  else {
665
681
  row.type = p.type || "github-issues";
@@ -734,14 +750,23 @@ function applyPendingOps(view, list) {
734
750
  extChanged = true;
735
751
  }
736
752
  else if (e.action === "add") {
737
- if (idx >= 0)
753
+ if (idx >= 0) {
754
+ // 本物の行が先に来ている。**add に畳まれた update(通知先の向き)だけは当てる**
755
+ // (#572)——畳んだまま捨てると、追加 → 5 分以内に本物の行 → 通知先 ON の順で
756
+ // 押した人の札が、据え置きの live で戻る
757
+ if (typeof p.notify === "boolean" && (externals[idx].notify === true) !== p.notify) {
758
+ externals[idx] = Object.assign({}, externals[idx], { notify: p.notify });
759
+ extChanged = true;
760
+ }
738
761
  continue;
762
+ }
739
763
  externals.push(pendingExternalRow(e));
740
764
  extChanged = true;
741
765
  }
742
- else if (e.kind === "slack" && idx >= 0) {
766
+ else if ((e.kind === "slack" || e.kind === "google-chat") && idx >= 0) {
767
+ // update: Slack は name / notify、Google Chat は notify だけ(名前はサーバが引く・#572)
743
768
  const patch = {};
744
- if (typeof p.name === "string" && p.name)
769
+ if (e.kind === "slack" && typeof p.name === "string" && p.name)
745
770
  patch.name = p.name;
746
771
  if (typeof p.notify === "boolean")
747
772
  patch.notify = p.notify;
package/dist/render.js CHANGED
@@ -18,6 +18,7 @@ exports.sanitizeStateNow = sanitizeStateNow;
18
18
  exports.overlayStateOnFleet = overlayStateOnFleet;
19
19
  exports.chatRowsToAdd = chatRowsToAdd;
20
20
  exports.gchatRowsToAdd = gchatRowsToAdd;
21
+ exports.overlayGchatNotify = overlayGchatNotify;
21
22
  exports.devRowsToAdd = devRowsToAdd;
22
23
  exports.markExcludedPendingRows = markExcludedPendingRows;
23
24
  exports.designRowsToAdd = designRowsToAdd;
@@ -55,6 +56,7 @@ exports.calendarWaiting = calendarWaiting;
55
56
  exports.calendarTemplateUrl = calendarTemplateUrl;
56
57
  exports.calendarAddUrl = calendarAddUrl;
57
58
  exports.deriveCapTile = deriveCapTile;
59
+ exports.derivePendingTile = derivePendingTile;
58
60
  exports.renderConnectionMap = renderConnectionMap;
59
61
  const node_crypto_1 = require("node:crypto");
60
62
  const google_chat_guide_images_1 = require("./google-chat-guide-images");
@@ -854,7 +856,8 @@ const REGISTERED_CAP_NAMES = {
854
856
  */
855
857
  const REGISTERED_FIELDS = {
856
858
  chat: ["ref", "name", "workspace", "orgUnknown", "origin", "goldState", "notify"],
857
- "google-chat": ["ref", "name"],
859
+ // notify(#572): スペースも通知先にできる。真偽値の扱いは chat notify と同じ篩
860
+ "google-chat": ["ref", "name", "notify"],
858
861
  dev: ["ref", "type"],
859
862
  // 🔴 **意図的に外した開発リポジトリ(#391)。** サーバは前から別キーで返していたが、
860
863
  // ここに無いので篩が丸ごと落としていた。**これが読めないと「読み取り停止中」の行を
@@ -1177,10 +1180,58 @@ function gchatRowsToAdd(externals, rows) {
1177
1180
  continue;
1178
1181
  if (have[String(r.ref)])
1179
1182
  continue;
1180
- out.push({ type: "google-chat", name: r.name || r.ref, ref: r.ref });
1183
+ const row = { type: "google-chat", name: r.name || r.ref, ref: r.ref };
1184
+ // 通知先の印(#572)。真偽値の印は true のときだけ載せる(chatRowsToAdd と同じ)
1185
+ if (r.notify === true)
1186
+ row.notify = true;
1187
+ out.push(row);
1181
1188
  }
1182
1189
  return out;
1183
1190
  }
1191
+ /**
1192
+ * まとめ表が既に持っている Google Chat の行に、live(registeredNow)の「通知先」の印を重ねる(#572)。
1193
+ *
1194
+ * 🔴 **engine のまとめ表(fleet-status)は Google Chat の行に `notify` を付けない**
1195
+ * (`resolve-external-sources.mjs` は Slack にだけ付ける)。重ねないと、押した直後の仮置きが
1196
+ * 切れたあと(最長 24 時間後)に、通知先のスペースが「通知先にする」に戻って見える
1197
+ * ——画面では通知先でないのに投稿される、という取り違えになる。
1198
+ *
1199
+ * **Slack の行には触らない**(まとめ表が正しく持っている・挙動を変えない)。live に
1200
+ * `google-chat` のキーが無ければ(読めていない)何も変えない。live に同じ ref の行があれば
1201
+ * その `notify` を正とし(true なら付け、そうでなければ外す)、live に無い行はそのまま
1202
+ * (消す判断は `subtractRemovedRegistered` の責務)。
1203
+ *
1204
+ * **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す)。
1205
+ */
1206
+ function overlayGchatNotify(externals, rows) {
1207
+ const list = externals || [];
1208
+ if (!Array.isArray(rows))
1209
+ return list;
1210
+ const live = {};
1211
+ for (const r of rows) {
1212
+ if (!r || !r.ref)
1213
+ continue;
1214
+ live[String(r.ref)] = r.notify === true;
1215
+ }
1216
+ let changed = false;
1217
+ const out = list.map(function (s) {
1218
+ if (!s || s.type !== "google-chat" || !s.ref)
1219
+ return s;
1220
+ if (!Object.prototype.hasOwnProperty.call(live, String(s.ref)))
1221
+ return s;
1222
+ const want = live[String(s.ref)];
1223
+ if ((s.notify === true) === want)
1224
+ return s;
1225
+ changed = true;
1226
+ const row = Object.assign({}, s);
1227
+ if (want)
1228
+ row.notify = true;
1229
+ else
1230
+ delete row.notify;
1231
+ return row;
1232
+ });
1233
+ return changed ? out : list;
1234
+ }
1184
1235
  /**
1185
1236
  * 開発の行で、まとめ表がまだ知らないもの。**鍵は `owner/repo` の生文字列。**
1186
1237
  *
@@ -1295,10 +1346,17 @@ function materialRowsToAdd(ids, rows) {
1295
1346
  * ⚠ 画面の `targetsGenerationOf`(サーバへ送る世代)は同じ規則を持つが、この関数を
1296
1347
  * 呼べない——配信物から関数1つを切り出して直接呼ぶテストがあり、外の名前を参照した
1297
1348
  * 瞬間にそのテストが動かなくなる。あちらを直すときはここも直す。
1349
+ *
1350
+ * 🔴 **規則の正本は `infra/lambda/shared/connection-state-keys.mjs` の `issuesRefOf`**(#556)。
1351
+ * ここはその**写し**で、`viewer/` は独立した npm パッケージ(`rootDir: src`)なので
1352
+ * 原本を import できない。**ずれを捕まえる網は契約表**——
1353
+ * `infra/test/fixtures/connection-state-contract.json` の `issuesRefOf` の節を、
1354
+ * 原本(infra 側のテスト)と写し(viewer/test/state-ref-parity.test.mjs)の**両方**に流す。
1355
+ * 片方だけ直したら、直していない側が赤くなる。
1298
1356
  */
1299
1357
  function issuesRefOf(domain, projectKey) {
1300
- return String(domain == null ? "" : domain).toLowerCase() + "/" +
1301
- String(projectKey == null ? "" : projectKey).toUpperCase();
1358
+ return String(domain == null ? "" : domain).trim().toLowerCase() + "/" +
1359
+ String(projectKey == null ? "" : projectKey).trim().toUpperCase();
1302
1360
  }
1303
1361
  /**
1304
1362
  * 課題管理の行で、まとめ表がまだ知らないもの。
@@ -1578,9 +1636,11 @@ function mergeRegisteredFleet(view, reg) {
1578
1636
  };
1579
1637
  if (!reg)
1580
1638
  return out;
1639
+ // Google Chat の行の「通知先」は live が正(#572・まとめ表は持っていない)。足す前に重ねる
1640
+ out.externalSources = overlayGchatNotify(v.externalSources, reg["google-chat"]);
1581
1641
  const add = registeredRowsToAdd(v, reg);
1582
1642
  if (add.externalSources.length)
1583
- out.externalSources = (v.externalSources || []).concat(add.externalSources);
1643
+ out.externalSources = (out.externalSources || []).concat(add.externalSources);
1584
1644
  if (add.design.length || add.materials.length) {
1585
1645
  const internals = (v.internalSources || []).slice();
1586
1646
  const put = (kind, patch) => {
@@ -2055,12 +2115,16 @@ function fleetLastSyncOf(internal) {
2055
2115
  * `issuesRefOf` はこれより**前**に流し込んである)。
2056
2116
  */
2057
2117
  function stateRefKey(capId, ref, workspace) {
2058
- var s = String(ref == null ? "" : ref);
2118
+ // 🔴 **`normalizeStateRef`(connection-state-keys.mjs)の写し**(#556)。ワークスペースを
2119
+ // 足すところだけが画面固有で、ref の畳み方そのものは向こうが正本
2120
+ // (契約表 `normalizeStateRef` の節を両側に流して突き合わせる)
2121
+ var s = String(ref == null ? "" : ref).trim();
2059
2122
  if (!s)
2060
2123
  return "";
2061
2124
  if (capId === "issues") {
2062
2125
  var at = s.indexOf("/");
2063
- if (at < 0)
2126
+ // ドメインもキーも欠けた値から `issues` の鍵は組めない(原本と同じ判断)
2127
+ if (at <= 0 || at === s.length - 1)
2064
2128
  return "";
2065
2129
  return issuesRefOf(s.slice(0, at), s.slice(at + 1));
2066
2130
  }
@@ -2094,24 +2158,27 @@ function registeredRefKeys(regKey, rows) {
2094
2158
  out[issuesRefOf(r.domain, r.projectKey)] = true;
2095
2159
  continue;
2096
2160
  }
2161
+ // 🔴 **相方(`stateRefKey`)と同じだけ畳む(#556)。** あちらが前後の空白を落とすので、
2162
+ // ここが落とさないと**登録してあるのに「登録簿に無い」と判定される**行ができる
2163
+ // (いまは登録簿の値がサーバ側で trim 済みなので起きないが、片側だけ畳む形を残さない)
2097
2164
  if (regKey === "design") {
2098
2165
  if (r.key)
2099
- out[String(r.key)] = true;
2166
+ out[String(r.key).trim()] = true;
2100
2167
  continue;
2101
2168
  }
2102
2169
  if (regKey === "materials") {
2103
2170
  if (r.id)
2104
- out[String(r.id)] = true;
2171
+ out[String(r.id).trim()] = true;
2105
2172
  continue;
2106
2173
  }
2107
2174
  if (!r.ref)
2108
2175
  continue;
2109
2176
  if (regKey === "chat") {
2110
- out[String(r.workspace == null ? "" : r.workspace) + "\t" + String(r.ref)] = true;
2111
- out["\t" + String(r.ref)] = true;
2177
+ out[String(r.workspace == null ? "" : r.workspace) + "\t" + String(r.ref).trim()] = true;
2178
+ out["\t" + String(r.ref).trim()] = true;
2112
2179
  continue;
2113
2180
  }
2114
- out[String(r.ref)] = true;
2181
+ out[String(r.ref).trim()] = true;
2115
2182
  }
2116
2183
  return out;
2117
2184
  }
@@ -2606,6 +2673,112 @@ function deriveCapTile(cap, ctx) {
2606
2673
  return { rule: 4, state: "idle", text: STATE_CONFIRMING_TEXT, keyMissing: false };
2607
2674
  return { rule: 4, state: "idle", text: meta.noFact, keyMissing: false };
2608
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
+ }
2609
2782
  function capabilityState(cap, ctx, adapter) {
2610
2783
  // **能力→パイプラインの対応はここに持つ。** 呼び出し側に渡させると、
2611
2784
  // クライアント側(この関数を .toString() で受け取る側)が同じ表を書き写すことになり、
@@ -4545,6 +4718,7 @@ var PROBE_WAIT_TIMEOUT_TEXT = ${JSON.stringify(exports.PROBE_WAIT_TIMEOUT_TEXT)}
4545
4718
  ${sanitizeRegisteredNow.toString()}
4546
4719
  ${chatRowsToAdd.toString()}
4547
4720
  ${gchatRowsToAdd.toString()}
4721
+ ${overlayGchatNotify.toString()}
4548
4722
  ${devRowsToAdd.toString()}
4549
4723
  ${markExcludedPendingRows.toString()}
4550
4724
  ${designRowsToAdd.toString()}
@@ -4639,6 +4813,9 @@ var BAD_STATE_STATUSES = ${JSON.stringify(BAD_STATE_STATUSES)};
4639
4813
  var STATE_CONFIRMING_TEXT = ${JSON.stringify(STATE_CONFIRMING_TEXT)};
4640
4814
  ${keyMissingText.toString()}
4641
4815
  ${deriveCapTile.toString()}
4816
+ // 登録直後のタイル(#576)。**registeredRefKeys / stateRefKey / BAD_STATE_STATUSES より後に置く**
4817
+ //(上の 3 つを引く。前に置くと var の BAD_STATE_STATUSES が undefined で判定が丸ごと死ぬ)
4818
+ ${derivePendingTile.toString()}
4642
4819
  // 案件カレンダー(#428)。**判定の実体はビルド時の定義**——画面側に書き写すと、
4643
4820
  // 「作成済みか」の判定が 2 枚になって片方だけ直る(=2 本目のカレンダーを作らせる)。
4644
4821
  // 🔴 **BAD_STATE_STATUSES より後に置く。** calendarPhase が「悪い」語をそこから引く
@@ -8402,6 +8579,71 @@ ${calendarAddUrl.toString()}
8402
8579
  return !!m[cap];
8403
8580
  }
8404
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
+
8405
8647
  /**
8406
8648
  * 能力の状態を決めるときに渡す値。**足した行は混ぜない**(誰も測っていないので状態を持たない)。
8407
8649
  *
@@ -9061,7 +9303,9 @@ ${calendarAddUrl.toString()}
9061
9303
  var asOfText = LIVE_FLEET_FAILED
9062
9304
  ? "最新の接続状況を取得できませんでした。表示しているのは " + asOf + " 時点の情報です。"
9063
9305
  : hasPending
9064
- ? "接続状況は " + asOf + " 時点の自動チェック結果です。いま登録した分も一覧に出していますが、読み取れているかどうかが分かるのは次回の自動チェックの後になります。"
9306
+ // 「接続を確認」で確かめた分はタイルにもその場で出る(#576)ので、「次回の自動チェックの後」
9307
+ // と言い切らない——同じ節でタイルが「接続OK」と言っているのに矛盾する
9308
+ ? "接続状況は " + asOf + " 時点の自動チェック結果です。いま登録した分も一覧に出しています。「接続を確認」で確かめたものはタイルにも出し、残りは次回の自動チェックの後に分かります。"
9065
9309
  : "接続状況は " + asOf + " 時点の自動チェック結果です。設定を変えた場合、ここに反映されるのは次回の実行後になります。";
9066
9310
  sec.appendChild(el("p", {
9067
9311
  class: "datasources-asof" + (LIVE_FLEET_FAILED ? " is-stale" : ""),
@@ -9866,6 +10110,17 @@ ${calendarAddUrl.toString()}
9866
10110
  // ここを逆にすると、まとめ表が追いつく翌朝まで「確認中です」が居座る。
9867
10111
  // 確認中の判定はアダプタで絞る(#153・判断⑭)
9868
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;
9869
10124
  // 🔴 **初回のライブ確認が答えるまで、状態を断言しない(#256・設計v2 の5)。**
9870
10125
  // この2行目は焼いた capabilities 由来の状態を先に言っていたので、図と同じ瞬き
9871
10126
  // (旧状態を見せて1〜2秒後に訂正)をタイルでも起こしていた。**兄弟機能の同型の
@@ -9876,11 +10131,11 @@ ${calendarAddUrl.toString()}
9876
10131
  // 答える能力だけ(derived が null / "on")=図と同じ規則5の線引き。
9877
10132
  var confirming = liveConfirming() && (derived === null || derived === "on");
9878
10133
  var subEl = el("div", {
9879
- 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),
9880
10135
  // **反映待ち(pend)の形は変えない。** あちらは「足した行がまとめ表に載るまで」の
9881
10136
  // 折衷で、件数を添えると足した分を数えたようにも読める。確認中は焼いた件数を残す
9882
10137
  text: pend
9883
- ? DS_GATE_PENDING.text
10138
+ ? (pendNow ? pendNow.text : DS_GATE_PENDING.text)
9884
10139
  : tileSub(cap, st, derived, adapter, internals, sources, derive, confirming),
9885
10140
  });
9886
10141
  var body = el("div", { class: "ds-tile-body" }, [
@@ -10512,6 +10767,14 @@ ${calendarAddUrl.toString()}
10512
10767
  // **GitHub は揃えない。** 行の名前は owner/repo そのもので、書き換える口
10513
10768
  //(op=config の refresh-channel-name)は channels.json 専用
10514
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();
10515
10778
  } catch (e) {
10516
10779
  msg.className = "ds-toggle-msg is-error";
10517
10780
  msg.textContent = "✗ 通信に失敗しました。";
@@ -10624,6 +10887,9 @@ ${calendarAddUrl.toString()}
10624
10887
  "次回の Gold 昇格(毎晩22時ごろ)から、このスペースは読み取られなくなります。",
10625
10888
  "",
10626
10889
  "※ 既に Gold 層に入った内容は消えません(Decision・用語集はそのまま残ります)。",
10890
+ // 解除は通知先の宣言(notify.json)を触らない(#572)。通知先のまま解除すると
10891
+ // 行が消えて画面から止められなくなる——先に止める手順をここで言う
10892
+ "※ 通知先にしている場合は、先に「通知を止める」を押してください(解除しても通知先は変わりません)。",
10627
10893
  "",
10628
10894
  "解除しますか?",
10629
10895
  ];
@@ -10682,6 +10948,12 @@ ${calendarAddUrl.toString()}
10682
10948
  *
10683
10949
  * **クロージャを持たない純関数に保つこと**(テストが配信物から切り出して直接呼ぶ)。
10684
10950
  * だから issuesRefOf を呼べない(規則の正本はあちら側。直すときは両方直す)。
10951
+ *
10952
+ * ⚠ **ここは前後の空白を落とさない。行の鍵(issuesRefOf)とは別物**(#556)。
10953
+ * 揃えたくなるが、**揃えなくても食い違わない**——サーバは受け取った世代を
10954
+ * normalizeGeneration に通してから比べ、そちらが空白を落とすため。逆に、ここで
10955
+ * 落とす形に変えるなら**サーバの両方**(targetsGeneration と normalizeGeneration)を
10956
+ * 同時に見ること。善意で片側だけ揃えると、正しい操作が毎回 409 になる。
10685
10957
  */
10686
10958
  function targetsGenerationOf(list) {
10687
10959
  return (list || [])
@@ -10931,9 +11203,9 @@ ${calendarAddUrl.toString()}
10931
11203
  // cap と question の両方で絞る。片方だけだと、同じ ref を持つ別の問い
10932
11204
  //(将来 issues に別の question が増えたとき)の行を取り込みとして読む
10933
11205
  if (!r || r.cap !== "issues" || r.question !== "ingested") continue;
10934
- var at = String(r.ref || "").indexOf("/");
10935
- if (at < 0) continue;
10936
- if (issuesRefOf(String(r.ref).slice(0, at), String(r.ref).slice(at + 1)) === want) return r;
11206
+ // 行側も畳み方の正本(stateRefKey normalizeStateRef の写し)を通す(#556)。
11207
+ // ここで indexOf を自前で切っていた間は、**切り方が5本目**になっていた
11208
+ if (stateRefKey("issues", r.ref) === want) return r;
10937
11209
  }
10938
11210
  return null;
10939
11211
  }
@@ -11351,7 +11623,7 @@ ${calendarAddUrl.toString()}
11351
11623
  showProbeVerdict(row, capId, checkBtn, j);
11352
11624
  // ストアにも 「button」 の行が書かれている。**取りに行くのは1回だけ**——
11353
11625
  // 届いた行で一覧が描き直され、この行の札が正本の値に揃う
11354
- loadLiveFleet();
11626
+ reloadLiveState();
11355
11627
  } catch (e) {
11356
11628
  note.textContent = "通信に失敗しました。";
11357
11629
  delete PROBE_WAITS[key];
@@ -13227,6 +13499,11 @@ ${calendarAddUrl.toString()}
13227
13499
  // だけなので、Slack のような origin の出し分けは不要
13228
13500
  if (SITE.intake && s.type === "google-chat" && s.ref) {
13229
13501
  card.appendChild(removeSpaceButton(s, card));
13502
+ // 通知先の入り切り(#572)。Slack と同じ notifyToggle・同じ文言・同じ op(サーバは
13503
+ // ref の形(spaces/…)で Google Chat と判定する)。スペースの行に workspace は無いので
13504
+ // 「読み取り専用の接続」の分岐には入らず、常にボタンが出る。仮置きの行(本物の行が
13505
+ // 来るまで)には Slack と同じく出さない
13506
+ if (s.pending !== true) card.appendChild(notifyToggle(s, badges));
13230
13507
  }
13231
13508
  // GitHubリポの登録から削除。**ON/OFF のトグルは撤去した(2026-08-21)**
13232
13509
  //(stateToggle のコメント参照)。
@@ -13607,6 +13884,21 @@ ${calendarAddUrl.toString()}
13607
13884
  * 戻るので、旗として残し、応答後にもう一度取りに行く
13608
13885
  */
13609
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
+ }
13610
13902
  function loadLiveFleet(fresh) {
13611
13903
  if (fresh) freshWanted = true;
13612
13904
  if (liveFleetRequested) return;
@@ -13701,10 +13993,15 @@ ${calendarAddUrl.toString()}
13701
13993
  syncAiLinks();
13702
13994
  // Home を開いていれば「使用ツール」の状態を実測で描き直す(#275)
13703
13995
  syncHomeTools();
13704
- // 取得中に保存が効いていたら、迂回付きでもう一度取りに行く
13705
- if (freshWanted) loadLiveFleet();
13996
+ // 取得中に保存が効いていたら、迂回付きでもう一度取りに行く。
13997
+ // 取得中に「接続を確認」が通っていたら、据え置きのままもう一度(reloadLiveState の項)
13998
+ var again = againWanted;
13999
+ againWanted = false;
14000
+ if (freshWanted || again) loadLiveFleet();
13706
14001
  })
13707
14002
  .catch(function () {
14003
+ // 失敗の側では「もう一度」の旗を捨てる(reloadLiveState の項)
14004
+ againWanted = false;
13708
14005
  clearGiveUp();
13709
14006
  // 静的ビルドの値で描き続ける。ただし「古いかもしれない」ことは画面に出す
13710
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.56",
3
+ "version": "0.12.58",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,