@takagaki/cortex-decisions-viewer 0.10.0 → 0.10.2

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 +324 -10
  2. package/package.json +1 -1
package/dist/render.js CHANGED
@@ -15,6 +15,7 @@ exports.materialRowsToAdd = materialRowsToAdd;
15
15
  exports.issueRowsToAdd = issueRowsToAdd;
16
16
  exports.unsettledRegisteredCount = unsettledRegisteredCount;
17
17
  exports.registeredRowsToAdd = registeredRowsToAdd;
18
+ exports.subtractRemovedRegistered = subtractRemovedRegistered;
18
19
  exports.mergeRegisteredFleet = mergeRegisteredFleet;
19
20
  exports.nextStateDelay = nextStateDelay;
20
21
  exports.renderConnectionMap = renderConnectionMap;
@@ -769,9 +770,140 @@ function registeredRowsToAdd(view, reg) {
769
770
  };
770
771
  }
771
772
  /**
772
- * まとめ表の3キー(externalSources / internalSources / backlogTarget)に、足りない行を足す。
773
+ * 登録ファイルから**消された**行を、まとめ表の写しから引く(段A補・2026-08-22)。
773
774
  *
774
- * **足すだけ。** 既存の行・欄は1つも書き換えない。
775
+ * 「解除しました」と出るのに、リロードすると数分間残る——追加は足していたのに、
776
+ * 削除はまとめ表の作り直し待ちだった。登録ファイルは今の正本なので、
777
+ * そこに無い行は消されたということ。
778
+ *
779
+ * 🔴 **能力のキーが在るときだけ引く。** キーが無い=登録ファイルが読めなかった、なので
780
+ * 引くと「読めない」が「全部消えた」に化ける。
781
+ * 🔴 **チャットは origin が "derived" の行だけ。** dev-sources.json 側にも宣言された行
782
+ * (explicit / both)は channels.json から消してもエンジンがまだ読む——隠すと
783
+ * 「読まれているのに画面に無い」という逆向きの嘘になる。
784
+ * 🔴 **チャットの突き合わせは ref だけ(追加側と非対称にしてある)。** まとめ表の
785
+ * workspace はエンジンが実測で埋めることがあり、宣言(登録ファイル)側と食い違う。
786
+ * ID+WSで引くと、実測で埋まった行を**誤って隠す**。ref 一致の登録が1つでも残って
787
+ * いれば引かない——引き損ね(数分の遅れ)は安全側、誤削除は嘘なので。
788
+ */
789
+ function subtractRemovedRegistered(view, reg) {
790
+ const v = view || {};
791
+ if (!reg)
792
+ return v;
793
+ let out = v;
794
+ const set = (patch) => { out = out === v ? Object.assign({}, v, patch) : Object.assign(out, patch); };
795
+ if (Array.isArray(reg.chat) || Array.isArray(reg.dev)) {
796
+ const chatRefs = {};
797
+ for (const r of reg.chat || [])
798
+ if (r && r.ref)
799
+ chatRefs[String(r.ref)] = true;
800
+ const devRefs = {};
801
+ for (const r of reg.dev || [])
802
+ if (r && r.ref)
803
+ devRefs[String(r.ref)] = true;
804
+ const before = v.externalSources || [];
805
+ const after = before.filter((s) => {
806
+ if (!s || !s.ref)
807
+ return true;
808
+ if (s.type === "slack" && Array.isArray(reg.chat)) {
809
+ if (s.origin !== "derived")
810
+ return true;
811
+ return chatRefs[String(s.ref)] === true;
812
+ }
813
+ if (String(s.type || "").indexOf("github") === 0 && Array.isArray(reg.dev)) {
814
+ // 「読み取り停止中」(exclude)の行は登録簿の sources に居ないが、
815
+ // まとめ表はバッジを出すために保持している——引くと停止中の表示ごと消える
816
+ if (s.goldState === "excluded")
817
+ return true;
818
+ return devRefs[String(s.ref)] === true;
819
+ }
820
+ return true;
821
+ });
822
+ if (after.length !== before.length)
823
+ set({ externalSources: after });
824
+ }
825
+ if (Array.isArray(reg.design) || Array.isArray(reg.materials)) {
826
+ const internals = ((out.internalSources || v.internalSources) || []).slice();
827
+ let changed = false;
828
+ if (Array.isArray(reg.design)) {
829
+ const keys = {};
830
+ for (const r of reg.design)
831
+ if (r && r.key)
832
+ keys[String(r.key)] = true;
833
+ for (let i = 0; i < internals.length; i++) {
834
+ const it = internals[i];
835
+ if (!it || it.kind !== REGISTERED_CAP_NAMES.design || !Array.isArray(it.figmaFiles))
836
+ continue;
837
+ const files = it.figmaFiles.filter((f) => f && keys[String(f.key)] === true);
838
+ if (files.length !== it.figmaFiles.length) {
839
+ internals[i] = Object.assign({}, it, { figmaFiles: files });
840
+ changed = true;
841
+ }
842
+ }
843
+ }
844
+ if (Array.isArray(reg.materials)) {
845
+ const ids = {};
846
+ for (const r of reg.materials)
847
+ if (r && r.id)
848
+ ids[String(r.id)] = true;
849
+ for (let i = 0; i < internals.length; i++) {
850
+ const it = internals[i];
851
+ if (!it || it.kind !== REGISTERED_CAP_NAMES.materials || !Array.isArray(it.driveFolderIds))
852
+ continue;
853
+ const kept = it.driveFolderIds.filter((id) => ids[String(id)] === true);
854
+ if (kept.length !== it.driveFolderIds.length) {
855
+ const names = {};
856
+ for (const id of kept)
857
+ if (it.driveFolderNames && it.driveFolderNames[id])
858
+ names[id] = it.driveFolderNames[id];
859
+ const patch = { driveFolderIds: kept, driveFolderNames: names };
860
+ // 0件になったら empty に倒す(正本が0件と言っている)。倒さないと
861
+ // 一覧が**無言で空**になり「まだ登録されていません」も出ない。
862
+ // **倒すのは on のときだけ**——broken(設定が読めない)/ off(意図的な停止)は
863
+ // 別の答えなので潰さない
864
+ if (!kept.length && it.driveState === "on")
865
+ patch.driveState = "empty";
866
+ internals[i] = Object.assign({}, it, patch);
867
+ changed = true;
868
+ }
869
+ }
870
+ }
871
+ if (changed)
872
+ set({ internalSources: internals });
873
+ }
874
+ if (Array.isArray(reg.issues)) {
875
+ // 鍵の畳み方は足す側(issueRowsToAdd)と同じにする——別の畳み方にすると
876
+ // 大小の違いで「引いた直後に別綴りで足し直される」形になる
877
+ const issueKey = (t) => String(t.domain).toLowerCase() + "/" + String(t.projectKey).toUpperCase();
878
+ const keys = {};
879
+ for (const r of reg.issues)
880
+ if (r)
881
+ keys[issueKey(r)] = true;
882
+ const bt = (out.backlogTarget || v.backlogTarget);
883
+ const cur = bt && Array.isArray(bt.targets) ? bt.targets : bt && bt.domain && bt.projectKey ? [bt] : [];
884
+ const kept = cur.filter((t) => t && keys[issueKey(t)] === true);
885
+ if (kept.length !== cur.length) {
886
+ const patch = Object.assign({}, bt || {}, { targets: kept });
887
+ // 単数の2つは先頭の接続先。0件なら欄ごと消す(「無い」を装った値で残さない)
888
+ if (kept.length) {
889
+ patch.domain = kept[0].domain;
890
+ patch.projectKey = kept[0].projectKey;
891
+ }
892
+ else {
893
+ delete patch.domain;
894
+ delete patch.projectKey;
895
+ }
896
+ set({ backlogTarget: patch });
897
+ }
898
+ }
899
+ return out;
900
+ }
901
+ /**
902
+ * まとめ表の3キー(externalSources / internalSources / backlogTarget)を、登録ファイルの
903
+ * 今の姿に合わせる——**消された行を引いてから、足りない行を足す**(段A補)。
904
+ *
905
+ * 引き算の条件と守りは subtractRemovedRegistered が持つ。既存の行の**欄**は書き換えない
906
+ * (状態・origin・名前はまとめ表のまま)。
775
907
  *
776
908
  * ⚠ 同期ソースの行がまだ無い能力(=夜間バッチが一度も走っていない案件)には、
777
909
  * **その能力の行を作る**——作らないと、登録した Figma ファイル・Drive フォルダを
@@ -781,7 +913,8 @@ function registeredRowsToAdd(view, reg) {
781
913
  * (まとめ表が持っている値は上書きしない)。
782
914
  */
