@takagaki/cortex-decisions-viewer 0.4.69 → 0.4.71

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
@@ -260,11 +260,42 @@ async function readFleetSources(repoRoot) {
260
260
  if (!Object.keys(tools).length)
261
261
  tools = undefined;
262
262
  }
263
+ // 能力ごとの導出済みの状態(#125①が載せ、#125②で画面の正本になった)。
264
+ //
265
+ // **列挙し忘れは静かに消える。** このマッピングは素通しではなく明示の許可リストで、
266
+ // 落ちても例外も型エラーも出ない——画面は「capabilities が無い古いデータ」と解釈して
267
+ // 旧表示へ劣化するので、**壊れているようには見えない**。origin / figmaFiles /
268
+ // driveFolderIds で3回踏んでいる罠なので、実 build を通した配線テストで見張っている。
269
+ //
270
+ // **知らないフィールドは黙って捨てる。** engine が adapters[] / byTargets を足す余地が
271
+ // ある(#107 / #127)が、読み方を「知っているキーだけ」に留めておけば、増えた回に
272
+ // 画面が壊れることはない。使うようになった時点でここへ足す。
273
+ let capabilities;
274
+ if (obj.capabilities != null && typeof obj.capabilities === "object" && !Array.isArray(obj.capabilities)) {
275
+ const out = {};
276
+ for (const [cap, v] of Object.entries(obj.capabilities)) {
277
+ if (v == null || typeof v !== "object" || Array.isArray(v))
278
+ continue;
279
+ const c = v;
280
+ // **state が無い要素は通さない。** 状態を持たない能力を渡すと、画面が
281
+ // 「導出はあるが答えていない」という、どちらでもない状態を描くことになる
282
+ const state = c.state != null ? String(c.state) : "";
283
+ if (!state)
284
+ continue;
285
+ out[cap] = {
286
+ adapter: c.adapter != null ? String(c.adapter) : "",
287
+ state,
288
+ detail: c.detail != null ? String(c.detail) : undefined,
289
+ };
290
+ }
291
+ if (Object.keys(out).length)
292
+ capabilities = out;
293
+ }
263
294
  const fleetGeneratedAt = typeof obj.generatedAt === "string" ? obj.generatedAt : undefined;
264
295
  const secretSources = obj.secretSources && typeof obj.secretSources === "object"
265
296
  ? obj.secretSources
266
297
  : undefined;
267
- return { externalSources, internalSources, pipelines, tools, fleetGeneratedAt, secretSources, checks, githubOwners };
298
+ return { externalSources, internalSources, pipelines, tools, capabilities, fleetGeneratedAt, secretSources, checks, githubOwners };
268
299
  }
269
300
  /** パスがディレクトリとして存在するか */
