@takagaki/cortex-decisions-viewer 0.4.55 → 0.4.57

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 +160 -45
  2. package/package.json +1 -1
package/dist/render.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.renderSite = renderSite;
4
+ exports.renderConnectionMap = renderConnectionMap;
4
5
  /** SiteDataを単一の自己完結HTML(埋め込みデータ+CSS+JS)にレンダリングする */
5
6
  function renderSite(site) {
6
7
  // </script> によるブレイクアウトを防ぐため < をエスケープして埋め込む
@@ -140,36 +141,99 @@ function renderConnectionMap(site) {
140
141
  // マップが⚠️なのに直下のグループが「接続OK」という矛盾が同じ画面に出る。
141
142
  ? liveExts.some((s) => sourceNeedsAttention(s))
142
143
  : pipes.some((p) => c.pipe != null && c.pipe.test(p.id) && p.applicable !== false && p.lastConclusion === "failure"));
143
- const state = off ? "cm-off" : warn ? "cm-warn" : "cm-ok";
144
- const toolName = toolVal && toolVal !== "none"
145
- ? CM_TOOL_LABEL[toolVal] ?? toolVal
146
- : internal
147
- ? CM_TOOL_LABEL[internal.tool] ?? internal.tool
144
+ // **宣言されているだけで、実際には何も繋がっていない**状態を分けて表す。
145
+ //
146
+ // 会議に Teams、チャットに Teams を宣言した案件で、どちらも「接続済み」の見た目になっていた。
147
+ // notetaker は Teams の文字起こしを取り込めず(Google Meet 前提)、Teams の MCP も配っていないので、
148
+ // **実体は何も繋がっていない**。それを繋がって見せると、設定が足りないことに誰も気づけない。
149
+ //
150
+ // - 会議: 取り込みが有効(ingestState === "on")でなければ「自動連携なし」
151
+ // - チャット/開発: ライブ参照の実体(登録されたチャンネル・リポジトリ)が無ければ同じ
152
+ //
153
+ // **能力ごとに場当たりで判定しない。** 最初は会議とライブ参照だけを見ていたが、
154
+ // 課題管理が「宣言があるだけ」で接続済み(青)に見えていた——同期が一度も走っておらず、
155
+ // APIキーすら入っていない案件で。指摘されるまで気づけなかった。
156
+ //
157
+ // 「実際に繋がっている」の証拠は共通で次のどれか:
158
+ // - 一度でも同期が走った(lastSync)
159
+ // - 正本のURLが導出できる(url。設定ファイルが実在して初めて出る)
160
+ // - 会議は取り込みが有効(ingestState === "on")
161
+ // - ライブ参照は読む対象が1件以上ある
162
+ const idle = !off &&
163
+ (isLive
164
+ ? liveExts.length === 0
165
+ : c.cap === "会議"
166
+ ? internal?.ingestState !== "on"
167
+ : !(internal?.lastSync || internal?.url));
168
+ // **未接続は警告より優先する。** 繋がっていない能力のパイプラインが失敗するのは
169
+ // 「壊れた」のではなく「まだ設定が済んでいない」結果で、順番を逆にすると
170
+ // **⚠️の箱に「未接続」と書かれ、しかも繋がっていないのに線が引かれる**(実際にそうなった:
171
+ // APIキー未投入で sync-backlog が失敗し、課題管理が警告色で線付きになった)。
172
+ // 失敗そのものは下の「自動化パイプライン」の一覧に出るので、情報は失われない。
173
+ const state = off ? "cm-off" : idle ? "cm-idle" : warn ? "cm-warn" : "cm-ok";
174
+ // **ラベルは能力名(会議・課題管理…)。ツール名は下に小さく置く。**
175
+ // ツールは「その能力をいま何で実現しているか」という差し替え可能なアダプタで、
176
+ // 主語ではない。ツール名を主にすると、Teams のように複数の能力に使われるものが
177
+ // 同じ名前で並び(会議とチャットに Teams が2つ)、何が繋がっているのか読めなくなる。
178
+ // "none"(未使用の宣言)はツール名ではないので出さない。そのまま表示すると
179
+ // 「ツール: none」という読めない行になる(実際にそう出ていた)
180
+ const rawTool = toolVal && toolVal !== "none"
181
+ ? toolVal
182
+ : internal && internal.tool && internal.tool !== "none"
183
+ ? internal.tool
148
184
  : isLive && liveExts.length
149
- ? CM_TOOL_LABEL[c.cap === "チャット" ? "slack" : "github"]
150
- : c.cap;
151
- const label = `${c.icon} ${off && (toolVal === "none" || !toolName) ? c.cap : toolName}`;
152
- // ノード下の補足: 未使用 / ライブ参照 / 最終同期(時刻はクライアントでローカルタイム整形するため data-ts で渡す)
153
- let sub = "";
185
+ ? c.cap === "チャット"
186
+ ? "slack"
187
+ : "github"
188
+ : "";
189
+ const toolName = rawTool ? CM_TOOL_LABEL[rawTool] ?? rawTool : "";
190
+ const label = `${c.icon} ${c.cap}`;
191
+ // 2行目=ツール名(小さく)/3行目=状態(未使用・自動連携なし・ライブ参照・最終同期)
192
+ const toolLine = toolName
193
+ ? `<text class="cm-tool" x="${cx}" y="53" text-anchor="middle">${escapeHtml(toolName)}</text>`
194
+ : "";
195
+ let statusText = "";
196
+ let statusTs = "";
154
197
  if (off) {
155
- sub = `<text class="cm-sync" x="${cx}" y="60" text-anchor="middle">未使用</text>`;
198
+ statusText = "未使用";
199
+ }
200
+ else if (idle) {
201
+ // **「未接続」と言い切る。** 「自動連携なし」だと運用でそう決めたようにも読めるが、
202
+ // 実際は「宣言はあるが、まだ一度も繋がっていない」——やることが残っている状態
203
+ statusText = "未接続";
156
204
  }
157
205
  else if (isLive) {
158
- sub = `<text class="cm-sync" x="${cx}" y="60" text-anchor="middle">ライブ参照</text>`;
206
+ statusText = "ライブ参照";
159
207
  }
160
208
  else if (internal?.lastSync) {
161
- sub = `<text class="cm-sync" x="${cx}" y="60" text-anchor="middle" data-ts="${escapeHtml(internal.lastSync)}"></text>`;
209
+ statusTs = internal.lastSync;
162
210
  }
163
- const badge = warn ? `<text class="cm-badge" x="${cx + 62}" y="26" text-anchor="middle">⚠️</text>` : "";
211
+ // 時刻はクライアントでローカルタイム整形するため data-ts で渡す
212
+ const sub = statusText || statusTs
213
+ ? `<text class="cm-sync" x="${cx}" y="67" text-anchor="middle"` +
214
+ (statusTs ? ` data-ts="${escapeHtml(statusTs)}"` : "") +
215
+ `>${escapeHtml(statusText)}</text>`
216
+ : "";
217
+ const badge = warn ? `<text class="cm-badge" x="${cx + 62}" y="24" text-anchor="middle">⚠️</text>` : "";
164
218
  // クリックでページ内の該当詳細カード(アンカー)へスクロールする
165
219
  const anchor = `ds-${c.cap}`;
166
220
  nodes.push(`<g class="cm-node ${state}" data-target="${anchor}" tabindex="0" role="link" aria-label="${escapeHtml(c.cap)}の詳細へ">` +
167
- `<rect x="${cx - 74}" y="16" width="148" height="54" rx="10"></rect>` +
168
- `<text class="cm-label" x="${cx}" y="40" text-anchor="middle">${escapeHtml(label)}</text>` +
221
+ `<rect x="${cx - 74}" y="12" width="148" height="64" rx="10"></rect>` +
222
+ `<text class="cm-label" x="${cx}" y="35" text-anchor="middle">${escapeHtml(label)}</text>` +
223
+ toolLine +
169
224
  sub +
170
225
  badge +
171
226
  `</g>`);
172
- links.push(`<path class="cm-link ${state}" d="M ${cx} 72 C ${cx} 150, ${REPO_X} 160, ${REPO_X} 238"></path>`);
227
+ // **線は「実際に繋がっていることが証明できたもの」だけに引く。**
228
+ // 未使用(cm-off)・宣言だけで実体が無いもの(cm-idle)からは線を出さない。
229
+ // 薄い線でも引いてしまうと、データが流れていない経路が流れているように読める——
230
+ // 実際、同期が一度も走っていない課題管理が、リポジトリと線で結ばれて見えていた。
231
+ //
232
+ // cm-warn は引く。**壊れているのは「繋がったうえで失敗している」状態**で、
233
+ // 経路自体は存在する(線を消すと、設定が無いのと区別が付かなくなる)。
234
+ if (state === "cm-ok" || state === "cm-warn") {
235
+ links.push(`<path class="cm-link ${state}" d="M ${cx} 78 C ${cx} 152, ${REPO_X} 162, ${REPO_X} 238"></path>`);
236
+ }
173
237
  });
