@takagaki/cortex-decisions-viewer 0.4.82 → 0.4.84

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 +260 -2
  2. package/package.json +1 -1
package/dist/render.js CHANGED
@@ -1294,6 +1294,19 @@ a.ds-name:hover { text-decoration: underline; }
1294
1294
  (flex: 1 1 100%)——状態バッジの横に押し込むと、いちばん読ませたい一文が
1295
1295
  カード幅に負けて省略される。状態の語より弱く、しかし読める大きさで出す */
1296
1296
  .ds-gate-help { flex: 1 1 100%; margin: 4px 0 0; font-size: 12px; line-height: 1.7; color: #6b7280; }
1297
+ /* 「接続を確認」を押した結果(2026-08-19)。**必ず自分の行を占める**(flex: 1 1 100%)。
1298
+ 夜間のスナップショットの状態バッジと**同時に2つの判定を並べない**ため、押したあとは
1299
+ 状態バッジをこの中の判定へ置き換える(同じ画面で「⚠️」と「接続OK」が並ぶのが、
1300
+ この節で3回直してきた壊れ方そのもの) */
1301
+ .ds-probe { flex: 1 1 100%; margin-top: 8px; padding-top: 8px; border-top: 1px dashed var(--border); }
1302
+ .ds-probe-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
1303
+ /* いつ測った結果かを必ず添える。夜間のスナップショットと混ざると、どちらの話か読めない */
1304
+ .ds-probe-at { font-size: 11.5px; color: var(--muted); }
1305
+ .ds-probe-help { margin: 6px 0 0; font-size: 12px; line-height: 1.7; color: #6b7280;
1306
+ min-width: 0; white-space: normal; overflow-wrap: anywhere; }
1307
+ /* Slack が名指しした不足スコープ。**そのまま読める形で出す**(言い換えると検索できない) */
1308
+ .ds-probe-scope { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; background: #fef3c7;
1309
+ border: 1px solid #fde68a; border-radius: 6px; padding: 1px 6px; user-select: all; }
1297
1310
  /* 意図的にOFFにした能力(#125②)。**グレーにして誘わない**——未接続(入口を出してよい)
1298
1311
  とは別物で、同じ色にすると「押して繋ぎましょう」と読まれる */
1299
1312
  .ds-gate-off { color: #9ca3af; }
@@ -2022,6 +2035,11 @@ ${capabilityState.toString()}
2022
2035
  // **状態だけでなく、次にすることを書く。** 「bot未招待」では何をすればよいか分からない
2023
2036
  // (公開チャンネルでも招待が要ることは Slack の仕様で、知らない人のほうが多い)
2024
2037
  "not_in_channel": { text: "Slackで招待してください", cls: "ds-gate-warn" },
2038
+ // **招待では直らない(2026-08-19 の不具合報告)。** 非公開チャンネルで groups:read が
2039
+ // 足りていたのに、実測がこれを「不明」に潰していたので画面には「接続OK」としか出ず、
2040
+ // 名前が引けない理由が一切分からなかった(原因の特定に半日かかった)。
2041
+ // **状態の語で「招待」と言わない**——言うと、何度招待しても直らないものを繰り返させる
2042
+ "missing_scope": { text: "権限が足りません", cls: "ds-gate-warn" },
2025
2043
  "unreachable": { text: "アクセス不可", cls: "ds-gate-warn" },
2026
2044
  "no_token": { text: "トークン未設定", cls: "ds-gate-muted" },
2027
2045
  "unknown": { text: "不明", cls: "ds-gate-muted" },
@@ -2065,6 +2083,25 @@ ${capabilityState.toString()}
2065
2083
  function isInternalSlack(s) {
2066
2084
  return s.type === "slack" && !s.workspace;
2067
2085
  }
2086
+ /**
2087
+ * Slack チャンネルの表示名。**# は表示のときだけ付ける**(保存値は素・2026-08-19)。
2088
+ *
2089
+ * 保存値に # を持たせていたせいで、人が手で # 無しに書いた行と Slack から引いた値が
2090
+ * 「# の有無だけ」で食い違い、「名前が違います」と出た(実機で発生)。**保存は素に
2091
+ * 決めた**ので、付けるのはここ1箇所だけ。決定の理由は infra/lambda/shared/slack-gate.mjs。
2092
+ *
2093
+ * **冪等にしてある**(先頭の # を落としてから付ける)。既存データには # 付きで
2094
+ * 保存された行が10件あり、これが無いと「##tf-project-cortex」になる。冪等なので
2095
+ * **データの移行を待たずにこの表示へ切り替えられる**(揃うのは名前が引き直された行から順に)。
2096
+ *
2097
+ * 印付きの名前(「(名前未取得: C0BQ…)」)には付けない——# は実名の印なので、
2098
+ * 引けていない名前に付けると実名に見えてしまう。
2099
+ */
2100
+ function slackDisplayName(name) {
2101
+ var s = String(name || "");
2102
+ if (!s || s.indexOf("(名前未取得") === 0) return s;
2103
+ return "#" + s.replace(/^#+/, "");
2104
+ }
2068
2105
  /** 行に出す接続状態。no_token だけは経路(社内/顧客)で文面が分かれる */
2069
2106
  function dsGateOf(s) {
2070
2107
  if (s.gate === "no_token" && s.type === "slack") {
@@ -3034,6 +3071,210 @@ ${capabilityState.toString()}
3034
3071
  * 関わらず必ず出し、成功しても一覧からは消さずに行へ取り消し線を引く(一覧は夜間
3035
3072
  * スナップショットなので、翌朝まで残るのが正しい状態)。
3036
3073
  */
3074
+ /* ---------------------------------------------------------------------------
3075
+ * 「接続を確認」(2026-08-19 の不具合報告3件)
3076
+ * ---------------------------------------------------------------------------
3077
+ * 3つの別々の原因(トークン未登録・groups:read 不足・スナップショットが古い)が、
3078
+ * **画面上はどれも区別できなかった。** 共通していたのは「画面を見ても次に何をすれば
3079
+ * いいか決まらない」こと。実測は毎晩1回しか走らないので、招待やスコープを直した人が
3080
+ * 直ったことを確かめる手段も無かった。**押した瞬間に測って、理由と次の一手を返す。**
3081
+ *
3082
+ * **判定はここで作らない。** 状態の語は DS_GATE、社内/顧客の出し分けは
3083
+ * DS_GATE_NO_TOKEN が持っていて、どちらも dsGateOf を通して取る。ここで文面を
3084
+ * 書き写すと、**同じ行の状態バッジと確認結果で語が割れる**(この節で3回直した形)。
3085
+ *
3086
+ * 他アダプタ(GitHub / Backlog / Figma / Drive)を足すときの拡張点:
3087
+ * - 判定と語彙は infra/lambda/shared/slack-gate.mjs 側に足す(画面は DS_GATE のまま)
3088
+ * - この節に増やすのは probeHelpNodes の「次の一手」だけ。**アダプタ固有の分岐を
3089
+ * probeVerdict に入れない**(入れた瞬間に判定が画面側にも生まれる)
3090
+ * - 行に出す条件(下の extSourceCard)を s.type === "slack" から広げる
3091
+ * ------------------------------------------------------------------------- */
3092
+
3093
+ /**
3094
+ * 確認結果の状態バッジ。**既存の dsGateOf をそのまま通す。**
3095
+ *
3096
+ * こうしておくと no_token の社内/顧客の出し分け(DS_GATE_NO_TOKEN)が確認結果にも
3097
+ * そのまま効く。写経すると、同じ画面で行は「管理者の設定が必要です」・確認結果は
3098
+ * 「トークン未設定」になり、社内チャンネルを登録した人が**また**顧客ワークスペースの
3099
+ * 接続へ吸い込まれる(2026-08-18 の不具合そのもの)。
3100
+ */
3101
+ function probeVerdict(s, r) {
3102
+ return dsGateOf({ type: s.type, workspace: s.workspace, gate: r.gate });
3103
+ }
3104
+
3105
+ /**
3106
+ * 判定ごとの表示。**成功は1行で終わらせ、失敗は「理由+次の一手」だけを残す。**
3107
+ *
3108
+ * ## 短くした理由(2026-08-19・オーナー指示)
3109
+ *
3110
+ * 「接続を確認を押して出てくる文言が長ったらしい」。実際、成功時に4行出ていた。
3111
+ * **成功時は「読めている」以上の情報が要らない。**
3112
+ *
3113
+ * ## ただし失敗時は削りすぎない
3114
+ *
3115
+ * 短くしすぎると、今日と同じ「画面を見ても次に何をすればいいか決まらない」状態に戻る
3116
+ * (それで半日かかった)。落とすのは**内部の仕組みの説明**——「設定ファイルの表示名だけが
3117
+ * 書き換わります」「読み取りの対象・範囲は変わりません」のような、読む人の次の行動を
3118
+ * 変えない文。残すのは「何が起きているか」と「次に何をするか」だけ。
3119
+ *
3120
+ * 状態の語そのものは DS_GATE / DS_GATE_NO_TOKEN が持つ(probeVerdict 経由)ので、
3121
+ * ここが返すのは**それに足りない分だけ**。二重に言わない。
3122
+ *
3123
+ * @returns 本文に並べるノード(0〜2個)
3124
+ */
3125
+ function probeHelpNodes(s, r) {
3126
+ var p = function (t) { return el("p", { class: "ds-probe-help", text: t }); };
3127
+ // 成功。**1行で終わり。** 状態の語(「接続OK」)が既に出ているので本文は空
3128
+ if (r.gate === "ok") return [];
3129
+ if (r.gate === "not_in_channel") {
3130
+ // 状態の語が「Slackで招待してください」なので、説明は足さずコマンドだけ出す。
3131
+ // **手で打たせない**——打ち間違いは黙って失敗し、「招待したのに読まれない」に見える
3132
+ return [copyRow("/invite @Cortex")];
3133
+ }
3134
+ if (r.gate === "missing_scope") {
3135
+ // 不足スコープは頭のバッジ横に出る(probeSection)。ここは次の一手だけ
3136
+ return [p("招待では直りません。cortex の担当者にこのスコープ名をお伝えください。")];
3137
+ }
3138
+ if (r.gate === "no_token") {
3139
+ // **社内/顧客の出し分けは変えない。** 社内は画面から設定できるものが無いので
3140
+ // 連絡先、顧客は接続へ誘導する(2026-08-18 の不具合の再発防止)。
3141
+ // 長い説明は落とし、次の行動の1行だけ残す
3142
+ return [p(isInternalSlack(s)
3143
+ ? "cortex の担当者にご連絡ください。"
3144
+ : "下の「お客様の Slack ワークスペースを接続」から接続してください。")];
3145
+ }
3146
+ if (r.gate === "unreachable") {
3147
+ return [p("チャンネルが見つからないか、アーカイブされています。不要なら「登録を解除」してください。")];
3148
+ }
3149
+ // unknown。**生のエラーは隠さない**——「不明」だけだと切り分けが始められない
3150
+ return [p("Slack の応答:" + (r.error || "不明") + "。時間をおいて再度お試しください。")];
3151
+ }
3152
+
3153
+ /**
3154
+ * 表示名を Slack の実名へ**自動で揃える**(2026-08-19・オーナー判断)。
3155
+ *
3156
+ * ## なぜ自動なのか
3157
+ *
3158
+ * 「表示名は人が決めるものではなく、Slack から引いたものをそのまま出す」と決まった。
3159
+ * 正解が1つしかないので、**人に選ばせるボタンは摩擦でしかない**(差分表示と
3160
+ * 「表示名を更新する」ボタンは撤去した。Gold トグルを撤去したときと同じ理屈)。
3161
+ *
3162
+ * ## なぜサーバの ?op=probe ではなく画面から送るのか
3163
+ *
3164
+ * **疎通確認そのものは読むだけ、を保ちたいから。** 「読むだけで設定を変えない」が
3165
+ * ボタンを誰でも押せるようにした根拠なので、?op=probe を書き込む口にはしない。
3166
+ * 書き込みは既存の ?op=config を通す——他案件チェック・コミットのリトライ・
3167
+ * 画面への反映まで、設定変更と同じ守りがそのまま効く。
3168
+ *
3169
+ * **本来の置き場は夜間の接続状況生成(engine 側)。** そちらなら人の操作と無関係に
3170
+ * 1日1回揃い、改名にも追従する。engine が今回の変更対象外なので、いまは押したときに揃える。
3171
+ *
3172
+ * ## 静かに失敗させる
3173
+ *
3174
+ * 揃えるのはおまけで、押した人の目的は「読めているか確かめる」こと。
3175
+ * 失敗しても疎通確認の結果は出したままにする(名前が古いままなだけ)。
3176
+ */
3177
+ function autoAlignName(s, r, card) {
3178
+ // 引けていない/既に同じなら何もしない(押すたびにコミットを積まない)
3179
+ if (!r.name || !s.ref) return;
3180
+ if (String(s.name || "").replace(/^#+/, "") === String(r.name)) return;
3181
+ // **channels.json に無い行は書き換えられない。** dev-sources.json だけに載っている
3182
+ // 行(origin: explicit)へ送ると必ず404になる(「登録を解除」と同じ条件)
3183
+ if (s.origin === "explicit") return;
3184
+ var intake = SITE.intake;
3185
+ if (!intake) return;
3186
+ var payload = { op: "refresh-channel-name", ref: s.ref };
3187
+ if (s.workspace) payload.workspace = s.workspace;
3188
+ var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
3189
+ fetch(pfx + "op=config&t=" + encodeURIComponent(intake.token), {
3190
+ method: "POST",
3191
+ headers: { "content-type": "application/json" },
3192
+ body: JSON.stringify(payload),
3193
+ }).then(function (res) {
3194
+ if (!res.ok) return;
3195
+ // 一覧は夜間のスナップショットなので、その場では変わらない。行だけ追いつかせる
3196
+ var nameEl = card.querySelector(".ds-name");
3197
+ if (nameEl) nameEl.textContent = slackDisplayName(r.name);
3198
+ }).catch(function () { /* 揃えられなくても疎通確認の結果は出したまま */ });
3199
+ }
3200
+
3201
+ /**
3202
+ * 「接続を確認」の一式(ボタン+結果)。
3203
+ *
3204
+ * **押したら、その行の状態バッジを置き換える。** 夜間のスナップショットの判定と
3205
+ * いま測った判定を**同じ行に並べない**ため——並べると「⚠️ と 接続OK が同時に出ている」
3206
+ * になり、この節で3回直してきた食い違いを画面の側から作ることになる。
3207
+ * 置き換えたことが分かるよう、いつ測ったかを必ず添える。
3208
+ *
3209
+ * **判定は保存しない。** fleet-status.json は夜間のスナップショットのまま置く
3210
+ * (押すたびにコミットが増えるのを避ける)。表示名だけが例外で、そちらは
3211
+ * Slack の実名が正なので自動で揃える(autoAlignName)。
3212
+ */
3213
+ function probeSection(s, card, gateEl) {
3214
+ var wrap = el("div", { class: "ds-probe" });
3215
+ var head = el("div", { class: "ds-probe-head" });
3216
+ var btn = el("button", { class: "ds-toggle-btn", type: "button", text: "接続を確認" });
3217
+ var msg = el("span", { class: "ds-toggle-msg" });
3218
+ var at = el("span", { class: "ds-probe-at" });
3219
+ var body = el("div");
3220
+ head.appendChild(btn);
3221
+ head.appendChild(msg);
3222
+ head.appendChild(at);
3223
+ wrap.appendChild(head);
3224
+ wrap.appendChild(body);
3225
+
3226
+ btn.addEventListener("click", async function () {
3227
+ var intake = SITE.intake;
3228
+ if (!intake) return;
3229
+ btn.disabled = true;
3230
+ msg.className = "ds-toggle-msg";
3231
+ msg.textContent = "確認中…";
3232
+ at.textContent = "";
3233
+ body.textContent = "";
3234
+ var payload = { type: "slack", ref: s.ref };
3235
+ if (s.workspace) payload.workspace = s.workspace;
3236
+ var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
3237
+ try {
3238
+ var res = await fetch(pfx + "op=probe&t=" + encodeURIComponent(intake.token), {
3239
+ method: "POST",
3240
+ headers: { "content-type": "application/json" },
3241
+ body: JSON.stringify(payload),
3242
+ });
3243
+ var j = await res.json().catch(function () { return {}; });
3244
+ if (!res.ok) {
3245
+ // サーバの文言をそのまま出す(回数制限の案内・未対応のアダプタの説明を含んでいる)
3246
+ msg.className = "ds-toggle-msg is-error";
3247
+ msg.textContent = "✗ " + (j.message || "確認できませんでした。");
3248
+ btn.disabled = false;
3249
+ return;
3250
+ }
3251
+ msg.className = "ds-toggle-msg" + (j.gate === "ok" ? " is-ok" : " is-error");
3252
+ msg.textContent = j.gate === "ok" ? "✓" : "✗";
3253
+ var v = probeVerdict(s, j);
3254
+ gateEl.className = "ds-gate " + v.cls;
3255
+ gateEl.textContent = v.text;
3256
+ // 不足スコープは**状態の語のすぐ横**に出す(「権限が足りません:groups:read」)。
3257
+ // 本文へ落とすと、いちばん要る情報が説明文に埋もれる
3258
+ if (j.gate === "missing_scope") {
3259
+ head.appendChild(el("code", { class: "ds-probe-scope", text: j.needed || "不明" }));
3260
+ }
3261
+ at.textContent = "いま測った結果(" + (dsFmtTime(j.checkedAt) || "たった今") + ")";
3262
+ probeHelpNodes(s, j).forEach(function (n) { body.appendChild(n); });
3263
+ // **夜間の判定に添えてあった案内は隠す。** 置き換えた判定と食い違う案内が
3264
+ // 下に残ると、同じ行に2つのことが書いてある状態になる
3265
+ var stale = card.querySelector(".ds-gate-help");
3266
+ if (stale) stale.hidden = true;
3267
+ // 表示名を Slack の実名へ自動で揃える(差分表示も更新ボタンも出さない)
3268
+ autoAlignName(s, j, card);
3269
+ } catch (e) {
3270
+ msg.className = "ds-toggle-msg is-error";
3271
+ msg.textContent = "✗ 通信に失敗しました。";
3272
+ }
3273
+ btn.disabled = false;
3274
+ });
3275
+ return wrap;
3276
+ }
3277
+
3037
3278
  function removeChannelButton(s, card) {
3038
3279
  return removeButton({
3039
3280
  label: "登録を解除",
@@ -4098,14 +4339,18 @@ ${capabilityState.toString()}
4098
4339
  }));
4099
4340
  }
4100
4341
  var gate = dsGateOf(s);
4342
+ // **「接続を確認」が押されたら、この要素を live の判定で置き換える**(probeSection)。
4343
+ // 2つの判定を同じ行に並べないため、参照を持っておく
4344
+ var gateEl = el("span", { class: "ds-gate " + gate.cls, text: gate.text });
4101
4345
  var card = el("div", { class: "ds-card" }, [
4102
4346
  el("div", { class: "ds-card-main" }, [
4103
4347
  el("span", { class: "ds-icon", text: meta.icon }),
4104
4348
  el("span", { class: "ds-type", text: meta.label }),
4105
- dsNameEl(s.name || "", s.url),
4349
+ // Slack だけ # を表示側で付ける(保存値は素)。他のアダプタの名前には触らない
4350
+ dsNameEl(s.type === "slack" ? slackDisplayName(s.name) : (s.name || ""), s.url),
4106
4351
  ]),
4107
4352
  badges,
4108
- el("span", { class: "ds-gate " + gate.cls, text: gate.text }),
4353
+ gateEl,
4109
4354
  ]);
4110
4355
  // チャンネルの登録解除。書き込み経路(intake Lambda)がある案件でだけ出す。
4111
4356
  // **Slack だけ。** github は登録ファイルが別(dev-sources.json)で、下のトグルと
@@ -4171,6 +4416,19 @@ ${capabilityState.toString()}
4171
4416
  // **ボタンより後ろに置く。** カードは横並びの flex なので、間に挟むと
4172
4417
  // 「登録を解除」が案内文の下の行へ落ちて、既存案件の行の形が変わる
4173
4418
  if (gate.help) card.appendChild(el("p", { class: "ds-gate-help", text: gate.help }));
4419
+ // 「接続を確認」(2026-08-19)。**押せる人を絞らない**——読むだけで設定を変えないので、
4420
+ // 気づいた人がその場で確かめられるほうがよい(オーナー判断)。書き込みになる
4421
+ // 「表示名を更新する」は、押した結果の中から別のボタンとして出る(probeSection)。
4422
+ //
4423
+ // **いまは Slack だけ。** GitHub は夜間の実測(probeGithubGate)が既にあるので繋ぐだけ、
4424
+ // Backlog / Figma / Drive は実測そのものが新規になる。同じ型で足せるよう、
4425
+ // 判定は infra/lambda/shared/slack-gate.mjs 側に置いてある(末尾に拡張点の説明がある)。
4426
+ //
4427
+ // **origin === "explicit" でも出す。** 確認は読むだけで、どの登録ファイル由来かに
4428
+ // 関係なく効く(書き込みになる「表示名を更新する」だけが由来で出し分かれる)
4429
+ if (SITE.intake && s.type === "slack" && s.ref) {
4430
+ card.appendChild(probeSection(s, card, gateEl));
4431
+ }
4174
4432
  return card;
4175
4433
  }
4176
4434
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.4.82",
3
+ "version": "0.4.84",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,