270
301
  async function isDirectory(dirAbs) {
@@ -1145,6 +1176,7 @@ async function build(opts) {
1145
1176
  internalSources: fleet.internalSources,
1146
1177
  pipelines: fleet.pipelines,
1147
1178
  tools: fleet.tools,
1179
+ capabilities: fleet.capabilities,
1148
1180
  checks: fleet.checks,
1149
1181
  githubOwners: fleet.githubOwners,
1150
1182
  };
package/dist/render.js CHANGED
@@ -82,6 +82,48 @@ const ADAPTER_ICONS = {
82
82
  backlog: `<svg width="20" height="20" aria-hidden="true" viewBox="0 0 142 142" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="Nulab-logos-" transform="translate(-1348.000000, -2619.000000)"> <g id="Group-7" transform="translate(1348.000000, 2619.000000)"> <path d="M112.518975,141.27641 L29.5477542,141.27641 C13.8226507,141.27641 0.955380933,128.389559 0.955380933,112.638806 L0.955380933,29.5308457 C0.955380933,13.7800933 13.8226507,0.893241768 29.5477542,0.893241768 L112.518975,0.893241768 C128.244079,0.893241768 141.109945,13.7800933 141.109945,29.5308457 L141.109945,112.638806 C141.109945,128.389559 128.244079,141.27641 112.518975,141.27641" id="Fill-16" fill="#42CE9F"></path> <path d="M60.1988625,61.3236616 C60.1441201,72.9746913 60.1146434,84.3121962 60.1441201,91.0958596 C70.2756738,90.5461368 75.2937266,88.482216 79.2632519,83.7132655 C82.0775724,80.3319784 82.686757,76.4586121 80.9799172,72.8073843 C78.7467083,68.0328101 72.1804284,62.9897012 60.1988625,61.3236616 M53.5357306,104.795345 L53.5357306,104.795345 C51.7348462,104.795345 50.0069517,104.075503 48.7380511,102.796097 L48.7352438,102.793285 C46.7434629,100.784196 46.7153899,98.8622754 46.6648584,95.3741368 C46.6395927,93.6504536 46.6241525,91.2083348 46.6171343,88.2896022 C46.6059051,83.1719784 46.6185379,76.0705725 46.6536292,67.1808101 C46.7153899,51.8855824 46.8262783,36.6381566 46.8262783,36.6381566 L60.3448422,36.7365725 C60.3195765,40.1150477 60.2957144,43.8295428 60.2718523,47.705721 C76.2678604,49.5531269 88.283114,56.4998794 93.2225623,67.065523 C97.1640146,75.4941368 95.8263351,84.9589289 89.6474629,92.3837012 C80.1166718,103.836493 66.5812641,104.795345 53.5357306,104.795345" id="Fill-18" fill="#FFFFFF"></path> </g> </g> </g> </svg>`,
83
83
  github: GITHUB_ICON,
84
84
  };
85
+ /**
86
+ * **アダプタのカタログ(能力→この製品に存在するアダプタ)。#125②**
87
+ *
88
+ * これは「この案件が何を使っているか」ではなく「製品として何があるか」の定義。
89
+ * 案件ごとの状態は engine が設定ファイルから導出して `capabilities` に載せる
90
+ * (宣言 `tools` は廃止した。宣言と実態は必ずずれる——配線ゼロの Teams が
91
+ * 「接続OK」と画面に出た #124 がその帰結)。
92
+ *
93
+ * **カタログに無いツール名は画面に一文字も出さない。** teams / jira / box / local は
94
+ * アダプタが存在しないので、名前を出した瞬間「対応している」と読まれる。
95
+ * 状態を出す場所すら無いのに名前だけ出るのが #124 の症状そのもの。
96
+ *
97
+ * **値は配列。** 今日はどの能力も1個だが、「能力=1アダプタ」を焼き込むと
98
+ * #107 / #127(複数アダプタ・複数 Backlog)で読み手の書き換えが要る。
99
+ * ロゴは上の ADAPTER_ICONS を、表示名は CM_TOOL_LABEL を引く(キーはここの値)。
100
+ */
101
+ const ADAPTER_CATALOG = {
102
+ "課題管理": ["backlog"],
103
+ "会議": ["google-meet"],
104
+ "共有資料": ["google-drive"],
105
+ "デザイン": ["figma"],
106
+ "チャット": ["slack"],
107
+ "開発": ["github"],
108
+ };
109
+ /**
110
+ * 導出された状態 → 画面に出す文言。**マップとタイルで同じ語を使う。**
111
+ *
112
+ * - `off` は「OFFに設定されています」。**接続へ誘わない**——意図的に止めたものに
113
+ * 「接続しましょう」と出すのは、設定する必要が無いものを押させるのと同じ誤り。
114
+ * - `unset` は「未接続」。こちらは入口を出してよい。
115
+ *
116
+ * **`empty` の行は無い。** 表示に入る前に `capabilityDisplayState` が unset へ畳むので、
117
+ * ここへ来ることはない(オーナー決定・2026-08-12。理由はその関数の項に書いてある)。
118
+ */
119
+ const CAP_STATE_TEXT = {
120
+ unset: "未接続",
121
+ off: "OFFに設定されています",
122
+ broken: "設定に異常があります",
123
+ on: "接続OK",
124
+ };
125
+ /** 状態の印。CAP_STATE_TEXT の前に付ける(マップは付けない——⚠️バッジと二重になる) */
126
+ const CAP_STATE_MARK = { ok: "● ", idle: "⦿ ", warn: "⚠️ ", off: "" };
85
127
  const CM_CAPS = [
86
128
  { cap: "課題管理", icon: "📋", pipe: /sync-backlog/ },
87
129
  { cap: "会議", icon: "🎙", pipe: /ingest-minutes/ },
@@ -169,6 +211,60 @@ function externalCapability(type) {
169
211
  return "開発";
170
212
  return CAP_OTHER;
171
213
  }
214
+ /**
215
+ * その能力をいま担っているアダプタ(#125②)。
216
+ *
217
+ * **カタログにある値しか返さない。** `capabilities[cap].adapter` を素通しすると、
218
+ * engine が古い宣言を引きずった回や、将来 teams のような未対応値が入った回に、
219
+ * 対応していないツール名が顧客の画面へ出る(#124)。カタログ外なら既定
220
+ * (そのカタログの先頭)へ倒す——「名前は出ないが状態は出る」ほうが、
221
+ * 「名前だけ出て何も繋がっていない」より読み手を誤らせない。
222
+ *
223
+ * **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す)。
224
+ */
225
+ function capabilityAdapter(cap, caps) {
226
+ var list = ADAPTER_CATALOG[cap] || [];
227
+ var c = caps ? caps[cap] : null;
228
+ var a = c && c.adapter ? String(c.adapter) : "";
229
+ if (a && list.indexOf(a) >= 0)
230
+ return a;
231
+ return list.length ? list[0] : "";
232
+ }
233
+ /**
234
+ * engine が導出した5値の状態。**capabilities を持たない古いデータでは null。**
235
+ *
236
+ * null は「まだ判定材料が無い」であって「未接続」ではない。呼び出し側は null のときだけ
237
+ * 旧来(宣言 tools + 行の有無)の判定へ落ちる——**判定の分岐はここ1箇所**で、
238
+ * 状態の導出を viewer 側にもう一組作らない(導出の正本は engine の1本)。
239
+ */
240
+ function capabilityRawState(cap, caps) {
241
+ var c = caps ? caps[cap] : null;
242
+ if (!c)
243
+ return null;
244
+ var s = c.state == null ? "" : String(c.state);
245
+ return s ? s : null;
246
+ }
247
+ /**
248
+ * **画面が使う状態。`empty` を `unset` に畳む。**(オーナー決定・2026-08-12)
249
+ *
250
+ * scaffold が設定ファイルを必ず作るので、「設定ファイルはある」は利用者にとって無情報。
251
+ * ダミーキーのまま置かれている=繋がっていない、が実態で、`unset` と `empty` の違いは
252
+ * **実装の都合**でしかない。利用者にとっての答えはどちらも「未接続」。
253
+ * 実機の受け入れテストで「設定はありますが、対象がありません」が何を指すのか伝わらず、
254
+ * 次の一手(=接続する)に繋がらなかった。
255
+ *
256
+ * **engine の状態機械は変えない。** `empty` は今後も導出されてくる(プレースホルダを
257
+ * 実キーと数えない判定は engine の責務で、そこを潰すと無駄な同期が走る #125①)。
258
+ * 畳むのは表示だけ——**この1関数だけ**が写像を持つので、「やっぱり区別したい」に
259
+ * なったらここの1行を消せば5値の表示へ戻る。
260
+ *
261
+ * 畳んだ先が unset なので、文言・接続の入口の案内・マップの線の有無・グレーの見た目は
262
+ * すべて unset と同一になる。`off` / `broken` の区別は従来どおり残す。
263
+ */
264
+ function capabilityDisplayState(cap, caps) {
265
+ var s = capabilityRawState(cap, caps);
266
+ return s === "empty" ? "unset" : s;
267
+ }
172
268
  /**
173
269
  * 貼られたURLから Backlog の接続先を読む。
174
270
  *
@@ -218,6 +314,39 @@ function capabilityState(cap, ctx) {
218
314
  const toolVal = ctx.tools[cap];
219
315
  const internal = ctx.internals.filter(function (s) { return s.kind === cap; })[0];
220
316
  const pipeRe = PIPE[cap];
317
+ // ---- #125②: 導出(capabilities)があるならそれが状態の正本 ----
318
+ //
319
+ // **ここが唯一の分岐点。** 導出の有無で描き分けるのはこの1箇所に閉じ、
320
+ // 呼び出し側(マップ・タイル・鍵カード)は4値の答えだけを受け取る。
321
+ // 導出を viewer 側にもう一組作らない(設定ファイルの再パース禁止)。
322
+ //
323
+ // **「問題あり(warn)」は別軸なので導出に混ぜない。** 設定は済んでいる(on)のに
324
+ // 読めていない・同期が失敗している、は設定ファイルからは分からない。
325
+ // broken(設定そのものが壊れている)だけは導出が答えを持っている。
326
+ const derived = capabilityRawState(cap, ctx.capabilities);
327
+ if (derived !== null) {
328
+ if (derived === "off")
329
+ return "off";
330
+ if (derived === "broken")
331
+ return "warn";
332
+ if (derived === "on") {
333
+ return (isLive
334
+ ? liveExts.some(function (s) { return sourceNeedsAttention(s); })
335
+ : ctx.pipes.some(function (p) {
336
+ return pipeRe != null && pipeRe.test(p.id) && p.applicable !== false && p.lastConclusion === "failure";
337
+ }))
338
+ ? "warn"
339
+ : "ok";
340
+ }
341
+ // unset / empty / 未知の値。**未知を ok にも off にも寄せない**——どちらも嘘になる。
342
+ // 「まだ繋がっていない」側に倒すのが安全(繋がっていないのに線を引かない)。
343
+ //
344
+ // ここは**生の状態**(capabilityRawState)で見ている。empty と unset は元々どちらも
345
+ // idle に落ちるので、表示側の写像(capabilityDisplayState が empty を unset に畳む)と
346
+ // 答えが割れることはない。**この2つを分けるなら、写像とここの両方を直すこと。**
347
+ return "idle";
348
+ }
349
+ // ---- ここから下は capabilities を持たない古い fleet-status.json のための劣化表示 ----
221
350
  // 使用中か: tools宣言がnone以外 / internalSourceが存在(enabled:false除く) / ライブ参照の実体がある
222
351
  const used = (toolVal != null && toolVal !== "none") ||
223
352
  (internal != null && internal.enabled !== false) ||
@@ -271,9 +400,19 @@ function renderConnectionMap(site) {
271
400
  const externals = site.externalSources ?? [];
272
401
  const pipes = site.pipelines ?? [];
273
402
  const tools = site.tools ?? {};
274
- if (!internals.length && !externals.length && !pipes.length && !Object.keys(tools).length) {
403
+ const caps = site.capabilities ?? {};
404
+ // **fleet のデータが1つでも取れているか、だけで判定する(#125②)。**
405
+ // 素の Decisions ビュー(fleet-status.json が無い構成)ではマップを出さない現行の意図は
406
+ // そのまま。`tools` を条件から外さないのは、capabilities を持たない古いスナップショットで
407
+ // マップが丸ごと消えるのを防ぐため(移行中は両方が現物として存在する)。
408
+ if (!internals.length &&
409
+ !externals.length &&
410
+ !pipes.length &&
411
+ !Object.keys(tools).length &&
412
+ !Object.keys(caps).length) {
275
413
  return null;
276
414
  }
415
+ const hasCaps = Object.keys(caps).length > 0;
277
416
  const slackExts = externals.filter((s) => s.type === "slack");
278
417
  const ghExts = externals.filter((s) => s.type.startsWith("github"));
279
418
  const nodes = [];
@@ -288,26 +427,32 @@ function renderConnectionMap(site) {
288
427
  const liveExts = c.cap === "チャット" ? slackExts : c.cap === "開発" ? ghExts : [];
289
428
  // **判定は capabilityState に一本化している。** 画面側(クライアントJS)も同じ関数を
290
429
  // 呼ぶので、ここで写経すると「同じ画面でマップが⚠️・直下が『接続OK』」が復活する。
291
- const st = capabilityState(c.cap, { tools, internals, externals, pipes });
430
+ const st = capabilityState(c.cap, { tools, capabilities: caps, internals, externals, pipes });
292
431
  const off = st === "off";
293
432
  const idle = st === "idle";
294
433
  const warn = st === "warn";
295
434
  const state = "cm-" + st;
435
+ const derived = capabilityDisplayState(c.cap, caps);
296
436
  // **ラベルは能力名(会議・課題管理…)。ツール名は下に小さく置く。**
297
437
  // ツールは「その能力をいま何で実現しているか」という差し替え可能なアダプタで、
298
438
  // 主語ではない。ツール名を主にすると、Teams のように複数の能力に使われるものが
299
439
  // 同じ名前で並び(会議とチャットに Teams が2つ)、何が繋がっているのか読めなくなる。
300
440
  // "none"(未使用の宣言)はツール名ではないので出さない。そのまま表示すると
301
441
  // 「ツール: none」という読めない行になる(実際にそう出ていた)
302
- const rawTool = toolVal && toolVal !== "none"
303
- ? toolVal
304
- : internal && internal.tool && internal.tool !== "none"
305
- ? internal.tool
306
- : isLive && liveExts.length
307
- ? c.cap === "チャット"
308
- ? "slack"
309
- : "github"
310
- : "";
442
+ //
443
+ // **導出があるならカタログのアダプタを常に出す(#125②)。** 未接続でも「何で繋ぐのか」が
444
+ // 分からないと入口にならない。カタログ外の値(teams 等)は capabilityAdapter が弾く。
445
+ const rawTool = hasCaps
446
+ ? capabilityAdapter(c.cap, caps)
447
+ : toolVal && toolVal !== "none"
448
+ ? toolVal
449
+ : internal && internal.tool && internal.tool !== "none"
450
+ ? internal.tool
451
+ : isLive && liveExts.length
452
+ ? c.cap === "チャット"
453
+ ? "slack"
454
+ : "github"
455
+ : "";
311
456
  const toolName = rawTool ? CM_TOOL_LABEL[rawTool] ?? rawTool : "";
312
457
  const label = `${c.icon} ${c.cap}`;
313
458
  // 2行目=ツール名(小さく)/3行目=状態(未使用・自動連携なし・ライブ参照・最終同期)
@@ -316,7 +461,13 @@ function renderConnectionMap(site) {
316
461
  : "";
317
462
  let statusText = "";
318
463
  let statusTs = "";
319
- if (off) {
464
+ if (derived !== null && derived !== "on") {
465
+ // **導出の5値をそのままの語で出す(#125②)。** 「未使用」に丸めると、意図的なOFFと
466
+ // 未設置と壊れているのが同じ1語になる——直せる人が「何を直すか」を決められない。
467
+ // タイル側と同じ CAP_STATE_TEXT を引くので、マップと直下の一覧で語が割れない。
468
+ statusText = CAP_STATE_TEXT[derived] || "状態が判定できません";
469
+ }
470
+ else if (off) {
320
471
  statusText = "未使用";
321
472
  }
322
473
  else if (idle) {
@@ -404,8 +555,19 @@ function buildTutorialData(site, codeUrl, repoSlug) {
404
555
  ];
405
556
  // ここから先は「書き込み・外部連携」の体験。案件の構成に無い能力のチャレンジは出さない(表示条件つき)
406
557
  const tools = site.tools ?? {};
558
+ const caps = site.capabilities ?? {};
407
559
  const internals = site.internalSources ?? [];
408
560
  const externals = site.externalSources ?? [];
561
+ /**
562
+ * その能力が配線済みか。**導出(capabilities)があるならそれだけで決める(#125②)。**
563
+ * 宣言でチャレンジを出していた頃は、Backlog を宣言しているだけで鍵も入っていない案件に
564
+ * 「記票してみよう」が出ていた——押すと必ず失敗する体験を最初の5分に置くことになる。
565
+ * 導出が無い古いデータでは null を返し、呼び出し側が従来の判定へ落ちる。
566
+ */
567
+ const capOn = (cap) => {
568
+ const s = capabilityRawState(cap, caps);
569
+ return s === null ? null : s === "on";
570
+ };
409
571
  // メンバーの自分のレコードを直す体験(Members セクションにレコードがある案件のみ出す)
410
572
  if ((site.sections ?? []).some((s) => (s.name === "Members" || s.name === "メンバー") && s.records.length)) {
411
573
  specs.push({
@@ -415,8 +577,9 @@ function buildTutorialData(site, codeUrl, repoSlug) {
415
577
  });
416
578
  }
417
579
  // Backlogに記票する体験(課題管理にBacklogを使う案件のみ出す)
418
- const usesBacklog = tools["課題管理"] === "backlog" ||
419
- internals.some((s) => s.kind === "課題管理" && s.tool === "backlog" && s.enabled !== false);
580
+ const usesBacklog = capOn("課題管理") ??
581
+ (tools["課題管理"] === "backlog" ||
582
+ internals.some((s) => s.kind === "課題管理" && s.tool === "backlog" && s.enabled !== false));
420
583
  if (usesBacklog) {
421
584
  specs.push({
422
585
  title: "Backlogに記票してみよう",
@@ -425,8 +588,9 @@ function buildTutorialData(site, codeUrl, repoSlug) {
425
588
  });
426
589
  }
427
590
  // 画面インベントリを読む体験(デザインを同期している案件のみ出す)
428
- const usesDesign = (tools["デザイン"] != null && tools["デザイン"] !== "none") ||
429
- internals.some((s) => s.kind === "デザイン" && s.enabled !== false);
591
+ const usesDesign = capOn("デザイン") ??
592
+ ((tools["デザイン"] != null && tools["デザイン"] !== "none") ||
593
+ internals.some((s) => s.kind === "デザイン" && s.enabled !== false));
430
594
  if (usesDesign) {
431
595
  specs.push({
432
596
  title: "デザインを読んでみよう",
@@ -435,7 +599,7 @@ function buildTutorialData(site, codeUrl, repoSlug) {
435
599
  });
436
600
  }
437
601
  // 完了をSlackに知らせる体験(チャットにSlackを宣言している案件のみ出す。最終チャレンジ)
438
- const usesSlack = tools["チャット"] === "slack" || externals.some((s) => s.type === "slack");
602
+ const usesSlack = capOn("チャット") ?? (tools["チャット"] === "slack" || externals.some((s) => s.type === "slack"));
439
603
  if (usesSlack) {
440
604
  specs.push({
441
605
  title: "Slackに完了を知らせよう",
@@ -1019,8 +1183,24 @@ a.ds-name:hover { text-decoration: underline; }
1019
1183
  .ds-check-detail { font-size: 12px; color: var(--muted); }
1020
1184
  .ds-tile-desc { font-size: 12.5px; color: var(--muted); }
1021
1185
  .ds-tile-sub { font-size: 12.5px; margin-top: 2px; }
1022
- .ds-tile-chev { color: var(--muted); }
1186
+ .ds-tile-chev { color: var(--muted); flex: 0 0 auto; }
1023
1187
  .ds-tile-note { font-size: 12.5px; color: #a15c00; margin-top: 2px; }
1188
+ /* ---- 可変長の文字列は必ず折り返す(0.4.71) ----
1189
+ 実機の受け入れテストで、状態の文言がタイル幅を超えて溢れた(スシローのデザインタイル)。
1190
+ ここに出るのは engine の detail・チェックの直し方・チャンネル名・ファイルパスなど
1191
+ **長さが読めない**文字列で、増えるたびに個別に手当てしていたら必ず取りこぼす。
1192
+ **省略(ellipsis)ではなく折り返しで受ける**——何が足りないのかを伝える文なので、
1193
+ 途中で切ると画面の目的そのものが消える(名前だけの .ds-name は従来どおり省略のまま)。
1194
+ white-space: normal … button の中に置く行があるので UA 既定に委ねない
1195
+ overflow-wrap: anywhere … 空白の無い連なり(課題管理/issues-config.json・ID・URL)を切る
1196
+ min-width: 0 … flex の子が min-content より縮めるようにする(無いと折り返しても溢れる) */
1197
+ .ds-tile-name, .ds-tile-desc, .ds-tile-sub, .ds-tile-note,
1198
+ .ds-check-label, .ds-check-action, .ds-check-detail,
1199
+ .ds-pipe-label, .ds-pipe-todo, .ds-owner-state,
1200
+ .ds-empty, .sec-how, .modal-desc, .ds-toggle-msg,
1201
+ .ds-drive-note, .ds-inactive-note, .ds-legacy-chip, .ds-keys-code {
1202
+ min-width: 0; white-space: normal; overflow-wrap: anywhere;
1203
+ }
1024
1204
  .badge.ds-undeclared { background: #fff4e5; color: #9a6700; }
1025
1205
  .ds-toggle { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 6px; }
1026
1206
  .ds-toggle-btn { font-size: 12px; padding: 3px 10px; border-radius: 999px; border: 1px solid var(--border); background: #fff; color: var(--text); cursor: pointer; }
@@ -1046,6 +1226,9 @@ a.ds-name:hover { text-decoration: underline; }
1046
1226
  .ds-gate-ok { color: #1f7a45; }
1047
1227
  .ds-gate-warn { color: #b45309; }
1048
1228
  .ds-gate-muted { color: #6b7280; }
1229
+ /* 意図的にOFFにした能力(#125②)。**グレーにして誘わない**——未接続(入口を出してよい)
1230
+ とは別物で、同じ色にすると「押して繋ぎましょう」と読まれる */
1231
+ .ds-gate-off { color: #9ca3af; }
1049
1232
  .ds-pipes { display: flex; flex-direction: column; gap: 8px; }
1050
1233
  /* 組織ごとの接続状況。トークンは組織ごとにしか効かないので、ここが判断の単位になる */
1051
1234
  .ds-owners { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
@@ -1219,7 +1402,13 @@ const CLIENT_JS = `
1219
1402
  // **この2つはクロージャを持たない純関数に保つこと。** 外の変数を掴むと、
1220
1403
  // 直列化した先(ここ)で undefined を参照して壊れる。
1221
1404
  var CAP_OTHER = ${JSON.stringify(CAP_OTHER)};
1405
+ var ADAPTER_CATALOG = ${JSON.stringify(ADAPTER_CATALOG)};
1406
+ var CAP_STATE_TEXT = ${JSON.stringify(CAP_STATE_TEXT)};
1407
+ var CAP_STATE_MARK = ${JSON.stringify(CAP_STATE_MARK)};
1222
1408
  ${externalCapability.toString()}
1409
+ ${capabilityAdapter.toString()}
1410
+ ${capabilityRawState.toString()}
1411
+ ${capabilityDisplayState.toString()}
1223
1412
  ${backlogTargetOf.toString()}
1224
1413
  ${sourceNeedsAttention.toString()}
1225
1414
  ${capabilityState.toString()}
@@ -1272,8 +1461,11 @@ ${capabilityState.toString()}
1272
1461
  }
1273
1462
  // Cortex自体を操作するアクションと、接続・自動化の情報の受け皿(取り込み・接続マップ・データソース)。
1274
1463
  // 投入フォーム設定 or fleet-status由来の接続情報のどちらかがあれば出す。
1464
+ // **fleet のデータが取れているか、だけで決める(#125②)。** 能力の宣言(tools)は廃止したので
1465
+ // capabilities を条件に足す。**常に真にはしない**——fleet-status.json を持たない素の
1466
+ // Decisions ビューでは操作タブそのものを出さない現行の意図を保つ
1275
1467
  var HAS_FLEET = !!((SITE.internalSources || []).length || (SITE.externalSources || []).length ||
1276
- (SITE.pipelines || []).length || SITE.tools);
1468
+ (SITE.pipelines || []).length || SITE.capabilities || SITE.tools);
1277
1469
  if (SITE.intake || HAS_FLEET) {
1278
1470
  TABS.push({ key: "ops", label: "操作" });
1279
1471
  }
@@ -1816,6 +2008,25 @@ ${capabilityState.toString()}
1816
2008
  return SITE[key === "generatedAt" ? "fleetGeneratedAt" : key];
1817
2009
  }
1818
2010
 
2011
+ /**
2012
+ * その能力にそのアダプタの操作を出してよいか(#125②)。
2013
+ *
2014
+ * **カタログにあるなら常に出す。** 以前は宣言(Home.md の tools)でゲートしていたが、
2015
+ * 宣言は実態とずれる二重帳簿で、「teams と書いてあるから Slack の追加口を消す」ような
2016
+ * 誤った封鎖を生んでいた(実際に Slack を渡されているのに追加できない案件があった)。
2017
+ * カタログはこの製品が実装しているアダプタの定義なので、押して必ず失敗することはない。
2018
+ *
2019
+ * **capabilities を持たない古いデータでは、従来どおり宣言で見る。** 移行前の
2020
+ * スナップショットには宣言しか無く、そこで全面開放すると別ツール宣言の案件に
2021
+ * 必ず400になるボタンが復活する。
2022
+ */
2023
+ function capabilityUsable(cap, tool) {
2024
+ var caps = fleetVal("capabilities") || {};
2025
+ if (Object.keys(caps).length) return (ADAPTER_CATALOG[cap] || []).indexOf(tool) >= 0;
2026
+ var v = (fleetVal("tools") || {})[cap];
2027
+ return !v || v === "none" || v === tool;
2028
+ }
2029
+
1819
2030
  /** 組み立てだけを行う(設置は呼び出し側)。ライブ差し替えで同じ節を作り直すため分けている */
1820
2031
  /** ツールid→表示名(クライアント側。接続マップの CM_TOOL_LABEL と同じ内容) */
1821
2032
  var CM_TOOL_LABEL_C = {
@@ -1828,12 +2039,16 @@ ${capabilityState.toString()}
1828
2039
  * 能力モーダル。**中身は既存の部品をそのまま入れる。**
1829
2040
  * 一覧・追加フォーム・鍵・手動投入は作り直さない(先出しバッジやライブ差し替えを壊さないため)。
1830
2041
  */
1831
- function openCapabilityModal(cap, items) {
2042
+ function openCapabilityModal(cap, items, note) {
1832
2043
  var m = makeModal(cap, true);
1833
2044
  // **凡例を必ず入れる。** goldState の4状態は凡例があって初めて読める
1834
2045
  if (cap === "チャット" || cap === "開発") {
1835
2046
  m.modal.appendChild(el("p", { class: "modal-desc", text: "「Gold昇格の読み取り対象」のものだけを毎晩の Gold 昇格が読み取ります。「除外」は意図的に外したもの、「未宣言(対象外)」はまだ判断されていないもので、どちらも読み取りません。" }));
1836
2047
  }
2048
+ // **なぜこの状態なのかを、押した先でも読めるようにする(#125②)。** タイルは常時出す
2049
+ // ようになったので、未接続の能力を押したときに中身が空になる場面が増えた。
2050
+ // 「設定できるものがありません」だけだと、繋げないのか繋ぐ必要が無いのかが分からない
2051
+ if (note) m.modal.appendChild(el("p", { class: "modal-desc", text: note }));
1837
2052
  if (!items.length) {
1838
2053
  m.modal.appendChild(el("p", { class: "ds-empty", text: "この能力にはまだ設定できるものがありません。" }));
1839
2054
  }
@@ -1853,8 +2068,12 @@ ${capabilityState.toString()}
1853
2068
  var internals = fleetVal("internalSources") || [];
1854
2069
  var sources = fleetVal("externalSources") || [];
1855
2070
  var pipes = fleetVal("pipelines") || [];
1856
- // 能力→ツールの宣言(例: チャット: "none")。"none" の能力は「未使用」のグレー行で存在だけ見せる
1857
- var tools = fleetVal("tools") || {};
2071
+ // 能力ごとの導出済みの状態(#125② の正本)。持たない古いデータでは {}
2072
+ var caps = fleetVal("capabilities") || {};
2073
+ var hasCaps = Object.keys(caps).length > 0;
2074
+ // 能力→ツールの宣言(例: チャット: "none")。**capabilities が無いときだけ効く劣化表示。**
2075
+ // "none" の能力は「未使用」のグレー行で存在だけ見せる
2076
+ var tools = (!hasCaps && fleetVal("tools")) || {};
1858
2077
  var chatOff = tools["チャット"] === "none";
1859
2078
  var devOff = tools["開発"] === "none";
1860
2079
  // **この節に入るものが1つでもあるなら出す。**
@@ -1866,18 +2085,20 @@ ${capabilityState.toString()}
1866
2085
  //
1867
2086
  // 個別に足し続けると次も漏らすので、**この節に出す候補を列挙して判定する**形にする。
1868
2087
  var hasSecrets = ((SITE.secretKinds || []).filter(function (k) { return SECRET_FORMS[k]; })).length > 0;
1869
- var hasTools = Object.keys(tools).length > 0;
2088
+ // **fleet のデータが取れているか、だけを見る(#125②)。** 宣言(tools)は廃止したが、
2089
+ // capabilities を持たない古いスナップショットで節ごと消えないよう条件には残す
2090
+ var hasCapModel = hasCaps || Object.keys(tools).length > 0;
1870
2091
  // **投入の口も候補。** fleet-status.json はGitHub Actionsが夜間に生成するので、
1871
2092
  // 立ち上げ直後の案件には存在しない。それを理由に節ごと消すと、
1872
2093
  // 文字起こし・資料を入れる口が**一番必要な瞬間に**画面から無くなる
1873
2094
  var hasIntake = !!SITE.intake;
1874
- if (!internals.length && !sources.length && !pipes.length && !hasTools && !hasSecrets && !hasIntake) return null;
2095
+ if (!internals.length && !sources.length && !pipes.length && !hasCapModel && !hasSecrets && !hasIntake) return null;
1875
2096
 
1876
2097
  var sec = el("section", { class: "datasources" });
1877
2098
  sec.appendChild(el("h2", { class: "datasources-head", text: "データソースと自動化" }));
1878
2099
  // **まだ何も繋がっていない案件では、文言を変える。** 接続がゼロの状態で
1879
2100
  // 「接続している情報源の一覧です」と書くと、中身(手動投入だけ)と噛み合わない
1880
- var connected = internals.length > 0 || sources.length > 0 || hasTools;
2101
+ var connected = internals.length > 0 || sources.length > 0 || hasCapModel;
1881
2102
  sec.appendChild(el("p", { class: "datasources-intro", text: connected
1882
2103
  ? "このCortexが接続している情報源と、毎晩自動でGoldに昇格する処理の一覧です。"
1883
2104
  : "情報源はまだ接続されていません。下から手元のファイルを取り込めます(自動連携の設定はこれから行います)。" }));
@@ -1900,12 +2121,12 @@ ${capabilityState.toString()}
1900
2121
 
1901
2122
  // ---- 能力ごとの受け皿 ----
1902
2123
  //
1903
- // **能力の一覧は tools のキーで決める。データ行の有無で決めない。**
2124
+ // **能力の一覧はカタログ(ADAPTER_CATALOG)で決める。データ行の有無で決めない。**
1904
2125
  // internalSources に現れるのは 課題管理/会議/共有資料/デザイン の4つだけで、
1905
- // チャットと開発は externalSources 側にしかない。さらに tools.チャット = "teams" の
1906
- // 案件(三菱)は**どちらにも行が無い**——Cortex が Teams に対応していないため。
1907
- // 行の有無でセクションを作ると、その案件でチャットが丸ごと消える。
1908
- // 「未対応です」と伝えるべき場面で何も出ないのが一番悪い。
2126
+ // チャットと開発は externalSources 側にしかない。さらに未接続の能力は
2127
+ // **どちらにも行が無い**。行の有無でセクションを作ると、その案件でチャットが丸ごと消え、
2128
+ // 「これから繋げる入口」も接続マップのアンカーの受け皿も無くなる。
2129
+ // 何を繋げるかは製品が決めていて、案件データでは変わらない(#125②)。
1909
2130
  var CAP_ORDER = ["課題管理", "会議", "共有資料", "チャット", "デザイン", "開発"];
1910
2131
  var capBox = {};
1911
2132
  var manualBox = {};
@@ -2169,6 +2390,23 @@ ${capabilityState.toString()}
2169
2390
  }));
2170
2391
  return;
2171
2392
  }
2393
+ // **意図的にOFFにした能力にだけ、入力欄を出さない(0.4.71)。** 使わないと決めたものに
2394
+ // 鍵を求めるのは、設定する必要が無いものに設定を促す偽の警告と同じ。
2395
+ // 何も出さないと「トークンはどこで入れるのか」が分からなくなるので、理由を1行だけ残す
2396
+ // (上の GitHub の畳み方と同じ流儀)。
2397
+ //
2398
+ // **未接続(unset・畳んだ empty)には出す。** 0.4.70 では「繋ぐ先が無い状態で鍵を
2399
+ // 入れても効かない」として畳んでいたが、engine の checks は「この画面の入力欄に
2400
+ // 貼ってください」と案内しており、**入力欄が無いのは案内の嘘**だった(cortex-context の
2401
+ // デザインで実際にそうなっていた)。「未接続=これから繋げる入口」なら、その入口に
2402
+ // 最初の一歩を置けるほうが整合する。broken も出す——鍵を貼り直して直る場合がある
2403
+ if (capabilityDisplayState(SECRET_CAP[k], caps) === "off") {
2404
+ pushCap(SECRET_CAP[k], el("p", {
2405
+ class: "sec-how ds-all-connected",
2406
+ text: "この能力はOFFに設定されています。使う場合は設定ファイルで有効にしてください(鍵の登録はそのあとで構いません)。",
2407
+ }));
2408
+ return;
2409
+ }
2172
2410
  pushCap(SECRET_CAP[k], secretCard(k, SECRET_FORMS[k]));
2173
2411
  });
2174
2412
 
@@ -2239,12 +2477,23 @@ ${capabilityState.toString()}
2239
2477
  return row;
2240
2478
  }
2241
2479
 
2242
- /** タイルの2行目(状態と読み取り範囲)。**件数は出せる能力だけ出す** */
2243
- function tileSub(cap, st) {
2480
+ /**
2481
+ * タイルの2行目(状態と読み取り範囲)。**件数は出せる能力だけ出す**
2482
+ *
2483
+ * @param derived engine が導出した5値(capabilities 由来)。古いデータでは null
2484
+ * @param adapter このタイルのアダプタ(件数の出し分けに使う)
2485
+ */
2486
+ function tileSub(cap, st, derived, adapter) {
2244
2487
  var internal = internals.filter(function (s) { return s.kind === cap; })[0];
2245
2488
  var live = sources.filter(function (s) { return externalCapability(s.type) === cap; });
2246
2489
  var parts = [];
2247
- if (st === "warn") {
2490
+ // **導出が on 以外なら、導出の語を優先する(#125②)。** broken は状態としては
2491
+ // 警告(⚠️)だが、原因は「設定そのものが壊れている」であって同期の失敗ではない。
2492
+ // 下の warn の文(同期が失敗しています/N件が読めていません)を出すと、
2493
+ // 直す場所が違うところを指す
2494
+ if (derived && derived !== "on") {
2495
+ parts.push((CAP_STATE_MARK[st] || "") + (CAP_STATE_TEXT[derived] || "状態が判定できません"));
2496
+ } else if (st === "warn") {
2248
2497
  if (live.length) {
2249
2498
  var bad = 0, noToken = 0;
2250
2499
  live.forEach(function (s) {
@@ -2259,6 +2508,10 @@ ${capabilityState.toString()}
2259
2508
  // 内部能力の warn はパイプラインの失敗由来。数えるものが無いので件数を出さない
2260
2509
  parts.push("⚠️ 同期が失敗しています");
2261
2510
  }
2511
+ } else if (derived) {
2512
+ // 導出が on で、問題も無い。接続マップと同じ CAP_STATE_TEXT を引くので、
2513
+ // 上のマップと直下の一覧で語が割れない
2514
+ parts.push((CAP_STATE_MARK[st] || "") + (CAP_STATE_TEXT[derived] || "状態が判定できません"));
2262
2515
  } else {
2263
2516
  // **off には2つの意味が同居している。** 「使わないと宣言した」(tools が none)と
2264
2517
  // 「まだ何も分かっていない」(fleet-status.json を夜間バッチがまだ作っていない)。
@@ -2281,9 +2534,10 @@ ${capabilityState.toString()}
2281
2534
  var unit = cap === "チャット" ? "チャンネル" : cap === "開発" ? "リポジトリ" : "件";
2282
2535
  var gold = live.filter(function (s) { return s.gold === true; }).length;
2283
2536
  parts.push(live.length + unit + "/" + gold + "件をGoldに使用");
2284
- } else if (cap === "共有資料" && tools[cap] === "google-drive" && internal && internal.driveFolderIds) {
2285
- // **ツールを見る。** local の案件(gdo・三菱)は Drive を使っていないのに
2286
- // 「ローカル」と名乗ったタイルが「Driveフォルダ 0件」と語っていた
2537
+ } else if (cap === "共有資料" && adapter === "google-drive" && internal && internal.driveFolderIds) {
2538
+ // **そのタイルのアダプタを見る。** 宣言(tools)を見ていた頃は、local の案件
2539
+ //(gdo・三菱)で「ローカル」と名乗ったタイルが「Driveフォルダ 0件」と語っていた。
2540
+ // いまカタログにあるのは google-drive だけなので、ここは常に真になる
2287
2541
  parts.push("Driveフォルダ " + internal.driveFolderIds.length + "件");
2288
2542
  } else if (cap === "デザイン" && internal && internal.figmaFiles) {
2289
2543
  // **キーが無ければ何も出さない。** 未取得を「登録ゼロ」と断定して顧客に見せない
@@ -2304,38 +2558,74 @@ ${capabilityState.toString()}
2304
2558
  var anyCap = false;
2305
2559
  CAP_ORDER.concat(extraCaps).forEach(function (cap) {
2306
2560
  var items = capBox[cap] || [];
2561
+ // **カタログにある能力のタイルは常時出す(#125②)。** 使っているかどうかで出し分けない
2562
+ // ——未接続のタイルは「これから繋げる入口」で、消すと繋ぎ方を知る場所が無くなる。
2563
+ // カタログは製品の定義なので、案件データが何であってもここは変わらない
2564
+ var adapters = (hasCaps && ADAPTER_CATALOG[cap]) || [];
2565
+ // 旧データ(capabilities 無し)は従来どおり「宣言があれば行が無くても出す」
2307
2566
  var declared = tools[cap] != null;
2308
- // **tools に宣言があれば、行が無くてもタイルを出す。** 出さないと
2309
- // Teams案件でチャットが丸ごと消え、「未対応です」と伝える場所も無くなる。
2310
- // 接続マップのアンカー(ds-<能力>)の受け皿にもなる(いま2案件で解決できていない)
2311
2567
  // **手動投入はアダプタに依存しない。** 宣言や行の有無で弾くと、
2312
2568
  // fleet-status.json がまだ無い新規案件で投入の口ごと消える
2313
- if (!items.length && !declared && !manualBox[cap]) return;
2569
+ if (!adapters.length && !items.length && !declared && !manualBox[cap]) return;
2314
2570
  anyCap = true;
2315
2571
  sec.appendChild(el("div", { class: "ds-cap", text: cap }));
2316
- // アダプタのタイルは、宣言か行があるときだけ。手動投入だけが理由でここへ来た
2317
- // 新規案件に、中身の無い「会議」タイルを立てても押す先が無い
2318
- if (items.length || declared) renderAdapterTile(cap, items);
2572
+ if (adapters.length) {
2573
+ // **カタログの全アダプタを並べる(今日は各1個)。** 能力=1アダプタを
2574
+ // 焼き込まないための形(#107 / #127)。導出が答えているアダプタにだけ
2575
+ // その状態を割り当て、残りは未接続——「知らない」を「繋がっている」に寄せない
2576
+ var active = capabilityAdapter(cap, caps);
2577
+ adapters.forEach(function (a, ai) {
2578
+ renderAdapterTile(cap, items, a, a === active ? capabilityDisplayState(cap, caps) : "unset", ai);
2579
+ });
2580
+ } else if (items.length || declared) {
2581
+ // 旧データ: アダプタのタイルは宣言か行があるときだけ。手動投入だけが理由でここへ来た
2582
+ // 新規案件に、中身の無い「会議」タイルを立てても押す先が無い
2583
+ var legacyTool = tools[cap] && tools[cap] !== "none" ? tools[cap] : "";
2584
+ renderAdapterTile(cap, items, legacyTool, null, 0);
2585
+ }
2319
2586
  renderManualTile(cap);
2320
2587
  });
2321
2588
 
2322
- function renderAdapterTile(cap, items) {
2589
+ /**
2590
+ * 能力×アダプタ1件のタイル。
2591
+ *
2592
+ * @param adapter アダプタ識別子(カタログ由来。旧データでは宣言 tools 由来)
2593
+ * @param derived engine が導出した5値。旧データ(capabilities 無し)では null
2594
+ * @param ai その能力の中での並び順。先頭だけが接続マップの飛び先(ds-<能力>)になる
2595
+ */
2596
+ function renderAdapterTile(cap, items, adapter, derived, ai) {
2323
2597
  var meta = CAP_META[cap] || { icon: "🔗", desc: "" };
2324
- var st = capabilityState(cap, { tools: tools, internals: internals, externals: sources, pipes: pipes });
2598
+ var st = derived === "unset" && capabilityDisplayState(cap, caps) !== "unset"
2599
+ // カタログにあるが導出が答えていないアダプタ(今日は発生しない。#107 の前方互換)
2600
+ ? "idle"
2601
+ : capabilityState(cap, { tools: tools, capabilities: caps, internals: internals, externals: sources, pipes: pipes });
2325
2602
  // **ロゴはタイルの左に独立して置く。** 名前の行に入れると名前の高さに
2326
2603
  // 縛られて小さくなる。ロゴで能力を見分けられるのが一覧の主な用途
2327
- var logo = ADAPTER_ICONS[tools[cap]];
2604
+ var logo = ADAPTER_ICONS[adapter];
2328
2605
  var ic = el("span", { class: "ds-tile-logo" });
2329
2606
  if (logo) ic.innerHTML = logo; // 公式ロゴ。ビルド時に埋め込み済みで外部通信は起きない
2330
2607
  else ic.textContent = meta.icon;
2331
- // "none"(未使用の宣言)はツール名ではない。素通しすると顧客の画面に
2332
- // none」というタイル名が出る(接続マップ側で一度直したのと同じ穴)
2333
- var rawTool = tools[cap] && tools[cap] !== "none" ? tools[cap] : "";
2608
+ // カタログ外の値はここへ来ない(capabilityAdapter が弾く)。旧データの "none" だけは
2609
+ // 素通しすると顧客の画面に「none」というタイル名が出るので、上で空にしてある
2610
+ var rawTool = adapter || "";
2334
2611
  var body = el("div", { class: "ds-tile-body" }, [
2335
2612
  el("div", { class: "ds-tile-name", text: (rawTool && (CM_TOOL_LABEL_C[rawTool] || rawTool)) || cap }),
2336
2613
  el("div", { class: "ds-tile-desc", text: meta.desc }),
2337
- el("div", { class: "ds-tile-sub ds-gate-" + st, text: tileSub(cap, st) }),
2614
+ el("div", { class: "ds-tile-sub ds-gate-" + st, text: tileSub(cap, st, derived, adapter) }),
2338
2615
  ]);
2616
+ // **なぜその状態なのかを添える(#125②)。** engine が書いた一文(detail)で、
2617
+ // 「何が無いのか」を出す。on のときは読み取り範囲(件数)が既に同じ行にある。
2618
+ //
2619
+ // **未接続(unset・畳んだ empty)はタイル表面に出さない(0.4.71)。** 未接続の答えは
2620
+ // 「まだ繋がっていない」の一言で足り、files が空です/figma.json がありません、の
2621
+ // 区別は押した先で読めばよい実装の話。表面に置くと一覧が理由文で埋まるうえ、
2622
+ // 実機で幅を超えて溢れた。off(何件残して止めたか)と broken(異常)は
2623
+ // 押さずに気づけないと困るので、表面に残す
2624
+ var capInfo = caps[cap];
2625
+ var detailOnTile = derived === "off" || derived === "broken";
2626
+ if (detailOnTile && capInfo && capInfo.detail && adapter === capabilityAdapter(cap, caps)) {
2627
+ body.appendChild(el("div", { class: "ds-tile-note ds-tile-todo", text: capInfo.detail }));
2628
+ }
2339
2629
  // トークン未設定は**状態とは別軸**。状態機械に混ぜると判定が1箇所でなくなる。
2340
2630
  // ただし「使わない」と**宣言した**能力では出さない——設定する必要が無いものに
2341
2631
  // ついて設定を促す偽の警告になる(sushiro のデザインで出ていた)。
@@ -2349,7 +2639,14 @@ ${capabilityState.toString()}
2349
2639
  // 案件に未設定の警告を出すと、直す必要が無いものを直させることになる。
2350
2640
  // 読めていない組織があるときは、この下の checks とタイルの状態(warn)が拾う
2351
2641
  var tokenIsFallback = sk === "external-sources" && !githubTokenNeeded();
2352
- if (!tokenIsFallback && tools[cap] !== "none" && sk && (SITE.secretKinds || []).indexOf(sk) >= 0 && !((fleetVal("secretSources") || {})[sk])) {
2642
+ // **#125②: 導出があるときは導出で判断する。** OFF(意図的に止めた)にだけ出さない
2643
+ // ——入れる必要が無いものについて設定を促す偽の警告になる。
2644
+ //
2645
+ // **注記の条件は、入力欄を出す条件と同じにする(0.4.71)。** 別々にすると
2646
+ // 「入力欄はあるのに未設定だと言われない」「言われるのに入力欄が無い」のどちらかが起きる。
2647
+ // 0.4.70 は unset で入力欄を畳んでいたので注記も抑えていたが、案A で欄を出すようにしたため揃えた
2648
+ var tokenRelevant = derived != null ? derived !== "off" : tools[cap] !== "none";
2649
+ if (!tokenIsFallback && tokenRelevant && sk && (SITE.secretKinds || []).indexOf(sk) >= 0 && !((fleetVal("secretSources") || {})[sk])) {
2353
2650
  body.appendChild(el("div", { class: "ds-tile-note", text: "⚠ トークンが未設定です" }));
2354
2651
  }
2355
2652
  // セットアップの残りのうち、この能力のもの。**鍵の注記と同じ位置**に出して
@@ -2364,12 +2661,17 @@ ${capabilityState.toString()}
2364
2661
  shownOnTile[c.id] = true;
2365
2662
  body.appendChild(el("div", { class: "ds-tile-note ds-tile-todo", text: "→ " + c.action }));
2366
2663
  });
2367
- var tile = el("button", { class: "ds-tile", type: "button", id: "ds-" + cap }, [
2664
+ // **id は能力ごとに1つ(先頭のアダプタ)。** 接続マップのノードの飛び先で、
2665
+ // 同じ id が2つある不正なHTMLになると getElementById が思わぬ側を指す
2666
+ var tile = el("button", { class: "ds-tile", type: "button", id: ai ? "ds-" + cap + "-" + adapter : "ds-" + cap }, [
2368
2667
  ic,
2369
2668
  body,
2370
2669
  el("span", { class: "ds-tile-chev", text: "›" }),
2371
2670
  ]);
2372
- tile.addEventListener("click", function () { openCapabilityModal(cap, items); });
2671
+ var modalNote = derived && derived !== "on" && capInfo && capInfo.detail
2672
+ ? CAP_STATE_TEXT[derived] + "(" + capInfo.detail + ")"
2673
+ : "";
2674
+ tile.addEventListener("click", function () { openCapabilityModal(cap, items, modalNote); });
2373
2675
  sec.appendChild(tile);
2374
2676
  }
2375
2677
 
@@ -2553,8 +2855,7 @@ ${capabilityState.toString()}
2553
2855
  // 「足したい案件でだけ足せない」という逆転になっていた。
2554
2856
  // 押しても必ず失敗するもの(別ツールが宣言済み)は usable() で弾く。
2555
2857
  if (!SITE.intake) return null;
2556
- var tools = fleetVal("tools") || {};
2557
- var usable = function (cap, tool) { var v = tools[cap]; return !v || v === "none" || v === tool; };
2858
+ var usable = capabilityUsable;
2558
2859
  var box = el("div", {});
2559
2860
  var used = false;
2560
2861
 
@@ -2808,14 +3109,14 @@ ${capabilityState.toString()}
2808
3109
  /**
2809
3110
  * 外部ソースの追加フォーム(Slack・GitHub)。
2810
3111
  *
2811
- * **tools の宣言でゲートする。** 別のツールが宣言されている案件(チャット: teams 等)では
2812
- * サーバが必ず400で断るので、押せるのに必ず失敗するボタンを出さない。
3112
+ * **カタログでゲートする(#125②)。** 宣言(tools)は廃止した。古いデータのときだけ
3113
+ * 従来どおり宣言を見る(capabilityUsable の項を参照)——別のツールが宣言されている
3114
+ * 案件ではサーバが必ず400で断るので、押せるのに必ず失敗するボタンを出さない。
2813
3115
  */
2814
3116
  function renderExternalAdders() {
2815
3117
  var box = el("div", {});
2816
3118
  if (!SITE.intake) return box;
2817
- var tools = fleetVal("tools") || {};
2818
- var usable = function (cap, tool) { var v = tools[cap]; return !v || v === "none" || v === tool; };
3119
+ var usable = capabilityUsable;
2819
3120
 
2820
3121
  // **Slack は tools 宣言で隠さない。** tools.チャット は能力ごとに1値しか持てないので、
2821
3122
  // 「会議はTeams・連絡はSlack」という普通の構成を表せない。teams を宣言した案件では
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.4.69",
3
+ "version": "0.4.71",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,