@takagaki/cortex-decisions-viewer 0.4.80 → 0.4.82

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/build.js CHANGED
@@ -137,9 +137,10 @@ async function readFleetSources(repoRoot) {
137
137
  name: String(s.name ?? ""),
138
138
  ref: s.ref != null ? String(s.ref) : undefined,
139
139
  url: s.url != null ? String(s.url) : undefined,
140
- gold: typeof s.gold === "boolean" ? s.gold : undefined,
141
- // goldState gold の理由。`gold: false` だけだと「意図的に外した」「宣言し忘れ」
142
- // exclude による除外」が同じ値になり、**正常な除外にまで警告を出して麻痺する**。
140
+ // **通すのは goldState だけ(`gold` の真偽値は落とす)。** slack は常に対象なので
141
+ // 真偽値は情報を持たず、github の除外は `excluded` の方が「正常な除外」だと
142
+ // 分かる(真偽値で見ていた頃は exclude 済みのリポにまで警告が付いた)。
143
+ // 画面はこの値で github の読み取り ON/OFF トグルの向きを決める。
143
144
  goldState: s.goldState != null ? String(s.goldState) : undefined,
144
145
  notify: typeof s.notify === "boolean" ? s.notify : undefined,
145
146
  gate: s.gate != null ? String(s.gate) : undefined,
@@ -149,7 +150,7 @@ async function readFleetSources(repoRoot) {
149
150
  origin: s.origin != null ? String(s.origin) : undefined,
150
151
  // どの Slack ワークスペースのチャンネルか(#104)。**列挙し忘れると静かに消える**
151
152
  // (このマッピングは素通しではなく明示の許可リスト)。落とすと、同じIDの
152
- // チャンネルを2つのワークスペースに持つ案件で Gold トグルが別の行を撃つ
153
+ // チャンネルを2つのワークスペースに持つ案件で「登録を解除」が別の行を消す
153
154
  workspace: s.workspace != null ? String(s.workspace) : undefined,
154
155
  }))
155
156
  : undefined;