783
915
  function mergeRegisteredFleet(view, reg) {
784
- const v = view || {};
916
+ // 先に「消された行」を引き、そのうえで「足りない行」を足す(登録ファイルが今の正本)
917
+ const v = subtractRemovedRegistered(view || {}, reg);
785
918
  const out = {
786
919
  externalSources: v.externalSources,
787
920
  internalSources: v.internalSources,
@@ -1954,6 +2087,12 @@ a.ds-name:hover { text-decoration: underline; }
1954
2087
  .ds-keyrow { flex: 1 1 100%; min-width: 0; margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border); }
1955
2088
  /* 一覧の直後に置くとき(Figma)は、行の中ではないので区切り線を持たない */
1956
2089
  .ds-keyrow-list { margin-top: 10px; padding-top: 0; border-top: none; }
2090
+ /* 🔴 **hidden を効かせる(2026-08-22)。** 鍵の欄は接続先が0件のあいだ hidden 属性で
2091
+ 畳む(作らないのではなく隠す——理由は syncKeyCardVisibility の項)。ところが
2092
+ .ops-card は display: flex、.ds-keyrow は flex アイテムを名乗るので、
2093
+ UA 既定の [hidden] { display: none } が**負ける**。畳んだつもりで出たままになる。
2094
+ ⚠ CSS は jsdom から見えないので、この行はテストでは守れない(実機で見ること)。 */
2095
+ [hidden] { display: none !important; }
1957
2096
  /* 行の中に入った鍵カードは、カードの枠を持たない(行の一部に見せる) */
1958
2097
  .ops-card.ds-keycard { border: none; box-shadow: none; padding: 0; background: transparent; gap: 6px; }
1959
2098
  .ops-card.ds-keycard > .ops-card-head { gap: 8px; }
@@ -2184,6 +2323,7 @@ ${designRowsToAdd.toString()}
2184
2323
  ${materialRowsToAdd.toString()}
2185
2324
  ${issueRowsToAdd.toString()}
2186
2325
  ${internalOf.toString()}
2326
+ ${subtractRemovedRegistered.toString()}
2187
2327
  ${registeredRowsToAdd.toString()}
2188
2328
  ${unsettledRegisteredCount.toString()}
2189
2329
  ${mergeRegisteredFleet.toString()}
@@ -3662,6 +3802,29 @@ ${nextStateDelay.toString()}
3662
3802
  if (add.design.length) out[REGISTERED_CAP_NAMES.design] = 1;
3663
3803
  if (add.materials.length) out[REGISTERED_CAP_NAMES.materials] = 1;
3664
3804
  if (add.issues.length) out[REGISTERED_CAP_NAMES.issues] = 1;
3805
+ // **引き算で0件になった能力も同じ語に倒す(段A補)。** 夜間の capabilities は
3806
+ // まだ「on」なので、そのまま出すとタイルは「接続OK」・一覧は0件という矛盾になる。
3807
+ // 「未接続」と言い切るのは夜間が追いついてから(capabilities は引き算の対象外)
3808
+ var raw = rawView();
3809
+ var sub = subtractRemovedRegistered(raw, LIVE_REGISTERED);
3810
+ var count = function (list, isType) {
3811
+ var n = 0;
3812
+ for (var j = 0; j < (list || []).length; j++) if (list[j] && isType(list[j])) n++;
3813
+ return n;
3814
+ };
3815
+ var isSlack = function (x) { return x.type === "slack"; };
3816
+ var isGh = function (x) { return String(x.type || "").indexOf("github") === 0; };
3817
+ if (count(raw.externalSources, isSlack) > 0 && count(sub.externalSources, isSlack) === 0) out["チャット"] = 1;
3818
+ if (count(raw.externalSources, isGh) > 0 && count(sub.externalSources, isGh) === 0) out["開発"] = 1;
3819
+ var dRaw = internalOf(raw.internalSources || [], REGISTERED_CAP_NAMES.design);
3820
+ var dSub = internalOf(sub.internalSources || [], REGISTERED_CAP_NAMES.design);
3821
+ if (dRaw && (dRaw.figmaFiles || []).length > 0 && dSub && (dSub.figmaFiles || []).length === 0) out[REGISTERED_CAP_NAMES.design] = 1;
3822
+ var mRaw = internalOf(raw.internalSources || [], REGISTERED_CAP_NAMES.materials);
3823
+ var mSub = internalOf(sub.internalSources || [], REGISTERED_CAP_NAMES.materials);
3824
+ if (mRaw && (mRaw.driveFolderIds || []).length > 0 && mSub && (mSub.driveFolderIds || []).length === 0) out[REGISTERED_CAP_NAMES.materials] = 1;
3825
+ var btRaw = raw.backlogTarget;
3826
+ var rawTargets = btRaw && Array.isArray(btRaw.targets) ? btRaw.targets.length : btRaw && btRaw.domain ? 1 : 0;
3827
+ if (rawTargets > 0 && Array.isArray(LIVE_REGISTERED.issues) && LIVE_REGISTERED.issues.length === 0) out[REGISTERED_CAP_NAMES.issues] = 1;
3665
3828
  return out;
3666
3829
  }
