@takagaki/cortex-decisions-viewer 0.10.7 → 0.11.0

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 +525 -52
  2. package/package.json +1 -1
package/dist/render.js CHANGED
@@ -19,6 +19,13 @@ exports.registeredRowsToAdd = registeredRowsToAdd;
19
19
  exports.subtractRemovedRegistered = subtractRemovedRegistered;
20
20
  exports.mergeRegisteredFleet = mergeRegisteredFleet;
21
21
  exports.nextStateDelay = nextStateDelay;
22
+ exports.sanitizeSecretsNow = sanitizeSecretsNow;
23
+ exports.secretStateFromNow = secretStateFromNow;
24
+ exports.secretRegisteredAnywhere = secretRegisteredAnywhere;
25
+ exports.sanitizeCapabilitiesNow = sanitizeCapabilitiesNow;
26
+ exports.sanitizeGithubOwnersNow = sanitizeGithubOwnersNow;
27
+ exports.splitNeededParts = splitNeededParts;
28
+ exports.ingestedAtOf = ingestedAtOf;
22
29
  exports.renderConnectionMap = renderConnectionMap;
23
30
  /** SiteDataを単一の自己完結HTML(埋め込みデータ+CSS+JS)にレンダリングする */
24
31
  function renderSite(site) {
@@ -1058,6 +1065,232 @@ function nextStateDelay(count, since, now) {
1058
1065
  return null;
1059
1066
  return left < UNSETTLED_POLL_MS ? left : UNSETTLED_POLL_MS;
1060
1067
  }
1068
+ // ---------------------------------------------------------------------------
1069
+ // 工程1.5: 正本を直接読む(op=state v2 の secretsNow / capabilitiesNow / githubOwners)
1070
+ //
1071
+ // 🔴 **これは React 移行(#224)までの止血パッチ。** 差し替えるのは「読む先」だけで、
1072
+ // 4層(静的 → まとめ表 → registeredNow → stateNow)の合成そのものには手を付けない。
1073
+ // 捨てる予定のコードなので、ここを起点に整理を始めない。
1074
+ //
1075
+ // 3つに共通する規則:
1076
+ // 1. **読めなかったら従来のまとめ表の値へ落ちる。** 画面と Lambda は別々に配られるので、
1077
+ // 新しいキーを返さない応答を読む期間が必ずある
1078
+ // 2. **"unknown"(確かめられなかった)を「無い」に丸めない。** 鍵が入っている案件に
1079
+ // 「登録してください」と言うのが、この修正がまさに殺そうとしている嘘(#227)
1080
+ // 3. ライブ専用キーは build.ts の許可リストの網にかからないので、**画面側でも通す**
1081
+ // ---------------------------------------------------------------------------
1082
+ /**
1083
+ * 応答のキー(ASCII)→ 画面の能力名。**registeredNow の対応表に会議を足したもの。**
1084
+ *
1085
+ * 会議は登録の一覧を返さない(`REGISTERED_FIELDS` に無い)が、**能力の状態は持つ**
1086
+ * (`ingest-config.json` を読んだ結果)。対応表を分けると、能力を足したときに
1087
+ * 片方だけ書き足す形になるので、こちらは向こうを取り込んで1箇所に保つ。
1088
+ */
1089
+ const STATE_CAP_NAMES = Object.assign({}, REGISTERED_CAP_NAMES, { meetings: "会議" });
1090
+ /** 鍵の登録状況(列2)の語彙。サーバ側 `secretsNowRow` と同じ(増やさない) */
1091
+ const SECRET_NOW_STATES = ["registered", "missing", "unknown"];
1092
+ /**
1093
+ * `secretsNow`(鍵が登録されているか・列2)の許可リスト。
1094
+ *
1095
+ * 応答の1行は `{ adapter, kind, state, source, space? }`。画面が使うのは kind / state /
1096
+ * space だけで、**adapter は使わない**(鍵カードは種別で引く)。source は「どの棚が
1097
+ * 答えたか」の内部語なので、画面に出す語へ畳むのは読み出し側(`secretStateFromNow`)。
1098
+ */
1099
+ function sanitizeSecretsNow(v) {
1100
+ if (!Array.isArray(v))
1101
+ return null;
1102
+ const out = [];
1103
+ for (const r of v) {
1104
+ if (out.length >= 50)
1105
+ break;
1106
+ if (r == null || typeof r !== "object" || Array.isArray(r))
1107
+ continue;
1108
+ const kind = typeof r.kind === "string" ? r.kind : "";
1109
+ const state = typeof r.state === "string" ? r.state : "";
1110
+ // 知らない語は「未登録」に化けさせず落とす(落ちればまとめ表へフォールバックする)
1111
+ if (!kind || SECRET_NOW_STATES.indexOf(state) < 0)
1112
+ continue;
1113
+ const row = { kind, state };
1114
+ // 🔴 **スペースは小文字に畳む。** 突き合わせる相手(画面の行のドメイン)が小文字なので、
1115
+ // 揃えないと**入っている鍵を「未登録」と言う**(`issuesRefOf` と同じ理由)
1116
+ if (typeof r.space === "string" && r.space)
1117
+ row.space = r.space.toLowerCase();
1118
+ out.push(row);
1119
+ }
1120
+ return out.length ? out : null;
1121
+ }
1122
+ /**
1123
+ * `secretsNow` から、その鍵カードの答えを引く。**答えられないときは null。**
1124
+ *
1125
+ * null は「まとめ表の値へ落ちる」の合図で、次の3つで返る:
1126
+ * - その種別の行が1つも来ていない(古い Lambda・鍵を持たないアダプタ)
1127
+ * - スペース別に聞かれたのに、そのスペースの行が来ていない
1128
+ * - `unknown`(権限・スロットリングで確かめられなかった)
1129
+ *
1130
+ * @param space Backlog のスペース(画面の行のドメイン)。他の鍵では空
1131
+ */
1132
+ function secretStateFromNow(rows, kind, space) {
1133
+ if (!Array.isArray(rows) || !rows.length || !kind)
1134
+ return null;
1135
+ const mine = rows.filter((r) => r && r.kind === kind);
1136
+ if (!mine.length)
1137
+ return null;
1138
+ let hit = null;
1139
+ if (space) {
1140
+ hit = mine.filter((r) => r.space === String(space).toLowerCase())[0] || null;
1141
+ if (!hit)
1142
+ return null;
1143
+ }
1144
+ else {
1145
+ // **1行に決まるときだけ答える。** スペースが複数ある Backlog を種別だけで聞かれたら、
1146
+ // どの棚のことか決まらない——片方だけ入れた状態を「登録済み」と言うことになる
1147
+ if (mine.length > 1)
1148
+ return null;
1149
+ hit = mine[0];
1150
+ }
1151
+ if (hit.state === "registered")
1152
+ return { known: "yes", src: "Secrets Manager" };
1153
+ if (hit.state === "missing")
1154
+ return { known: "no", src: "" };
1155
+ return null;
1156
+ }
1157
+ /**
1158
+ * その種別の鍵が**どこかに**登録されているか(タイルの「⚠ トークンが未設定です」用)。
1159
+ *
1160
+ * タイルは能力に1枚しかないので、スペース別の答えは持てない。**まとめ表の
1161
+ * `secretSources[kind]`(種別ごとの1つの値)と同じ粒度**に畳む——1本でも入っていれば
1162
+ * 注記を出さない。スペースごとの充足は行の中の鍵カードが答える。
1163
+ *
1164
+ * @returns "yes" | "no" | "unknown"(unknown はまとめ表へフォールバック)
1165
+ */
1166
+ function secretRegisteredAnywhere(rows, kind) {
1167
+ if (!Array.isArray(rows) || !kind)
1168
+ return "unknown";
1169
+ const mine = rows.filter((r) => r && r.kind === kind);
1170
+ if (!mine.length)
1171
+ return "unknown";
1172
+ if (mine.some((r) => r.state === "registered"))
1173
+ return "yes";
1174
+ if (mine.every((r) => r.state === "missing"))
1175
+ return "no";
1176
+ return "unknown";
1177
+ }
1178
+ /** 能力の状態の語彙。サーバ側 `CAPABILITY_STATES` と同じ(増やさない) */
1179
+ const CAPABILITY_NOW_STATES = ["on", "off", "empty", "broken", "unset"];
1180
+ /**
1181
+ * `capabilitiesNow`(能力が配線されているか)を、**まとめ表の `capabilities` と同じ形**
1182
+ * (能力名=日本語のキー)に畳む。読めた能力だけを返し、1つも無ければ null。
1183
+ *
1184
+ * ⚠ **応答は `caps` で要求した能力ぶんしか来ない**(画面が開いた能力だけを読む・
1185
+ * `OPEN_CAPS` の項)。だから**キー単位でまとめ表へ落ちる**——ここだけは
1186
+ * `rawFleetVal` の「キーごとにフォールバックしない」の例外で、そうしないと
1187
+ * 開いていない能力のタイルが全部「未接続」に落ちる。
1188
+ */
1189
+ function sanitizeCapabilitiesNow(v) {
1190
+ if (!v || typeof v !== "object" || Array.isArray(v))
1191
+ return null;
1192
+ const out = {};
1193
+ let any = false;
1194
+ for (const id in STATE_CAP_NAMES) {
1195
+ if (!Object.prototype.hasOwnProperty.call(STATE_CAP_NAMES, id))
1196
+ continue;
1197
+ const c = v[id];
1198
+ if (c == null || typeof c !== "object" || Array.isArray(c))
1199
+ continue;
1200
+ const state = typeof c.state === "string" ? c.state : "";
1201
+ if (CAPABILITY_NOW_STATES.indexOf(state) < 0)
1202
+ continue;
1203
+ const row = { state };
1204
+ if (typeof c.adapter === "string" && c.adapter)
1205
+ row.adapter = c.adapter;
1206
+ if (typeof c.detail === "string" && c.detail)
1207
+ row.detail = c.detail;
1208
+ out[STATE_CAP_NAMES[id]] = row;
1209
+ any = true;
1210
+ }
1211
+ return any ? out : null;
1212
+ }
1213
+ /**
1214
+ * `registeredNow.githubOwners`(組織ごとの App の導入状況・#233)の許可リスト。
1215
+ *
1216
+ * 🔴 **`appInstalled` は `=== true` / `=== false` / それ以外は null の厳格正規化。**
1217
+ * サーバ側(`withGithubInstallations`)が同じ畳み方をしているが、**画面でも畳む**
1218
+ * ——生の値(インストールトークンが入りうる)を素通しする経路をどちらにも作らない。
1219
+ * null は「その回は確かめられなかった」で、**どちらの案内も出させない値**。
1220
+ */
1221
+ function sanitizeGithubOwnersNow(v) {
1222
+ if (!Array.isArray(v))
1223
+ return null;
1224
+ const out = [];
1225
+ for (const r of v) {
1226
+ if (out.length >= 50)
1227
+ break;
1228
+ if (r == null || typeof r !== "object" || Array.isArray(r))
1229
+ continue;
1230
+ const owner = typeof r.owner === "string" ? r.owner : "";
1231
+ if (!owner)
1232
+ continue;
1233
+ const row = { owner, appInstalled: r.appInstalled === true ? true : r.appInstalled === false ? false : null };
1234
+ if (typeof r.repos === "number" && Number.isFinite(r.repos) && r.repos >= 0)
1235
+ row.repos = r.repos;
1236
+ out.push(row);
1237
+ }
1238
+ return out.length ? out : null;
1239
+ }
1240
+ /**
1241
+ * サーバが返した「次の一手」(`needed`)を、素の文字列と URL に切り分ける。
1242
+ *
1243
+ * 🔴 **正規表現をクライアント側に書き写さないこと。** 埋め込みJSはテンプレート
1244
+ * リテラルの中なので、`\/` や `\s` がリテラルの解釈で潰れて**別物の正規表現**になる
1245
+ * (`backlogTargetOf` の項に、実際にクライアントJSが丸ごと構文エラーで落ちた経緯がある)。
1246
+ * ここで切り分けて、画面側は DOM を組み立てるだけにする。
1247
+ *
1248
+ * @returns [{ text, url }] の配列。url:true の部分だけをリンクにする
1249
+ */
1250
+ function splitNeededParts(text) {
1251
+ const s = String(text == null ? "" : text);
1252
+ const out = [];
1253
+ // 日本語の文の途中に URL が挟まる(「… https://… で追加してください」)。
1254
+ // 空白と句読点で切る——読点・句点まで飲み込むと、開けないリンクになる
1255
+ const re = /https?:\/\/[^\s、。]+/g;
1256
+ let at = 0;
1257
+ let m;
1258
+ while ((m = re.exec(s)) !== null) {
1259
+ if (m.index > at)
1260
+ out.push({ text: s.slice(at, m.index), url: false });
1261
+ out.push({ text: m[0], url: true });
1262
+ at = m.index + m[0].length;
1263
+ }
1264
+ if (at < s.length)
1265
+ out.push({ text: s.slice(at), url: false });
1266
+ return out;
1267
+ }
1268
+ /**
1269
+ * `stateNow` から、その能力の「最後に取り込めた時刻」を引く(#227 の時刻の一貫化)。
1270
+ *
1271
+ * **「最終同期」(`internalSources.lastSync`)より優先する。** あちらは run 全体の
1272
+ * 最終成功時刻(空振りした run も含む run 単位のスナップショット)で、こちらは
1273
+ * **接続先ごとに実際に取り込めた事実**。同じ行に2つの時刻の意味を混ぜない。
1274
+ *
1275
+ * **成功の時刻だけを採る。** 失敗した行の `at` は「最後に試した時刻」で、
1276
+ * 「最終同期」として出すと**取り込めていないのに同期できたように読める**。
1277
+ *
1278
+ * @param capId 応答側の能力名(issues / design / materials / meetings)
1279
+ * @returns ISO文字列。1件も無ければ ""
1280
+ */
1281
+ function ingestedAtOf(rows, capId) {
1282
+ if (!Array.isArray(rows) || !capId)
1283
+ return "";
1284
+ let best = "";
1285
+ for (const r of rows) {
1286
+ if (!r || r.cap !== capId || r.question !== "ingested")
1287
+ continue;
1288
+ const at = r.lastOkAt || (r.status === "ok" ? r.at : "");
1289
+ if (typeof at === "string" && at > best)
1290
+ best = at;
1291
+ }
1292
+ return best;
1293
+ }
1061
1294
  function capabilityState(cap, ctx) {
1062
1295
  // **能力→パイプラインの対応はここに持つ。** 呼び出し側に渡させると、
1063
1296
  // クライアント側(この関数を .toString() で受け取る側)が同じ表を書き写すことになり、
@@ -2386,6 +2619,18 @@ var STATE_ROW_FIELDS = ${JSON.stringify(STATE_ROW_FIELDS)};
2386
2619
  ${sanitizeStateNow.toString()}
2387
2620
  ${overlayStateOnFleet.toString()}
2388
2621
  ${nextStateDelay.toString()}
2622
+ // 工程1.5: 正本を直接読む(secretsNow / capabilitiesNow / githubOwners)。
2623
+ // **判定の実体はビルド時の定義**——画面側に書き写すと許可リストが2枚になる
2624
+ var STATE_CAP_NAMES = ${JSON.stringify(STATE_CAP_NAMES)};
2625
+ var SECRET_NOW_STATES = ${JSON.stringify(SECRET_NOW_STATES)};
2626
+ var CAPABILITY_NOW_STATES = ${JSON.stringify(CAPABILITY_NOW_STATES)};
2627
+ ${sanitizeSecretsNow.toString()}
2628
+ ${secretStateFromNow.toString()}
2629
+ ${secretRegisteredAnywhere.toString()}
2630
+ ${sanitizeCapabilitiesNow.toString()}
2631
+ ${sanitizeGithubOwnersNow.toString()}
2632
+ ${splitNeededParts.toString()}
2633
+ ${ingestedAtOf.toString()}
2389
2634
  // 「はじめに」セクションのチュートリアルデータ(ビルド時に埋め込み。ボタンURL・チャレンジ4の決定は解決済み)
2390
2635
  var TUTORIAL = (function () {
2391
2636
  var elx = document.getElementById("tutorial-data");
@@ -3327,7 +3572,7 @@ ${nextStateDelay.toString()}
3327
3572
  */
3328
3573
  /** 台帳から、その組織の情報を引く(無ければ null=断定しない) */
3329
3574
  function githubOwnerMeta(owner) {
3330
- var list = fleetVal("githubOwners") || [];
3575
+ var list = githubOwnersView() || [];
3331
3576
  for (var i = 0; i < list.length; i++) {
3332
3577
  if (list[i] && list[i].owner === owner) return list[i];
3333
3578
  }
@@ -3358,7 +3603,7 @@ ${nextStateDelay.toString()}
3358
3603
  // どちらを先に出しても片側だけ新しい期間ができる。その間に 「token」 を落とすと、
3359
3604
  // コミット済みの値を読んだ画面が偽の警告を出す。**外すのはフェーズ2**(engine の
3360
3605
  // v1 が全案件へ行き渡ったことを確かめてから)。
3361
- if (o) {
3606
+ if (o && o.source) {
3362
3607
  state = (o.source === "app" || o.source === "workflow" || o.source === "token") ? "ok"
3363
3608
  : o.source === "unknown" ? "unsure"
3364
3609
  : "warn";
@@ -3369,6 +3614,50 @@ ${nextStateDelay.toString()}
3369
3614
  }
3370
3615
  g.items.push(s);
3371
3616
  });
3617
+ // 🔴 **「source」 を持たない行(op=state の registeredNow.githubOwners・工程1.5)は
3618
+ // 「appInstalled」 と行の実測から決める。** PR-B でまとめ表から 「githubOwners」 が
3619
+ // 消えたため、ここが無いと #233 の出し分け(App 未導入 / リポジトリ未選択)が
3620
+ // 画面から消える。**行の判定を数え直すのは、この関数がそれを持っているから**
3621
+ // ——「items」 は既に stateNow を重ねた後の行(overlayStateOnFleet)なので、
3622
+ // 「押して確かめた結果」もそのまま効く。
3623
+ //
3624
+ // ⚠ **まとめ表の行(「source」 を持つ)には触らない。** あちらは夜間が数えた
3625
+ // 「repos」 / 「unreadable」 を持っており、そちらが件数の正本
3626
+ order.forEach(function (g) {
3627
+ var o = g.meta;
3628
+ if (!o || o.source) return;
3629
+ // 読めていないリポジトリの数。**測った行だけを数える**——一度も測っていない行を
3630
+ // 「読めていない」に数えると、立ち上げ直後の組織に偽の警告を出す
3631
+ var seen = {};
3632
+ var shown = 0;
3633
+ var unread = 0;
3634
+ g.items.forEach(function (s) {
3635
+ var ref = String(s.ref || "");
3636
+ // 同じリポジトリの issues と discussions は1つと数える(行は2本でも接続は1つ)
3637
+ if (!ref || seen[ref]) return;
3638
+ seen[ref] = 1;
3639
+ shown++;
3640
+ if (s.gate && s.gate !== "ok") unread++;
3641
+ });
3642
+ // 総数は登録の件数(正本が数えたもの)を優先する。画面に出ている行は
3643
+ // フィルタや読み込み途中でこれより少ないことがある
3644
+ g.repos = o.repos != null ? o.repos : shown;
3645
+ if (o.appInstalled === false) {
3646
+ // App が入っていない=この組織のリポジトリは1つも読めない
3647
+ g.state = "warn";
3648
+ g.unread = g.repos;
3649
+ return;
3650
+ }
3651
+ if (o.appInstalled === true) {
3652
+ // 入っているのに読めない行がある=そのリポジトリが読み取り対象に選ばれていない
3653
+ g.state = unread > 0 ? "warn" : "ok";
3654
+ g.unread = unread;
3655
+ return;
3656
+ }
3657
+ // 「null」(確かめられなかった)。**どちらの案内も出さない**——押しても直るとは
3658
+ // 限らないし、「未接続」と言えば入っている組織に偽の警告を出す
3659
+ g.state = null;
3660
+ });
3372
3661
  // **繋がっている組織を先に、繋がっていない組織を後に。**
3373
3662
  // 直すものが無い人にも「未接続」が最初に見えるのは、優先順位が逆
3374
3663
  order.sort(function (a, b) {
@@ -3385,7 +3674,7 @@ ${nextStateDelay.toString()}
3385
3674
  * どの組織かは**行のラベル**が示し、**接続が要る組だけ**案内が行を抱える。
3386
3675
  */
3387
3676
  function githubRows(items) {
3388
- var owners = fleetVal("githubOwners");
3677
+ var owners = githubOwnersView();
3389
3678
  var out = [];
3390
3679
  groupGithubByOwner(items, owners).forEach(function (g) {
3391
3680
  // **行ごとに meta を引く。** issues と discussions でアイコンとラベルが違う
@@ -3447,8 +3736,11 @@ ${nextStateDelay.toString()}
3447
3736
  function githubNotConnectedNodes(g) {
3448
3737
  var out = [];
3449
3738
  var o = g.meta || {};
3450
- var repos = o.repos || g.items.length;
3451
- var unread = o.unreadable != null ? o.unreadable : g.items.length;
3739
+ // **件数はグループが数えたものを優先する(工程1.5)。** まとめ表の repos /
3740
+ // unreadable は夜間の値で、いま画面に出ている行の数と食い違うことがある
3741
+ //(#101 で3度直した形)。「groupGithubByOwner」 は**いま並んでいる行**を数えている
3742
+ var repos = o.repos != null ? o.repos : g.repos != null ? g.repos : g.items.length;
3743
+ var unread = o.unreadable != null ? o.unreadable : g.unread != null ? g.unread : g.items.length;
3452
3744
  var installed = o.appInstalled;
3453
3745
  out.push(el("div", { class: "ds-orgnote-title", text: "⚠️ " + g.title + " はまだ接続されていません" }));
3454
3746
  // **読める件数を出す。** 総数だけ出すと、同じ画面の集計と食い違う(#101 で3度直した形)
@@ -3783,6 +4075,30 @@ ${nextStateDelay.toString()}
3783
4075
  var LIVE_REGISTERED = null;
3784
4076
  // 接続状態ストアの行(届くか・取り込めたか)。op=state の stateNow から
3785
4077
  var LIVE_STATE = null;
4078
+ /**
4079
+ * 鍵がいま登録されているか(op=state の secretsNow・列2)。取得前・取得失敗時は null。
4080
+ *
4081
+ * 🔴 **まとめ表の secretSources / secretSpaces より必ず優先する。** あちらは夜間の
4082
+ * スナップショットなので、**鍵を保存した直後は前日の「未登録」を出し続ける**
4083
+ * ——「保存したのに未登録」(#227)の正体。こちらは Secrets Manager を都度見た答え。
4084
+ */
4085
+ var LIVE_SECRETS = null;
4086
+ /**
4087
+ * 能力が配線されているか(op=state の capabilitiesNow)。取得前・取得失敗時は null。
4088
+ *
4089
+ * **形はまとめ表の capabilities と同じ**(能力名のキー・{adapter, state, detail})なので、
4090
+ * 読む側は capsView() で重ねるだけでよい。**要求した能力ぶんしか来ない**ので、
4091
+ * 欠けている能力はまとめ表の値で描く(sanitizeCapabilitiesNow の項)。
4092
+ */
4093
+ var LIVE_CAPS = null;
4094
+ /**
4095
+ * 組織ごとの GitHub App の導入状況(op=state の registeredNow.githubOwners・#233)。
4096
+ *
4097
+ * **registeredNow の他のキーとは別に持つ。** あちらは「まとめ表がまだ知らない行を足す」
4098
+ * ための材料で、こちらは組織の状態そのもの。同じ入れ物に混ぜると、行の追加の判定
4099
+ * (unsettled/pending)に組織の行が紛れ込む。
4100
+ */
4101
+ var LIVE_GH_OWNERS = null;
3786
4102
  /** マージ済みの3キー(作り直しは setLive* だけが行う) */
3787
4103
  var MERGED = null;
3788
4104
  /** マージの対象になるキー。これ以外は素の値をそのまま返す */
@@ -3825,6 +4141,55 @@ ${nextStateDelay.toString()}
3825
4141
  return rawFleetVal(key);
3826
4142
  }
3827
4143
 
4144
+ /**
4145
+ * 能力の状態(capabilities)。**能力ごとに、正本(capabilitiesNow)があればそちら。**
4146
+ *
4147
+ * ⚠ **ここは rawFleetVal の「キー単位でフォールバックしない」の意図的な例外**(工程1.5)。
4148
+ * あちらの禁止は「同じ画面に2つの時点が混ざると、上に出す『◯◯時点』が嘘になる」ため
4149
+ * だが、capabilitiesNow は**画面が開いた能力ぶんしか来ない**(OPEN_CAPS の項)ので、
4150
+ * 丸ごと採るか丸ごと捨てるかしか無いと、**開いていない能力のタイルが全部「未接続」に
4151
+ * 落ちる**。能力ごとに正本を優先し、来ていない能力はまとめ表のまま出す。
4152
+ */
4153
+ function capsView() {
4154
+ var base = fleetVal("capabilities") || {};
4155
+ if (!LIVE_CAPS) return base;
4156
+ var out = {};
4157
+ for (var k in base) if (Object.prototype.hasOwnProperty.call(base, k)) out[k] = base[k];
4158
+ for (var n in LIVE_CAPS) if (Object.prototype.hasOwnProperty.call(LIVE_CAPS, n)) out[n] = LIVE_CAPS[n];
4159
+ return out;
4160
+ }
4161
+
4162
+ /**
4163
+ * 組織ごとの接続状況(#233 の出し分けの材料)。
4164
+ *
4165
+ * **まとめ表からは消えた(PR-B)。** fleet-status.json の専用プローブを廃止したので、
4166
+ * いまの供給元は op=state の 「registeredNow.githubOwners」(App の導入状況の実測)。
4167
+ * 古い生成物を読んでいる案件のために、まとめ表の行も残す(組織ごとに正本が勝つ)。
4168
+ */
4169
+ function githubOwnersView() {
4170
+ var fleet = fleetVal("githubOwners");
4171
+ if (!LIVE_GH_OWNERS) {
4172
+ // **「対象0件と確かめた」だけは正本から言える。** 開発の登録一覧が読めていて0件なら、
4173
+ // 組織の行が1つも無いのは当たり前(下の「まだありません」の1行はこれで出る)。
4174
+ // まとめ表を持たない案件で 「undefined」(まだ走っていない)と混ぜない
4175
+ if (!fleet && LIVE_REGISTERED && Array.isArray(LIVE_REGISTERED.dev) && LIVE_REGISTERED.dev.length === 0) return [];
4176
+ return fleet;
4177
+ }
4178
+ var out = [];
4179
+ var byOwner = {};
4180
+ for (var i = 0; i < LIVE_GH_OWNERS.length; i++) {
4181
+ byOwner[LIVE_GH_OWNERS[i].owner] = 1;
4182
+ out.push(LIVE_GH_OWNERS[i]);
4183
+ }
4184
+ // 正本が知らない組織(まとめ表にしか無い行)は落とさない——消すと、その組織の
4185
+ // 案内だけが画面から黙って消える
4186
+ var old = Array.isArray(fleet) ? fleet : [];
4187
+ for (var j = 0; j < old.length; j++) {
4188
+ if (old[j] && old[j].owner && !byOwner[old[j].owner]) out.push(old[j]);
4189
+ }
4190
+ return out;
4191
+ }
4192
+
3828
4193
  /** まとめ表がまだ答えていない行の数(取り直しを続けるかの判断) */
3829
4194
  function unsettledNow() {
3830
4195
  return unsettledRegisteredCount(rawView(), LIVE_REGISTERED);
@@ -3903,7 +4268,10 @@ ${nextStateDelay.toString()}
3903
4268
  String(rawFleetVal("generatedAt") || ""),
3904
4269
  );
3905
4270
  return {
3906
- capabilities: rawFleetVal("capabilities") || {},
4271
+ // 🔴 **能力の状態は正本(capabilitiesNow)を通す(工程1.5)。** タイルの語は
4272
+ // capsView から引いているので、ここを素のまとめ表のままにすると
4273
+ // 「タイルの文字は未接続・状態の色は接続OK」が同じタイルの中で並ぶ
4274
+ capabilities: capsView(),
3907
4275
  internals: rawFleetVal("internalSources") || [],
3908
4276
  externals: ov.externalSources || [],
3909
4277
  pipes: rawFleetVal("pipelines") || [],
@@ -4398,7 +4766,7 @@ ${nextStateDelay.toString()}
4398
4766
  if (noteIsUnset) reflectMark(noteEl, cap, "desc", function () {
4399
4767
  // 足した行があるなら、この一文(接続先が設定されていません)はもう事実ではない
4400
4768
  if (capPending(cap)) return { text: "", hidden: true };
4401
- var capsNow = fleetVal("capabilities") || {};
4769
+ var capsNow = capsView();
4402
4770
  var dNow = capabilityDisplayState(cap, capsNow);
4403
4771
  var infoNow = capsNow[cap];
4404
4772
  var t = dNow && dNow !== "on" && infoNow && infoNow.detail
@@ -4457,7 +4825,9 @@ ${nextStateDelay.toString()}
4457
4825
  var sources = fleetVal("externalSources") || [];
4458
4826
  var pipes = fleetVal("pipelines") || [];
4459
4827
  // 能力ごとの導出済みの状態(#125② の正本)。持たない古いデータでは {}
4460
- var caps = fleetVal("capabilities") || {};
4828
+ // **読む先は capsView(工程1.5)。** 能力ごとに、正本(capabilitiesNow)があれば
4829
+ // そちら・来ていない能力はまとめ表のまま
4830
+ var caps = capsView();
4461
4831
  var hasCaps = Object.keys(caps).length > 0;
4462
4832
  // **この節に入るものが1つでもあるなら出す。**
4463
4833
  //
@@ -4784,7 +5154,7 @@ ${nextStateDelay.toString()}
4784
5154
  ));
4785
5155
  if (liveListWanted("github")) pushCap("開発", liveList(
4786
5156
  "開発",
4787
- function () { return externalsSig("github", fleetVal("githubOwners") || null); },
5157
+ function () { return externalsSig("github", githubOwnersView() || null); },
4788
5158
  function (box) {
4789
5159
  var items = liveExternals("github");
4790
5160
  if (!items.length) return;
@@ -4837,7 +5207,7 @@ ${nextStateDelay.toString()}
4837
5207
  //
4838
5208
  // **「[]」(夜間バッチは走ったが対象0件)と 「undefined」(まだ走っていない)は別物。**
4839
5209
  // 後者に「まだありません」と書くと、実際には登録済みかもしれない案件に嘘を言う
4840
- var ownersNow = fleetVal("githubOwners");
5210
+ var ownersNow = githubOwnersView();
4841
5211
  if (Array.isArray(ownersNow) && ownersNow.length === 0) {
4842
5212
  pushCap("開発", el("p", {
4843
5213
  class: "sec-how ds-all-connected",
@@ -4948,14 +5318,9 @@ ${nextStateDelay.toString()}
4948
5318
  // 両方に出すと、同じ失敗が1画面に2回並ぶ。実行状況はパイプライン節に任せ、
4949
5319
  // ここでは直し方だけをその行に添える(下の renderPipelines で使う)
4950
5320
  var PIPE_CHECK = { "update-gold": "nightly_decisionlog", "sync-backlog": "nightly_backlog", "sync-designs": "figma_sync" };
4951
- // **engine が出しているチェックだけを並べる。** meeting_minutes / channels_json は
4952
- // 同語反復(適用されるときは必ず ok)だったので engine が撤去した(#125 ⑤)。
4953
- // 割り当ての無いチェックは消えず、下の「セットアップの残り」へ落ちる
4954
- var CHECK_CAP = {
4955
- backlog_secrets: "課題管理", backlog_synced: "課題管理",
4956
- figma_token: "デザイン", figma_inventory: "デザイン",
4957
- dev_sources_registered: "開発", dev_issues_derived: "開発",
4958
- };
5321
+ // **能力タイルへの振り分け表(CHECK_CAP)は撤去した(#230)。** タイルに長文の
5322
+ // 対処手順を出すのをやめたので、振り分け先が無い。チェックは全部「セットアップの残り」に
5323
+ // 並ぶ(実行状況の行に添える PIPE_CHECK だけが例外)
4959
5324
  // 🔴 **鍵の欄を指すチェックは、鍵の欄が出ているときだけ出す(オーナー決定 2026-08-22)。**
4960
5325
  // 「この画面の『Backlog のAPIキー』に貼ってください」は、接続先が0件で欄を畳んでいる
4961
5326
  // あいだは**存在しない欄を指す案内**になる(最後の接続先を外した直後に実際に出た。
@@ -4967,13 +5332,12 @@ ${nextStateDelay.toString()}
4967
5332
  var kc = KEY_POINTING_CHECKS[c.id];
4968
5333
  return !(kc && keyTargetCount(kc) === 0);
4969
5334
  });
4970
- // **タイルに出せたものを覚えておく。** 振り分け先の能力タイルが立たない案件
4971
- //(tools に宣言が無い・行も無い)では出す場所が無くなり、項目が**黙って消える**。
4972
- // 消えた分は下の節へ落とす——振り分けを間違えても、消えるよりは出る方がよい
5335
+ // **どこかに出せたものを覚えておく。** 出せなかった分は下の「セットアップの残り」へ
5336
+ // 落とす——振り分けを間違えても、消えるよりは出る方がよい。
5337
+ //
5338
+ // ⚠ **能力タイルはもう立てない(#230)。** 立てていた頃の名残で名前が残っているが、
5339
+ // いま立てるのは実行状況の行(同じ run の未達を2箇所に出さないため)だけ
4973
5340
  var shownOnTile = {};
4974
- function checksFor(cap) {
4975
- return allChecks.filter(function (c) { return CHECK_CAP[c.id] === cap; });
4976
- }
4977
5341
  /** 残りの1件を、直し方まで含めて1行にする */
4978
5342
  function checkLine(c) {
4979
5343
  var row = el("div", { class: "ds-check" });
@@ -5063,8 +5427,15 @@ ${nextStateDelay.toString()}
5063
5427
  }
5064
5428
  // **未接続・未使用のときは最終同期を出さない。** 空振りしたrunの時刻なので、
5065
5429
  // 「未接続」と並べると「一度は繋がった」と読めてしまう
5066
- if (internal && internal.lastSync && st !== "off" && st !== "idle") {
5067
- var t = dsFmtTime(internal.lastSync);
5430
+ //
5431
+ // 🔴 **接続状態ストアに取り込みの行があるなら、その時刻を優先する(工程1.5・#227)。**
5432
+ // 「internalSources.lastSync」 は run 全体の最終成功時刻で、**どの接続先のことか
5433
+ // 分からない**(鍵が無くて1件も取り込めていない run でも「成功」で終わる)。
5434
+ // ストアの行は「その能力の接続先が実際に取り込めた事実」なので、同じ「最終同期」の
5435
+ // 位置に出すならこちらが正しい。行が無ければ従来どおり run の時刻
5436
+ if (st !== "off" && st !== "idle") {
5437
+ var ingestedAt = ingestedAtOf(LIVE_STATE, capIdOf(cap));
5438
+ var t = dsFmtTime(ingestedAt || (internal && internal.lastSync) || "");
5068
5439
  if (t) parts.push("最終同期 " + t);
5069
5440
  }
5070
5441
  return parts.join(" ");
@@ -5141,7 +5512,7 @@ ${nextStateDelay.toString()}
5141
5512
  // 「⏳ 反映中」→「⦿ 未接続」になる——データが正しくなったあとに嘘をつく。
5142
5513
  // 状態の語も色(ds-gate-*)も、判定はタイルを作るときと同じ式で引く
5143
5514
  if (derived === "unset") reflectMark(subEl, cap, "sub", function (n) {
5144
- var capsNow = fleetVal("capabilities") || {};
5515
+ var capsNow = capsView();
5145
5516
  var insNow = fleetVal("internalSources") || [];
5146
5517
  var srcsNow = fleetVal("externalSources") || [];
5147
5518
  var dNow = adapter === capabilityAdapter(cap, capsNow) ? capabilityDisplayState(cap, capsNow) : "unset";
@@ -5191,21 +5562,21 @@ ${nextStateDelay.toString()}
5191
5562
  // 導出が無いデータでは判断材料が無いので、注記を出す側(=入力欄と揃う側)に倒す
5192
5563
  // 接続先0件の抑止は入力欄と**同じ関数**を通す(keyTargetCount の 🔴 不変条件)
5193
5564
  var tokenRelevant = (derived != null ? derived !== "off" : true) && keyTargetCount(cap) > 0;
5194
- if (tokenRelevant && sk && (SITE.secretKinds || []).indexOf(sk) >= 0 && !((fleetVal("secretSources") || {})[sk])) {
5565
+ if (tokenRelevant && sk && (SITE.secretKinds || []).indexOf(sk) >= 0 && !secretPresentForTile(sk)) {
5195
5566
  body.appendChild(el("div", { class: "ds-tile-note", text: "⚠ トークンが未設定です" }));
5196
5567
  }
5197
- // セットアップの残りのうち、この能力のもの。**鍵の注記と同じ位置**に出して
5198
- // 「何が足りないか」と「何をすればよいか」を同じ場所で読めるようにする
5199
- checksFor(cap).forEach(function (c) {
5200
- // **直し方が無いものはタイルに出さないが、出したことにもしない。**
5201
- // shownOnTile を先に立てると節からも外れ、画面のどこにも出なくなる
5202
- //(実データで mitsubishi nightly_backlog / figma_token、verdy の
5203
- // nightly_backlog が消えていた)。タイルは状態を読む場所なので
5204
- //「足りない」だけの行では埋めず、節の側で拾う
5205
- if (!c.action) return;
5206
- shownOnTile[c.id] = true;
5207
- body.appendChild(el("div", { class: "ds-tile-note ds-tile-todo", text: "→ " + c.action }));
5208
- });
5568
+ // 🔴 **セットアップの残り(checks)の長文はタイルに出さない(#230・オーナー指摘)。**
5569
+ //
5570
+ // 出所は engine の夜間チェックの 「action」 欄で、**この設定画面が無かった頃の
5571
+ // 手順**(「Secrets に FIGMA_TOKEN を登録しても同じです」「Run workflow を
5572
+ // 実行すると取り込まれます」「手元の Claude Code なら /sync-designs」)が
5573
+ // そのまま流れ込んでいた——UIから登録させたいのに UI を迂回する裏口を案内し、
5574
+ // しかも鍵を保存すれば同期は自動で起きるので**半分は嘘**になっていた。
5575
+ //
5576
+ // **タイルは短い状態表示だけにする。** 何が足りないかは「⚠ トークンが未設定です」で
5577
+ // 足り、対処は開いたモーダルの中の案内が持つ。
5578
+ // **shownOnTile を立てない**ので、これらの行は下の「セットアップの残り」で拾われる
5579
+ //(消すのではなく、置き場所を1つに戻す)。文言そのものの手当ては engine 側の別issue。
5209
5580
  // **id は能力ごとに1つ(先頭のアダプタ)。** 接続マップのノードの飛び先で、
5210
5581
  // 同じ id が2つある不正なHTMLになると getElementById が思わぬ側を指す
5211
5582
  var tile = el("button", { class: "ds-tile", type: "button", id: ai ? "ds-" + cap + "-" + adapter : "ds-" + cap }, [
@@ -5353,11 +5724,15 @@ ${nextStateDelay.toString()}
5353
5724
  * DS_GATE_NO_TOKEN が持っていて、どちらも dsGateOf を通して取る。ここで文面を
5354
5725
  * 書き写すと、**同じ行の状態バッジと確認結果で語が割れる**(この節で3回直した形)。
5355
5726
  *
5356
- * 他アダプタ(GitHub / Backlog / Figma / Drive)を足すときの拡張点:
5727
+ * 他アダプタ(Backlog / Figma / Drive)を足すときの拡張点:
5357
5728
  * - 判定と語彙は infra/lambda/shared/slack-gate.mjs 側に足す(画面は DS_GATE のまま)
5358
5729
  * - この節に増やすのは probeHelpNodes の「次の一手」だけ。**アダプタ固有の分岐を
5359
5730
  * probeVerdict に入れない**(入れた瞬間に判定が画面側にも生まれる)
5360
- * - 行に出す条件(下の extSourceCard)を s.type === "slack" から広げる
5731
+ * - 送る種別(probeTypeOf)と、行に出す条件(下の extSourceCard)に足す
5732
+ *
5733
+ * **開発(GitHub)は #234 で足した。** 判定は shared/github-gate.mjs、次の一手は
5734
+ * サーバの needed(理由と設定ページのURLを1文で持つ)をそのまま出している
5735
+ * ——画面に分岐が生まれないよう、言い分けを全部あちらに置いた
5361
5736
  * ------------------------------------------------------------------------- */
5362
5737
 
5363
5738
  /**
@@ -5372,6 +5747,50 @@ ${nextStateDelay.toString()}
5372
5747
  return dsGateOf({ type: s.type, workspace: s.workspace, gate: r.gate });
5373
5748
  }
5374
5749
 
5750
+ /**
5751
+ * 確認したいアダプタの種別(「op=probe」 の 「type」)。**画面の種別をそのまま送らない**
5752
+ * ——「github-issues」 / 「github-discussions」 は同じ1つのリポジトリの読み取りで、
5753
+ * サーバの受け口は 「github」 の1種別しか知らない(「PROBE_REF_RULES」)。
5754
+ */
5755
+ function probeTypeOf(type) {
5756
+ if (String(type || "").indexOf("github") === 0) return "github";
5757
+ return type === "slack" ? "slack" : "";
5758
+ }
5759
+
5760
+ /**
5761
+ * サーバが返した 「needed」(次の一手。URL を含むことがある)を、リンク付きの1段落にする。
5762
+ *
5763
+ * 🔴 **innerHTML を使わない。** 「needed」 はサーバの文だが、そこには**組織名や
5764
+ * リポジトリ名(人が登録した値)が埋め込まれている**。HTML として流し込む経路を
5765
+ * 1本でも作ると、登録した文字列が画面のマークアップになる。テキストノードと
5766
+ * 「<a>」 の組み立てだけで作る(この画面の他の表示と同じ流儀)。
5767
+ *
5768
+ * **URL 以外は素のまま出す。** 言い換えると検索できないので、サーバの文言を削らない。
5769
+ */
5770
+ function neededNode(text) {
5771
+ var p = el("p", { class: "ds-probe-help" });
5772
+ splitNeededParts(text).forEach(function (part) {
5773
+ if (part.url) p.appendChild(el("a", { href: part.text, target: "_blank", rel: "noopener", text: part.text }));
5774
+ else p.appendChild(document.createTextNode(part.text));
5775
+ });
5776
+ return p;
5777
+ }
5778
+
5779
+ /**
5780
+ * 開発(GitHub)の確認結果の本文(#233 / #234)。
5781
+ *
5782
+ * **理由と対処はサーバが1文で持っている**(「shared/github-gate.mjs」 の 「needed」)。
5783
+ * 「App が入っていない」と「App は入っているがこのリポジトリが選ばれていない」の
5784
+ * 言い分けも、設定ページの URL もあちらが決める——**画面で分岐を書き足さない**
5785
+ * (書いた瞬間に判定が2箇所になり、夜間の実測とボタンでずれる)。
5786
+ */
5787
+ function githubProbeHelpNodes(r) {
5788
+ if (r.gate === "ok") return [];
5789
+ if (r.needed) return [neededNode(r.needed)];
5790
+ // needed が無いのは gate に落とし込めなかったとき。**生のエラーは隠さない**
5791
+ return [el("p", { class: "ds-probe-help", text: "GitHub の応答:" + (r.error || "不明") + "。時間をおいて再度お試しください。" })];
5792
+ }
5793
+
5375
5794
  /**
5376
5795
  * 判定ごとの表示。**成功は1行で終わらせ、失敗は「理由+次の一手」だけを残す。**
5377
5796
  *
@@ -5394,6 +5813,7 @@ ${nextStateDelay.toString()}
5394
5813
  */
5395
5814
  function probeHelpNodes(s, r) {
5396
5815
  var p = function (t) { return el("p", { class: "ds-probe-help", text: t }); };
5816
+ if (probeTypeOf(s.type) === "github") return githubProbeHelpNodes(r);
5397
5817
  // 成功。**1行で終わり。** 状態の語(「接続OK」)が既に出ているので本文は空
5398
5818
  if (r.gate === "ok") return [];
5399
5819
  if (r.gate === "not_in_channel") {
@@ -5515,7 +5935,9 @@ ${nextStateDelay.toString()}
5515
5935
  msg.textContent = "確認中…";
5516
5936
  at.textContent = "";
5517
5937
  body.textContent = "";
5518
- var payload = { type: "slack", ref: s.ref };
5938
+ // **種別はサーバの語彙に畳む**(github-issues / github-discussions github)。
5939
+ // ワークスペースは Slack にしか無い概念なので、他アダプタでは載せない
5940
+ var payload = { type: probeTypeOf(s.type), ref: s.ref };
5519
5941
  if (s.workspace) payload.workspace = s.workspace;
5520
5942
  var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
5521
5943
  try {
@@ -5547,8 +5969,10 @@ ${nextStateDelay.toString()}
5547
5969
  var oldScopes = card.querySelectorAll(".ds-probe-scope");
5548
5970
  for (var oi = 0; oi < oldScopes.length; oi++) oldScopes[oi].remove();
5549
5971
  // 不足スコープは**状態の語のすぐ横**に出す(「権限が足りません:groups:read」)。
5550
- // 本文へ落とすと、いちばん要る情報が説明文に埋もれる
5551
- if (j.gate === "missing_scope") {
5972
+ // 本文へ落とすと、いちばん要る情報が説明文に埋もれる。
5973
+ // **札にするのは Slack のスコープ名だけ。** GitHub の needed は URL 込みの
5974
+ // 一文なので、札に押し込むとバッジの横で折り返して読めない(本文へ出す)
5975
+ if (j.gate === "missing_scope" && probeTypeOf(s.type) === "slack") {
5552
5976
  head.appendChild(el("code", { class: "ds-probe-scope", text: j.needed || "不明" }));
5553
5977
  }
5554
5978
  at.textContent = "いま測った結果(" + (dsFmtTime(j.checkedAt) || "たった今") + ")";
@@ -5557,8 +5981,10 @@ ${nextStateDelay.toString()}
5557
5981
  // 下に残ると、同じ行に2つのことが書いてある状態になる
5558
5982
  var stale = card.querySelector(".ds-gate-help");
5559
5983
  if (stale) stale.hidden = true;
5560
- // 表示名を Slack の実名へ自動で揃える(差分表示も更新ボタンも出さない)
5561
- autoAlignName(s, j, card);
5984
+ // 表示名を Slack の実名へ自動で揃える(差分表示も更新ボタンも出さない)。
5985
+ // **GitHub は揃えない。** 行の名前は owner/repo そのもので、書き換える口
5986
+ //(op=config の refresh-channel-name)は channels.json 専用
5987
+ if (s.type === "slack") autoAlignName(s, j, card);
5562
5988
  } catch (e) {
5563
5989
  msg.className = "ds-toggle-msg is-error";
5564
5990
  msg.textContent = "✗ 通信に失敗しました。";
@@ -7153,7 +7579,9 @@ ${nextStateDelay.toString()}
7153
7579
  // 内部事情を全行に並べることになる——押す必要が無いものを説明する側の混雑。
7154
7580
  // 一方 App の札は「この組織はもう繋いである」という**運用上の意味**を持つので残す。
7155
7581
  var ghMeta = githubOwnerMeta(ghOwner);
7156
- var via = ghMeta && ghMeta.source === "app" ? "(GitHub App)" : "";
7582
+ // **正本(registeredNow.githubOwners)は source を持たない**(工程1.5)。
7583
+ // App が入っていると実測できたときが、まとめ表の source === "app" に当たる
7584
+ var via = ghMeta && (ghMeta.source === "app" || ghMeta.appInstalled === true) ? "(GitHub App)" : "";
7157
7585
  badges.appendChild(el("span", {
7158
7586
  class: "badge ds-workspace" + (ghMeta && ghMeta.source === "unknown" ? " ds-workspace-unknown" : ""),
7159
7587
  text: ghOwner + via,
@@ -7260,13 +7688,15 @@ ${nextStateDelay.toString()}
7260
7688
  // 気づいた人がその場で確かめられるほうがよい(オーナー判断)。書き込みになる
7261
7689
  // 「表示名を更新する」は、押した結果の中から別のボタンとして出る(probeSection)。
7262
7690
  //
7263
- // **いまは Slack だけ。** GitHub は夜間の実測(probeGithubGate)が既にあるので繋ぐだけ、
7264
- // Backlog / Figma / Drive は実測そのものが新規になる。同じ型で足せるよう、
7265
- // 判定は infra/lambda/shared/slack-gate.mjs 側に置いてある(末尾に拡張点の説明がある)。
7691
+ // **いまは Slack 開発(GitHub)。** GitHub は #234 で足した——「アクセス不可 →
7692
+ // App Configure でリポジトリを追加 直ったか確かめる」が、それまで**翌朝まで
7693
+ // 待つしかなかった**(受け入れテストでは fleet-status.yml を手で起動して確かめた)。
7694
+ // 判定と文面は infra/lambda/shared/github-gate.mjs 側(画面に分岐を持たせない)。
7695
+ // Backlog / Figma / Drive は実測そのものが新規なので、まだ出さない。
7266
7696
  //
7267
7697
  // **origin === "explicit" でも出す。** 確認は読むだけで、どの登録ファイル由来かに
7268
7698
  // 関係なく効く(書き込みになる「表示名を更新する」だけが由来で出し分かれる)
7269
- if (SITE.intake && s.type === "slack" && s.ref) {
7699
+ if (SITE.intake && probeTypeOf(s.type) && s.ref) {
7270
7700
  var probe = probeSection(s, card, gateEl);
7271
7701
  // **ボタンは行の中(「登録を解除」の隣)。** 独立した段を作ると1行が2段になる
7272
7702
  card.appendChild(probe.btn);
@@ -7442,6 +7872,20 @@ ${nextStateDelay.toString()}
7442
7872
  * その能力の行が応答から消え、**タイルの件数だけが行ったり来たりする**。
7443
7873
  */
7444
7874
  var OPEN_CAPS = [];
7875
+ /**
7876
+ * 画面の能力名 → 接続状態ストアの能力名(対応表の実体は STATE_CAP_NAMES)。
7877
+ *
7878
+ * **「stateCapIdOf」(サーバへ「開いた」と伝える側)とは分ける。** あちらは
7879
+ * **登録ファイルを読ませる**要求なので、増やすと GitHub の枠を削る。こちらは
7880
+ * 届いた 「stateNow」 の行を引くだけで、**要求とは無関係に会議の行も届く**
7881
+ * (ストアの問い合わせは能力で絞られていない)
7882
+ */
7883
+ function capIdOf(cap) {
7884
+ for (var sid in STATE_CAP_NAMES) {
7885
+ if (Object.prototype.hasOwnProperty.call(STATE_CAP_NAMES, sid) && STATE_CAP_NAMES[sid] === cap) return sid;
7886
+ }
7887
+ return "";
7888
+ }
7445
7889
  /** 画面の能力名 → op=state の識別子(対応表の実体は REGISTERED_CAP_NAMES) */
7446
7890
  function stateCapIdOf(cap) {
7447
7891
  for (var id in REGISTERED_CAP_NAMES) {
@@ -7514,6 +7958,12 @@ ${nextStateDelay.toString()}
7514
7958
  // ここより後に置くと**その案件だけ素通り**する
7515
7959
  LIVE_REGISTERED = sanitizeRegisteredNow(j && j.registeredNow);
7516
7960
  LIVE_STATE = sanitizeStateNow(j && j.stateNow);
7961
+ // 🔴 **鍵・能力・組織も fleet より先に読む(工程1.5)。** まとめ表を
7962
+ // まだ持たない案件は下で throw するので、ここより後に置くと
7963
+ // **立ち上げ直後の案件=この修正が一番効く案件**だけ素通りする
7964
+ LIVE_SECRETS = sanitizeSecretsNow(j && j.secretsNow);
7965
+ LIVE_CAPS = sanitizeCapabilitiesNow(j && j.capabilitiesNow);
7966
+ LIVE_GH_OWNERS = sanitizeGithubOwnersNow(j && j.registeredNow && j.registeredNow.githubOwners);
7517
7967
  MERGED = null;
7518
7968
  if (!j || !j.fleet) throw new Error("no fleet");
7519
7969
  LIVE_FLEET = normalizeFleet(j.fleet);
@@ -7826,6 +8276,13 @@ ${nextStateDelay.toString()}
7826
8276
  * @returns { known, src } src は取得元の文字列("yes" のときだけ意味を持つ)
7827
8277
  */
7828
8278
  function secretStateOf(kind, stateSpace, spaceCount) {
8279
+ // 🔴 **正本(secretsNow)が答えられるならそれ(工程1.5・#227)。** まとめ表は夜間の
8280
+ // スナップショットなので、**鍵を保存した直後は前日の「未登録」を出し続ける**
8281
+ // ——入れた人は「保存できていない」と読んで貼り直す。正本は Secrets Manager を
8282
+ // 都度見た答えなので、次のポーリングで「登録済み」になる。
8283
+ // 答えられないとき(行が来ていない・unknown)だけ、下のまとめ表の判定へ落ちる
8284
+ var now = secretStateFromNow(LIVE_SECRETS, kind, kind === "backlog" ? stateSpace : "");
8285
+ if (now) return now;
7829
8286
  var perKind = (fleetVal("secretSources") || {})[kind] || "";
7830
8287
  var spaces = fleetVal("secretSpaces");
7831
8288
  if (kind === "backlog" && stateSpace && spaces && typeof spaces === "object") {
@@ -7842,6 +8299,22 @@ ${nextStateDelay.toString()}
7842
8299
  return { known: "unknown", src: "" };
7843
8300
  }
7844
8301
 
8302
+ /**
8303
+ * タイルの「⚠ トークンが未設定です」を出さないでよいか(工程1.5・#227)。
8304
+ *
8305
+ * **タイルは能力に1枚しかない**ので、スペースごとの答えは持てない。まとめ表の
8306
+ * 「secretSources[kind]」(種別ごとの1つの値)と**同じ粒度**に畳んで、1本でも
8307
+ * 入っていれば注記を出さない——スペースごとの充足は行の中の鍵カードが答える。
8308
+ *
8309
+ * 正本が「確かめられなかった」(unknown)と答えたときだけ、まとめ表の値に落ちる。
8310
+ */
8311
+ function secretPresentForTile(kind) {
8312
+ var now = secretRegisteredAnywhere(LIVE_SECRETS, kind);
8313
+ if (now === "yes") return true;
8314
+ if (now === "no") return false;
8315
+ return !!((fleetVal("secretSources") || {})[kind]);
8316
+ }
8317
+
7845
8318
  /**
7846
8319
  * @param opts { space, stateSpace, spaceCount, inRow }
7847
8320
  * space … 保存時に名乗る Backlog スペース(名乗らないときは空)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.10.7",
3
+ "version": "0.11.0",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,