package/dist/render.js CHANGED
@@ -160,15 +160,26 @@ const CM_TOOL_LABEL = {
160
160
  * **同じ判定を使う**ためにここに置く。片方だけ直すと、同じ画面でマップが⚠️・直下が「接続OK」になる。
161
161
  *
162
162
  * 判定の考え方:
163
- * - 明示的に Gold 対象から外したもの(`gold === false`)は、読めなくて当たり前なので警告しない。
164
- * ただし通知先(`notify`)に使っているなら、届いていないこと自体が問題なので拾う。
165
- * - `gold` が無い(旧 fleet-status.json)ときは**警告する側に倒す**。「不明」を「対象外」と扱うと、
166
- * 本当に壊れているものが黙って隠れる(フェイルオープン)。
163
+ * - 明示的に読み取りから外したもの(`goldState === "excluded"`)は、読めなくて当たり前なので
164
+ * 警告しない。ただし通知先(`notify`)に使っているなら、届いていないこと自体が問題なので拾う。
165
+ * - それ以外は**警告する側に倒す**。「不明」を「対象外」と扱うと、本当に壊れているものが
166
+ * 黙って隠れる(フェイルオープン)。
167
+ *
168
+ * **`excluded` は github だけの状態。** `dev-sources.json` の `exclude` による除外で、
169
+ * slack 側の状態ではない——`channels.json` に載っているチャンネルは例外なく全て Gold 昇格の
170
+ * 対象になったので、slack は常に「読めるべきもの」。判定を `gold === false` に置いたままだと、
171
+ * engine が github の除外リポに付ける `gold: false` を slack と同じ意味で読むことになり、
172
+ * **exclude 済みの github リポに警告が付く**(除外は正常な状態)。
173
+ *
174
+ * **`notify` は見ない。** 以前は「除外されていても通知先なら繋がっている必要がある」として
175
+ * 例外にしていたが、`excluded` が github 専用になった今、engine が `notify` を付けるのは
176
+ * slack だけ(resolve-external-sources)なので**この節は決して成立しない**。残しておくと、
177
+ * 読み手には現役の分岐に見えて「github の通知先」という存在しない状態を想定させる。
167
178
  */
168
179
  function sourceNeedsAttention(s) {
169
180
  if (s.gate === "ok")
170
181
  return false;
171
- if (s.gold === false && s.notify !== true)
182
+ if (s.goldState === "excluded")
172
183
  return false;
173
184
  return true;
174
185
  }
@@ -1192,13 +1203,6 @@ a.ds-name:hover { text-decoration: underline; }
1192
1203
  .ds-inactive .ds-type, .ds-inactive .ds-name, .ds-inactive a.ds-name, .ds-inactive .ds-pipe-label { color: #9ca3af; }
1193
1204
  .ds-inactive-note { font-size: 12px; color: #9ca3af; line-height: 1.6; flex: 0 0 auto; }
1194
1205
  .ds-badges { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; flex: 0 0 auto; }
1195
- .badge.ds-gold { background: #fef3c7; color: #92400e; border: 1px solid #fcd34d; }
1196
- .badge.ds-excluded { background: #eceef1; color: #6b7280; }
1197
- /* 保存は済んでいるが、接続状況の再計算(1〜2分)がまだのもの。**確定と同じ見た目にしない**
1198
- ——リロードで消えたときに「戻された」と読まれるため、点線で区別して (反映中) を添える。
1199
- **幅と色まで指定する。** border-style だけだと、border を持たない .ds-excluded 側は
1200
- 初期値の medium(≈3px)/currentColor に解決され、押した向きによって太さ3倍・色が別物になる */
1201
- .badge.is-pending { border: 1px dashed currentColor; opacity: 0.85; }
1202
1206
  /* 能力モーダルは一覧(17チャンネル・19リポジトリ)を入れるので広く取る。
1203
1207
  既定の480pxでは、チャンネル名+バッジ2つ+トグルが1行に収まらず折り返す。
1204
1208
  画面の広さいっぱいまで使う——この画面は一覧を読むためのもので、
@@ -1249,10 +1253,9 @@ a.ds-name:hover { text-decoration: underline; }
1249
1253
  .ds-check-label, .ds-check-action, .ds-check-detail,
1250
1254
  .ds-pipe-label, .ds-pipe-todo, .ds-owner-state,
1251
1255
  .ds-empty, .sec-how, .modal-desc, .ds-toggle-msg,
1252
- .ds-drive-note, .ds-inactive-note, .ds-legacy-chip, .ds-keys-code {
1256
+ .ds-drive-note, .ds-inactive-note, .ds-legacy-chip, .ds-keys-code, .ds-gate-help {
1253
1257
  min-width: 0; white-space: normal; overflow-wrap: anywhere;
1254
1258
  }
1255
- .badge.ds-undeclared { background: #fff4e5; color: #9a6700; }
1256
1259
  .ds-toggle { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 6px; }
1257
1260
  .ds-toggle-btn { font-size: 12px; padding: 3px 10px; border-radius: 999px; border: 1px solid var(--border); background: #fff; color: var(--text); cursor: pointer; }
1258
1261
  .ds-toggle-btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
@@ -1287,6 +1290,10 @@ a.ds-name:hover { text-decoration: underline; }
1287
1290
  .ds-gate-ok { color: #1f7a45; }
1288
1291
  .ds-gate-warn { color: #b45309; }
1289
1292
  .ds-gate-muted { color: #6b7280; }
1293
+ /* 行の状態に添える「次に誰が何をするか」(2026-08-18)。**必ず自分の行を占める**
1294
+ (flex: 1 1 100%)——状態バッジの横に押し込むと、いちばん読ませたい一文が
1295
+ カード幅に負けて省略される。状態の語より弱く、しかし読める大きさで出す */
1296
+ .ds-gate-help { flex: 1 1 100%; margin: 4px 0 0; font-size: 12px; line-height: 1.7; color: #6b7280; }
1290
1297
  /* 意図的にOFFにした能力(#125②)。**グレーにして誘わない**——未接続(入口を出してよい)
1291
1298
  とは別物で、同じ色にすると「押して繋ぎましょう」と読まれる */
1292
1299
  .ds-gate-off { color: #9ca3af; }
@@ -1319,6 +1326,9 @@ a.ds-name:hover { text-decoration: underline; }
1319
1326
  .ds-addform .sec-input { margin-bottom: 6px; }
1320
1327
  .sec-input-inline { width: auto; max-width: 220px; margin-bottom: 0; }
1321
1328
  .sec-how { color: var(--muted); font-size: 12px; line-height: 1.7; margin: 0 0 10px; }
1329
+ /* 「この接続は何のためのものでないか」(2026-08-18)。**説明文より強く出す**——
1330
+ ここを読み飛ばした人が、社内チャンネルのためにボタンを押し、必ず失敗する */
1331
+ .ds-scope-note { color: var(--text); }
1322
1332
  /* いまの接続先(設定を変える口の直前に置く。値は本文色で、ラベルだけ弱める) */
1323
1333
  .ds-current { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; margin: 0 0 8px; font-size: 12.5px; }
1324
1334
  .ds-current-label { color: var(--muted); flex: 0 0 auto; }
@@ -2016,6 +2026,70 @@ ${capabilityState.toString()}
2016
2026
  "no_token": { text: "トークン未設定", cls: "ds-gate-muted" },
2017
2027
  "unknown": { text: "不明", cls: "ds-gate-muted" },
2018
2028
  };
2029
+ /* ---------------------------------------------------------------------------
2030
+ * no_token には**経路が2つある**(2026-08-18 の不具合報告)
2031
+ *
2032
+ * Slack のチャンネルは、どのワークスペースにあるかで読み取りの鍵が別物になる:
2033
+ * - workspace 省略 … クラスメソッドの社内ワークスペース。中央 Bot の鍵(案件リポジトリの
2034
+ * 設定)で読む。**画面から設定できるものが1つも無い。**
2035
+ * - workspace: "T…" … お客様のワークスペース。下の「お客様の Slack ワークスペースを接続」で
2036
+ * 承認を貰って読む。**画面から自己解決できる。**
2037
+ *
2038
+ * どちらも no_token に潰して「トークン未設定」と出していたため、社内チャンネルを
2039
+ * 登録した人が「そのトークンをどこで設定するのか」を探し、直下にある唯一それらしい
2040
+ * ボタン(顧客ワークスペースの接続)へ吸い込まれた。押すと社内ワークスペースの承認画面が
2041
+ * 出て失敗し、「もう一度お試しください」と言われてまた押す——**何度試しても直らない**まま
2042
+ * 不具合報告になった。状態だけでなく、**この行で次に誰が何をするのか**を出し分ける。
2043
+ *
2044
+ * **社内側で「担当者にご連絡ください」と書くのは後退ではない。** 画面から設定できる
2045
+ * ものが本当に無いので、自己解決できると誤認させて空振りを繰り返させる方が損害が大きい。
2046
+ * ------------------------------------------------------------------------- */
2047
+ var DS_GATE_NO_TOKEN = {
2048
+ // 社内ワークスペース。**顧客ワークスペースの接続へ誘導しない**(押しても必ず失敗する)
2049
+ internal: {
2050
+ text: "管理者の設定が必要です",
2051
+ cls: "ds-gate-muted",
2052
+ help: "このチャンネルはクラスメソッドの社内ワークスペースにあります。読み取りを始めるには cortex 側(管理者)の設定が必要で、下の「お客様の Slack ワークスペースを接続」では設定できません。cortex の担当者にご連絡ください。",
2053
+ },
2054
+ // お客様のワークスペース。従来どおり接続へ誘導する(自己解決できる唯一の経路)
2055
+ customer: {
2056
+ text: "トークン未設定",
2057
+ cls: "ds-gate-muted",
2058
+ help: "このチャンネルがあるお客様の Slack ワークスペースが、まだ接続されていない(または接続が切れている)状態です。下の「お客様の Slack ワークスペースを接続」から接続してください。",
2059
+ },
2060
+ };
2061
+ /**
2062
+ * Slack のチャンネルが社内ワークスペースのものか。**workspace の有無だけで決まる**
2063
+ * (省略=社内。#104 でこの値が画面まで届いていることは slack-workspace.test.mjs が見張る)。
2064
+ */
2065
+ function isInternalSlack(s) {
2066
+ return s.type === "slack" && !s.workspace;
2067
+ }
2068
+ /** 行に出す接続状態。no_token だけは経路(社内/顧客)で文面が分かれる */
2069
+ function dsGateOf(s) {
2070
+ if (s.gate === "no_token" && s.type === "slack") {
2071
+ return isInternalSlack(s) ? DS_GATE_NO_TOKEN.internal : DS_GATE_NO_TOKEN.customer;
2072
+ }
2073
+ return DS_GATE[s.gate] || DS_GATE["unknown"];
2074
+ }
2075
+ /**
2076
+ * 一覧・タイルに出す「⚠️ …」の要約。**呼び出しは2箇所(グループの summary と能力タイル)。**
2077
+ * 写経すると片方だけ古くなり、同じ画面で語が割れる(この節では実際に起きた)。
2078
+ *
2079
+ * **社内チャンネルだけが読めていないときは「トークン」と言わない。** 探しても設定する
2080
+ * 場所が画面に無いので、探した人が顧客ワークスペースの接続へ吸い込まれる(DS_GATE_NO_TOKEN
2081
+ * の項の事故がここから始まっている)。1件でも顧客側が混ざるときは従来の文面のまま——
2082
+ * そちらは接続で直せるので、経路を名指しできない要約に倒す方が正しい。
2083
+ */
2084
+ function attentionSummary(items) {
2085
+ var bad = items.filter(function (s) { return sourceNeedsAttention(s); });
2086
+ if (!bad.length) return "";
2087
+ var head = "⚠️ " + bad.length + "件";
2088
+ var noToken = bad.filter(function (s) { return s.gate === "no_token"; });
2089
+ if (noToken.length !== bad.length) return head + "に問題";
2090
+ var allInternal = noToken.every(function (s) { return isInternalSlack(s); });
2091
+ return head + "が読めていません(" + (allInternal ? "管理者の設定が必要です" : "トークン未設定") + ")";
2092
+ }
2019
2093
  var DS_KIND_ICON = {
2020
2094
  "課題管理": "📋",
2021
2095
  "会議": "🗒️",
@@ -2169,9 +2243,12 @@ ${capabilityState.toString()}
2169
2243
  */
2170
2244
  function openCapabilityModal(cap, items, note) {
2171
2245
  var m = makeModal(cap, true);
2172
- // **凡例を必ず入れる。** goldState の4状態は凡例があって初めて読める
2173
- if (cap === "チャット" || cap === "開発") {
2174
- m.modal.appendChild(el("p", { class: "modal-desc", text: "「Gold昇格の読み取り対象」のものだけを毎晩の Gold 昇格が読み取ります。「除外」は意図的に外したもの、「未宣言(対象外)」はまだ判断されていないもので、どちらも読み取りません。" }));
2246
+ // **何が読み取られるのかを必ず書く。** 一覧に並ぶものと読み取られるものの関係は、
2247
+ // 書いていないと分からない(チャットは「載っていれば全部」、開発はリポジトリ単位でOFFできる)
2248
+ if (cap === "チャット") {
2249
+ m.modal.appendChild(el("p", { class: "modal-desc", text: "ここに並んでいるチャンネルは、例外なく全て毎晩の Gold 昇格が読み取り、顧客も見る Decision・用語集の材料になります。読み取りを止めるには「登録を解除」してください。" }));
2250
+ } else if (cap === "開発") {
2251
+ m.modal.appendChild(el("p", { class: "modal-desc", text: "ここに並んでいるリポジトリの Issues を毎晩の Gold 昇格が読み取ります。リポジトリ単位で読み取りを止められます(止めたものは読み取られません)。" }));
2175
2252
  }
2176
2253
  // **なぜこの状態なのかを、押した先でも読めるようにする(#125②)。** タイルは常時出す
2177
2254
  // ようになったので、未接続の能力を押したときに中身が空になる場面が増えた。
@@ -2407,24 +2484,16 @@ ${capabilityState.toString()}
2407
2484
  g.items.forEach(function (s) { cards.appendChild(extSourceCard(s, meta)); });
2408
2485
  if (g.items.length >= 4) {
2409
2486
  // 折りたたみ(既定は閉)。summaryに件数と状態の要約を出す。
2410
- // **判定は共有の sourceNeedsAttention を呼ぶ。** 以前はここに中身を写経していたが、
2411
- // 片方だけ直すと同じ画面でマップが⚠️・直下が「接続OK」になる(実際に起きた)。
2412
- var bad = 0;
2413
- var noToken = 0;
2414
- g.items.forEach(function (s) {
2415
- if (!sourceNeedsAttention(s)) return;
2416
- bad++;
2417
- if (s.gate === "no_token") noToken++;
2418
- });
2487
+ // **判定と文面は共有の attentionSummary を呼ぶ。** 以前はここに中身を写経して
2488
+ // いたが、片方だけ直すと同じ画面でマップが⚠️・直下が「接続OK」になる(実際に起きた)。
2489
+ // 原因が1つに集約できるときは原因を書く(「19件に問題」より、読んだ人が次に何を
2490
+ // すればよいか分かる)——その出し分けごと attentionSummary が持っている
2491
+ var warn = attentionSummary(g.items);
2419
2492
  var sum = el("summary", {}, [
2420
2493
  el("span", { text: meta.icon + " " + meta.label + "(" + g.items.length + (UNIT[g.type] || "件") + ")" }),
2421
- bad === 0
2494
+ warn === ""
2422
2495
  ? el("span", { class: "badge ds-sum-ok", text: "接続OK" })
2423
- // 原因が1つに集約できるときは原因を書く。「19件に問題」より「トークンが未設定」の方が、
2424
- // 読んだ人が次に何をすればよいか分かる(実際、19件の原因がトークン1本ということがあった)。
2425
- : el("span", { class: "badge ds-sum-warn", text: noToken === bad
2426
- ? "⚠️ " + bad + "件が読めていません(トークン未設定)"
2427
- : "⚠️ " + bad + "件に問題" }),
2496
+ : el("span", { class: "badge ds-sum-warn", text: warn }),
2428
2497
  ]);
2429
2498
  var det = el("details", { class: "ds-group" }, [sum, cards]);
2430
2499
  pushCap(capOf(g.type), det);
@@ -2670,15 +2739,14 @@ ${capabilityState.toString()}
2670
2739
  parts.push((CAP_STATE_MARK[st] || "") + (CAP_STATE_TEXT[derived] || "状態が判定できません"));
2671
2740
  } else if (st === "warn") {
2672
2741
  if (live.length) {
2673
- var bad = 0, noToken = 0;
2674
- live.forEach(function (s) {
2675
- if (!sourceNeedsAttention(s)) return;
2676
- bad++;
2677
- if (s.gate === "no_token") noToken++;
2678
- });
2679
- parts.push(noToken === bad && bad > 0
2680
- ? "⚠️ " + bad + "件が読めていません(トークン未設定)"
2681
- : "⚠️ " + bad + "件に問題");
2742
+ // **文面は共有の attentionSummary が持つ。** グループの summary と同じ文で、
2743
+ // 写経すると片方だけ古くなる(社内/顧客の出し分けもここに要る)。
2744
+ //
2745
+ // **数えて0件でも文を出す。** warn はパイプラインの失敗からも立つので
2746
+ // (ライブでない経路)、行の gate が全部 ok のまま warn になることがある。
2747
+ // 空にすると⚠️のタイルの2行目が消えるので、従来どおりの件数表現に倒す
2748
+ var warn = attentionSummary(live);
2749
+ parts.push(warn || "⚠️ 0件に問題");
2682
2750
  } else {
2683
2751
  // 内部能力の warn はパイプラインの失敗由来。数えるものが無いので件数を出さない
2684
2752
  parts.push("⚠️ 同期が失敗しています");
@@ -2697,7 +2765,7 @@ ${capabilityState.toString()}
2697
2765
  }
2698
2766
  // 読み取り範囲。**この画面は顧客も見る。何をGoldに読んでいるかの開示が主眼**
2699
2767
  //
2700
- // **繋がっているときだけ出す。** 「未使用」と「2リポジトリ/2件をGoldに使用」が
2768
+ // **繋がっているときだけ出す。** 「未使用」と読み取り範囲の内訳が
2701
2769
  // 同じ行に並ぶと読めない。tools を none に倒しても externalSources は翌朝の
2702
2770
  // 再生成まで残るので、使うのをやめた宣言の直後に必ず通る。
2703
2771
  // 「⦿ 未接続 Driveフォルダ 0件」も同様(最終同期を出さないのと同じ理由)
@@ -2705,10 +2773,11 @@ ${capabilityState.toString()}
2705
2773
  if (!connected) {
2706
2774
  // 状態だけ出す
2707
2775
  } else if (live.length) {
2708
- // 単位は分かるものだけ。知らない種別に勝手な単位を付けない
2776
+ // 単位は分かるものだけ。知らない種別に勝手な単位を付けない。
2777
+ // **「N件をGoldに使用」の内訳は出さない。** チャンネルは載っていれば全部が対象で、
2778
+ // github の除外は行のトグルが持つ(件数の内訳は Gold トグルがあった頃の名残)
2709
2779
  var unit = cap === "チャット" ? "チャンネル" : cap === "開発" ? "リポジトリ" : "件";
2710
- var gold = live.filter(function (s) { return s.gold === true; }).length;
2711
- parts.push(live.length + unit + "/" + gold + "件をGoldに使用");
2780
+ parts.push(live.length + unit);
2712
2781
  } else if (cap === "共有資料" && adapter === "google-drive" && internal && internal.driveFolderIds) {
2713
2782
  // **そのタイルのアダプタを見る。** 宣言(tools)を見ていた頃は、local の案件
2714
2783
  //(gdo・三菱)で「ローカル」と名乗ったタイルが「Driveフォルダ 0件」と語っていた。
@@ -2871,9 +2940,9 @@ ${capabilityState.toString()}
2871
2940
  }
2872
2941
  }
2873
2942
 
2874
- // 注記は全能力の後ろに1つ。**Gold の4状態は凡例があって初めて読める。**
2943
+ // 注記は全能力の後ろに1つ。**何が Gold の材料になるのかを1文で言う。**
2875
2944
  if (anyCap) {
2876
- sec.appendChild(el("p", { class: "ds-note", text: "「Gold昇格の読み取り対象」のものだけを毎晩の Gold 昇格が読み取ります。「除外」は意図的に外したもの、「未宣言(対象外)」はまだ判断されていないもので、どちらも読み取りません(チャンネル・リポジトリ単位で切り替えられます)。" }));
2945
+ sec.appendChild(el("p", { class: "ds-note", text: "登録されている Slack チャンネルと GitHub リポジトリは、毎晩の Gold 昇格が読み取り、顧客も見る Decision・用語集の材料になります。チャンネルは登録を解除すると、リポジトリは読み取りを止めると、以後は読み取られません。" }));
2877
2946
  }
2878
2947
 
2879
2948
  if (pipes.length) {
@@ -2948,65 +3017,46 @@ ${capabilityState.toString()}
2948
3017
  // 宣言を撤去したいま出す条件が無い。未接続の能力は能力タイル(常時表示)が状態と入口を持つ。
2949
3018
  // 外部ソース1件のカード(グループ化表示・展開表示の両方から使う)
2950
3019
  /**
2951
- * Gold昇格の切り替えボタン。押すと設定ファイル(channels.json)が書き換わる。
3020
+ * チャンネルの登録を解除するボタン(remove-channel)。押すと channels.json から行が消える。
3021
+ *
3022
+ * 注: ここは埋め込みJSのテンプレートリテラル内。**コメントでもバッククォートを使わない**
3023
+ *(リテラルが閉じて構文エラーになる。実際にそれで壊した)。
2952
3024
  *
2953
- * **広げる方向(OFF→ON)には確認を出す。** そのチャンネルの内容をAIが読んで、顧客も見る
2954
- * Decision・用語集に蒸留するようになる。社内限定のチャンネルをONにすると、顧客が見られない
2955
- * 会話が顧客可視の層に出てくる(実際にその事故が起きている)。狭める方向は安全なので確認しない。
3025
+ * **これが画面から読み取りを止める唯一の手段。** チャンネル単位の Gold トグルは撤去した
3026
+ * (載っているチャンネルは例外なく全て Gold 昇格の対象)。事故(社内限定の会話が顧客可視の
3027
+ * Gold へ流れ始めた)に画面から手を打てないと、git で channels.json を直接編集するしかない。
2956
3028
  *
2957
- * 保存しても画面はすぐには変わらない(接続状況は夜間バッチのスナップショット)。
2958
- * それを伝えないと「保存できていない」と受け取られるので、応答のメッセージをそのまま出す。
3029
+ * **確認文に不可逆性を必ず書く。** 止まるのは今後の読み取りだけで、**既に Gold 層へ
3030
+ * 入った内容は消えない**(案件リポの git 履歴に残る)。「読み取りを止めます」だけだと
3031
+ * 「入ってしまった内容も消える」と読まれ、事故対応の判断を誤らせる。
3032
+ *
3033
+ * 削除の作法は既存の removeButton(Figma・GitHub の削除)に揃える——確認は向きに
3034
+ * 関わらず必ず出し、成功しても一覧からは消さずに行へ取り消し線を引く(一覧は夜間
3035
+ * スナップショットなので、翌朝まで残るのが正しい状態)。
2959
3036
  */
2960
- function goldToggle(s) {
2961
- // **共通部品(stateToggle)に載せる。** 以前はここだけ変更者名をダイアログで聞いており、
2962
- // 同じカードの中で2つの流儀が並んでいた。送信・エラー表示・向きの反転も個別実装で、
2963
- // 片方だけ直したときにずれる状態だった。
2964
- // **控えはボタンにも効かせる。** バッジだけに効かせると、リロード後に同じカードの中で
2965
- // バッジが「対象です」・ボタンが「始めますか?」と逆を主張する。しかもその状態で
2966
- // ボタンの表示どおりに押すと、サーバは既に同じ値なので changed:false を返し、
2967
- // ボタンが永久に反転しない——**15分(TTL切れ)まで止められなくなる**。
2968
- // stateToggle の doc が要件として書いている「戻す操作を必ず残す」を破る。
2969
- var pendingNow = pendingGoldFor(s);
2970
- return stateToggle({
2971
- on: pendingNow ? pendingNow.value : s.gold === true,
2972
- labelOn: "Gold昇格を止める",
2973
- labelOff: "Gold昇格を始める",
2974
- confirmOn: [
2975
- "「" + (s.name || s.ref) + "」の内容を、顧客も見る Decision・用語集に蒸留するようになります。",
2976
- "",
2977
- "社内限定の会話が含まれるチャンネルでは、顧客に見せられない内容が出てしまう可能性があります。",
2978
- "顧客がこのチャンネルにいる(=すでに会話を見ている)場合にだけ有効にしてください。",
2979
- "",
2980
- "※ 今夜の蒸留(22時ごろ)までに止めれば取り込まれません。一度生成された記録は自動では消えません。",
2981
- "",
2982
- "有効にしますか?",
2983
- ],
2984
- build: function (next) {
3037
+ function removeChannelButton(s, card) {
3038
+ return removeButton({
3039
+ label: "登録を解除",
3040
+ confirmLines: function () {
3041
+ return [
3042
+ "「" + (s.name || s.ref) + "」を、Gold 昇格の読み取り対象から外します。",
3043
+ "",
3044
+ "次回の Gold 昇格(毎晩22時ごろ)から、このチャンネルは読み取られなくなります。",
3045
+ "",
3046
+ "※ 既に Gold 層に入った内容は消えません(Decision・用語集はそのまま残ります)。",
3047
+ "",
3048
+ "解除しますか?",
3049
+ ];
3050
+ },
3051
+ build: function () {
2985
3052
  // **どのワークスペースのチャンネルかを必ず送る(#104)。** 落とすと、サーバ側は
2986
3053
  // 既定ワークスペースの同じIDのエントリを探しに行き、「対象チャンネルが
2987
- // 見つかりません」か、最悪**別のチャンネルを Gold に上げる**
2988
- var payload = { op: "set-channel-gold", ref: s.ref, value: next };
3054
+ // 見つかりません」か、最悪**別のワークスペースの同じIDのチャンネルを消す**
3055
+ var payload = { op: "remove-channel", ref: s.ref };
2989
3056
  if (s.workspace) payload.workspace = s.workspace;
2990
3057
  return payload;
2991
3058
  },
2992
- // 保存できた直後に、この行のバッジだけを「反映中」に差し替える。
2993
- // 一覧そのものは触らない(結果メッセージや他行の状態を巻き込まないため)。
2994
- pending: function (next, toggleEl) {
2995
- // **表示済みの値に戻したなら「反映中」ではない。** ON→OFF と押し直した場合、
2996
- // 画面が出している値と一致するので待つものが無い。ここで消さないと、
2997
- // 何も保留していないのに (反映中) が残り、リロードで消えて不可解に見える。
2998
- var back = next === (s.gold === true);
2999
- // 控えの鍵もワークスペース込み(#104。pendingKeyOf の doc)
3000
- if (back) clearPendingGold(pendingKeyOf(s));
3001
- else markPendingGold(pendingKeyOf(s), next);
3002
- var card = toggleEl && toggleEl.parentNode;
3003
- var badges = card && card.querySelector(".ds-badges");
3004
- if (!badges) return;
3005
- var b = badges.querySelector(".badge.ds-gold, .badge.ds-excluded, .badge.ds-undeclared");
3006
- var spec = pendingBadgeSpec(next, back);
3007
- if (b) { b.className = spec.cls; b.textContent = spec.text; }
3008
- else badges.insertBefore(el("span", { class: spec.cls, text: spec.text }), badges.firstChild);
3009
- },
3059
+ onDone: function () { card.classList.add("ds-removed"); },
3010
3060
  });
3011
3061
  }
3012
3062
 
@@ -3529,7 +3579,7 @@ ${capabilityState.toString()}
3529
3579
  if (!list) return null;
3530
3580
 
3531
3581
  var box = el("div", { class: "ds-addform", "data-adder": "slack-workspace" }, [
3532
- el("div", { class: "ds-addform-title", text: "Slack ワークスペースを接続" }),
3582
+ el("div", { class: "ds-addform-title", text: "お客様の Slack ワークスペースを接続" }),
3533
3583
  el("p", {
3534
3584
  class: "sec-how",
3535
3585
  text:
@@ -3538,6 +3588,22 @@ ${capabilityState.toString()}
3538
3588
  "お願いする権限は読み取りだけで、Cortex から投稿することはありません。" +
3539
3589
  "接続したあと、読み取りたいチャンネルにボットを招待し、この画面から追加してください。",
3540
3590
  }),
3591
+ // **「これは何のためのものでないか」を書く(2026-08-18 の不具合報告)。**
3592
+ // 社内ワークスペースのチャンネルが読めていない人が、設定する場所を探して
3593
+ // ここへ来る。押すと社内ワークスペースの承認画面が出て必ず失敗し、
3594
+ // 「もう一度お試しください」に従ってまた押す——何度試しても直らない。
3595
+ //
3596
+ // **箱そのものは消さない。** 直上の renderExternalAdders のコメントが警告している
3597
+ // のと同じ失敗で、「顧客チャンネルがある案件だけ出す」にすると
3598
+ // **新規案件(0件)=一番繋ぎたい場面**でこの入口が消える。出したまま、
3599
+ // 使う場面と使わない場面をここで言い切る
3600
+ el("p", {
3601
+ class: "sec-how ds-scope-note",
3602
+ text:
3603
+ "クラスメソッドの社内ワークスペース(Classmethod Group)のチャンネルには、この接続は使いません。" +
3604
+ "社内のチャンネルは Cortex の中央ボットで読んでいるので、ここで接続しても読めるようにはなりません。" +
3605
+ "社内のチャンネルが読めていない場合は、cortex の担当者にご連絡ください。",
3606
+ }),
3541
3607
  ]);
3542
3608
 
3543
3609
  // 接続済みの一覧。**0件でも「まだありません」と書く**——空白だと、
@@ -3579,7 +3645,10 @@ ${capabilityState.toString()}
3579
3645
  }
3580
3646
  box.appendChild(listBox);
3581
3647
 
3582
- var btn = el("button", { class: "ds-toggle-btn", type: "button", text: "Slack ワークスペースを接続" });
3648
+ // **ボタン自身に「お客様の」と書く(2026-08-18 の不具合報告)。** 社内チャンネルが
3649
+ // 読めていない人が押したのはこのボタンで、当時の文言は「Slack ワークスペースを接続」
3650
+ // ——誰のワークスペースの話かが書かれておらず、唯一それらしいボタンに見えた
3651
+ var btn = el("button", { class: "ds-toggle-btn", type: "button", text: "お客様の Slack ワークスペースを接続" });
3583
3652
  var msg = el("div", { class: "ds-toggle-msg" });
3584
3653
  btn.addEventListener("click", async function () {
3585
3654
  var intake = SITE.intake;
@@ -3631,10 +3700,10 @@ ${capabilityState.toString()}
3631
3700
  // 「会議はTeams・連絡はSlack」という普通の構成を表せない。teams を宣言した案件では
3632
3701
  // フォームごと消え、**Slackを渡されているのに追加できない**状態になっていた。
3633
3702
  //
3634
- // 読む側(resolve-external-sources.mjs)も Slack tools ゲートを外してある。
3635
- // Gold昇格の本来のゲートは**チャンネル単位の gold: true**(追加直後は未宣言=読まれない。
3636
- // 上げたければ追加後にトグルを押す)。そこは変えていないので、宣言なしで顧客可視のGold
3637
- // 流れることはない。
3703
+ // **追加そのものが Gold 昇格の意思決定になった。** チャンネル単位の gold: true という
3704
+ // 二段目のゲートは撤去され、channels.json に載っているチャンネルは例外なく全て
3705
+ // 毎晩の Gold 昇格が読む。だから**確認はここに要る**——重い話(顧客可視の層へ出る)を
3706
+ // 伝える場所が他に無くなった。撤去した Gold トグルの確認文の趣旨をここへ移している。
3638
3707
  //
3639
3708
  // 注: ここは埋め込みJSのテンプレートリテラル内。**コメントでもバッククォートを使わない**
3640
3709
  //(リテラルが閉じて構文エラーになる。実際にそれで壊した)。
@@ -3648,22 +3717,27 @@ ${capabilityState.toString()}
3648
3717
  requiredNote: "チャンネルのURLを入力してください。",
3649
3718
  // **招待の話を先頭に置く。** Slack の仕様で、ボットは**公開チャンネルでも**招待されるまで
3650
3719
  // 中身を1件も読めない(conversations.history が not_in_channel を返す・2026-08-16 実測+公式ドキュメント)。
3651
- // 以前は「名前が取得できない」を主に書いていたが、読めない方が重い話なので順序を入れ替えた。
3652
- hint: "先に Slack でそのチャンネルを開き、下のコマンドを貼って Cortex を招待してください。公開チャンネルでも招待が必要で、招待していないと中身を読めません。そのあと、チャンネル名を右クリック →「リンクをコピー」で出るURLを下に貼ります。追加しただけでは Gold 昇格には載りません(載せるには各チャンネルの「Gold昇格を始める」を押します)。招待前に登録すると名前は「名前未取得」になり、招待後にGold昇格をONにすると実名に変わります。",
3720
+ // そのあとに「追加=Gold 昇格の対象」を置く(手順と、その手順の重さ)。
3721
+ hint: "先に Slack でそのチャンネルを開き、下のコマンドを貼って Cortex を招待してください。公開チャンネルでも招待が必要で、招待していないと中身を読めません。そのあと、チャンネル名を右クリック →「リンクをコピー」で出るURLを下に貼ります。追加したチャンネルは毎晩の Gold 昇格の対象になり、顧客も見る Decision・用語集の材料になります(読み取りを止めるには、追加した行の「登録を解除」を押します)。招待前に登録すると名前は「名前未取得」になるので、招待してから追加するのがおすすめです。",
3653
3722
  copyText: "/invite @Cortex",
3654
- // **「Goldに載る」と書かない。** 追加だけでは載らないので嘘になる。
3655
- // 確認する理由は「中央Botで読みに行く」「他案件の取り違え」。
3723
+ // 撤去した Gold トグルの確認文の趣旨をここへ移した。**追加が昇格の意思決定そのもの**
3724
+ // なので、社内限定の会話が顧客可視の層に出てしまう話はここで伝えるしかない
3725
+ // (実際にその事故が起きている)。あわせて他案件の取り違えも確認させる。
3656
3726
  confirmLines: function (v) {
3657
3727
  return [
3658
- "このチャンネルを、Cortex が読みに行く対象に追加します。",
3659
- "",
3660
- "貼ったURLがこの案件のものか、もう一度確認してください。",
3728
+ "このチャンネルの内容を、顧客も見る Decision・用語集に蒸留するようになります。",
3661
3729
  "",
3662
3730
  v,
3663
3731
  "",
3732
+ "社内限定の会話が含まれるチャンネルでは、顧客に見せられない内容が出てしまう可能性があります。",
3733
+ "顧客がこのチャンネルにいる(=すでに会話を見ている)場合にだけ追加してください。",
3734
+ "",
3735
+ "貼ったURLがこの案件のものか、もう一度確認してください。",
3736
+ "",
3664
3737
  // Slack の仕様(公開チャンネルでも招待が要る)を、最後にもう一度置く。
3665
3738
  // 「追加したのに何も取り込まれない」の原因のほとんどがこれ。
3666
3739
  "※ Slack でこのチャンネルに Cortex を招待していないと、中身は読めません。",
3740
+ "※ 今夜の蒸留(22時ごろ)までに登録を解除すれば取り込まれません。一度生成された記録は自動では消えません。",
3667
3741
  "",
3668
3742
  "追加しますか?",
3669
3743
  ];
@@ -3707,20 +3781,15 @@ ${capabilityState.toString()}
3707
3781
 
3708
3782
  // ---------- 設定の変更(?op=config)の共通部品 ----------
3709
3783
  //
3710
- // 確認 → 送信 → 結果表示、までを1箇所に置く。3つの操作で書き写すと、
3711
- // 片方だけ直したときにずれる(既存の goldToggle で使っている作法をそのまま一般化したもの)。
3784
+ // 確認 → 送信 → 結果表示、までを1箇所に置く。操作ごとに書き写すと、
3785
+ // 片方だけ直したときにずれる(トグル・削除・追加のどれも同じ経路を通す)。
3712
3786
 
3713
3787
  /**
3714
3788
  * サーバへ設定変更を送る。**一覧は書き換えない**
3715
3789
  * (接続状況は夜間バッチのスナップショットなので、「もう効いている」と誤解させないため)。
3716
3790
  * 呼び出し側は onDone で自分のボタンの向きを反転させる。
3717
3791
  *
3718
- * **例外はバッジの先出し。** opts.pending を持つ操作(いまは Gold の切り替えだけ)は、
3719
- * 押した内容を「(反映中)」として先に出す。出してよいのは (1) 保存の成否がサーバで
3720
- * 確定していて推測が入らない (2) **確定した表示と見た目で区別できる**、の両方を満たす
3721
- * ときだけ。区別しないと、リロードで消えたときに「戻された」と読まれる。
3722
- *
3723
- * **もう1つの例外が接続先の一覧(#127)。** 追加・削除の応答にはいまの一覧が必ず
3792
+ * **例外が接続先の一覧(#127)。** 追加・削除の応答にはいまの一覧が必ず
3724
3793
  * 添えられていて(成功も、重複で断られたときも、世代がずれていたときも)、これは
3725
3794
  * スナップショットではなく**サーバが書いた直後の確定値**なので、その場で描き直してよい。
3726
3795
  * @param hooks { onTargets(list) → 画面が見ていた集合と違ったか, staleNote }
@@ -3796,9 +3865,7 @@ ${capabilityState.toString()}
3796
3865
 
3797
3866
  /**
3798
3867
  * ON/OFF のトグル。**戻す操作を必ず残す**(誤って有効にしたとき翌朝まで止められないのは許容できない)。
3799
- * @param opts { on, labelOn, labelOff, confirmOn, build(next), pending(next, wrapEl) }
3800
- * pending … 保存できた直後に、その行のバッジだけを「(反映中)」へ差し替える任意のフック。
3801
- * 持たない操作は従来どおりボタンの向きだけを反転する(バッジは触らない)。
3868
+ * @param opts { on, labelOn, labelOff, confirmOn, build(next) }
3802
3869
  */
3803
3870
  function stateToggle(opts) {
3804
3871
  var wrap = el("div", { class: "ds-toggle" });
@@ -3817,11 +3884,9 @@ ${capabilityState.toString()}
3817
3884
  if (next && opts.confirmOn && !confirmDanger(opts.confirmOn)) { msg.textContent = ""; return; }
3818
3885
  sendConfig(opts.build(next), msg, btn, function () {
3819
3886
  // **押した内容をボタンに反映し、逆向きの操作を残す。**
3887
+ // **カードごと作り直さない。** 上の msg に出した結果表示が消えてしまう
3820
3888
  on = next;
3821
3889
  paint();
3822
- // バッジは「反映中」として先出しする(opts.pending がある操作だけ)。
3823
- // **カードごと作り直さない。** 上の msg に出した結果表示が消えてしまう
3824
- if (opts.pending) opts.pending(next, wrap);
3825
3890
  });
3826
3891
  });
3827
3892
  wrap.appendChild(btn);
@@ -3949,123 +4014,14 @@ ${capabilityState.toString()}
3949
4014
  }
3950
4015
 
3951
4016
  /* ---------------------------------------------------------------------------
3952
- * Gold切り替えの「反映中」表示。
3953
- *
3954
- * 押しても一覧のバッジが変わるのは fleet-status.yml が完走したあと(1〜2分)。
3955
- * その間バッジが古いままだと「押したのに何も起きない」に見える。
4017
+ * Gold 切り替えの「(画面反映待ち)」バッジ(pendingGold 一式)は**撤去した**。
3956
4018
  *
3957
- * **推測ではない。** Lambda は書き込む前に Bot の在籍を確認し、ダメなら 400 を返すので、
3958
- * 200 が返った時点で channels.json は確実に書き換わっている。ここで出すのは
3959
- * 「確定した事実の先出し」であって楽観的な当て推量ではない。
4019
+ * チャンネル単位の Gold トグルが無くなったので、先出しする値が無い。残っている
4020
+ * sessionStorage(cortex.pendingGold:案件キー)は読み手が消えるだけなので無害。
3960
4021
  *
3961
- * **確定したバッジと同じ見た目にはしない。** 30秒後にリロードした人には、付けたはずの
3962
- * ラベルが消えて見える——「変えたはずなのに違う値が出ている」という、この画面が
3963
- * 繰り返し警告してきた誤読そのものになる。だから (反映中) を付けて区別し、
3964
- * さらに sessionStorage に持たせてリロードしても残す。
3965
- *
3966
- * 消えるのは「fleet-status が追いついたとき」か「TTL 切れ」。TTL が無いと、
3967
- * ワークフローが落ちている案件で永久に「反映中」が残り、嘘の表示になる。
4022
+ * **削除の結果は行の取り消し線で示す**(removeButton の作法)。一覧は夜間
4023
+ * スナップショットから描かれるので、消したはずの行が翌朝まで残るのが正しい状態。
3968
4024
  * ------------------------------------------------------------------------- */
3969
- // **キーは案件別にする。** Slackの共有チャンネルは複数案件のCortexに同時に載りうるので、
3970
- // 案件をまたぐキーにすると、案件Aで押した「反映中」が案件Bの同じ行に出る
3971
- // (チュートリアルの既読キーが同じ理由で案件別になっている)。
3972
- var PENDING_KEY = "cortex.pendingGold:" + ((SITE.intake && SITE.intake.projectKey) || "");
3973
- var PENDING_TTL_MS = 15 * 60 * 1000;
3974
- /**
3975
- * 保存する形: { "<チャンネルID>": { value: boolean, at: number } }
3976
- * at が無い/0 のエントリは期限切れとして落とす。
3977
- * **型では守れない**(このコードは render.ts のテンプレートリテラル内で、tsc が見ない)。
3978
- * 守るのはテストの仕事。
3979
- */
3980
-
3981
- /**
3982
- * 先出しバッジの見た目。**1箇所に集約する**——文言とクラスを2箇所に書くと片方だけ直る。
3983
- *
3984
- * **「反映中」とは書かない。** この画面の既存語彙では「反映」が2つの意味で使われていて、
3985
- * 短いほう(数分)には必ず「一覧への」が付いている(サーバ文言「次回の Gold 昇格
3986
- * (毎晩22時ごろ)から反映されます。一覧への反映には数分かかります」)。修飾なしの
3987
- * 「反映中」だと、22時の蒸留の話と読めてしまう。
3988
- *
3989
- * とくに OFF 方向で害が大きい。**止めた時点で蒸留に対しては完全に効いている**
3990
- * (Slack はミラーせず Gold 昇格が直接読むため)。待っているのは画面だけなので、
3991
- * 「除外(反映中)」は「まだ除外されていない」と逆に読める。
3992
- */
3993
- function pendingBadgeSpec(value, settled) {
3994
- return {
3995
- cls: "badge " + (value ? "ds-gold" : "ds-excluded") + (settled ? "" : " is-pending"),
3996
- text: (value ? "Gold昇格の読み取り対象" : "除外") + (settled ? "" : "(画面反映待ち)"),
3997
- };
3998
- }
3999
-
4000
- function readPending() {
4001
- try {
4002
- var raw = sessionStorage.getItem(PENDING_KEY);
4003
- if (!raw) return {};
4004
- var obj = JSON.parse(raw);
4005
- if (!obj || typeof obj !== "object") return {};
4006
- var now = Date.now();
4007
- var out = {};
4008
- for (var k in obj) {
4009
- if (!Object.prototype.hasOwnProperty.call(obj, k)) continue;
4010
- var e = obj[k];
4011
- if (e && typeof e === "object" && now - (e.at || 0) < PENDING_TTL_MS) out[k] = e;
4012
- }
4013
- return out;
4014
- } catch (_) {
4015
- // sessionStorage が使えない環境(プライベートモード等)でも画面は出す
4016
- return {};
4017
- }
4018
- }
4019
- function writePending(map) {
4020
- try { sessionStorage.setItem(PENDING_KEY, JSON.stringify(map)); } catch (_) {}
4021
- }
4022
- function clearPendingGold(key) {
4023
- if (!key) return;
4024
- var map = readPending();
4025
- if (!(key in map)) return;
4026
- delete map[key];
4027
- writePending(map);
4028
- }
4029
- /**
4030
- * 控えの鍵。**チャンネルIDだけでは足りない(#104)。**
4031
- * IDはワークスペースの中でしか一意でないので、同じIDのチャンネルを2つのワークスペースに
4032
- * 持つ案件では、片方で押した「反映中」が**もう片方の行にも出る**。
4033
- */
4034
- function pendingKeyOf(s) {
4035
- if (!s || !s.ref) return "";
4036
- return s.workspace ? s.workspace + "\t" + s.ref : s.ref;
4037
- }
4038
- /** 押した内容を控える。fleet-status が追いつくまでの間だけ効く */
4039
- function markPendingGold(key, value) {
4040
- if (!key) return;
4041
- var map = readPending();
4042
- map[key] = { value: value === true, at: Date.now() };
4043
- writePending(map);
4044
- }
4045
- /**
4046
- * その行の「反映中」を引く。**実データが追いついていたら控えを捨てる。**
4047
- * 捨てないと、正しくなった後も (反映中) が残り続ける。
4048
- */
4049
- function pendingGoldFor(s) {
4050
- var key = pendingKeyOf(s);
4051
- if (!key) return null;
4052
- var map = readPending();
4053
- var e = map[key];
4054
- if (!e) return null;
4055
- // **「値が一致したら捨てる」だけでは足りない。** 別の人が直後に逆を押して、
4056
- // その結果で追いついた場合、値が一致しないので控えが残り、TTL切れまで
4057
- // 実際とは違う値を出し続ける。**押した時点より新しいスナップショットが来たら、
4058
- // 値が何であれ控えは用済み**——サーバの答えのほうが新しいので、そちらを信じる。
4059
- var snapshot = fleetVal("generatedAt");
4060
- var snapAt = snapshot ? Date.parse(snapshot) : NaN;
4061
- var stale = !isNaN(snapAt) && snapAt >= (e.at || 0);
4062
- if (s.gold === e.value || stale) {
4063
- delete map[s.ref];
4064
- writePending(map);
4065
- return null;
4066
- }
4067
- return e;
4068
- }
4069
4025
 
4070
4026
  /**
4071
4027
  * 登録の由来の表示(#118)。値は fleet-status.json の origin
@@ -4112,16 +4068,9 @@ ${capabilityState.toString()}
4112
4068
 
4113
4069
  function extSourceCard(s, meta) {
4114
4070
  var badges = el("div", { class: "ds-badges" });
4115
- var pending = pendingGoldFor(s);
4116
- // 「意図的に外した」と「宣言し忘れ」を分けて出す。同じ「除外」と書くと、
4117
- // 判断されていないだけのものが「判断済み」に見えてしまう。
4118
- if (pending) {
4119
- var spec = pendingBadgeSpec(pending.value, false);
4120
- badges.appendChild(el("span", { class: spec.cls, text: spec.text }));
4121
- }
4122
- else if (s.gold === true) badges.appendChild(el("span", { class: "badge ds-gold", text: "Gold昇格の読み取り対象" }));
4123
- else if (s.goldState === "undeclared") badges.appendChild(el("span", { class: "badge ds-undeclared", text: "未宣言(対象外)" }));
4124
- else if (s.gold === false) badges.appendChild(el("span", { class: "badge ds-excluded", text: "除外" }));
4071
+ // **Gold 対象かのバッジは持たない。** 一覧に並ぶ Slack チャンネルは例外なく全て対象で、
4072
+ // 行ごとに「対象です」と書いても情報が増えない。github の除外は行のトグル
4073
+ // (「読み取りを始める/止める」)が向きで示す。
4125
4074
  if (s.notify === true) badges.appendChild(el("span", { class: "badge ds-notify", text: "通知先" }));
4126
4075
  // **どのワークスペースのチャンネルかを出す(#104)。** 同じチャンネル名が
4127
4076
  // 2つのワークスペースに並びうるので、これが無いと「どっちの #general か」が読めない。
@@ -4136,8 +4085,10 @@ ${capabilityState.toString()}
4136
4085
  // 登録の由来(#118)。**「登録から削除」が出る/出ないの理由がこれ。** 導出されたリポは
4137
4086
  // 登録ファイルから消しても翌日また現れるので削除を出していないが、画面には
4138
4087
  // 「同じ見た目の行なのに、片方にだけ削除がある」としか映らず、毎回考え直すことになる。
4139
- // **GitHub の行だけに出す。** 由来で出し分けているのはここだけだから——Slack
4140
- // channels.json 由来の derived だが、あちらに削除は無く、由来を見せても打つ手が増えない。
4088
+ // **GitHub の行だけに出す。** ORIGIN_HELP の文面がリポジトリと .gitmodules に
4089
+ // 特化していて、Slack に出すと嘘になる。Slack も由来で「登録を解除」を出し分ける
4090
+ // ようになった(explicit には出さない)が、Slack で explicit なのは登録ファイルに
4091
+ // 直接書いた例外的な行だけで、バッジのために文面をもう一組増やすには釣り合わない。
4141
4092
  // #119 以降、新しいデータの github は必ず explicit(詳細は ORIGIN_LABEL)
4142
4093
  if (String(s.type).indexOf("github") === 0 && ORIGIN_LABEL[s.origin]) {
4143
4094
  badges.appendChild(el("span", {
@@ -4146,7 +4097,7 @@ ${capabilityState.toString()}
4146
4097
  title: ORIGIN_HELP[s.origin],
4147
4098
  }));
4148
4099
  }
4149
- var gate = DS_GATE[s.gate] || DS_GATE["unknown"];
4100
+ var gate = dsGateOf(s);
4150
4101
  var card = el("div", { class: "ds-card" }, [
4151
4102
  el("div", { class: "ds-card-main" }, [
4152
4103
  el("span", { class: "ds-icon", text: meta.icon }),
@@ -4156,10 +4107,16 @@ ${capabilityState.toString()}
4156
4107
  badges,
4157
4108
  el("span", { class: "ds-gate " + gate.cls, text: gate.text }),
4158
4109
  ]);
4159
- // Gold昇格の切り替え。書き込み経路(intake Lambda)がある案件でだけ出す。
4160
- // Slack以外は読み手が導出しないので出さない(押せても効かないボタンは出さない)。
4161
- if (SITE.intake && s.type === "slack" && s.ref) {
4162
- card.appendChild(goldToggle(s));
4110
+ // チャンネルの登録解除。書き込み経路(intake Lambda)がある案件でだけ出す。
4111
+ // **Slack だけ。** github は登録ファイルが別(dev-sources.json)で、下のトグルと
4112
+ // 「登録から削除」が担う。
4113
+ //
4114
+ // **explicit(=dev-sources.json だけに登録された行)には出さない。** 解除が書き換える
4115
+ // のは channels.json なので、そこに無い行は押しても必ず 404 になる。GitHub 側が
4116
+ // 同じ事故(押しても戻ってくる削除)を origin で避けているのと同じ理由で、向きが逆
4117
+ // ——Slack は channels.json 由来が derived なので、出すのは derived と both。
4118
+ if (SITE.intake && s.type === "slack" && s.ref && s.origin !== "explicit") {
4119
+ card.appendChild(removeChannelButton(s, card));
4163
4120
  }
4164
4121
  // GitHubリポの読み取り ON/OFF。OFF は exclude で表現される(導出されたリポも確実に落ちる)。
4165
4122
  if (SITE.intake && s.type.indexOf("github") === 0 && s.ref) {
@@ -4206,6 +4163,14 @@ ${capabilityState.toString()}
4206
4163
  }));
4207
4164
  }
4208
4165
  }
4166
+ // **「次に誰が何をするか」を、その行に書く(2026-08-18 の不具合報告)。**
4167
+ // 状態(トークン未設定)だけを出していたので、設定する場所を探した人が直下の
4168
+ // 顧客ワークスペースの接続へ吸い込まれた——社内チャンネルではそれが必ず失敗する。
4169
+ // 文面は DS_GATE_NO_TOKEN が経路ごとに持つ(社内は連絡先・顧客は接続の導線)。
4170
+ //
4171
+ // **ボタンより後ろに置く。** カードは横並びの flex なので、間に挟むと
4172
+ // 「登録を解除」が案内文の下の行へ落ちて、既存案件の行の形が変わる
4173
+ if (gate.help) card.appendChild(el("p", { class: "ds-gate-help", text: gate.help }));
4209
4174
  return card;
4210
4175
  }
4211
4176
 
@@ -4803,8 +4768,8 @@ ${capabilityState.toString()}
4803
4768
  * モーダルを app に載せて、中へフォーカスを移す。
4804
4769
  *
4805
4770
  * **「最初の focusable」に載せてはいけない。** 一覧の先頭に来るのは
4806
- * 「Gold昇格を止める」「読み取りを止める」「このファイルを外す」といった
4807
- * 設定を書き換えるボタンで、しかも**狭める向きは意図的に確認を出さない**
4771
+ * 「読み取りを止める」「このファイルを外す」といった設定を書き換えるボタンで、
4772
+ * しかも**狭める向きは意図的に確認を出さない**
4808
4773
  * (stateToggle の confirmOn は広げる向きだけ)。タイルを Enter で開いて
4809
4774
  * もう一度 Enter を押しただけで、確認なしに設定ファイルが書き換わる。
4810
4775
  * 移す先は閉じるボタン——押しても何も壊れないものにする。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.4.80",
3
+ "version": "0.4.82",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,