@takagaki/cortex-decisions-viewer 0.12.3 → 0.12.5

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 +348 -558
  2. package/package.json +1 -1
package/dist/render.js CHANGED
@@ -1,10 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.UNSETTLED_LIMITS = exports.REFLECT_OPS = exports.REFLECTING_LIMITS = void 0;
3
+ exports.UNSETTLED_LIMITS = exports.LIVE_CONFIRM_LIMITS = void 0;
4
4
  exports.renderSite = renderSite;
5
- exports.livePending = livePending;
6
- exports.nextCheckDelay = nextCheckDelay;
7
- exports.settledPending = settledPending;
8
5
  exports.sanitizeRegisteredNow = sanitizeRegisteredNow;
9
6
  exports.sanitizeStateNow = sanitizeStateNow;
10
7
  exports.overlayStateOnFleet = overlayStateOnFleet;
@@ -343,169 +340,49 @@ function backlogTargetOf(input) {
343
340
  return num.test(view[1]) ? null : { domain: domain, projectKey: view[1].toUpperCase() };
344
341
  return null;
345
342
  }
343
+ // ---------------------------------------------------------------------------
344
+ // 画面が「まだ答えが無い」あいだの見切り。
345
+ //
346
+ // **ここは純関数と定数だけを置く。** 画面側(クライアントJS)は toString() で実体を
347
+ // 流し込んで使い、テストは同じ実体を直接呼ぶ。クロージャの中に書くと DOM 経由でしか
348
+ // 触れず、判定を壊してもテストが気づけない。
349
+ //
350
+ // **クロージャを持たないこと**(外の変数を掴むと、流し込んだ先で undefined を参照して壊れる)。
351
+ // 参照してよいのは、クライアント側にも同名で注入している定数だけ。
352
+ //
353
+ // **「⏳ 反映中」の機構(Reflecting 型・REFLECT_OPS・livePending / settledPending /
354
+ // nextCheckDelay と localStorage の記録)は 2026-08-25 に退役した(#221)。**
355
+ // あれは「保存したのに一覧が変わらない」空白を埋めるためのもので、**埋める空白そのものが
356
+ // 無くなった**——画面は登録ファイルの現在値(registeredNow)を読み、足した行・消した行を
357
+ // その場で見せる。取り直しの間隔も未確定件数ベース(UNSETTLED_TTL_MS)へ移してある。
358
+ // 戻したくなったら、その削除コミットを revert すればよい。
359
+ // ---------------------------------------------------------------------------
346
360
  /**
347
- * 反映待ちの期限。**15分。**
348
- *
349
- * 実測(cortex-context「セットアップ状況の自己チェック」の完了 run 112件、
350
- * うち workflow_dispatch 51件):
351
- *
352
- * | | 全体(112) | dispatch のみ(51) |
353
- * | min | 4秒 | 6秒 |
354
- * | p50 | 41秒 | 47秒 |
355
- * | p90 | 57秒 | 62秒 |
356
- * | p95 | 65秒 | ― |
357
- * | max | 400秒 | 400秒 |
361
+ * ライブ確認(op=state)の見切り(#256・設計v2 の1)。
358
362
  *
359
- * 必要な余裕は 最悪400秒 + op=state のキャッシュ60秒 + 取り直し間隔とのズレ60秒 = 520秒。
360
- * 10分では余裕が1分20秒しかなく、自己チェックが待ち行列(concurrency)に入れば超える。
363
+ * 通常応答は1〜2秒・Lambda のコールドスタートを含めても10秒で足りる。
364
+ * **10秒「確認中」が出続けるのは、嘘を断言するより許容できる。**
361
365
  *
362
- * 🔴 **期限が無いと、反映が来ない案件で「反映中」が永久に出て、本当の「未設定」を隠す。**
363
- * 完了 run の 8%(9/112)は failure で、失敗した run はコミットしない。つまり
364
- * 8%程度の確率で「15分間 反映中 → 未設定に戻る」が起きる(設計どおりの挙動)。
366
+ * 🔴 **これは「取り直しの予約」ではない。** 60秒の取り直し(UNSETTLED_POLL_MS)を
367
+ * 数えているテストは、この見切りを数から除くこと。
365
368
  */
366
- const REFLECT_TTL_MS = 15 * 60 * 1000;
367
- /** 自動で取り直す間隔。60秒(op=state のキャッシュと同じ粒度) */
368
- const REFLECT_POLL_MS = 60 * 1000;
369
+ const LIVE_CONFIRM_TIMEOUT_MS = 10000;
369
370
  /**
370
- * 期限と間隔を**テストから固定するための入れ物**(infra の STATE_LIMITS と同じ形)。
371
+ * 見切りを**テストから固定するための入れ物**(infra の STATE_LIMITS と同じ作法)。
371
372
  *
372
373
  * ⚠ **定数そのものを export してはいけない。** `export const` は CommonJS へ落ちるときに
373
374
  * 参照が `exports.X` へ書き換えられ、**toString() で流し込んだ先には exports が無いので
374
375
  * クライアントJSが丸ごと落ちる**(実際に踏んだ)。流し込む関数から参照する値は、
375
376
  * export しないローカルの const に置くこと。
376
377
  */
377
- exports.REFLECTING_LIMITS = { ttlMs: REFLECT_TTL_MS, pollMs: REFLECT_POLL_MS };
378
- /**
379
- * 記録してよい操作の対応表。**既定は「記録しない」(null)。**
380
- *
381
- * 🔴 **settle できない操作を記録すると、期限まで「反映中」が出っぱなしになり、
382
- * 本当の状態を隠す。** 「現れた/消えた」で判定できるものだけを載せる。
383
- *
384
- * @see CONFIG_OPS(infra/lambda/intake/logic.mjs)— **突き合わせるテストがある**。
385
- * op を足してここに書き忘れると、その操作だけ黙って反映中にならない/
386
- * settle できないものを記録して出っぱなしになる、という気づきにくい壊れ方をする
387
- */
388
- exports.REFLECT_OPS = {
389
- // ---- 記録する ----
390
- // 接続先の追加・削除。一覧に「現れた/消えた」で反映を判定できる
391
- "backlog-target-add": { cap: "課題管理", dir: "add", from: "url" },
392
- // **削除はキーだけを送る**(サーバもキーだけで消す)ので、スペースは問わない
393
- //
394
- // ⚠ **最後の1件を外したときは、反映が届いても settle しない**(engine を読んで確認・2026-08-21)。
395
- // engine の fleet-status は、正本(issues-config.json)が空になると
396
- // 同期ミラー(issues/backlog-settings.json)の旧値へフォールバックし、
397
- // backlogTarget に旧 domain / projectKey を載せ続ける(capabilities も on のまま)。
398
- // ミラーは履歴保全のため**画面から外しても残す設計**なので、この値は消えない。
399
- // つまり「消えた」を観測できるのは、ミラーが1つも無い案件(一度も同期していない)だけ。
400
- // それ以外では**期限(15分)で落ちる**——嘘は残らないが、その間「反映中」と出る。
401
- // 直すなら engine 側(backlogTarget.source === "secrets" かつ configPath === null を
402
- // 「正本に接続先が無い」として画面に伝える)で、これは「未設定」の意味を変える
403
- // 製品判断なので別issueにする。
404
- "backlog-target-remove": { cap: "課題管理", dir: "remove", from: "backlogProjectKey" },
405
- // ---- 記録しない ----
406
- // **値の変更**。読み取りの ON/OFF は「現れた/消えた」では判定できない
407
- "set-github-source-enabled": null,
408
- // **表示名の取り直し**。sendConfig を通らない2本目の書き込み口でもある
409
- "refresh-channel-name": null,
410
- // 以下は add / remove の形だが、**その能力の「反映中」はまだ作っていない**。
411
- // 追加フォームには既存の控え(.ds-pending-item「追加済み・反映中」)が出ており、
412
- // 二重に出さないため、統合は別issueにする(設計 追補 中2)
413
- "add-channel": null,
414
- "remove-channel": null,
415
- "add-github-source": null,
416
- "remove-github-source": null,
417
- "add-figma-file": null,
418
- "remove-figma-file": null,
419
- "add-drive-folder": null,
420
- "remove-drive-folder": null,
421
- // 1件だけ持てた頃の置き換え。いまの画面は出さない(backlog-target-add に統合済み)
422
- "set-backlog-target": null,
423
- };
424
- /**
425
- * 期限内のものだけ返す。
426
- *
427
- * 🔴 **経過時間(now - at)で判定すること。** 取り直した回数で数えると、
428
- * スリープ復帰やバックグラウンドタブのスロットリングで期限が伸びる。
429
- *
430
- * 🔴 **経過時間は「上」だけでなく「下」も見る。** 上限(age < 期限)しか見ないと、
431
- * **at が未来の記録は永久に期限が切れない**——
432
- * - at = now + 1時間 … 1時間15分ずっと「反映中」
433
- * - at = Infinity … 永久に「反映中」のまま60秒ごとに取り直し続ける
434
- * 実在する経路: **端末の時計が進んだ状態で記録し、NTP で戻る**/別バージョンが書いた値/
435
- * localStorage の手編集。「期限が必ず切れるから嘘は残らない」というこの設計の
436
- * 唯一の安全弁が、入口で成立しなくなる。
437
- */
438
- function livePending(list, now) {
439
- const out = [];
440
- for (const e of list || []) {
441
- // NaN・欠けた at・未来の at・Infinity はここで落ちる
442
- const age = e ? now - e.at : NaN;
443
- if (!e || !Number.isFinite(age) || !(age >= 0 && age < REFLECT_TTL_MS))
444
- continue;
445
- out.push(e);
446
- }
447
- return out;
448
- }
449
- /**
450
- * 次に取り直すまでの ms。もう取り直す必要が無ければ null。
451
- *
452
- * 🔴 **最後の1回は期限ちょうどに来る**(min(60秒, 期限 − 経過))。
453
- * 固定60秒で張ると、最後の確認が期限の60秒前になり、そこから期限までに反映が来ても
454
- * 誰も見ない。さらに**期限そのものに誰も描き直さない**ので、「取り直しは止まったのに
455
- * 反映中の文字が残る」時間ができる。
456
- */
457
- function nextCheckDelay(list, now) {
458
- const live = livePending(list, now);
459
- if (!live.length)
460
- return null;
461
- let deadline = Infinity;
462
- for (const e of live)
463
- deadline = Math.min(deadline, e.at + REFLECT_TTL_MS);
464
- const left = deadline - now;
465
- return left < REFLECT_POLL_MS ? left : REFLECT_POLL_MS;
466
- }
467
- /**
468
- * 反映が来たものを落とす。
469
- *
470
- * @param presentByCap 能力ごとの「**スナップショットが答えている集合**」。
471
- * 🔴 **「画面に出ているか」を渡してはいけない。** 追加・削除の応答には一覧が添えられ、
472
- * その場で画面が差し替わるので、記録した瞬間に反映済みと判定されて消える
473
- * (=機能が丸ごと no-op になる)。渡すのは「未設定」を出すか決めているのと
474
- * 同じ関数の戻り値(画面側は backlogTargetsNow)。
475
- * その能力を判定できないときは null を渡す——落とさず、期限に任せる。
476
- */
477
- function settledPending(list, presentByCap) {
478
- const out = [];
479
- for (const e of list || []) {
480
- const present = presentByCap ? presentByCap[e.cap] : null;
481
- if (!present) {
482
- out.push(e);
483
- continue;
484
- }
485
- let hit = false;
486
- for (const p of present) {
487
- if (!p || !p.projectKey)
488
- continue;
489
- if (String(p.projectKey).toUpperCase() !== String(e.projectKey).toUpperCase())
490
- continue;
491
- // 空の domain は「どのスペースでもよい」(削除はキーだけを送るため)
492
- if (e.domain && String(p.domain || "").toLowerCase() !== String(e.domain).toLowerCase())
493
- continue;
494
- hit = true;
495
- }
496
- // 消す操作は「消えたら反映済み」。ここを add と同じにすると、削除した接続先が
497
- // 消えたその瞬間から期限まで「反映中」と言い続ける
498
- const reflected = e.dir === "remove" ? !hit : hit;
499
- if (!reflected)
500
- out.push(e);
501
- }
502
- return out;
503
- }
378
+ exports.LIVE_CONFIRM_LIMITS = { timeoutMs: LIVE_CONFIRM_TIMEOUT_MS };
504
379
  // ---------------------------------------------------------------------------