174
238
  const repoTitle = site.title.length > 24 ? site.title.slice(0, 23) + "…" : site.title;
175
239
  const repo = `<g class="cm-repo">` +
@@ -719,17 +783,30 @@ a.ds-name:hover { text-decoration: underline; }
719
783
  .cm-node.cm-ok rect { stroke: var(--accent); }
720
784
  .cm-node.cm-warn rect { fill: #fffbeb; stroke: #fcd34d; }
721
785
  .cm-node.cm-off rect { fill: #f8f9fa; stroke: #d1d5db; stroke-dasharray: 4 3; }
786
+ /* 宣言はあるが実体が繋がっていない。未使用(グレー)とも接続済み(青)とも違うことを見せる */
787
+ .cm-node.cm-idle rect { fill: var(--surface); stroke: #cbd5e1; stroke-dasharray: 4 3; }
788
+ .cm-node.cm-idle .cm-sync { fill: #b45309; }
722
789
  .cm-label { font-size: 13.5px; font-weight: 600; fill: var(--text); }
723
790
  .cm-node.cm-warn .cm-label { fill: #92400e; }
724
791
  .cm-node.cm-off .cm-label { fill: #9ca3af; }
725
792
  .cm-node.cm-off text { filter: grayscale(1); opacity: .8; }
793
+ .cm-tool { font-size: 11px; fill: var(--muted); }
794
+ .cm-node.cm-off .cm-tool { fill: #9ca3af; }
726
795
  .cm-sync { font-size: 10.5px; fill: var(--muted); font-variant-numeric: tabular-nums; }
727
796
  .cm-node.cm-off .cm-sync { fill: #9ca3af; }
728
797
  .cm-badge { font-size: 13px; }
798
+ /* 追加した直後にその場で見せる行。正式な一覧(スナップショット由来)に載るまでの繋ぎ */
799
+ .ds-pending { margin-top: .5rem; display: flex; flex-direction: column; gap: .25rem; }
800
+ .ds-pending-item { display: flex; align-items: center; gap: .5rem; font-size: .85rem;
801
+ padding: .35rem .6rem; border: 1px dashed var(--accent); border-radius: 6px; background: var(--accent-weak); }
802
+ .ds-pending-mark { color: var(--accent); font-weight: 700; }
803
+ .ds-pending-name { font-weight: 600; word-break: break-all; }
804
+ .ds-pending-note { color: var(--muted); font-size: .78rem; margin-left: auto; white-space: nowrap; }
729
805
  .cm-link { fill: none; stroke-width: 1.6; stroke-dasharray: 6 6; stroke: #94a3b8; }
730
806
  .cm-link.cm-ok { stroke: var(--accent); animation: cm-flow 1.1s linear infinite; }
731
807
  .cm-link.cm-warn { stroke: #d97706; }
732
808
  .cm-link.cm-off { stroke: #d1d5db; opacity: .6; }
809
+ .cm-link.cm-idle { stroke: #cbd5e1; opacity: .7; }
733
810
  @keyframes cm-flow { to { stroke-dashoffset: -24; } }
734
811
  @media (prefers-reduced-motion: reduce) { .cm-link.cm-ok { animation: none; } }
735
812
  .cm-repo rect { fill: var(--accent-weak); stroke: var(--accent); stroke-width: 1.5; }
@@ -1846,32 +1923,41 @@ const CLIENT_JS = `
1846
1923
  var tools = SITE.tools || {};
1847
1924
  var usable = function (cap, tool) { var v = tools[cap]; return !v || v === "none" || v === tool; };
1848
1925
 
1849
- if (usable("チャット", "slack")) {
1850
- box.appendChild(addForm({
1851
- title: "+ Slackチャンネルを追加",
1852
- placeholder: "https://…slack.com/archives/C0XXXXXXX",
1853
- // **名前は任意。** URLにはIDしか入っていないので、サーバがSlackから引く。
1854
- // 人に入力させると打ち間違いがそのまま登録されるうえ、手間も増える。
1855
- // 別名を付けたいときだけ入れてもらう。
1856
- namePlaceholder: "表示名(省略するとSlackから取得します)",
1857
- requiredNote: "チャンネルのURLを入力してください。",
1858
- hint: "チャンネル名を右クリック →「リンクをコピー」で出るURLを貼ってください。追加しただけでは Gold 昇格には載りません(載せるには各チャンネルの「Gold昇格を始める」を押します)。cortex の Bot が入っていないチャンネルは名前を取得できません(「名前未取得」で登録され、Bot招待後にGold昇格をONにすると実名に変わります)。",
1859
- // **「Goldに載る」と書かない。** 追加だけでは載らないので嘘になる。
1860
- // 確認する理由は「中央Botで読みに行く」「他案件の取り違え」。
1861
- confirmLines: function (v) {
1862
- return [
1863
- "このチャンネルを、Cortex が読みに行く対象に追加します。",
1864
- "",
1865
- "貼ったURLがこの案件のものか、もう一度確認してください。",
1866
- "",
1867
- v,
1868
- "",
1869
- "追加しますか?",
1870
- ];
1871
- },
1872
- build: function (url, name) { return { op: "add-channel", url: url, name: name }; },
1873
- }));
1874
- }
1926
+ // **Slack tools 宣言で隠さない。** tools.チャット は能力ごとに1値しか持てないので、
1927
+ // 「会議はTeams・連絡はSlack」という普通の構成を表せない。teams を宣言した案件では
1928
+ // フォームごと消え、**Slackを渡されているのに追加できない**状態になっていた。
1929
+ //
1930
+ // 読む側(resolve-external-sources.mjs)も Slack の tools ゲートを外してある。
1931
+ // Gold昇格の本来のゲートは**チャンネル単位の gold: true**(追加直後は未宣言=読まれない。
1932
+ // 上げたければ追加後にトグルを押す)。そこは変えていないので、宣言なしで顧客可視のGoldへ
1933
+ // 流れることはない。
1934
+ //
1935
+ // 注: ここは埋め込みJSのテンプレートリテラル内。**コメントでもバッククォートを使わない**
1936
+ //(リテラルが閉じて構文エラーになる。実際にそれで壊した)。
1937
+ box.appendChild(addForm({
1938
+ title: "+ Slackチャンネルを追加",
1939
+ placeholder: "https://…slack.com/archives/C0XXXXXXX",
1940
+ // **名前は任意。** URLにはIDしか入っていないので、サーバがSlackから引く。
1941
+ // 人に入力させると打ち間違いがそのまま登録されるうえ、手間も増える。
1942
+ // 別名を付けたいときだけ入れてもらう。
1943
+ namePlaceholder: "表示名(省略するとSlackから取得します)",
1944
+ requiredNote: "チャンネルのURLを入力してください。",
1945
+ hint: "チャンネル名を右クリック →「リンクをコピー」で出るURLを貼ってください。追加しただけでは Gold 昇格には載りません(載せるには各チャンネルの「Gold昇格を始める」を押します)。cortex の Bot が入っていないチャンネルは名前を取得できません(「名前未取得」で登録され、Bot招待後にGold昇格をONにすると実名に変わります)。",
1946
+ // **「Goldに載る」と書かない。** 追加だけでは載らないので嘘になる。
1947
+ // 確認する理由は「中央Botで読みに行く」「他案件の取り違え」。
1948
+ confirmLines: function (v) {
1949
+ return [
1950
+ "このチャンネルを、Cortex が読みに行く対象に追加します。",
1951
+ "",
1952
+ "貼ったURLがこの案件のものか、もう一度確認してください。",
1953
+ "",
1954
+ v,
1955
+ "",
1956
+ "追加しますか?",
1957
+ ];
1958
+ },
1959
+ build: function (url, name) { return { op: "add-channel", url: url, name: name }; },
1960
+ }));
1875
1961
 
1876
1962
  if (usable("開発", "github")) {
1877
1963
  box.appendChild(addForm({
@@ -1938,8 +2024,20 @@ const CLIENT_JS = `
1938
2024
  msg.appendChild(document.createTextNode(" "));
1939
2025
  msg.appendChild(el("a", { href: j.runsUrl, target: "_blank", rel: "noopener", text: "実行状況を見る →" }));
1940
2026
  }
2027
+ // **画面への反映も起きていることを伝える。** 一覧はスナップショットから描かれるので、
2028
+ // 「保存した」だけでは「一覧が変わらない=失敗した」と読まれる。
2029
+ // 数分かかること・再読み込みで正式な一覧に載ることまで書く(黙っていると待てない)
2030
+ // 反映にかかる時間はサーバの文言(reflectionNote)が言うので、ここでは繰り返さない。
2031
+ // 二重に書くと同じ一文に同じ話が並ぶ
2032
+ if (j.refreshUrl) {
2033
+ msg.appendChild(document.createTextNode(" "));
2034
+ msg.appendChild(el("a", { href: j.refreshUrl, target: "_blank", rel: "noopener", text: "反映の状況 →" }));
2035
+ }
1941
2036
  btn.disabled = false;
1942
- if (onDone) onDone();
2037
+ // **何も変わっていないなら「追加した」と見せない。** 既に同じものが入っていた場合
2038
+ // サーバは 200 + changed:false を返す。ここを res.ok だけで判定すると
2039
+ // 「変更はありません」と言いながら追加した行が出る
2040
+ if (onDone && j.changed !== false) onDone();
1943
2041
  } catch (e) {
1944
2042
  msg.className = "ds-toggle-msg is-error";
1945
2043
  msg.textContent = "✗ 通信に失敗しました。";
@@ -2040,20 +2138,37 @@ const CLIENT_JS = `
2040
2138
  return;
2041
2139
  }
2042
2140
  if (opts.confirmLines && !confirmDanger(opts.confirmLines(main.value.trim()))) { msg.textContent = ""; return; }
2043
- var payload = opts.build(main.value.trim(), name ? name.value.trim() : "");
2141
+ var added = main.value.trim();
2142
+ var addedName = name ? name.value.trim() : "";
2143
+ var payload = opts.build(added, addedName);
2044
2144
  sendConfig(payload, msg, btn, function () {
2045
2145
  // 追加できたら入力欄を空にする(同じものを二度押ししないように)
2046
2146
  main.value = "";
2047
2147
  if (name) name.value = "";
2148
+ // **追加したものをその場に出す。** 上の一覧は fleet-status.json(夜間スナップショット)
2149
+ // から描かれているので、追加してもその場では変わらない。何も変わらないと
2150
+ // 「追加できていない」と読まれ、もう一度押して「既に登録されています」になる
2151
+ // (実際にそうなった)。だから押した結果をここで見せる。
2152
+ pending.appendChild(
2153
+ el("div", { class: "ds-pending-item" }, [
2154
+ el("span", { class: "ds-pending-mark", text: "+" }),
2155
+ el("span", { class: "ds-pending-name", text: addedName || added }),
2156
+ el("span", { class: "ds-pending-note", text: "追加済み・反映中" }),
2157
+ ]),
2158
+ );
2048
2159
  });
2049
2160
  });
2050
2161
 
2162
+ // このセッションで追加したものを並べる場所(リロードすると消える=正式な一覧は上に出る)
2163
+ var pending = el("div", { class: "ds-pending" });
2164
+
2051
2165
  var box = el("div", { class: "ds-addform" }, [
2052
2166
  el("div", { class: "ds-addform-title", text: opts.title }),
2053
2167
  opts.hint ? el("p", { class: "sec-how", text: opts.hint }) : null,
2054
2168
  main,
2055
2169
  name,
2056
2170
  el("div", { class: "ds-toggle" }, [btn, msg]),
2171
+ pending,
2057
2172
  ]);
2058
2173
  return box;
2059
2174
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.4.55",
3
+ "version": "0.4.57",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,