3667
3830
  /** その能力に、まとめ表がまだ知らない行があるか */
@@ -3773,6 +3936,113 @@ ${nextStateDelay.toString()}
3773
3936
  return null;
3774
3937
  }
3775
3938
 
3939
+ /** 鍵の種別 → その鍵が効く能力。**3つの口が共有する対応表**(下の不変条件を参照) */
3940
+ var SECRET_CAP = { figma: "デザイン", backlog: "課題管理" };
3941
+
3942
+ /**
3943
+ * 「この画面の鍵の欄に貼ってください」と案内しているチェックの id → その鍵が効く能力。
3944
+ *
3945
+ * **欄が出ていないときに出すと、存在しない場所を指す案内になる。** 最後の接続先を
3946
+ * 外した直後(登録は0件・夜間のチェックは古いまま)に実際にそうなっていた。
3947
+ * **id をここ1箇所に集める**——散らすと片方だけ直る。
3948
+ */
3949
+ var KEY_POINTING_CHECKS = { backlog_secrets: "課題管理", figma_token: "デザイン" };
3950
+
3951
+ /**
3952
+ * 応答に添えられた接続先の一覧を採ったときの件数(Backlog の adoptTargets)。
3953
+ *
3954
+ * **鍵の欄の出し入れだけがこれを見る。** まとめ表(fleetVal)が新しい接続先を知るのは
3955
+ * 次の取り直しの後なので、これが無いと「行はその場で出るのに、鍵の欄だけ遅れて出る」。
3956
+ * 一覧そのものの正本は adoptTargets が持つローカルの targets で、ここは写しにしない
3957
+ *(件数しか持たないのは、判断に要るのが件数だけだから)。
3958
+ * **節を作り直すときに捨てる**(buildDataSources の先頭)——作り直しはスナップショットから
3959
+ * 描き直すことなので、そこから先は素直にスナップショットを信じる。
3960
+ */
3961
+ var ADOPTED_TARGET_COUNT = null;
3962
+
3963
+ /**
3964
+ * 鍵(APIキー/トークン)が意味を持つ接続先の数(オーナー決定 2026-08-22)。
3965
+ * **0件なら、鍵の入力欄もタイルの「⚠ トークンが未設定です」も出さない**——
3966
+ * 繋ぐ先が無いのに鍵だけ求めるのは順番の逆な案内になる。最初の一歩は
3967
+ * 「+ 接続先を追加」に揃える(2026-08-12 の「未接続でも欄を出す」B判断を上書き)。
3968
+ *
3969
+ * 🔴 **口は3つ。全部この関数を通す**(0.4.71 からの不変条件を 2026-08-22 に拡張):
3970
+ * 1. 鍵の入力カードの表示/非表示(syncKeyCardVisibility)
3971
+ * 2. タイルの「⚠ トークンが未設定です」(tokenRelevant)
3972
+ * 3. 鍵の欄を指すチェック行(KEY_POINTING_CHECKS)
3973
+ * 別々に書くと「欄はあるのに未設定と言われない」「言われるのに欄が無い」
3974
+ * 「無い欄を指す案内だけが出る」のどれかが起きる(3つとも実機で出た形)。
3975
+ */
3976
+ function keyTargetCount(cap) {
3977
+ if (cap === "課題管理") {
3978
+ // 応答で採った一覧のほうが新しい(まとめ表が追いつくのは次の取り直しから)
3979
+ if (ADOPTED_TARGET_COUNT != null) return ADOPTED_TARGET_COUNT;
3980
+ return backlogTargetsNow(internalSourceOf("課題管理")).length;
3981
+ }
3982
+ if (cap === "デザイン") {
3983
+ var d = internalSourceOf("デザイン");
3984
+ return d && d.figmaFiles ? d.figmaFiles.length : 0;
3985
+ }
3986
+ return 1;
3987
+ }
3988
+
3989
+ /**
3990
+ * この画面がいま持っている鍵カード(種別つき)。secretCard が作るたびに載せ、
3991
+ * buildDataSources が節を作り直すたびに入れ替える。
3992
+ *
3993
+ * **DOM を検索しないのはなぜか。** カードは buildDataSources の中で作られるが、
3994
+ * 置かれるのは能力モーダルの中身(=タイルを押すまで文書に入らない切り離されたノード)。
3995
+ * 組み立て直後に document から探しても**1枚も見つからない**。
3996
+ */
3997
+ var KEY_CARDS = [];
3998
+
3999
+ /**
4000
+ * 鍵の欄と、その受け皿の出し入れ(オーナー決定 2026-08-22)。
4001
+ *
4002
+ * 🔴 **カードは常に作る。隠すだけにする。** 生成そのものを止めると、
4003
+ * **0件でモーダルを開いたまま最初の接続先を足しても欄が現れない**——
4004
+ * カードを作るのは buildDataSources のループ1回きりで、そのあとの描き直し
4005
+ *(paint / liveList)は**鍵の欄を作り直さない**(入力中の値と「保存中…」を守るため)。
4006
+ * 節ごとの差し替え(refreshDataSources)もモーダルが開いている間は何もしない。
4007
+ * つまり**閉じて開き直すまで出ない**。表示の切り替えなら描き直しのたびに当て直せる。
4008
+ *
4009
+ * **受け皿ごと隠す。** .ds-keyrow は margin-top / padding-top / border-top を持つので、
4010
+ * カードだけ畳むと**理由の無い余白と区切り線**が残る(レビューで実測)。
4011
+ * 判定は「中に見えているカードが1枚も無いか」——OFF にした能力で受け皿だけが
4012
+ * 残る形(カードを作らずに return する経路)も同じ理由でここに拾われる。
4013
+ *
4014
+ * ⚠ **3つの口のうち、描き直しに追随するのはこのカードだけ。** タイルの注記
4015
+ *(tokenRelevant)とチェック行(allChecks)は**節を組み立てるときに固定**される。
4016
+ * したがって**モーダルを開いたまま最後の1件を外した瞬間だけ**、鍵の欄は消えるのに
4017
+ * タイル側は古いまま(「トークンが未設定です」や鍵の欄を指す案内が残る)になりうる。
4018
+ * 閉じれば節が作り直されて揃う。タイルはオーバーレイの裏で読めないので実害が小さいと
4019
+ * 判断して、ここは合わせていない——**揃えるなら3つとも同じ描き直しに載せること**
4020
+ *(注記だけを動的にすると、判定が1箇所という不変条件のほうが先に壊れる)。
4021
+ */
4022
+ function syncKeyCardVisibility() {
4023
+ var shown = {};
4024
+ for (var k in SECRET_CAP) {
4025
+ if (Object.prototype.hasOwnProperty.call(SECRET_CAP, k)) shown[k] = keyTargetCount(SECRET_CAP[k]) > 0;
4026
+ }
4027
+ for (var i = 0; i < KEY_CARDS.length; i++) {
4028
+ // **対応表に無い種別は隠さない**(keyTargetCount の既定と同じく出す側に倒す)。
4029
+ // 3つ目の鍵が足されて分岐を書き忘れた場合は key-card-visibility の [全種0件] が落ちる
4030
+ KEY_CARDS[i].el.hidden = shown[KEY_CARDS[i].kind] === false;
4031
+ }
4032
+ eachKeySlot(function (slot) {
4033
+ slot.hidden = !slot.querySelector("[data-secret-kind]:not([hidden])");
4034
+ });
4035
+ }
4036
+
4037
+ /** いま在る鍵の受け皿(.ds-keyrow)を順に渡す。置き場の正本は KEY_SLOTS */
4038
+ function eachKeySlot(fn) {
4039
+ var slots = KEY_SLOTS.backlog || {};
4040
+ for (var d in slots) {
4041
+ if (Object.prototype.hasOwnProperty.call(slots, d) && slots[d]) fn(slots[d]);
4042
+ }
4043
+ if (KEY_SLOTS.figma) fn(KEY_SLOTS.figma);
4044
+ }
4045
+
3776
4046
  /**
3777
4047
  * 能力ごとの「**スナップショットが答えている集合**」。
3778
4048
  *
@@ -4125,6 +4395,10 @@ ${nextStateDelay.toString()}
4125
4395
 
4126
4396
  function buildDataSources() {
4127
4397
  KEY_SLOTS = { backlog: {}, backlogNamed: false, figma: null };
4398
+ // 鍵カードも受け皿と同じく作り直す(前回の切り離されたカードを掴んだままにしない)
4399
+ KEY_CARDS = [];
4400
+ // 作り直しはスナップショットから描き直すこと。応答で採った件数はここで捨てる
4401
+ ADOPTED_TARGET_COUNT = null;
4128
4402
  var internals = fleetVal("internalSources") || [];
4129
4403
  var sources = fleetVal("externalSources") || [];
4130
4404
  var pipes = fleetVal("pipelines") || [];
@@ -4521,7 +4795,7 @@ ${nextStateDelay.toString()}
4521
4795
  // 連携の鍵は、その能力のところへ置く(画面下まで往復させない)
4522
4796
  // **GitHub(「external-sources」)はここに無い。** 読み取りトークンは廃止し、
4523
4797
  // 読み取りは GitHub App に一本化した。**この表に戻すと入力欄が復活する**
4524
- var SECRET_CAP = { figma: "デザイン", backlog: "課題管理" };
4798
+ // (対応表そのものは SECRET_CAP。3つの口で共有するので上に置いてある)
4525
4799
  (SITE.secretKinds || []).forEach(function (k) {
4526
4800
  if (!SECRET_FORMS[k]) return;
4527
4801
  // **意図的にOFFにした能力にだけ、入力欄を出さない(0.4.71)。** 使わないと決めたものに
@@ -4541,6 +4815,11 @@ ${nextStateDelay.toString()}
4541
4815
  }));
4542
4816
  return;
4543
4817
  }
4818
+ // 🔴 **接続先が0件でも、ここでは return しない(オーナー決定 2026-08-22 の実装注意)。**
4819
+ // 「0件なら鍵の欄を出さない」のは決まりだが、**生成を止めると直せなくなる**——
4820
+ // モーダルを開いたまま最初の接続先を足しても、欄は閉じて開き直すまで現れない
4821
+ //(作るのはこのループ1回きりで、以後の描き直しは鍵の欄を作り直さない)。
4822
+ // カードは常に作り、出し入れは syncKeyCardVisibility が hidden で行う
4544
4823
  // **Backlog は接続先のスペースごとにカードを出す(2026-08-20・#127)。**
4545
4824
  // キーはスペースごとに別なので、欄が1つだと**どちらの鍵を入れたのか
4546
4825
  // 誰にも分からない**まま片方の棚に入る(入れたのに動かない、が起きる)。
@@ -4623,8 +4902,16 @@ ${nextStateDelay.toString()}
4623
4902
  figma_token: "デザイン", figma_inventory: "デザイン",
4624
4903
  dev_sources_registered: "開発", dev_issues_derived: "開発",
4625
4904
  };
4905
+ // 🔴 **鍵の欄を指すチェックは、鍵の欄が出ているときだけ出す(オーナー決定 2026-08-22)。**
4906
+ // 「この画面の『Backlog のAPIキー』に貼ってください」は、接続先が0件で欄を畳んでいる
4907
+ // あいだは**存在しない欄を指す案内**になる(最後の接続先を外した直後に実際に出た。
4908
+ // 夜間のチェックは古いままなので、登録が0件になっても missing が残っている)。
4909
+ // **ここで落とすので、タイルの注記と「セットアップの残り」の両方から消える**
4910
+ // ——片方だけで畳むと、落ちた行がもう片方に湧いて出る(loose の拾い直しに落ちるため)
4626
4911
  var allChecks = (fleetVal("checks") || []).filter(function (c) {
4627
- return c != null && c.status === "missing";
4912
+ if (c == null || c.status !== "missing") return false;
4913
+ var kc = KEY_POINTING_CHECKS[c.id];
4914
+ return !(kc && keyTargetCount(kc) === 0);
4628
4915
  });
4629
4916
  // **タイルに出せたものを覚えておく。** 振り分け先の能力タイルが立たない案件
4630
4917
  //(tools に宣言が無い・行も無い)では出す場所が無くなり、項目が**黙って消える**。
@@ -4848,7 +5135,8 @@ ${nextStateDelay.toString()}
4848
5135
  // 「入力欄はあるのに未設定だと言われない」「言われるのに入力欄が無い」のどちらかが起きる。
4849
5136
  // 0.4.70 は unset で入力欄を畳んでいたので注記も抑えていたが、案A で欄を出すようにしたため揃えた
4850
5137
  // 導出が無いデータでは判断材料が無いので、注記を出す側(=入力欄と揃う側)に倒す
4851
- var tokenRelevant = derived != null ? derived !== "off" : true;
5138
+ // 接続先0件の抑止は入力欄と**同じ関数**を通す(keyTargetCount 🔴 不変条件)
5139
+ var tokenRelevant = (derived != null ? derived !== "off" : true) && keyTargetCount(cap) > 0;
4852
5140
  if (tokenRelevant && sk && (SITE.secretKinds || []).indexOf(sk) >= 0 && !((fleetVal("secretSources") || {})[sk])) {
4853
5141
  body.appendChild(el("div", { class: "ds-tile-note", text: "⚠ トークンが未設定です" }));
4854
5142
  }
@@ -4970,6 +5258,8 @@ ${nextStateDelay.toString()}
4970
5258
  sec.appendChild(todo);
4971
5259
  }
4972
5260
 
5261
+ // 組み立てたカードに、いまの件数で表示/非表示を当てる(詳細は syncKeyCardVisibility)
5262
+ syncKeyCardVisibility();
4973
5263
  return sec;
4974
5264
  }
4975
5265
  function renderDataSources() {
@@ -5266,6 +5556,10 @@ ${nextStateDelay.toString()}
5266
5556
  if (x && x.domain && x.projectKey) out.push({ domain: String(x.domain), projectKey: String(x.projectKey) });
5267
5557
  }
5268
5558
  if (out.length) return out;
5559
+ // 🔴 **正本(issues-config.json)が読めていて0件のときは、url から復元しない(段A補)。**
5560
+ // このフォールバックは「古いまとめ表に targets が無い」案件を救うためのもの。
5561
+ // 正本が「無い」と言っているのに復元すると、**最後の1件を解除してもリロードで戻ってくる**
5562
+ if (LIVE_REGISTERED && Array.isArray(LIVE_REGISTERED.issues) && LIVE_REGISTERED.issues.length === 0) return [];
5269
5563
  var u = (s && s.url && backlogTargetOf(s.url)) || null;
5270
5564
  return u ? [u] : [];
5271
5565
  }
@@ -5516,6 +5810,10 @@ ${nextStateDelay.toString()}
5516
5810
  // **出し入れのたびに反映中を当て直す。** 外している間の描き直しはこの行に届かない
5517
5811
  // ので、戻すときに当てないと古い文言のまま出る
5518
5812
  applyReflect(emptyEl, reflectingNow());
5813
+ // 🔴 **鍵の欄の出し入れは、描き直しのたびに当て直す(2026-08-22)。** 最初の1件を
5814
+ // 足した瞬間に欄が現れ、最後の1件を外した瞬間に消えるのはここが効いているから。
5815
+ // 節ごとの差し替え(refreshDataSources)はモーダルが開いている間は何もしない
5816
+ syncKeyCardVisibility();
5519
5817
  }
5520
5818
 
5521
5819
  /**
@@ -5533,6 +5831,9 @@ ${nextStateDelay.toString()}
5533
5831
  if (x && x.domain && x.projectKey) next.push({ domain: String(x.domain), projectKey: String(x.projectKey) });
5534
5832
  }
5535
5833
  targets = next;
5834
+ // **鍵の欄の判断もこの一覧に従わせる(2026-08-22)。** keyTargetCount はまとめ表を
5835
+ // 見るので、ここで教えないと「行はその場で出るのに鍵の欄だけ次の取り直しまで出ない」
5836
+ ADOPTED_TARGET_COUNT = targets.length;
5536
5837
  paint();
5537
5838
  return targetsGenerationOf(targets) !== before;
5538
5839
  }
@@ -5561,9 +5862,14 @@ ${nextStateDelay.toString()}
5561
5862
  var dom = String(targets[ti].domain || "").toLowerCase();
5562
5863
  if (dom && domains.indexOf(dom) === -1) domains.push(dom);
5563
5864
  }
5564
- // **0件の案件にも1つ出す。** 「接続先を足してからでないと鍵を入れられない」形にすると、
5565
- // 立ち上げ直後の案件が最初の一歩を踏めない(#138 で直したのと同じ穴)。
5865
+ // **0件の案件にも受け皿は1つ作る。ただし中身は隠れている(2026-08-22)。**
5866
+ // オーナー決定で「接続先が0件なら鍵の欄は出さない」になったが、**作らないのではなく
5867
+ // 隠す**——受け皿ごと無いと、モーダルを開いたまま最初の1件を足したときに
5868
+ // 鍵の欄を置く場所が無く、閉じて開き直すまで現れない(syncKeyCardVisibility の項)。
5869
+ // 受け皿は0件のあいだ hidden なので、区切り線と余白も出ない。
5566
5870
  // 空文字のキーは「どのスペースとも結び付いていない受け皿」で、行の中には入らない
5871
+ //(足した接続先のドメインに対応するスロットは無いので、鍵の欄はこの受け皿に
5872
+ // 残ったまま一覧の下に現れる。paint() はこのキーを移動の対象から外している)
5567
5873
  if (!domains.length) domains.push("");
5568
5874
  for (var di = 0; di < domains.length; di++) keySlots[domains[di]] = el("div", { class: "ds-keyrow" });
5569
5875
  KEY_SLOTS.backlog = keySlots;
@@ -5927,7 +6233,7 @@ ${nextStateDelay.toString()}
5927
6233
  // how / note)。全文をここに移すと二重になり、片方だけ直る形になる
5928
6234
  // ——カードは接続マップとタイルの注記と条件を揃えているので、動かすと壊れやすい。
5929
6235
  hint: [
5930
- "Figma でトークンを発行します(読み取りだけ。スコープ2つ)。手順は上の「Figma のトークン」の欄にあります。",
6236
+ "Figma でトークンを発行します(読み取りだけ。スコープ2つ)。手順は「Figma のトークン」の欄にあります(ファイルを追加すると上に出ます)。",
5931
6237
  "発行したアカウントを、読み取りたい Figma ファイルに閲覧者として招待します(招待が無いと、トークンが正しくても読めません)。",
5932
6238
  "そのファイルのURLを下の欄に貼って「追加」を押します。",
5933
6239
  "追加したファイルは、画面名と Figma へのリンクが毎晩同期されます(画像は取り込まれません)。",
@@ -6840,6 +7146,9 @@ ${nextStateDelay.toString()}
6840
7146
  box.__sig = next;
6841
7147
  while (box.firstChild) box.removeChild(box.firstChild);
6842
7148
  build(box);
7149
+ // 一覧が変わった=接続先の件数が変わったかもしれない。鍵の欄を当て直す
7150
+ // (Figma の鍵の欄は一覧の**外**にあるので、ここで見ないと取り残される)
7151
+ syncKeyCardVisibility();
6843
7152
  };
6844
7153
  box.__rebuild();
6845
7154
  return box;
@@ -7360,7 +7669,7 @@ ${nextStateDelay.toString()}
7360
7669
  // 押すまで見えないと成立しない
7361
7670
  // - .sec-preferred … 「誰のアカウントで発行するのか」を**手順より先に**伝える行。
7362
7671
  // 未登録のときだけ出る=これから貼る人にだけ出るので、畳むと存在理由が消える
7363
- return el("div", { class: "ops-card" + (inRow ? " ds-keycard" : ""), "data-secret-kind": kind }, [
7672
+ var card = el("div", { class: "ops-card" + (inRow ? " ds-keycard" : ""), "data-secret-kind": kind }, [
7364
7673
  el("div", { class: "ops-card-head" }, [
7365
7674
  el("span", { class: "ops-card-icon", text: meta.icon }),
7366
7675
  el("h3", { class: "ops-card-title", text: cardTitle }),
@@ -7381,6 +7690,11 @@ ${nextStateDelay.toString()}
7381
7690
  // 説明が落ちるのは、いちばん落としてはいけない種類の欠落
7382
7691
  el("p", { class: "sec-note", text: "入れた値はここには表示されません(保存だけできます)。保存先は案件ごとに分かれているので、他の案件には影響しません。" }),
7383
7692
  ]);
7693
+ // **作った鍵カードは1枚残らずここに載せる。** 出し入れ(接続先が0件のときに畳む)は
7694
+ // DOM 検索ではなくこの控えで回す——カードは能力モーダルの中身で、タイルを押すまで
7695
+ // 文書に入っていないため(syncKeyCardVisibility の項)
7696
+ KEY_CARDS.push({ kind: kind, el: card });
7697
+ return card;
7384
7698
  }
7385
7699
 
7386
7700
  // ---------- 接続マップ(ビルド時生成のSVGを<template>から取り出して表示) ----------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,