505
380
  // 登録の即時値(registeredNow)— **まとめ表がまだ知らない行を足す**(段階1)
506
381
  //
507
382
  // 守るべき不変条件は3つ:
508
- // 1. **足すだけ・置き換えない**(origin / goldState / notify / gate はまとめ表にしか無い)
383
+ // 1. **足すだけ・置き換えない**(実測(gate / gateAt / needed)はまとめ表と状態ストアに
384
+ // しか無く、既存の行の欄は書き換えない。チャットの登録メタ(origin / goldState /
385
+ // notify)は 2026-08-25 から**足す行にだけ**載る——#221)
509
386
  // 2. **既知の欄だけ・葉は文字列だけ**(ライブ専用のキーなので build.ts の網にかからない)
510
387
  // 3. **正規化しない**(engine が読むのは生値。揃えにいくと「画面には出るのに読まれない」行ができる)
511
388
  // ---------------------------------------------------------------------------
@@ -520,11 +397,16 @@ const REGISTERED_CAP_NAMES = {
520
397
  /**
521
398
  * 通してよい欄。**ここに無い欄は1つも通さない**(Lambda 側の列挙と同じ形)
522
399
  *
523
- * 🔴 **`orgUnknown` は真偽値**(他の欄は全部文字列)。**この1欄だけの例外**で、
400
+ * 🔴 **真偽値は `orgUnknown` と `notify` の2欄だけ**(他の欄は全部文字列)。
524
401
  * 篩の側にも `=== true` の分岐がある——`sanitizeRegisteredNow` を参照
402
+ *
403
+ * 🔴 **チャットの登録メタ(origin / goldState / notify)を通す(2026-08-25・#221)。**
404
+ * これが無いと、いま足した行の「登録を解除」「通知先」が出ない——夜間バッチが
405
+ * 追いつく翌朝まで、**追加した直後の主役の行だけ操作の無い行**になる。
406
+ * `orgUnknown` で同じ穴を1つ埋めた(0.12.1)のに、同族の3欄が残っていた。
525
407
  */
526
408
  const REGISTERED_FIELDS = {
527
- chat: ["ref", "name", "workspace", "orgUnknown"],
409
+ chat: ["ref", "name", "workspace", "orgUnknown", "origin", "goldState", "notify"],
528
410
  dev: ["ref", "type"],
529
411
  design: ["key", "name"],
530
412
  materials: ["id", "name"],
@@ -564,13 +446,18 @@ function sanitizeRegisteredNow(v) {
564
446
  const item = {};
565
447
  for (const f of REGISTERED_FIELDS[cap]) {
566
448
  const fv = r[f];
567
- // 🔴 **真偽値を通すのは orgUnknown だけ**(2026-08-25)。葉を文字列に限った篩が
568
- // この印を落としており、**追加した直後のチャンネルから「Slack ワークスペースに
569
- // 接続する」の導線が消えていた**(顧客Slackを繋ぐ唯一の入口)。
449
+ // 🔴 **真偽値を通すのは orgUnknown と notify の2欄だけ**(2026-08-25)。葉を文字列に
450
+ // 限った篩がこの印を落としており、**追加した直後のチャンネルから「Slack ワークスペースに
451
+ // 接続する」の導線(orgUnknown)と「通知先」の札(notify)が消えていた**。
570
452
  // **`=== true` に限る。** 文字列の "true" や 1 を真と読むと、意図しない値が
571
- // 「組織不明」の案内を呼び、直すものが無い人に接続を勧めることになる。
572
- // 例外はこの1欄だけ——他の欄まで真偽値を通すと、許可リストが篩の意味を失う
573
- if (f === "orgUnknown") {
453
+ // 「組織不明」の案内を呼んで直すものが無い人に接続を勧め、通知していない
454
+ // チャンネルに「通知先」と書くことになる。
455
+ //
456
+ // **false はキーごと落とす。** 読み手は `=== true` で見る(`s.notify === true` /
457
+ // `r.orgUnknown === true`)ので、false を載せても意味は同じ——載せないほうが
458
+ // 「印が無い」と「印が false」の2通りを画面に持ち込まない。
459
+ // 例外はこの2欄だけ——他の欄まで真偽値を通すと、許可リストが篩の意味を失う
460
+ if (f === "orgUnknown" || f === "notify") {
574
461
  if (fv === true)
575
462
  item[f] = true;
576
463
  continue;
@@ -685,9 +572,20 @@ function overlayStateOnFleet(view, rows, generatedAt) {
685
572
  * 一意でないので、IDだけで突き合わせると別ワークスペースの同IDを同じものと見て
686
573
  * 行が出ない(#104 で同じ理由の事故がある)。
687
574
  *
688
- * 🔴 **`orgUnknown` の印をここで落とさない(2026-08-25)。** 落とすと、この関数が作る行
689
- * (=追加した直後の主役)が `groupSlackByOrg` で社内の組に落ち、
690
- * **「Slack ワークスペースに接続する」が画面から消える**——顧客Slackを繋ぐ唯一の入口。
575
+ * 🔴 **登録メタ(orgUnknown / origin / goldState / notify)をここで落とさない(2026-08-25)。**
576
+ * 篩を通しても**この関数がコピーしなければ行に乗らない**——0.12.1 orgUnknown を
577
+ * 篩に通したときに、ここへのコピーを1行足して初めて画面が直った。落とすと:
578
+ *
579
+ * - `orgUnknown` … 行が `groupSlackByOrg` で社内の組に落ち、
580
+ * **「Slack ワークスペースに接続する」が画面から消える**(顧客Slackを繋ぐ唯一の入口)
581
+ * - `origin` … 「登録を解除」の出し分けが `s.origin !== "explicit"` なので、
582
+ * 欄が無い行にも出る。いまは derived しか来ないので結果は同じだが、
583
+ * **判定の材料を欠いたまま偶然合っている**状態を残さない
584
+ * - `goldState` … `sourceNeedsAttention` と「読み取り停止中」の札が見る。slack は
585
+ * 常に `"on"` なので表示は変わらないが、まとめ表の行と**同じ形にしておく**
586
+ * (片方に無い欄があると、差し替わった瞬間だけ判定が変わる経路ができる)
587
+ * - `notify` … **「通知先」の札が追加直後に出ない。** 通知が飛ぶチャンネルなのに
588
+ * 画面がそう言わないのは、行の意味を取り違えさせる
691
589
  */
692
590
  function chatRowsToAdd(externals, rows) {
693
591
  const have = {};
@@ -707,6 +605,13 @@ function chatRowsToAdd(externals, rows) {
707
605
  row.workspace = r.workspace;
708
606
  if (r.orgUnknown === true)
709
607
  row.orgUnknown = true;
608
+ if (r.origin)
609
+ row.origin = r.origin;
610
+ if (r.goldState)
611
+ row.goldState = r.goldState;
612
+ // **真偽値の印は true のときだけ載せる**(読み手は `s.notify === true`。篩の項を参照)
613
+ if (r.notify === true)
614
+ row.notify = true;
710
615
  out.push(row);
711
616
  }
712
617
  return out;
@@ -1048,22 +953,23 @@ function mergeRegisteredFleet(view, reg) {
1048
953
  // 未確定の列がある間の取り直し(段階1の前提)
1049
954
  //
1050
955
  // 🔴 **これが無いと、登録したあと画面が二度と更新されない。** 60秒ごとの取り直しは
1051
- // これまで「反映中」の記録がある間だけ張られていたが、registeredNow が入ると
1052
- // 追加した瞬間に「もう出ている」と判定されて記録が消える。
956
+ // 退役した「反映中」の記録がある間だけ張られていた(#221)。**取り直しの根拠を
957
+ // 「まだ答えが無い行があること」に移したのがここ**——registeredNow が入ると、
958
+ // 追加した瞬間に「もう出ている」と判定されて記録のほうは消えてしまう。
1053
959
  //
1054
- // **上限とタブ非表示の扱いは、反映待ち(livePending / nextCheckDelay)と同じ考え方にする。**
960
+ // **上限とタブ非表示の扱いは、次の2つを守る。**
1055
961
  // - 上限は**経過時間**で見る(回数カウンタで数えない)。バックグラウンドタブの
1056
962
  // スロットリングやスリープ復帰は、経過時間で見る限り自己修復する
1057
963
  // - **可視性のイベントは足さない。** 遅れて起きたタイマがその回で期限切れを落とすので、
1058
964
  // 非表示のあいだは自然に間隔が伸びるだけ。イベントを足すと、復帰の瞬間に
1059
965
  // 全タブが同時に叩く形を新しく作ることになる
1060
966
  // ---------------------------------------------------------------------------
1061
- /** 未確定の行があっても、取り直しを続ける上限(開始からの経過)。反映待ちと同じ15分 */
967
+ /** 未確定の行があっても、取り直しを続ける上限(開始からの経過)。15分 */
1062
968
  const UNSETTLED_TTL_MS = 15 * 60 * 1000;
1063
969
  /** 取り直しの間隔。60秒(op=state のキャッシュと同じ粒度) */
1064
970
  const UNSETTLED_POLL_MS = 60 * 1000;
1065
971
  /**
1066
- * 期限と間隔を**テストから固定するための入れ物**(REFLECTING_LIMITS と同じ作法)。
972
+ * 期限と間隔を**テストから固定するための入れ物**(infra の STATE_LIMITS と同じ作法)。
1067
973
  *
1068
974
  * ⚠ **定数そのものを export してはいけない**(CommonJS へ落ちるときに `exports.X` へ
1069
975
  * 書き換えられ、toString() で流し込んだ先には exports が無い)。
@@ -1075,8 +981,9 @@ exports.UNSETTLED_LIMITS = { ttlMs: UNSETTLED_TTL_MS, pollMs: UNSETTLED_POLL_MS
1075
981
  * @param count 未確定の行数(0 なら張らない)
1076
982
  * @param since 取り直しを始めた時刻(Date.now())
1077
983
  *
1078
- * 🔴 **経過は「上」だけでなく「下」も見る**(livePending と同じ理由)。端末の時計が
1079
- * 進んだ状態で始めて NTP で戻ると、`since` が未来になり**永久に期限が切れない**。
984
+ * 🔴 **経過は「上」だけでなく「下」も見る。** 端末の時計が進んだ状態で始めて
985
+ * NTP で戻ると、`since` が未来になり**永久に期限が切れない**——上限(`age < 期限`)
986
+ * しか見ないと、この画面は60秒ごとに永久に Lambda を叩き続ける。
1080
987
  */
1081
988
  function nextStateDelay(count, since, now) {
1082
989
  if (!(count > 0))
@@ -2364,6 +2271,11 @@ a.ds-name:hover { text-decoration: underline; }
2364
2271
  /* 宣言はあるが実体が繋がっていない。未使用(グレー)とも接続済み(青)とも違うことを見せる */
2365
2272
  .cm-node.cm-idle rect { fill: var(--surface); stroke: #cbd5e1; stroke-dasharray: 4 3; }
2366
2273
  .cm-node.cm-idle .cm-sync { fill: #b45309; }
2274
+ /* ライブ確認の応答を待っている間(#256)。**状態を何も言っていない見た目にする。**
2275
+ 破線(未使用・未接続)にも青(接続OK)にもしない——どちらも状態の断言になる。
2276
+ 語彙は一覧側の「登録状況を確認中です」(ds-gate-muted)と同じ灰色に揃える */
2277
+ .cm-node.cm-pending rect { fill: var(--surface); stroke: var(--border); }
2278
+ .cm-node.cm-pending .cm-sync { fill: #6b7280; }
2367
2279
  .cm-label { font-size: 13.5px; font-weight: 600; fill: var(--text); }
2368
2280
  .cm-node.cm-warn .cm-label { fill: #92400e; }
2369
2281
  .cm-node.cm-off .cm-label { fill: #9ca3af; }
@@ -2391,6 +2303,9 @@ a.ds-name:hover { text-decoration: underline; }
2391
2303
  .cm-link.cm-warn { stroke: #d97706; }
2392
2304
  .cm-link.cm-off { stroke: #d1d5db; opacity: .6; }
2393
2305
  .cm-link.cm-idle { stroke: #cbd5e1; opacity: .7; }
2306
+ /* 確認中の線(#256)。**薄い中立**——流れ(animation)を付けない。
2307
+ 動いている線は「いまデータが通っている」と読めるので、確認中に出すと断言になる */
2308
+ .cm-link.cm-pending { stroke: #cbd5e1; opacity: .4; }
2394
2309
  /* 線は6本とも焼いて、出す/出さないをここで切り替える(0.12.0・renderConnectionMap の項)。
2395
2310
  SVG 要素に HTML の hidden 属性は効かないので、クラスで消す */
2396
2311
  .cm-link.is-hidden { display: none; }
@@ -2796,15 +2711,9 @@ ${backlogTargetOf.toString()}
2796
2711
  ${issuesRefOf.toString()}
2797
2712
  ${sourceNeedsAttention.toString()}
2798
2713
  ${capabilityState.toString()}
2799
- // 反映待ち(⏳ 反映中)の判定。**期限・間隔・対応表もビルド時の定義から流し込む**
2800
- // ——画面側に書き写すと、片方だけ動かしたときに死角(取り直しは止まったのに
2801
- // 反映中が残る時間)ができ、しかも画面は正常に見えるので気づけない
2802
- var REFLECT_TTL_MS = ${JSON.stringify(REFLECT_TTL_MS)};
2803
- var REFLECT_POLL_MS = ${JSON.stringify(REFLECT_POLL_MS)};
2804
- var REFLECT_OPS = ${JSON.stringify(exports.REFLECT_OPS)};
2805
- ${livePending.toString()}
2806
- ${nextCheckDelay.toString()}
2807
- ${settledPending.toString()}
2714
+ // ライブ確認の見切り(#256)。**書き写さない**——テストが固定している値と
2715
+ // 画面が使う値が割れると、「10秒で戻る」検査が通ったまま実機では戻らなくなる
2716
+ var LIVE_CONFIRM_TIMEOUT_MS = ${JSON.stringify(LIVE_CONFIRM_TIMEOUT_MS)};
2808
2717
  // 登録の即時値(registeredNow)の受け入れとマージ。**判定の実体はビルド時の定義**
2809
2718
  // ——画面側に書き写すと、許可リストが2枚になって列挙し忘れが静かに消える
2810
2719
  var REGISTERED_CAP_NAMES = ${JSON.stringify(REGISTERED_CAP_NAMES)};
@@ -4562,79 +4471,6 @@ ${deriveCapTile.toString()}
4562
4471
  return d && d !== "on" ? d : null;
4563
4472
  }
4564
4473
 
4565
- // ---------- 反映待ち(「⏳ 反映中」)----------
4566
- //
4567
- // ## 何のためか
4568
- //
4569
- // 設定を保存してから画面に出るまで、自己チェック(fleet-status.json の作り直し)に
4570
- // 数分かかる。その間、画面は「未設定」「未接続」「未使用」と出す
4571
- // ——**保存はできているのに、事実に反する。** 利用者は「保存できていない」と読んで
4572
- // 同じ操作を繰り返し、「既に登録されています」と言われる(この repo が何度も踏んだ形)。
4573
- //
4574
- // **嘘をやめる。** 反映待ちのあいだは「未設定」と言わず「反映中」と言う。
4575
- //
4576
- // ## 出すのは状態だけ。行は先出ししない
4577
- //
4578
- // 行そのものを先に出すと、保存に失敗していても入っているように見えて気づけないし、
4579
- // 反映が来ないまま消えない行は削除もできない幽霊になる(撤去した pendingGold の
4580
- // 「(画面反映待ち)」バッジと同じ罠)。**必ず期限切れする**ので、反映が来なければ
4581
- // 元の「未設定」に戻る=嘘が残らない。
4582
- //
4583
- // ## 期限は Date.now() - at で見る
4584
- //
4585
- // 🔴 **回数カウンタで数えない。** バックグラウンドタブのスロットリングやスリープ復帰は、
4586
- // 経過時間で見る限り自己修復する(遅れて起きたタイマがその回で期限切れを落とす)。
4587
-
4588
- /** 取り直しのタイマ(setTimeout の連鎖。setInterval は使わない) */
4589
- var reflectTimer = null;
4590
-
4591
- /**
4592
- * 保存先のキー。**案件ごとに分ける**(tutStoreKey と同じ作法)。
4593
- *
4594
- * intake が無い案件では案件キーが空文字になるが、それでも他案件のキーとは別物なので、
4595
- * 別の案件の反映待ちを読むことはない。
4596
- *
4597
- * **cortex.pendingGold: と紛らわしい名前にしない。** 前方一致で掃除するコードが
4598
- * 後から書かれると事故る(撤去済みの pendingGold と別語にしてある)。
4599
- */
4600
- function reflectStoreKey() {
4601
- return "cortex.reflecting:" + ((SITE.intake && SITE.intake.projectKey) || "");
4602
- }
4603
-
4604
- /**
4605
- * localStorage は使えない環境がある(プライベートモード等)。**必ず try/catch で囲む**
4606
- *
4607
- * 🔴 **読み込み側でも「既定は記録しない」を効かせる。** 書き込み側(reflectEntryOf)が
4608
- * REFLECT_OPS に無い op を弾いても、localStorage の中身は**別バージョンの画面・
4609
- * 手編集・壊れた値**でいくらでも汚れる。settledPending は「判定できない能力は
4610
- * 落とさない(期限に任せる)」設計なので、**表に無い cap の記録は期限(15分)まで
4611
- * そのまま「反映中」として画面に出る**——本当の状態を隠す。
4612
- * op / cap / dir を対応表と突き合わせ、1つでも食い違えば落とす。
4613
- */
4614
- function readReflecting() {
4615
- try {
4616
- var raw = localStorage.getItem(reflectStoreKey());
4617
- var v = raw ? JSON.parse(raw) : null;
4618
- if (!Array.isArray(v)) return [];
4619
- return v.filter(function (e) {
4620
- if (!e || typeof e.at !== "number" || !Number.isFinite(e.at)) return false;
4621
- if (!e.cap || !e.projectKey) return false;
4622
- var d = REFLECT_OPS[String(e.op)];
4623
- // 対応表に無い op(記録してはいけないもの・未知のもの)は落とす
4624
- if (!d) return false;
4625
- // 表と食い違う cap / dir も落とす(表の cap 以外は settle できない)
4626
- return d.cap === e.cap && d.dir === e.dir;
4627
- });
4628
- } catch (e) { return []; }
4629
- }
4630
-
4631
- function writeReflecting(list) {
4632
- try {
4633
- if (!list || !list.length) localStorage.removeItem(reflectStoreKey());
4634
- else localStorage.setItem(reflectStoreKey(), JSON.stringify(list));
4635
- } catch (e) {}
4636
- }
4637
-
4638
4474
  /** その能力の情報源(fleet のスナップショット由来) */
4639
4475
  function internalSourceOf(cap) {
4640
4476
  var list = fleetVal("internalSources") || [];
@@ -4749,183 +4585,12 @@ ${deriveCapTile.toString()}
4749
4585
  if (KEY_SLOTS.figma) fn(KEY_SLOTS.figma);
4750
4586
  }
4751
4587
 
4752
- /**
4753
- * 能力ごとの「**スナップショットが答えている集合**」。
4754
- *
4755
- * 🔴 **「画面に出ているか」で判定してはいけない。** 追加・削除の応答には一覧が
4756
- * 添えられていて adoptTargets がその場で差し替えるので、**記録した瞬間に
4757
- * 「もう出ている」と判定されて消え、機能が丸ごと無意味になる**(査読者が実測)。
4758
- *
4759
- * 引くのは backlogTargetsNow ——「未設定」を出すか決めているのと**同一の関数**。
4760
- * 別々に書くと、片方が「未設定」と言い、もう片方が「反映済み」と言う状態を作れてしまう。
4761
- *
4762
- * 載っていない能力は判定できない(null)=落とさず期限に任せる。
4763
- * ただし REFLECT_OPS がその能力の op を記録しないので、実際には溜まらない。
4764
- */
4765
- function reflectPresent() {
4766
- var out = {};
4767
- out["課題管理"] = backlogTargetsNow(internalSourceOf("課題管理"));
4768
- return out;
4769
- }
4770
-
4771
- /** いま「反映中」と言うべきもの。**読むたびに期限切れ・反映済みを落として保存し直す** */
4772
- function reflectingNow() {
4773
- var before = readReflecting();
4774
- if (!before.length) return before;
4775
- var after = settledPending(livePending(before, Date.now()), reflectPresent());
4776
- if (after.length !== before.length) writeReflecting(after);
4777
- return after;
4778
- }
4779
-
4780
- /** 送った操作から反映待ちの1件を組み立てる。**対応表に無い op は必ず null** */
4781
- function reflectEntryOf(op, payload, refreshUrl) {
4782
- var d = REFLECT_OPS[String(op)];
4783
- if (!d) return null;
4784
- var t = null;
4785
- if (d.from === "url") t = backlogTargetOf(payload.url);
4786
- else if (d.from === "backlogProjectKey") t = { domain: "", projectKey: String(payload.backlogProjectKey || "").toUpperCase() };
4787
- // 対象を特定できないものは記録しない(settle できず、期限まで出っぱなしになる)
4788
- if (!t || !t.projectKey) return null;
4789
- return {
4790
- cap: d.cap, op: String(op), dir: d.dir,
4791
- domain: t.domain || "", projectKey: t.projectKey,
4792
- at: Date.now(), url: refreshUrl || "",
4793
- };
4794
- }
4795
-
4796
- /**
4797
- * 反映待ちとして記録し、その場で画面を「反映中」に差し替える。
4798
- *
4799
- * **同じ接続先の古い記録は捨てる**(追加のあと削除、のような入れ違いを残さない)。
4800
- */
4801
- function rememberReflecting(payload, refreshUrl) {
4802
- var e = reflectEntryOf(payload && payload.op, payload || {}, refreshUrl);
4803
- if (!e) return;
4804
- var list = readReflecting().filter(function (x) {
4805
- return !(x.cap === e.cap && x.projectKey === e.projectKey && x.domain === e.domain);
4806
- });
4807
- list.push(e);
4808
- writeReflecting(list);
4809
- // **開いているモーダルの中も含めてその場で差し替える。** 削除で最後の1件を外すと
4810
- // 応答で一覧が空になり「未設定」の1行が出る——記録しただけでは、その嘘が残る
4811
- repaintReflect(document);
4812
- scheduleReflectRefresh();
4813
- }
4814
-
4815
- // 差し替える文言。**engine 側の文言は変えない**——engine の detail は
4816
- // 「そのスナップショット時点では未設定だった」という事実で、嘘ではない。
4817
- // 変えるのは画面の出し分けだけ。
4818
- var REFLECT_TEXT = {
4819
- empty: "⏳ 反映中 — いま変更した接続先を画面に反映しています(数分かかります)。",
4820
- note: "⏳ 反映中(いま変更した内容を画面に反映しています)",
4821
- sub: "⏳ 反映中",
4822
- // 見出し下の説明は出さない(下の「反映中」が説明している)
4823
- desc: "",
4824
- };
4825
-
4826
- /**
4827
- * 反映が終わったときに、その箇所を**いまのスナップショットから引き直す**関数を
4828
- * 走らせている最中か。
4829
- *
4830
- * 引き直しは一覧の描き直し(paint)まで含み、その中でまた applyReflect が呼ばれる。
4831
- * 目印が無いと無限に呼び合うので、内側は下地(data-reflect-base)に落とす。
4832
- */
4833
- var reflectRestoring = false;
4834
-
4835
- /**
4836
- * 「未設定」と言っている箇所に印を付ける。反映待ちのあいだだけ文言を差し替える。
4837
- *
4838
- * @param role empty=一覧の0件行 / note=能力カードの「未使用」 /
4839
- * sub=タイルの2行目 / desc=モーダルの見出し下
4840
- * @param fresh 🔴 **反映が届いたときに、届いたデータからこの箇所を引き直す関数。**
4841
- * 引数はその節点。{ text, hidden } の形で返す(返さなければ下地へ落とす)。
4842
- * **省くと「反映中 → 未設定」に戻る**——applyReflect のコメントを読むこと。
4843
- */
4844
- function reflectMark(node, cap, role, fresh) {
4845
- node.setAttribute("data-reflect-cap", cap);
4846
- node.setAttribute("data-reflect-role", role);
4847
- node.setAttribute("data-reflect-base", node.textContent || "");
4848
- if (fresh) node.__reflectFresh = fresh;
4849
- applyReflect(node, reflectingNow());
4850
- return node;
4851
- }
4852
-
4853
- function applyReflect(node, live) {
4854
- var cap = node.getAttribute("data-reflect-cap");
4855
- var role = node.getAttribute("data-reflect-role");
4856
- var hit = null;
4857
- for (var i = 0; i < live.length; i++) if (live[i].cap === cap) { hit = live[i]; break; }
4858
- if (!hit) {
4859
- // ------------------------------------------------------------------
4860
- // 反映が来た(あるいは期限が切れた)。
4861
- //
4862
- // 🔴 **下地(data-reflect-base)へ戻してはいけない。** 下地は
4863
- // **モーダルを開いた時点=古いスナップショットで作った文字列**で、その中身は
4864
- // 「未接続」「未設定」「未使用」——**この機能が直そうとしている誤読そのもの**。
4865
- // しかも settle が起きるのは「**新しいスナップショットが届いた**」瞬間なので、
4866
- // 戻した先は既に事実ではない。
4867
- //
4868
- // モーダルを開いたまま待つと refreshDataSources が下地を作り直さない
4869
- // (開いているモーダルの中身が切り離されるため、あのガードは外せない)ので、
4870
- // 「⏳ 反映中」→(数分)→「未設定」が**閉じるかリロードするまで残った**。
4871
- //
4872
- // **届いたデータで引き直す。** 引き直せない箇所だけ下地に落とす。
4873
- // ------------------------------------------------------------------
4874
- var f = null;
4875
- if (node.__reflectFresh && !reflectRestoring) {
4876
- reflectRestoring = true;
4877
- try { f = node.__reflectFresh(node); } catch (e) { f = null; }
4878
- reflectRestoring = false;
4879
- }
4880
- node.textContent = f && typeof f.text === "string" ? f.text : (node.getAttribute("data-reflect-base") || "");
4881
- node.hidden = !!(f && f.hidden);
4882
- return;
4883
- }
4884
- var t = REFLECT_TEXT[role] || "";
4885
- node.textContent = t;
4886
- // 「出さない」箇所は空にして hidden にする。**消さずに残す**のは、
4887
- // 反映が終わったときに元の文へ戻す先が要るため
4888
- node.hidden = t === "";
4889
- if (role === "empty" && hit.url) {
4890
- node.appendChild(document.createTextNode(" "));
4891
- node.appendChild(el("a", { href: hit.url, target: "_blank", rel: "noopener", text: "反映の状況 →" }));
4892
- }
4893
- }
4894
-
4895
- /**
4896
- * 反映中の箇所だけを描き直す。
4897
- *
4898
- * 🔴 **これが「モーダルを開いたまま待つ」への答え。** 画面へ反映する唯一の手段だった
4899
- * refreshDataSources は、モーダルが開いている間は何もしない(下地を入れ替えると
4900
- * 開いているモーダルの中身が切り離されるので、**そのガードは外せない**)。
4901
- * ここは該当箇所だけを局所的に差し替えるので、開いたままでも追いつく。
4902
- */
4903
- function repaintReflect(root) {
4904
- if (!root || !root.querySelectorAll) return;
4905
- var live = reflectingNow();
4906
- var nodes = root.querySelectorAll("[data-reflect-role]");
4907
- for (var i = 0; i < nodes.length; i++) applyReflect(nodes[i], live);
4908
- }
4909
-
4910
- /**
4911
- * 次の取り直しを予約する。
4912
- *
4913
- * 🔴 **反映待ちが1件も無ければタイマを張らない。** typeof fetch のガードでは足りない
4914
- * ——テストは load の**後**に fetch を注入するので、そこで張ってしまうと
4915
- * jsdom が閉じられず node --test が返らなくなる(この repo で実際に踏んだ形)。
4916
- */
4917
- function scheduleReflectRefresh() {
4918
- if (reflectTimer) return;
4919
- var delay = nextCheckDelay(reflectingNow(), Date.now());
4920
- if (delay == null) return;
4921
- reflectTimer = setTimeout(reflectTick, delay);
4922
- }
4923
-
4924
4588
  // ---------- 未確定の行がある間の取り直し(段階1の前提)----------
4925
4589
  //
4926
- // 🔴 **「反映中」の記録がある間だけ取り直す**のをやめ、**まとめ表がまだ答えていない
4927
- // 行がある間**取り直す。registeredNow が入ると「反映中」は追加した瞬間に settle して
4928
- // 消えるので、これが無いと**登録した直後に画面が二度と更新されなくなる。**
4590
+ // 🔴 **取り直しの根拠は「まとめ表がまだ答えていない行があること」。** 退役した
4591
+ // 「反映中」の記録(#221)はこれを持てなかった——registeredNow が入ると、記録は
4592
+ // 追加した瞬間に「もう出ている」と判定されて消えるので、**登録した直後に画面が
4593
+ // 二度と更新されない**状態になっていた。
4929
4594
  //
4930
4595
  // 経路は増やさない(loadLiveFleet 1本)。取得の口が2つになると、上に出す
4931
4596
  // 「◯◯時点」がどちらの値を指すのか説明できなくなる。
@@ -4963,17 +4628,6 @@ ${deriveCapTile.toString()}
4963
4628
  loadLiveFleet();
4964
4629
  }
4965
4630
 
4966
- function reflectTick() {
4967
- reflectTimer = null;
4968
- var live = reflectingNow();
4969
- // **期限に達した回でも、一度だけ描き直してから止める。** これが無いと
4970
- // 「取り直しは止まったのに反映中の文字が残る」時間ができる
4971
- repaintReflect(document);
4972
- // **新しい取得経路を作らない。** 2箇所になると「いつ時点の値か」が説明できなくなる
4973
- if (live.length) loadLiveFleet();
4974
- scheduleReflectRefresh();
4975
- }
4976
-
4977
4631
  /**
4978
4632
  * その能力にそのアダプタの操作を出してよいか(#125②)。
4979
4633
  *
@@ -5042,22 +4696,32 @@ ${deriveCapTile.toString()}
5042
4696
  // ようになったので、未接続の能力を押したときに中身が空になる場面が増えた。
5043
4697
  // 「設定できるものがありません」だけだと、繋げないのか繋ぐ必要が無いのかが分からない
5044
4698
  if (note) {
5045
- var noteEl = el("p", { class: "modal-desc", text: note });
5046
- // 反映が届いたら**届いたスナップショットから引き直す**(開いた時点の
5047
- // 「未接続(…)」へ戻さない)。作り方はタイル側の modalNote と同じ式。
4699
+ // 🔴 **未接続の一文だけは、開いた時点の値で引き直す。** 渡された note
4700
+ // **タイルを作ったときの**スナップショット由来で、そのあとライブ確認や登録の
4701
+ // 反映が届いている。とくに**登録した行がある能力に「接続先が設定されていません」**
4702
+ // と出すのは、この画面が何度も直してきた嘘(タイル側は抑制をこちらへ委ねている
4703
+ // ——renderAdapterTile の modalNote の項)。
4704
+ //
4705
+ // 抑制してよいのは unset 由来のときだけ。この経路は「OFFに設定されています」
4706
+ // 「**設定に異常があります**」も同じ枠で運んでいるので、無条件に引き直すと
4707
+ // 異常が画面から消える——直したい誤読より悪い。
4708
+ //
5048
4709
  // ⚠ 引き直しでは能力の代表アダプタの状態を使う(openCapabilityModal は
5049
4710
  // どのアダプタのタイルから開かれたかを知らない。カタログは今日どの能力も1個)
5050
- if (noteIsUnset) reflectMark(noteEl, cap, "desc", function () {
4711
+ var noteText = note;
4712
+ if (noteIsUnset) {
5051
4713
  // 足した行があるなら、この一文(接続先が設定されていません)はもう事実ではない
5052
- if (capPending(cap)) return { text: "", hidden: true };
5053
4714
  var capsNow = capsView();
5054
4715
  var dNow = capabilityDisplayState(cap, capsNow);
5055
4716
  var infoNow = capsNow[cap];
5056
- var t = dNow && dNow !== "on" && infoNow && infoNow.detail
5057
- ? (CAP_STATE_TEXT[dNow] || "") + "(" + infoNow.detail + ")"
5058
- : "";
5059
- return { text: t, hidden: t === "" };
5060
- });
4717
+ noteText = capPending(cap) || !(dNow && dNow !== "on" && infoNow && infoNow.detail)
4718
+ ? ""
4719
+ : (CAP_STATE_TEXT[dNow] || "") + "(" + infoNow.detail + ")";
4720
+ }
4721
+ var noteEl = el("p", { class: "modal-desc", text: noteText });
4722
+ // **消さずに hidden で伏せる。** 節点を残しておかないと、この一文を探している
4723
+ // 検査が「文言が変わった」と「箱ごと無くなった」を見分けられない
4724
+ noteEl.hidden = noteText === "";
5061
4725
  m.modal.appendChild(noteEl);
5062
4726
  }
5063
4727
  if (!items.length) {
@@ -5277,25 +4941,8 @@ ${deriveCapTile.toString()}
5277
4941
  // いま登録した行が一覧に出ているのに「この案件では設定されていません」と並ぶ
5278
4942
  var inactive = s.enabled === false && !capPending(s.kind);
5279
4943
  if (inactive) {
5280
- // **反映待ちのあいだは「⏳ 反映中」に差し替える(消さない)。** 消すと
5281
- // .ds-inactive の灰色だけが残り、なぜ灰色なのかが読めなくなる
5282
- col.appendChild(reflectMark(
5283
- el("span", { class: "ds-inactive-note", text: "未使用(この案件では設定されていません)" }),
5284
- s.kind, "note",
5285
- // **反映が届いたら、届いたスナップショットで判定し直す。** 下地へ戻すと
5286
- // 「⏳ 反映中」→「未使用(この案件では設定されていません)」になり、
5287
- // データが正しくなった**あと**に嘘をつく。灰色の見た目もここで外す
5288
- //(節ごとの作り直しはモーダルを閉じるまで走らない)
5289
- function (n) {
5290
- var srcNow = internalSourceOf(s.kind);
5291
- var stillInactive = !!(srcNow && srcNow.enabled === false) && !capPending(s.kind);
5292
- if (!stillInactive) {
5293
- var card = n && n.closest ? n.closest(".ds-card") : null;
5294
- if (card) card.classList.remove("ds-inactive");
5295
- }
5296
- return { text: "未使用(この案件では設定されていません)", hidden: !stillInactive };
5297
- },
5298
- ));
4944
+ // **灰色(.ds-inactive)だけにしない。** 注記が無いと、なぜ灰色なのかが読めない
4945
+ col.appendChild(el("span", { class: "ds-inactive-note", text: "未使用(この案件では設定されていません)" }));
5299
4946
  }
5300
4947
  // id は接続マップのノードclickの飛び先(アンカー)
5301
4948
  // **id は付けない。** #101 以前はこのカードが接続マップの飛び先だったが、
@@ -5713,7 +5360,11 @@ ${deriveCapTile.toString()}
5713
5360
  * ——ここで引き直すと、タイルの色(st)を決めた材料と2回引くことになり、
5714
5361
  * 片方だけずれる余地ができる。null なら規則5=従来表示へフォールバック
5715
5362
  */
5716
- function tileSub(cap, st, derived, adapter, ins, srcs, derive) {
5363
+ /**
5364
+ * @param confirming 初回のライブ確認を待っている(#256)。**状態の断言だけを
5365
+ * 「確認中」に差し替える**——件数・最終同期は焼いた事実なので下でそのまま積む
5366
+ */
5367
+ function tileSub(cap, st, derived, adapter, ins, srcs, derive, confirming) {
5717
5368
  var internal = ins.filter(function (s) { return s.kind === cap; })[0];
5718
5369
  var live = srcs.filter(function (s) { return externalCapability(s.type) === cap; });
5719
5370
  var parts = [];
@@ -5724,7 +5375,13 @@ ${deriveCapTile.toString()}
5724
5375
  //
5725
5376
  // 🔴 **ここが正本からの導出(下)より先。** 意図的に OFF にした能力に
5726
5377
  // 「登録済み(接続は未確認です)」と出さないため(deriveCapTile の項)
5727
- if (derived && derived !== "on") {
5378
+ if (confirming) {
5379
+ // 🔴 **確認中は状態を言わない(#256)。** 下の分岐はどれも焼いた値からの断言で、
5380
+ // ライブが届くと訂正される=瞬きの出どころ。**語は既存の中間状態と揃える**
5381
+ //(DS_GATE_PENDING「登録状況を確認中です」)——新しい言い方を増やすと、
5382
+ // 同じ画面で「確認中」を意味する語が2つ並ぶ
5383
+ parts.push(DS_GATE_PENDING.text);
5384
+ } else if (derived && derived !== "on") {
5728
5385
  parts.push((CAP_STATE_MARK[st] || "") + (CAP_STATE_TEXT[derived] || "状態が判定できません"));
5729
5386
  } else if (derive) {
5730
5387
  // 🔴 **正本(登録・実測)からの導出(0.12.0・#231 / #235 / #227)。**
@@ -5871,42 +5528,33 @@ ${deriveCapTile.toString()}
5871
5528
  // カタログ外の値はここへ来ない(capabilityAdapter が弾く)。旧データの "none" だけは
5872
5529
  // 素通しすると顧客の画面に「none」というタイル名が出るので、上で空にしてある
5873
5530
  var rawTool = adapter || "";
5874
- // **2行目は反映待ちのあいだ「⏳ 反映中」にする。** モーダルを開かなくても分かる
5875
- // ようにするのが目的だが、別のバッジを足すのではなく**この行を差し替える**
5876
- // ——抑制しないと、同じタイルの中で「⏳ 反映中」と「⦿ 未接続」が並んで矛盾する。
5877
- // **抑制するのは unset のときだけ**(off / broken は残す。R4 と同じ理由)
5878
- // まとめ表がまだ知らない行がある能力は、**行と同じ語**に倒す(未接続とも接続OKとも言わない)
5531
+ // **2行目の状態は、まとめ表がまだ知らない行がある能力では断言しない**——
5532
+ // **行と同じ語**に倒す(未接続とも接続OKとも言わない)。モーダルを開かなくても
5533
+ // 分かるようにするのが目的で、別のバッジを足すのではなくこの行を差し替える
5534
+ // ——並べると、同じタイルの中で2つの答えが矛盾する。
5535
+ // **倒すのは unset のときだけ**(off / broken は残す。R4 と同じ理由)
5879
5536
  // 🔴 **導出が答えているなら、「登録状況を確認中です」より導出が勝つ(0.12.0)。**
5880
5537
  // あの語は「まとめ表しか材料が無く、登録の現在値が分からない」ときの折衷だった。
5881
5538
  // 正本(registeredNow)を読むようになった今は分かる——外した直後は「未接続」、
5882
5539
  // 足した直後は「登録済み(まだ取り込みがありません)」と、**何が起きたかを言える**。
5883
5540
  // ここを逆にすると、まとめ表が追いつく翌朝まで「確認中です」が居座る
5884
5541
  var pend = capPending(cap) && !derive;
5542
+ // 🔴 **初回のライブ確認が答えるまで、状態を断言しない(#256・設計v2 の5)。**
5543
+ // この2行目は焼いた capabilities 由来の状態を先に言っていたので、図と同じ瞬き
5544
+ // (旧状態を見せて1〜2秒後に訂正)をタイルでも起こしていた。**兄弟機能の同型の
5545
+ // 穴は同便で掃く**——図だけ直すと、同じ画面の下半分で同じ嘘が続く。
5546
+ //
5547
+ // **差し替えるのは状態の語だけ。** 件数・名前・最終同期は焼いた**事実**なので
5548
+ // そのまま残す(ライブで訂正されるのは事実ではなく状態の解釈)。対象は導出が
5549
+ // 答える能力だけ(derived が null / "on")=図と同じ規則5の線引き。
5550
+ var confirming = liveConfirming() && (derived === null || derived === "on");
5885
5551
  var subEl = el("div", {
5886
- class: "ds-tile-sub " + (pend ? DS_GATE_PENDING.cls : "ds-gate-" + st),
5887
- text: pend ? DS_GATE_PENDING.text : tileSub(cap, st, derived, adapter, internals, sources, derive),
5888
- });
5889
- // **反映が届いたら、届いたスナップショットから引き直す。** 下地へ戻すと
5890
- // 「⏳ 反映中」→「⦿ 未接続」になる——データが正しくなったあとに嘘をつく。
5891
- // 状態の語も色(ds-gate-*)も、判定はタイルを作るときと同じ式で引く
5892
- if (derived === "unset") reflectMark(subEl, cap, "sub", function (n) {
5893
- var capsNow = capsView();
5894
- var insNow = fleetVal("internalSources") || [];
5895
- var srcsNow = fleetVal("externalSources") || [];
5896
- var dNow = adapter === capabilityAdapter(cap, capsNow) ? capabilityDisplayState(cap, capsNow) : "unset";
5897
- var stNow = dNow === "unset" && capabilityDisplayState(cap, capsNow) !== "unset"
5898
- ? "idle"
5899
- : capabilityState(cap, stateCtx());
5900
- // 正本からの導出も、タイルを作るときと同じ式で引き直す(0.12.0)。
5901
- // ここを省くと、反映が届いた瞬間だけ「● 接続OK」へ戻る
5902
- var deriveNow = dNow && dNow !== "on" ? null : capDerivation(cap);
5903
- if (deriveNow) stNow = deriveNow.state;
5904
- var pendNow = capPending(cap) && !deriveNow;
5905
- n.className = "ds-tile-sub " + (pendNow ? DS_GATE_PENDING.cls : "ds-gate-" + stNow);
5906
- return {
5907
- text: pendNow ? DS_GATE_PENDING.text : tileSub(cap, stNow, dNow, adapter, insNow, srcsNow, deriveNow),
5908
- hidden: false,
5909
- };
5552
+ class: "ds-tile-sub " + (confirming || pend ? DS_GATE_PENDING.cls : "ds-gate-" + st),
5553
+ // **反映待ち(pend)の形は変えない。** あちらは「足した行がまとめ表に載るまで」の
5554
+ // 折衷で、件数を添えると足した分を数えたようにも読める。確認中は焼いた件数を残す
5555
+ text: pend
5556
+ ? DS_GATE_PENDING.text
5557
+ : tileSub(cap, st, derived, adapter, internals, sources, derive, confirming),
5910
5558
  });
5911
5559
  var body = el("div", { class: "ds-tile-body" }, [
5912
5560
  el("div", { class: "ds-tile-name", text: (rawTool && (CM_TOOL_LABEL_C[rawTool] || rawTool)) || cap }),
@@ -5975,7 +5623,7 @@ ${deriveCapTile.toString()}
5975
5623
  el("span", { class: "ds-tile-chev", text: "›" }),
5976
5624
  ]);
5977
5625
  // ⚠ **ここで足した行(段階1)を見て抑制しない。** unset の一文は開くときに引き直される
5978
- // (openCapabilityModal reflectMark の fresh 側が capPending を見る)ので二重になるうえ、
5626
+ // (openCapabilityModal capPending を見る)ので二重になるうえ、
5979
5627
  // ここで抑えると off / broken まで消える——R4 で「消さない」と決めたもの
5980
5628
  var modalNote = derived && derived !== "on" && capInfo && capInfo.detail
5981
5629
  ? CAP_STATE_TEXT[derived] + "(" + capInfo.detail + ")"
@@ -6748,29 +6396,12 @@ ${deriveCapTile.toString()}
6748
6396
  var listEl = el("div", { class: "ds-list" });
6749
6397
  // **0件の1行は一覧の外に置く**(Figma / Drive と同じ形)。中に .ds-card として
6750
6398
  // 置くと「行が1件ある」と読まれ、一覧を数えるテストにも行として数えられる
6751
- // **反映待ちのあいだは「未設定」と言わない。** 保存はできているので事実に反する
6399
+ //
6400
+ // **出し入れは paint() が持つ。** 開いたまま待っているあいだに新しい値が届いたときの
6401
+ // 差し替えは box.__rebuild(取り直しの応答から呼ばれる)——0件の1行だけを
6402
+ // 別経路で追いつかせると、行の一覧と食い違う瞬間ができる
6752
6403
  var EMPTY_TEXT = "同期する接続先はまだ登録されていません(未設定)。";
6753
- var emptyEl = reflectMark(
6754
- el("p", { class: "ds-empty", text: EMPTY_TEXT }),
6755
- "課題管理", "empty",
6756
- // 🔴 **反映が届いたら、届いた一覧で描き直す。** 下地(=この0件の文言)へ戻すと、
6757
- // 反映が届いた瞬間に「⏳ 反映中」→「同期する接続先はまだ登録されていません(未設定)。」
6758
- // になり、**行があるのに無いと言う**。settle は「新しいスナップショットが届いた」
6759
- // ことなので、そのデータで一覧を作り直す(adoptTargets と同じ形)。
6760
- // モーダルを開いたまま待っているときは、これが唯一の描き直しの機会
6761
- function () {
6762
- // まだ箱を作っていない(=最初の描画の途中)。paint() は箱へ出し入れするので、
6763
- // ここで走らせない。最初の描画はこの直後の paint() が受け持つ
6764
- if (!box) return null;
6765
- // 🔴 **応答の一覧を上書きしない。** 追加・削除の応答に添えられた一覧
6766
- //(adoptTargets)は、この画面が持っている中でいちばん新しい正本。
6767
- // スナップショットから引き直すと、外したばかりの接続先が戻って見える
6768
- if (adopted) return null;
6769
- targets = backlogTargetsNow(internalSourceOf("課題管理"));
6770
- paint();
6771
- return { text: EMPTY_TEXT, hidden: false };
6772
- },
6773
- );
6404
+ var emptyEl = el("p", { class: "ds-empty", text: EMPTY_TEXT });
6774
6405
  // 追加・削除の結果はここに1つだけ出す(行を描き直しても消えない位置)
6775
6406
  var msg = el("div", { class: "ds-toggle-msg" });
6776
6407
 
@@ -6863,9 +6494,6 @@ ${deriveCapTile.toString()}
6863
6494
  var here = emptyEl.parentNode === box;
6864
6495
  if (!targets.length && !here) box.insertBefore(emptyEl, listEl.nextSibling);
6865
6496
  else if (targets.length && here) box.removeChild(emptyEl);
6866
- // **出し入れのたびに反映中を当て直す。** 外している間の描き直しはこの行に届かない
6867
- // ので、戻すときに当てないと古い文言のまま出る
6868
- applyReflect(emptyEl, reflectingNow());
6869
6497
  // 🔴 **鍵の欄の出し入れは、描き直しのたびに当て直す(2026-08-22)。** 最初の1件を
6870
6498
  // 足した瞬間に欄が現れ、最後の1件を外した瞬間に消えるのはここが効いているから。
6871
6499
  // 節ごとの差し替え(refreshDataSources)はモーダルが開いている間は何もしない
@@ -7696,28 +7324,14 @@ ${deriveCapTile.toString()}
7696
7324
  msg.appendChild(document.createTextNode(" "));
7697
7325
  msg.appendChild(el("a", { href: j.runsUrl, target: "_blank", rel: "noopener", text: "実行状況を見る →" }));
7698
7326
  }
7699
- // **画面への反映も起きていることを伝える。** 一覧はスナップショットから描かれるので、
7700
- // 「保存した」だけでは「一覧が変わらない=失敗した」と読まれる。
7701
- // 数分かかること・再読み込みで正式な一覧に載ることまで書く(黙っていると待てない)
7702
- // 反映にかかる時間はサーバの文言(reflectionNote)が言うので、ここでは繰り返さない。
7703
- // 二重に書くと同じ一文に同じ話が並ぶ
7704
- if (j.refreshUrl) {
7705
- msg.appendChild(document.createTextNode(" "));
7706
- msg.appendChild(el("a", { href: j.refreshUrl, target: "_blank", rel: "noopener", text: "反映の状況 →" }));
7707
- }
7327
+ // **「反映の状況 →」のリンクは無くなった(#221・2026-08-25)。** 指していたのは
7328
+ // 「画面を追いつかせるための fleet-status の実行」で、その起動ごと撤去した
7329
+ // ——保存の直後に読み直す(下の loadLiveFleet)ので、待つ対象がもう無い。
7330
+ // 応答に refreshUrl が載ることも二度と無い(Lambda 側で撤去済み)。
7331
+
7708
7332
  // 🔴 **語を戻すのは onDone より先。** removeButton の onDone は押せない状態へ
7709
7333
  // 戻す(外した行のボタン)ので、後に置くと「処理中…」の語が居座る
7710
7334
  endBusy();
7711
- // **反映待ちとして記録する。** 一覧はスナップショットから描かれるので、
7712
- // 保存できていても数分は「未設定」と出る。その間だけ「⏳ 反映中」に差し替える。
7713
- //
7714
- // ⚠ **onDone の中に置かない。** removeRow は onDone に null を渡しているので、
7715
- // 中に置くと**接続先の「削除」だけ黙って記録されない**(remove の判定が守りたい当のケース)。
7716
- //
7717
- // - changed === false(既に同じものが入っていた)は記録しない——待つものが無い
7718
- // - refreshUrl が無い(自己チェックを起こせなかった)ときも記録しない
7719
- // ——待っても来ないので「反映中」が嘘になる
7720
- if (j.changed !== false && j.refreshUrl) rememberReflecting(payload, j.refreshUrl);
7721
7335
  // **保存が効いたら、いまの登録を読み直す(段階1)。** 一覧はまとめ表(夜間バッチ)から
7722
7336
  // 描かれるので、保存しただけでは行が出ない。ここで読み直すと、登録ファイルの中身が
7723
7337
  // 返ってきて**足りない行として一覧に出る**——さらに「まとめ表がまだ答えていない行」が
@@ -8398,7 +8012,7 @@ ${deriveCapTile.toString()}
8398
8012
  * 🔴 **チャンネルやフォルダを足すのは「モーダルを開いたまま」。** 節ごとの差し替え
8399
8013
  * (refreshDataSources)はモーダルが開いている間は何もしない——下地を入れ替えると
8400
8014
  * 開いているモーダルの中身が切り離され、入力中の値も「保存中…」も消えるので、
8401
- * **あのガードは外せない**。だから「反映中」と同じく、**その箇所だけを差し替える口**を置く。
8015
+ * **あのガードは外せない**。だから**その箇所だけを差し替える口**を置く。
8402
8016
  *
8403
8017
  * 差し替えるのは**レコードの一覧だけ**(追加フォーム・鍵の欄・結果メッセージは触らない)。
8404
8018
  * 課題管理の adoptTargets が先にこの形を採っている。
@@ -8447,6 +8061,57 @@ ${deriveCapTile.toString()}
8447
8061
  * **操作タブに入り直したときだけ**もう一度取りに行く(ポーリングはしない)。
8448
8062
  */
8449
8063
  var liveFleetRequested = false;
8064
+
8065
+ /* ---------------------------------------------------------------------------
8066
+ * 「確認中 → 確定」の2段表示(#256)
8067
+ * ---------------------------------------------------------------------------
8068
+ * 図とタイルの初期表示は**夜間ビルド時点の状態を断言**していた。ページを開くと
8069
+ * ライブ確認(op=state)が走り、届いた瞬間に正本からの導出で当て直すので、
8070
+ * 焼いた状態とライブが食い違う案件では**「先に旧状態を見せて、あとで訂正する」
8071
+ * 瞬き**になる(リロード直後に線が一瞬つながって見え、1〜2秒後に消える)。
8072
+ *
8073
+ * **旧状態の先行断言をやめる。** 取得が始まる瞬間に「確認中」へ倒し、応答が届いて
8074
+ * から確定する。図の形(ノード・線のジオメトリ)は焼いたものをそのまま使う
8075
+ * ——捨てるとオフライン時のフォールバックと曲線の正本を失う。
8076
+ * ------------------------------------------------------------------------- */
8077
+
8078
+ /**
8079
+ * ライブ確認が実際に飛ぶ構成か。**loadLiveFleet の発火条件そのもの。**
8080
+ *
8081
+ * 🔴 **「確認中」を出すかどうかも、この1本で決める(設計v2 の2)。** 条件を書き写すと、
8082
+ * intake が無い案件・fetch を持たない環境では**取得が始まらないのに確認中が出たまま
8083
+ * 固まる**——直そうとした瞬きより悪い(永久に何も言わない画面になる)。
8084
+ */
8085
+ function liveConfirmPossible() {
8086
+ return !!SITE.intake && typeof fetch === "function";
8087
+ }
8088
+ /**
8089
+ * 最初のライブ確認が答えたか(成功・失敗・見切りのどれでも立つ)。
8090
+ *
8091
+ * **能力ごとに持たない。** 応答の到着は1回で、届いたあとに能力ごとの語を決めるのは
8092
+ * 導出(capDerivation)の仕事。ここを能力ごとに割ると判定が2箇所になる。
8093
+ */
8094
+ var liveAnswered = false;
8095
+ /**
8096
+ * 図を「確認中」にしたか。**ページ生存中の最初の取得の1回だけ(設計v3)。**
8097
+ *
8098
+ * loadLiveFleet は初回以外にも何度も呼ばれる(能力モーダルを開くたびの noteOpenCap・
8099
+ * 反映待ちの60秒ポーリング・保存直後の freshWanted 再取得)。呼び出しごとに確認中へ
8100
+ * 戻すと、**確定済みのノードが無関係な操作のたびに確認中→再確定を繰り返す**
8101
+ * ——直す前より体感が悪い再発になる。2回目以降は confirming 段を経ず、
8102
+ * applyStateToConnMap の冪等な当て直しに直接委ねる。
8103
+ */
8104
+ var connMapConfirmingShown = false;
8105
+ /** いま「確認中」を出す局面か(図の初回切り替えとタイルの両方が見る) */
8106
+ function liveConfirming() {
8107
+ return liveConfirmPossible() && !liveAnswered;
8108
+ }
8109
+ /**
8110
+ * 図の3行目に出す確認中の語。一覧側は DS_GATE_PENDING(「登録状況を確認中です」)で、
8111
+ * 図は幅が 148px しかないので短くする。**語彙は同じ「確認中」に揃える。**
8112
+ */
8113
+ var CONN_CONFIRMING_TEXT = "確認中…";
8114
+
8450
8115
  /**
8451
8116
  * op=state に送る能力。**常に全能力**(0.12.0・#231 の正本化)。
8452
8117
  *
@@ -8529,17 +8194,33 @@ ${deriveCapTile.toString()}
8529
8194
  if (fresh) freshWanted = true;
8530
8195
  if (liveFleetRequested) return;
8531
8196
  var intake = SITE.intake;
8532
- if (!intake) return;
8533
- // **fetch の有無を確かめてから呼ぶ。** テスト環境(JSDOM)は window.fetch を持たない。
8534
- // 素で呼ぶと描画中の ReferenceError になり、「画面が例外を出さずに描き切る」検査を割る。
8535
- if (typeof fetch !== "function") return;
8197
+ // **intake が無い/fetch が無い(JSDOM)なら取りに行かない。** 素で fetch を呼ぶと
8198
+ // 描画中の ReferenceError になり、「画面が例外を出さずに描き切る」検査を割る。
8199
+ // 判定は liveConfirmPossible に一本化してある——「確認中」を出す条件と同じでないと、
8200
+ // 取得が始まらない案件に確認中が出たまま残る(#256)
8201
+ if (!liveConfirmPossible()) return;
8536
8202
  liveFleetRequested = true;
8537
8203
  var useFresh = freshWanted;
8538
8204
  freshWanted = false;
8205
+ // 🔴 **図を確認中に倒すのは、fetch を発火する直前の1回だけ(#256・設計v3)。**
8206
+ // ここより後(応答の中)でやると意味が無く、ここより前(関数の入口)でやると
8207
+ // 取りに行かない経路でも確認中になる
8208
+ if (!connMapConfirmingShown) {
8209
+ connMapConfirmingShown = true;
8210
+ showConfirmingConnMap();
8211
+ }
8212
+ // **10秒で見切る(設計v2 の1)。** 応答が返らないまま待ち続けると、確認中が永久に
8213
+ // 残る=「何も言わない画面」になる。見切ったら失敗と同じ扱い(焼いた表示へ復元)。
8214
+ // AbortController が無い環境では見切らない(素の fetch は中断できない)
8215
+ var ctrl = typeof AbortController === "function" ? new AbortController() : null;
8216
+ var giveUp = ctrl ? setTimeout(function () { ctrl.abort(); }, LIVE_CONFIRM_TIMEOUT_MS) : null;
8217
+ /** 見切りのタイマを外す。**応答が来たら必ず呼ぶ**(残すと次の取得を巻き込んで中断する) */
8218
+ var clearGiveUp = function () { if (giveUp != null) { clearTimeout(giveUp); giveUp = null; } };
8539
8219
  var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
8540
8220
  fetch(pfx + "op=state&t=" + encodeURIComponent(intake.token), {
8541
8221
  method: "POST",
8542
8222
  headers: { "content-type": "application/json" },
8223
+ signal: ctrl ? ctrl.signal : undefined,
8543
8224
  // **常に全能力を要求する(ALL_STATE_CAPS の項)。** 知らない値は Lambda 側が黙って
8544
8225
  // 捨てる(断らない)ので、画面と Lambda の版が食い違っても接続状況の表示は消えない
8545
8226
  // **保存直後だけ据え置きを迂回させる。** これが無いと、サーバが保存前の答えを返し、
@@ -8548,8 +8229,15 @@ ${deriveCapTile.toString()}
8548
8229
  })
8549
8230
  // **非2xxも失敗として扱う。** ビューアとLambdaの版は独立に進むので、
8550
8231
  // op=state 未対応のLambdaは 400 を返す。通信エラーだけを見ていると空表示になる。
8551
- .then(function (res) { if (!res.ok) throw new Error("http " + res.status); return res.json(); })
8232
+ .then(function (res) {
8233
+ clearGiveUp();
8234
+ if (!res.ok) throw new Error("http " + res.status);
8235
+ return res.json();
8236
+ })
8552
8237
  .then(function (j) {
8238
+ // 🔴 **タイルの「確認中」を解くのは、下地を作り直す前(#256)。** 後に置くと、
8239
+ // 応答が届いた回のタイルだけ確認中のまま残る(次の再描画まで訂正が出ない)
8240
+ liveAnswered = true;
8553
8241
  // **fleet より先に読む。** fleet-status.json をまだ持たない案件(立ち上げ直後)は
8554
8242
  // 下で throw するが、リアルタイム同期の可否は索引由来なので独立に決まる
8555
8243
  LIVE_REALTIME = (j && j.realtime) || null;
@@ -8577,10 +8265,9 @@ ${deriveCapTile.toString()}
8577
8265
  // モーダルが開いている間・入力中は何もせずに戻るので、中に入れると
8578
8266
  // **モーダルを開いたまま最初の応答が届いた回だけ、図が「接続OK」のまま残る**
8579
8267
  applyStateToConnMap();
8580
- // **モーダルを開いたままでも「反映中」だけは追いつかせる。** refreshDataSources は
8268
+ // **モーダルを開いたままでも一覧は追いつかせる。** refreshDataSources は
8581
8269
  // モーダルが開いている間・入力中は何もしない(下地を入れ替えると開いているモーダルの
8582
- // 中身が切り離される)ので、局所的に差し替える口をここに置く
8583
- repaintReflect(document);
8270
+ // 中身が切り離される)ので、開いている一覧だけを差し替える口をここに置く
8584
8271
  repaintOpenLists();
8585
8272
  // まだ足りない行があるなら取り直しを続ける(無くなったら止める)
8586
8273
  refreshStateTimer();
@@ -8588,13 +8275,20 @@ ${deriveCapTile.toString()}
8588
8275
  if (freshWanted) loadLiveFleet();
8589
8276
  })
8590
8277
  .catch(function () {
8278
+ clearGiveUp();
8591
8279
  // 静的ビルドの値で描き続ける。ただし「古いかもしれない」ことは画面に出す
8592
8280
  LIVE_FLEET = null;
8593
8281
  LIVE_FLEET_FAILED = true;
8594
8282
  liveFleetRequested = false;
8283
+ liveAnswered = true;
8595
8284
  MERGED = null;
8285
+ // 🔴 **確認中に倒した図を焼いた表示へ戻す(#256・設計v2 の3)。** ライブが
8286
+ // 取れなかったときのフォールバックはここに残る——戻さないと「確認中…」の
8287
+ // ままの図が残り、オフラインでは**何も分からない画面**になる。
8288
+ // 「この図は◯◯時点です」の注記は既存のまま(ライブが取れていないときは
8289
+ // 図も一覧も同じ焼いた時点なので、注記なしが正直・設計v2 の4)
8290
+ restoreConfirmingConnMap();
8596
8291
  refreshDataSources();
8597
- repaintReflect(document);
8598
8292
  repaintOpenLists();
8599
8293
  // **失敗しても取り直しは続ける**(期限が来れば止まる)。ここで止めると、
8600
8294
  // 一時的な失敗のあと画面が二度と追いつかない
@@ -8611,9 +8305,6 @@ ${deriveCapTile.toString()}
8611
8305
  // **まとめ表がまだ答えていない行があれば取り直す。** 取得前は0件なので、
8612
8306
  // 実際に張られるのは応答が届いてから(loadLiveFleet が呼び直す)
8613
8307
  refreshStateTimer();
8614
- // **反映待ちがあれば自動で取り直す(再読み込みしても続く)。** 反映待ちが
8615
- // 1件も無ければタイマは張らない
8616
- scheduleReflectRefresh();
8617
8308
  // **取り込みカードと鍵カードは能力セクションへ移した**(#101)。
8618
8309
  // ここで再度出すと二重になる。手動投入は 会議 / 共有資料 の中にある。
8619
8310
  }
@@ -9141,6 +8832,10 @@ ${deriveCapTile.toString()}
9141
8832
  *
9142
8833
  * **導出が答えない能力(規則5・設定が on 以外)は触らない。** 焼いた図がそのまま
9143
8834
  * 残る=従来表示へのフォールバック。タイル側と同じ規律。
8835
+ *
8836
+ * 🔴 **触らない能力は「焼いた表示に戻す」(#256・設計v2 の3)。** 確認中に倒した
8837
+ * あとで素通りすると、規則5の「焼いた図のまま」が**「確認中のまま固まる」に化ける**
8838
+ * ——応答は届いているのに、その能力だけ永久に何も言わない図になる。
9144
8839
  */
9145
8840
  function applyStateToConnMap() {
9146
8841
  var wrap = app.querySelector(".conn-map");
@@ -9148,9 +8843,9 @@ ${deriveCapTile.toString()}
9148
8843
  for (var cap in TILE_CAPS) {
9149
8844
  if (!Object.prototype.hasOwnProperty.call(TILE_CAPS, cap)) continue;
9150
8845
  // 設定そのものが OFF / 異常 / 未設置なら、その語(焼いた図)が正しい
9151
- if (capConfigState(cap)) continue;
8846
+ if (capConfigState(cap)) { restoreBakedConnMap(cap); continue; }
9152
8847
  var d = capDerivation(cap);
9153
- if (!d) continue; // 規則5: 材料が無い → 焼いた図のまま
8848
+ if (!d) { restoreBakedConnMap(cap); continue; } // 規則5: 材料が無い → 焼いた図のまま
9154
8849
  var node = wrap.querySelector('[data-target="ds-' + cap + '"]');
9155
8850
  if (!node) continue;
9156
8851
  node.setAttribute("class", "cm-node cm-" + d.state);
@@ -9169,7 +8864,102 @@ ${deriveCapTile.toString()}
9169
8864
  link.setAttribute("class", "cm-link cm-" + d.state
9170
8865
  + (d.state === "ok" || d.state === "warn" ? "" : " is-hidden"));
9171
8866
  }
8867
+ // 確定したので退避は要らない(設計v3)。**残すと、あとの取得が失敗したときに
8868
+ // 確定済みの表示へ焼いた値が上書きされる**——訂正した嘘が戻ってくる
8869
+ forgetBakedConnMap(node, link);
8870
+ }
8871
+ }
8872
+
8873
+ /**
8874
+ * 図の状態表現を「確認中」に倒す(#256)。**形は触らない。**
8875
+ *
8876
+ * 呼ぶのは loadLiveFleet が最初に fetch を発火する直前の1回だけ
8877
+ *(connMapConfirmingShown の項)。**「確認中にしてから取得を始める」順序にする**
8878
+ * ことで、取得が始まらない経路(intake の無い案件・fetch の無い環境)では
8879
+ * 確認中が絶対に出ない構造にしている(設計v2 の2)。
8880
+ *
8881
+ * 🔴 **上書きする直前に、焼いた表示を DOM のデータ属性へ退避する(設計v2 の3)。**
8882
+ * 退避を JS の変数に持つと、再描画・デバッグのときに DOM と食い違う。属性なら
8883
+ * **DOM が正本**のまま——「いま確認中の能力はどれか」も属性の有無で読める。
8884
+ *
8885
+ * **対象は applyStateToConnMap が当て直す能力だけ**(capConfigState が答えない=
8886
+ * derived が null / "on")。OFF・異常・未設置の語は設定そのものの話で、ライブが
8887
+ * 届いても変わらない——確認中にしたら、応答が来ても戻す相手がいなくなる。
8888
+ *
8889
+ * ⚠ **⚠️バッジ(cm-badge)は触らない。** あれは applyStateToConnMap も当て直して
8890
+ * いない(ビルド時に warn のノードにだけ焼かれ、以後そのまま)。確認中の間だけ
8891
+ * 隠すと、応答で warn が確定したときに**失敗が図から消えたまま**になる
8892
+ * ——「cm-warn を隠さない」の逆をこちらの都合で作る。バッジごと2段化するなら、
8893
+ * 6ノードに常時バッジを焼いて表示を切り替える形(線と同じ)にする別便が要る。
8894
+ */
8895
+ function showConfirmingConnMap() {
8896
+ var wrap = app.querySelector(".conn-map");
8897
+ if (!wrap) return;
8898
+ for (var cap in TILE_CAPS) {
8899
+ if (!Object.prototype.hasOwnProperty.call(TILE_CAPS, cap)) continue;
8900
+ if (capConfigState(cap)) continue;
8901
+ var node = wrap.querySelector('[data-target="ds-' + cap + '"]');
8902
+ if (!node) continue;
8903
+ node.setAttribute("data-cm-baked-class", node.getAttribute("class") || "");
8904
+ node.setAttribute("class", "cm-node cm-pending");
8905
+ var sync = node.querySelector(".cm-sync");
8906
+ if (sync) {
8907
+ // **data-ts は残して textContent だけ差し替える(設計v2 の3)。** 外すと
8908
+ // 復元したときに時刻の整形(renderConnMap の [data-ts])が二度と効かない
8909
+ sync.setAttribute("data-cm-baked-text", sync.textContent);
8910
+ sync.textContent = CONN_CONFIRMING_TEXT;
8911
+ }
8912
+ var link = wrap.querySelector('.cm-link[data-cm-cap="' + cap + '"]');
8913
+ if (link) {
8914
+ // **薄い中立の線を出す(is-hidden を外す)。** 線が消えていると「未接続」を
8915
+ // 断言したことになる。確認中は接続OK/未接続のどちらも言わない
8916
+ link.setAttribute("data-cm-baked-class", link.getAttribute("class") || "");
8917
+ link.setAttribute("class", "cm-link cm-pending");
8918
+ }
8919
+ }
8920
+ }
8921
+
8922
+ /**
8923
+ * 確認中に倒した能力を、焼いた表示へ戻す(#256・設計v2 の3)。
8924
+ *
8925
+ * 呼ぶのは2箇所——**ライブが取れなかったとき**(失敗・非2xx・見切り。全能力ぶん)と、
8926
+ * **応答は届いたが導出が答えなかった能力**(applyStateToConnMap の continue)。
8927
+ *
8928
+ * 退避属性が無ければ何もしない。**「確認中でない能力を戻す」経路を作らない**
8929
+ * ——確定済みのノードに焼いた値を書けば、それは訂正の取り消しになる。
8930
+ */
8931
+ function restoreBakedConnMap(cap) {
8932
+ var wrap = app.querySelector(".conn-map");
8933
+ if (!wrap) return;
8934
+ var node = wrap.querySelector('[data-target="ds-' + cap + '"]');
8935
+ if (!node || !node.hasAttribute("data-cm-baked-class")) return;
8936
+ node.setAttribute("class", node.getAttribute("data-cm-baked-class"));
8937
+ var sync = node.querySelector(".cm-sync");
8938
+ if (sync && sync.hasAttribute("data-cm-baked-text")) {
8939
+ sync.textContent = sync.getAttribute("data-cm-baked-text");
8940
+ }
8941
+ var link = wrap.querySelector('.cm-link[data-cm-cap="' + cap + '"]');
8942
+ if (link && link.hasAttribute("data-cm-baked-class")) {
8943
+ link.setAttribute("class", link.getAttribute("data-cm-baked-class"));
8944
+ }
8945
+ forgetBakedConnMap(node, link);
8946
+ }
8947
+
8948
+ /** 確認中に倒した全能力を焼いた表示へ戻す(ライブが1件も取れなかったとき) */
8949
+ function restoreConfirmingConnMap() {
8950
+ for (var cap in TILE_CAPS) {
8951
+ if (Object.prototype.hasOwnProperty.call(TILE_CAPS, cap)) restoreBakedConnMap(cap);
8952
+ }
8953
+ }
8954
+
8955
+ /** 退避を捨てる(確定した/戻し終えた)。属性の有無が「確認中か」の正本 */
8956
+ function forgetBakedConnMap(node, link) {
8957
+ if (node) {
8958
+ node.removeAttribute("data-cm-baked-class");
8959
+ var sync = node.querySelector(".cm-sync");
8960
+ if (sync) sync.removeAttribute("data-cm-baked-text");
9172
8961
  }
8962
+ if (link) link.removeAttribute("data-cm-baked-class");
9173
8963
  }
9174
8964
 
9175
8965
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.3",
3
+ "version": "0.12.5",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,