@takagaki/cortex-decisions-viewer 0.10.4 → 0.10.6

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 +136 -9
  2. package/package.json +1 -1
package/dist/render.js CHANGED
@@ -12,6 +12,7 @@ exports.chatRowsToAdd = chatRowsToAdd;
12
12
  exports.devRowsToAdd = devRowsToAdd;
13
13
  exports.designRowsToAdd = designRowsToAdd;
14
14
  exports.materialRowsToAdd = materialRowsToAdd;
15
+ exports.issuesRefOf = issuesRefOf;
15
16
  exports.issueRowsToAdd = issueRowsToAdd;
16
17
  exports.unsettledRegisteredCount = unsettledRegisteredCount;
17
18
  exports.registeredRowsToAdd = registeredRowsToAdd;
@@ -734,6 +735,25 @@ function materialRowsToAdd(ids, rows) {
734
735
  }
735
736
  return out;
736
737
  }
738
+ /**
739
+ * 課題管理(Backlog)の接続先1件を1つの鍵にする。**ドメインは小文字・キーは大文字。**
740
+ *
741
+ * 🔴 **この規則の正本はここ1つ。** 設定ファイルの値は人が書いたもので大小が揃って
742
+ * いないので、揃えずに突き合わせると**同じ接続先を別物と見る**。それが起きる場所が
743
+ * 3つある——行の重複判定(`issueRowsToAdd`)、登録から消えた行の引き算
744
+ * (`subtractRemovedRegistered`)、そして接続状態ストアの行との突き合わせ
745
+ * (画面の `ingestedStateOf`。ストアの `ref` も engine 側がこの規則で書く)。
746
+ *
747
+ * **クロージャを持たないこと**(クライアントへ `.toString()` で流し込む)。
748
+ *
749
+ * ⚠ 画面の `targetsGenerationOf`(サーバへ送る世代)は同じ規則を持つが、この関数を
750
+ * 呼べない——配信物から関数1つを切り出して直接呼ぶテストがあり、外の名前を参照した
751
+ * 瞬間にそのテストが動かなくなる。あちらを直すときはここも直す。
752
+ */
753
+ function issuesRefOf(domain, projectKey) {
754
+ return String(domain == null ? "" : domain).toLowerCase() + "/" +
755
+ String(projectKey == null ? "" : projectKey).toUpperCase();
756
+ }
737
757
  /**
738
758
  * 課題管理の行で、まとめ表がまだ知らないもの。
739
759
  *
@@ -742,7 +762,7 @@ function materialRowsToAdd(ids, rows) {
742
762
  * 二重に並ぶ(片方は削除ボタンを持たない)。
743
763
  */
744
764
  function issueRowsToAdd(targets, rows) {
745
- const key = (d, k) => String(d).toLowerCase() + "/" + String(k).toUpperCase();
765
+ const key = issuesRefOf;
746
766
  const have = {};
747
767
  for (const t of targets || [])
748
768
  if (t && t.domain && t.projectKey)
@@ -899,7 +919,7 @@ function subtractRemovedRegistered(view, reg) {
899
919
  if (Array.isArray(reg.issues)) {
900
920
  // 鍵の畳み方は足す側(issueRowsToAdd)と同じにする——別の畳み方にすると
901
921
  // 大小の違いで「引いた直後に別綴りで足し直される」形になる
902
- const issueKey = (t) => String(t.domain).toLowerCase() + "/" + String(t.projectKey).toUpperCase();
922
+ const issueKey = (t) => issuesRefOf(t.domain, t.projectKey);
903
923
  const keys = {};
904
924
  for (const r of reg.issues)
905
925
  if (r)
@@ -2051,6 +2071,12 @@ a.ds-name:hover { text-decoration: underline; }
2051
2071
  .ds-all-connected { color: var(--muted); }
2052
2072
  .ds-gate { display: inline-flex; align-items: center; gap: 5px; font-size: 12.5px; font-weight: 600; flex: 0 0 auto; }
2053
2073
  .ds-gate-at { font-size: 11px; color: var(--muted); flex: 0 0 auto; }
2074
+ /* 接続先1件ごとの「取り込み」(段C・2026-08-24)。**「最終同期」(.ds-sync・run単位)とは
2075
+ 別の行に並ぶ**ので、同じ強さにしない——run の時刻より弱い添え物として読ませる。
2076
+ 失敗だけは .ds-gate-warn と同じ色にする(同じ画面で警告の色が2種類あると、
2077
+ どちらが重いのか読めない) */
2078
+ .ds-ingest { font-size: 11px; color: var(--muted); flex: 0 0 auto; font-variant-numeric: tabular-nums; }
2079
+ .ds-ingest-failed { color: #b45309; font-weight: 600; }
2054
2080
  .ds-gate::before { content: ""; width: 8px; height: 8px; border-radius: 999px; background: currentColor; }
2055
2081
  .ds-gate-ok { color: #1f7a45; }
2056
2082
  .ds-gate-warn { color: #b45309; }
@@ -2324,6 +2350,7 @@ ${capabilityAdapter.toString()}
2324
2350
  ${capabilityRawState.toString()}
2325
2351
  ${capabilityDisplayState.toString()}
2326
2352
  ${backlogTargetOf.toString()}
2353
+ ${issuesRefOf.toString()}
2327
2354
  ${sourceNeedsAttention.toString()}
2328
2355
  ${capabilityState.toString()}
2329
2356
  // 反映待ち(⏳ 反映中)の判定。**期限・間隔・対応表もビルド時の定義から流し込む**
@@ -5608,6 +5635,7 @@ ${nextStateDelay.toString()}
5608
5635
  * ——食い違わないことは backlog-targets-ui.test.mjs が実物どうしで突き合わせている。
5609
5636
  *
5610
5637
  * **クロージャを持たない純関数に保つこと**(テストが配信物から切り出して直接呼ぶ)。
5638
+ * だから issuesRefOf を呼べない(規則の正本はあちら側。直すときは両方直す)。
5611
5639
  */
5612
5640
  function targetsGenerationOf(list) {
5613
5641
  return (list || [])
@@ -5710,6 +5738,75 @@ ${nextStateDelay.toString()}
5710
5738
  return domains.length === 1 ? domains[0] : "";
5711
5739
  }
5712
5740
 
5741
+ /**
5742
+ * その接続先の「取り込み」の行(接続状態ストア・段C)。**無ければ null。**
5743
+ *
5744
+ * 🔴 **overlayStateOnFleet は通らない。** あれは externalSources(チャット / 開発)に
5745
+ * 重ねる仕組みで、**Backlog の行はそこに存在しない**(まとめ表を作る側が作るのは
5746
+ * slack と github 系だけ)。だから接続先の行を描くこちらがストアを直接引く。
5747
+ *
5748
+ * **突き合わせは両側とも issuesRefOf を通す。** 設定ファイルの値は人が書いたもので
5749
+ * 大小が揃っておらず("CM1.Backlog.JP" / "pj_a" が実在する)、素の文字列で比べると
5750
+ * **取り込めているのに何も出ない**——「測っていないものは何も言わない」設計なので、
5751
+ * 突き合わせに失敗しても画面は静かに黙るだけで、誰も気づけない。
5752
+ */
5753
+ function ingestedStateOf(domain, projectKey) {
5754
+ var rows = LIVE_STATE;
5755
+ if (!rows || !rows.length || !domain || !projectKey) return null;
5756
+ var want = issuesRefOf(domain, projectKey);
5757
+ for (var i = 0; i < rows.length; i++) {
5758
+ var r = rows[i];
5759
+ // cap と question の両方で絞る。片方だけだと、同じ ref を持つ別の問い
5760
+ //(将来 issues に別の question が増えたとき)の行を取り込みとして読む
5761
+ if (!r || r.cap !== "issues" || r.question !== "ingested") continue;
5762
+ var at = String(r.ref || "").indexOf("/");
5763
+ if (at < 0) continue;
5764
+ if (issuesRefOf(String(r.ref).slice(0, at), String(r.ref).slice(at + 1)) === want) return r;
5765
+ }
5766
+ return null;
5767
+ }
5768
+
5769
+ /**
5770
+ * 接続先1件の「取り込み」の表示。**測っていなければ何も出さない(null)。**
5771
+ *
5772
+ * 段A・段Bと同じ原則。行が無いのは「取り込みが動いていない」ではなく
5773
+ * 「まだ誰も書いていない」(engine の版が古い案件・一度も同期していない案件)で、
5774
+ * そこに「未取り込み」と書くと**直す必要があると読ませる嘘**になる。
5775
+ *
5776
+ * **「最終同期」(.ds-sync)を置き換えない。** あちらは run 全体の最終成功時刻で、
5777
+ * どの接続先のことか分からない。こちらは接続先1件の成否。意味が違うので併記する。
5778
+ */
5779
+ function ingestedEl(r) {
5780
+ if (!r) return null;
5781
+ var when = rtAgo(r.at);
5782
+ var text = "";
5783
+ if (r.status === "ok") text = when ? "取り込み: " + when + "に成功" : "取り込み: 成功";
5784
+ else if (r.status === "failed") text = when ? "取り込み: " + when + "に失敗" : "取り込み: 失敗";
5785
+ else if (r.status === "skipped") text = "取り込み: スキップ";
5786
+ if (!text) return null;
5787
+ return el("span", {
5788
+ class: "ds-ingest" + (r.status === "failed" ? " ds-ingest-failed" : ""),
5789
+ text: text,
5790
+ });
5791
+ }
5792
+
5793
+ /**
5794
+ * いま一覧に出ている接続先の「取り込み」をひとつの文字列にする(差し替えの要否の判定用)。
5795
+ *
5796
+ * 🔴 **接続先の世代(targetsGenerationOf)だけでは足りない。** モーダルを開くと
5797
+ * その能力の状況をもう一度取りに行くので、**応答が返る前に開いた人は取り込みの行を
5798
+ * 1件も持っていない**。届いた応答で接続先の集合が変わっていなければ、世代だけを見る
5799
+ * 早期return が描き直しを飛ばし、**閉じて開き直すまで空のまま**になる。
5800
+ * 空表示は「まだ誰も測っていない」と区別が付かないので、**取り込みの失敗を黙る**という
5801
+ * この機能が殺そうとしている欠陥そのものが、別の経路で復活する。
5802
+ */
5803
+ function ingestSigOf(list) {
5804
+ return (list || []).map(function (t) {
5805
+ var r = ingestedStateOf(t.domain, t.projectKey);
5806
+ return r ? r.status + "@" + r.at : "-";
5807
+ }).join(",");
5808
+ }
5809
+
5713
5810
  /**
5714
5811
  * 「課題管理」カードの接続先(一覧+追加+削除・#127)。
5715
5812
  *
@@ -5768,6 +5865,8 @@ ${nextStateDelay.toString()}
5768
5865
  el("span", { class: "ds-type", text: "Backlog" }),
5769
5866
  dsNameEl(t.domain + " / " + t.projectKey, "https://" + t.domain + "/projects/" + t.projectKey),
5770
5867
  ]),
5868
+ // この接続先の取り込みが最後にどうなったか(段C)。測っていなければ null
5869
+ ingestedEl(ingestedStateOf(t.domain, t.projectKey)),
5771
5870
  ]);
5772
5871
  if (!editable) return row;
5773
5872
  var btn = el("button", { class: "ds-toggle-btn ds-danger", type: "button", text: "この接続先を外す" });
@@ -5912,6 +6011,8 @@ ${nextStateDelay.toString()}
5912
6011
  // 箱が無いと最初の描画で「未設定」の行が出ない
5913
6012
  var box = el("div", { "data-live-cap": REGISTERED_CAP_NAMES.issues }, [listEl]);
5914
6013
  paint();
6014
+ /** いま描いてある取り込みの状態(段C)。接続先の集合とは別に変わるので、別に覚える */
6015
+ var lastIngestSig = ingestSigOf(targets);
5915
6016
  /**
5916
6017
  * 取り直しで新しい値が届いたときの差し替え(段階1)。**一覧だけを描き直す。**
5917
6018
  *
@@ -5923,8 +6024,13 @@ ${nextStateDelay.toString()}
5923
6024
  // 🔴 応答に添えられた一覧(adoptTargets)のほうが新しい。上書きしない
5924
6025
  if (adopted) return;
5925
6026
  var next = backlogTargetsNow(internalSourceOf(REGISTERED_CAP_NAMES.issues));
5926
- if (targetsGenerationOf(next) === targetsGenerationOf(targets)) return;
6027
+ // 🔴 **変化が無いと言えるのは、接続先の集合と取り込みの状態が両方同じときだけ(段C)。**
6028
+ // 世代だけで判断すると、**応答を待たずにモーダルを開いた人には取り込みが永久に出ない**
6029
+ //(届いた応答で集合は変わらないので、ここで飛ばされる)。ingestSigOf の項に経緯
6030
+ var sig = ingestSigOf(next);
6031
+ if (targetsGenerationOf(next) === targetsGenerationOf(targets) && sig === lastIngestSig) return;
5927
6032
  targets = next;
6033
+ lastIngestSig = sig;
5928
6034
  paint();
5929
6035
  };
5930
6036
  // 接続先がまだ無い案件の受け皿は、0件の1行の直後に置く(行が無いので中へ入れられない)
@@ -7382,20 +7488,32 @@ ${nextStateDelay.toString()}
7382
7488
  */
7383
7489
  function realtimeSyncCard() {
7384
7490
  var rt = LIVE_REALTIME;
7385
- if (!rt) return null; // 取得前・取得失敗・Backlogを使わない案件
7386
- if (rt.supported === false) {
7491
+ // **未対応(固定IP経由の同期)だけは、手順ではなく理由を出す。** ここは索引の状態ではなく
7492
+ // 経路の話で、設定しても効かないことが確定している唯一の形。黙って欠けさせない
7493
+ if (rt && rt.supported === false) {
7387
7494
  return el("p", {
7388
7495
  class: "sec-how",
7389
7496
  text: "この案件は固定IP経由でBacklogを同期しているため、リアルタイム同期(Webhook)には未対応です。反映は毎晩の同期のタイミングになります。",
7390
7497
  });
7391
7498
  }
7392
7499
  var wh = SITE.backlogWebhook;
7393
- if (!rt.ready || !wh || !wh.url || !wh.token) return null;
7500
+ // **URLが無い案件には出さない。** 貼るものが無い手順は、一番読めない形になる
7501
+ if (!wh || !wh.url || !wh.token) return null;
7502
+ // **索引に行があるか(ready)でカードごと隠さない(2026-08-24 オーナー決定)。**
7503
+ // それまでは「手順どおり設定すれば実際に効く案件だけに出す」としていたが、
7504
+ // ready の材料(ルーティング索引の行)は**設定を始める前には揃わない**ので、
7505
+ // これから設定する人ほど手順に辿り着けなかった。索引がまだ追いついていない案件も、
7506
+ // 取得前・取得失敗(rt が null)も、手順は出す——URLは案件専用で、先に貼っておいても
7507
+ // 困らない。ready のときだけ受信実績の行を足す
7508
+ var ready = !!(rt && rt.ready);
7394
7509
  var url = wh.url + (wh.url.indexOf("?") === -1 ? "?" : "&") + "t=" + encodeURIComponent(wh.token);
7395
7510
  // 接続先ごとの受信実績(#123)。**配列で受ける**——1案件=複数 Backlog プロジェクトは
7396
7511
  // 確定した将来要件(#107)で、今日は必ず1件。**時刻が読めない値は稼働中に数えない**
7397
7512
  // ——「最終受信: NaN分前」を出すくらいなら、未設定と同じ手順の画面のほうがまだ読める
7398
- var targets = (rt.targets || []).map(function (t) {
7513
+ // **ready でないうちは受信実績を読まない。** ready の材料は索引の行(=自リポ宛だと
7514
+ // 確かめられた行)で、それが無いのに lastEventAt だけ来ている応答を信じると、
7515
+ // **届いていない案件に「✓ 稼働中」を出す**——一番出してはいけない嘘になる
7516
+ var targets = (ready && rt.targets ? rt.targets : []).map(function (t) {
7399
7517
  return { projectKey: t && t.projectKey ? String(t.projectKey) : "", ago: t && t.lastEventAt ? rtAgo(t.lastEventAt) : "" };
7400
7518
  });
7401
7519
  var live = targets.some(function (t) { return t.ago; });
@@ -7488,7 +7606,9 @@ ${nextStateDelay.toString()}
7488
7606
  ]);
7489
7607
  }
7490
7608
 
7491
- return el("div", { class: "ops-card rt-card" }, [
7609
+ // まだ受け入れ体制が整っていない案件(接続先がこれから・索引が追いついていない)には、
7610
+ // **次の一手だけを1行足す。** 担当者への連絡は促さない——この画面で完結する話だから
7611
+ var head = [
7492
7612
  el("div", { class: "ops-card-head" }, [
7493
7613
  el("span", { class: "ops-card-icon", text: "⚡" }),
7494
7614
  // **稼働中の枝と同じ見出しにする**(上の ⚠ 参照。片方だけ直すと、
@@ -7496,7 +7616,14 @@ ${nextStateDelay.toString()}
7496
7616
  el("h3", { class: "ops-card-title", text: "リアルタイム同期(任意)" }),
7497
7617
  ]),
7498
7618
  el("p", { class: "ops-card-desc", text: "Backlog で課題やコメントが変わったときに、その内容をすぐ取り込みます(毎晩の同期を待たずに反映されます)。Backlog 側で一度だけ設定してください。" }),
7499
- ].concat(steps));
7619
+ ];
7620
+ if (!ready) {
7621
+ head.push(el("p", {
7622
+ class: "sec-how",
7623
+ text: "上の「接続先」を先に設定してから貼り付けてください。設定した直後は、この画面が追いつくまで少し時間がかかることがあります。",
7624
+ }));
7625
+ }
7626
+ return el("div", { class: "ops-card rt-card" }, head.concat(steps));
7500
7627
  }
7501
7628
 
7502
7629
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.10.4",
3
+ "version": "0.10.6",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,