@takagaki/cortex-decisions-viewer 0.12.41 → 0.12.42

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 +154 -335
  2. package/package.json +1 -1
package/dist/render.js CHANGED
@@ -34,9 +34,6 @@ exports.secretStateFromNow = secretStateFromNow;
34
34
  exports.secretRegisteredAnywhere = secretRegisteredAnywhere;
35
35
  exports.sanitizeCapabilitiesNow = sanitizeCapabilitiesNow;
36
36
  exports.sanitizeGithubOwnersNow = sanitizeGithubOwnersNow;
37
- exports.sanitizeOnboardingNow = sanitizeOnboardingNow;
38
- exports.claudeAppRepoSlug = claudeAppRepoSlug;
39
- exports.claudeAppRequestText = claudeAppRequestText;
40
37
  exports.splitNeededParts = splitNeededParts;
41
38
  exports.ingestedAtOf = ingestedAtOf;
42
39
  exports.keyMissingText = keyMissingText;
@@ -69,19 +66,10 @@ const pending_ops_1 = require("./pending-ops");
69
66
  function renderSite(site, assets) {
70
67
  // </script> によるブレイクアウトを防ぐため < をエスケープして埋め込む
71
68
  // Claude Code Web ディープリンク(この案件リポ+狙ったプロンプトで開く)。repoBaseUrl から slug を導出(追加入力なし)。
72
- const repoSlug = site.repoBaseUrl
73
- ? site.repoBaseUrl
74
- .replace(/^https?:\/\/github\.com\//, "")
75
- .replace(/\.git$/, "")
76
- : "";
77
- // 組み立ての正本は claudeCodeUrl 1か所(#404)。ここは薄い包みに留める。
78
- // `?? []` は必須——build.ts は fleet-status.json が無い案件で externalSources を持たない
79
- // 第4引数は使っているツールの段落(#275)。**7導線すべてに同じ段落を付ける**ため、
80
- // 渡す場所も codeUrl 1か所に寄せる(クライアント側の codeUrl と対)
81
- const codeUrl = (prompt) => claudeCodeUrl(prompt, repoSlug, site.externalSources ?? [], site.tools && site.tools.paragraph);
82
- /** ヘッダー「AIに聞く」の指示文。data-ai-prompt に同じ値を残し、接続状況が届いたら href を組み直す */
83
- const ASK_PROMPT = "このプロジェクトの概要について教えてください。";
84
- const askUrl = codeUrl(ASK_PROMPT);
69
+ const repoSlug = projectRepoSlug(site.repoBaseUrl);
70
+ // ヘッダーの「AIに聞く」と、接続マップの下の「Claude Code で開く」(#465)は
71
+ // **同じ askAiUrl を呼ぶ**(同じ先を開くことを、引数の写しではなく構造で保証する)
72
+ const askUrl = askAiUrl(site);
85
73
  // ※ 投入フォーム設定 site.intake は build.ts が --intake-url/--intake-token/--project-key から付与する。
86
74
  const dataJson = JSON.stringify(site).replace(/</g, "\\u003c");
87
75
  const safeTitle = escapeHtml(site.title);
@@ -393,6 +381,44 @@ function sourceAdapter(type) {
393
381
  const CLAUDE_CODE_HOME = "https://claude.ai/code/";
394
382
  /** 素のリンクを**テストから固定するための入れ物**(LIVE_CONFIRM_LIMITS と同じ作法) */
395
383
  exports.CLAUDE_CODE_LINK = { home: CLAUDE_CODE_HOME };
384
+ /**
385
+ * 「この案件について聞く」導線の指示文。
386
+ *
387
+ * ヘッダーの「💬 プロジェクトについてAIに聞く」と、接続マップの下の
388
+ * 「Claude Code で開く」(#465)が**同じ先を開くと決めた**ので、指示文はここ1か所に置く。
389
+ * 片方に書き写すと、URL は同じでも**開いた先の AI への最初の一言が2通り**になる。
390
+ *
391
+ * `data-ai-prompt` にも同じ値を残す(接続状況が届いたら href を組み直す・syncAiLinks)。
392
+ */
393
+ const ASK_PROMPT = "このプロジェクトの概要について教えてください。";
394
+ /**
395
+ * `repoBaseUrl`(https://github.com/owner/repo)から Claude Code に渡す `owner/repo`。
396
+ *
397
+ * **ヘッダーの導線(renderSite)と図の下の箱(renderConnectionMap)が同じ先を開く**ので、
398
+ * 導出もここ1か所に置く。形の検査は claudeCodeUrl 側(isRepositorySlug)が行うため、
399
+ * ここは「取れなければ空文字」に留める——呼び出し側は空でもそのまま渡してよい
400
+ * (claudeCodeUrl が素のリンクへ倒す)。
401
+ */
402
+ function projectRepoSlug(repoBaseUrl) {
403
+ return repoBaseUrl
404
+ ? String(repoBaseUrl)
405
+ .replace(/^https?:\/\/github\.com\//, "")
406
+ .replace(/\.git$/, "")
407
+ : "";
408
+ }
409
+ /**
410
+ * 「この案件を Claude Code Web で開く」URL。
411
+ *
412
+ * 🔴 **ヘッダーの「AIに聞く」と、図の下の「Claude Code で開く」(#465)はこの1本を呼ぶ。**
413
+ * 呼び出し側で `claudeCodeUrl` を組み直すと、引数を1つ落とした側だけが
414
+ * **開発リポジトリを載せない/ツールの段落を付けない**——「同じ先」が静かに割れる。
415
+ *
416
+ * `?? []` は必須——build.ts は fleet-status.json が無い案件で externalSources を持たない。
417
+ * 第4引数は使っているツールの段落(#275・全導線に同じ段落を付ける)。
418
+ */
419
+ function askAiUrl(site) {
420
+ return claudeCodeUrl(ASK_PROMPT, projectRepoSlug(site.repoBaseUrl), site.externalSources ?? [], site.tools && site.tools.paragraph);
421
+ }
396
422
  /**
397
423
  * owner/repo の形か(ポータル web/src/features/harness/harness-link.ts の REPOSITORY_SLUG と同じ形)。
398
424
  *
@@ -1797,100 +1823,6 @@ function sanitizeGithubOwnersNow(v) {
1797
1823
  }
1798
1824
  return out.length ? out : null;
1799
1825
  }
1800
- /**
1801
- * 立ち上げの申請の印(`registeredNow.onboarding`・#94)の語彙。
1802
- * サーバ側 `CLAUDE_APP_STATES`(`infra/lambda/intake/logic.mjs`)の写し。
1803
- *
1804
- * 🔴 **増やすときは `logic.mjs` の `CLAUDE_APP_STATES` と `registeredOnboarding` を
1805
- * 同じ便で直すこと。順番は「サーバが先・画面が後」。**
1806
- *
1807
- * 片方だけ増やすと、その語の扱いが写しの間でずれる:
1808
- * - 画面だけ増やす … サーバが `null` に倒した語を画面が受け取ることは無いので実害は
1809
- * 出ないが、次に触る人は「画面は知っているのに出ない」を追うことになる
1810
- * - サーバだけ増やす … サーバが新しい語を返し始めるのに画面の篩が落とす。
1811
- * `sanitizeOnboardingNow` は落としたものを `null` にするので、**印を出してはいけない
1812
- * 案件へ節が出る**方向には倒れないが、印を出すべき案件で節が二度と出なくなる
1813
- *
1814
- * **写しを互いに固定する検査は置けない**(intake とビューアは別パッケージで、
1815
- * ビューアは infra を実行時に読まない)。だから順番をここに書いて守る。
1816
- */
1817
- const CLAUDE_APP_STATES = ["unrequested", "requested"];
1818
- /** op=state に足す能力名。**`STATE_CAP_NAMES` には入れない**(`ALL_STATE_CAPS` の項) */
1819
- const ONBOARDING_CAP = "onboarding";
1820
- /**
1821
- * `registeredNow.onboarding`(立ち上げの印)の許可リスト。
1822
- *
1823
- * **他の registeredNow のキーとは器が違う**——あちらは行の配列、こちらは1つの状態
1824
- * (`{ claudeGithubApp }`)なので、`sanitizeRegisteredNow` には混ぜない
1825
- * (混ぜると配列を期待している篩がオブジェクトを踏む)。
1826
- *
1827
- * 🔴 **知らない語は `null` に倒す。** 画面は `"unrequested"` のときだけ節を出すので、
1828
- * `null` に倒れる=**印を出さない側**へ倒れる。ここを truthy 判定に緩めると、
1829
- * サーバが新しい語を返し始めた日に、印を出してはいけない案件へ節が出る。
1830
- * `_note` のような運用メモは通さない(誰も確認していない文章を顧客の画面に出さない)。
1831
- */
1832
- function sanitizeOnboardingNow(v) {
1833
- if (!v || typeof v !== "object" || Array.isArray(v))
1834
- return null;
1835
- const s = typeof v.claudeGithubApp === "string" ? v.claudeGithubApp : "";
1836
- if (CLAUDE_APP_STATES.indexOf(s) < 0)
1837
- return null;
1838
- return { claudeGithubApp: s };
1839
- }
1840
- /**
1841
- * 申請文に埋める `owner/repo`(`repoBaseUrl` から導く。追加入力なし)。
1842
- *
1843
- * 🔴 **正規表現をクライアント側に書き写さないこと。** 埋め込みJSはテンプレート
1844
- * リテラルの中なので、`\/` がリテラルの解釈で潰れて**別物の正規表現**になる
1845
- * (`splitNeededParts` / `backlogTargetOf` と同じ理由。実際にここで一度壊した)。
1846
- *
1847
- * **`repoNameOf` とは別に持つ。** あちらは画面に出す短い名前(owner を落とす)で、
1848
- * こちらは申請の宛先を一意に決める値——**owner を落としてはいけない**。
1849
- *
1850
- * **github.com のURLだけを受ける。** 別ホストの slug で申請文を作ると、
1851
- * 存在しないリポジトリの追加を依頼させることになる。
1852
- * owner だけの URL(github.com/owner)は空文字(呼び出し側が節ごと出さない)。
1853
- *
1854
- * **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す)。
1855
- */
1856
- function claudeAppRepoSlug(repoBaseUrl) {
1857
- const m = /^https?:\/\/github\.com\/(.+)$/.exec(String(repoBaseUrl == null ? "" : repoBaseUrl));
1858
- if (!m)
1859
- return "";
1860
- const slug = m[1].replace(/\/+$/, "").replace(/\.git$/, "");
1861
- return slug.indexOf("/") > 0 ? slug : "";
1862
- }
1863
- /**
1864
- * Claude GitHub App の追加申請の申請文(画面がコピーして Slack へ貼る)。
1865
- *
1866
- * 🔴 **申請者本人の GitHub アカウント名はここで決めない。** 画面はログインしている人の
1867
- * GitHub アカウントを知らない(ビューアは Basic 認証で、GitHub とは繋がっていない)ので、
1868
- * **本人が書き足す**と文中で頼む——ポータルの `buildApplicationText` と同じ判断で、
1869
- * 間違ったアカウントを admin に入れる事故を防ぐ。
1870
- *
1871
- * ⚠ 装飾のない素のテキスト。申請の宛先(Slack のチャンネル)は Markdown を解釈しない。
1872
- *
1873
- * **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す)。
1874
- *
1875
- * **案件名も入れる。** 承認する側(GitHub の管理チーム)はリポジトリ名だけでは
1876
- * どの案件の話か分からず、名前が似た案件が並ぶと取り違えの元になる。
1877
- *
1878
- * @param repoSlug `owner/repo`。取れなければ空文字(呼び出し側が節ごと出さない)
1879
- * @param projectName この案件の表示名(画面のタイトル)。空なら行ごと落とす
1880
- */
1881
- function claudeAppRequestText(repoSlug, projectName) {
1882
- const name = typeof projectName === "string" ? projectName.trim() : "";
1883
- return [
1884
- "Claude GitHub App の追加をお願いします。",
1885
- "",
1886
- // **空の「案件: 」を出さない。** 名前の無い行は、書き忘れたようにしか読めない
1887
- ...(name ? ["案件: " + name] : []),
1888
- "対象リポジトリ: " + repoSlug,
1889
- "用途: Cortex(AI 案件ビューア)のコンテキストリポジトリを Claude Code から読む",
1890
- "",
1891
- "申請者: (ここにご自身の GitHub アカウント名を書いてください)",
1892
- ].join("\n");
1893
- }
1894
1826
  /**
1895
1827
  * サーバが返した「次の一手」(`needed`)を、素の文字列と URL に切り分ける。
1896
1828
  *
@@ -2545,6 +2477,23 @@ function repoNameOf(repoBaseUrl) {
2545
2477
  // owner だけの URL(github.com/owner)を名前と取り違えない
2546
2478
  return slug.includes("/") ? name : "";
2547
2479
  }
2480
+ /**
2481
+ * 図の下(「Claude Code で開く」の箱の直下)に出す注釈(#465)。
2482
+ *
2483
+ * 🔴 **社内の手続き(Slack のチャンネル名・宛先・申請文)は書かない。** 他社をテナントと
2484
+ * して入れる前提の画面に、クラスメソッド社内の申請の宛先を一律で出さない。
2485
+ * 🔴 **出し分けない。** Claude GitHub App が追加済みかは Cortex からは確かめられない
2486
+ * (追加先を一覧する API は組織オーナー権限か App 自身の資格が要る)。**判定できないものを
2487
+ * 出し分けると必ずどちらかで嘘になる**ので、1行を常に出す。
2488
+ */
2489
+ const CONN_MAP_NOTE = "リポジトリが選べないときは、GitHub App「Claude」にこのリポジトリを追加してもらってください(GitHub 組織の管理者が行います)";
2490
+ /**
2491
+ * 操作タブ最上部の接続マップを組み立てる(ビルド時。`<template id="conn-map">` に焼く)。
2492
+ *
2493
+ * 戻り値は **SVG の図+その下の注釈 1 行(`<p class="conn-map-note">`)** で、
2494
+ * クライアント側(`renderConnMap`)は丸ごと clone して置くだけ。注釈を SVG の中に
2495
+ * 入れないのは、SVG の `<text>` が折り返せないため(CONN_MAP_NOTE の項)。
2496
+ */
2548
2497
  function renderConnectionMap(site) {
2549
2498
  const internals = site.internalSources ?? [];
2550
2499
  const externals = site.externalSources ?? [];
@@ -2689,6 +2638,11 @@ function renderConnectionMap(site) {
2689
2638
  // 案件の表示名へ落とす——名前の無い箱にしない。
2690
2639
  const repoName = repoNameOf(site.repoBaseUrl) || site.title;
2691
2640
  const repoLabel = repoName.length > 24 ? repoName.slice(0, 23) + "…" : repoName;
2641
+ // 中央の箱のジオメトリ(下の「Claude Code で開く」もここから導く。数字を2度書かない)
2642
+ const REPO_LEFT = REPO_X - 170;
2643
+ const REPO_W = 340;
2644
+ const REPO_Y = 240;
2645
+ const REPO_H = 52;
2692
2646
  // アイコンと名前を**まとめて中央へ置く**。text-anchor=middle だけだとアイコンの分だけ
2693
2647
  // 全体が右に寄るので、合計幅を見積もってから左端を決める。日本語が混じる案件名へ
2694
2648
  // フォールバックした場合に備え、全角は倍幅で数える(14px/700 の実測に合わせた概算)
@@ -2697,17 +2651,50 @@ function renderConnectionMap(site) {
2697
2651
  const textW = [...repoLabel].reduce((w, ch) => w + (ch.charCodeAt(0) < 128 ? 8.2 : 14), 0);
2698
2652
  const startX = REPO_X - (ICON + GAP + textW) / 2;
2699
2653
  const repo = `<g class="cm-repo">` +
2700
- `<rect x="${REPO_X - 170}" y="240" width="340" height="52" rx="12"></rect>` +
2654
+ `<rect x="${REPO_LEFT}" y="${REPO_Y}" width="${REPO_W}" height="${REPO_H}" rx="12"></rect>` +
2701
2655
  // ロゴはビルド時に埋め込む(ヘッダ右上・開発ソースのカードと同じ GITHUB_ICON)。外部通信は起きない。
2702
2656
  // 入れ子の svg を transform で置き、色は cm-repo-logo の color を継ぐ(fill="currentColor")
2703
2657
  `<g class="cm-repo-logo" transform="translate(${startX.toFixed(1)}, ${266 - ICON / 2 - 1})">${GITHUB_ICON}</g>` +
2704
2658
  `<text class="cm-repo-label" x="${(startX + ICON + GAP).toFixed(1)}" y="272">${escapeHtml(repoLabel)}</text>` +
2705
2659
  `</g>`;
2706
- return (`<svg class="cm-svg" viewBox="0 0 ${W} 312" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="データソースとの接続マップ">` +
2660
+ // ---- 「Claude Code で開く」(#465)------------------------------------------
2661
+ // **図の流れを1本にする。** つないだツール → コンテキストリポジトリ →
2662
+ // **それを読む Claude Code**。押す先はヘッダーの「AIに聞く」と同じ(askAiUrl)。
2663
+ //
2664
+ // 🔴 **線は実線・静止。** 点線+流れ(.cm-link.cm-ok の animation)は「いまデータが
2665
+ // 通っている経路」の合図として上の6本で使っている。ここは経路ではなく
2666
+ // 「このリポジトリを Claude Code が読む」という関係なので、同じ記号を使うと嘘になる。
2667
+ //
2668
+ // 🔴 **クラス名に `cm-link` を使わない。** 図の線を数えるテスト(connection-map の
2669
+ // linkStates・new-project-onboarding)が6本を前提にしており、派生名でも当たる。
2670
+ //
2671
+ // 🔴 **`data-ai-prompt` を付ける。** 接続状況が届いたときの貼り替え(syncAiLinks)の
2672
+ // 対象に入る=登録した開発リポジトリが当日から載る。ヘッダーと同じ先を保つ唯一の道。
2673
+ const OPEN_Y = 336;
2674
+ const OPEN_H = 46;
2675
+ const openBox = `<path class="cm-open-line" d="M ${REPO_X} ${REPO_Y + REPO_H} L ${REPO_X} ${OPEN_Y}"></path>` +
2676
+ `<a class="cm-open" href="${escapeHtml(askAiUrl(site))}" data-ai-prompt="${escapeHtml(ASK_PROMPT)}"` +
2677
+ ` target="_blank" rel="noopener">` +
2678
+ `<rect x="${REPO_LEFT}" y="${OPEN_Y}" width="${REPO_W}" height="${OPEN_H}" rx="12"></rect>` +
2679
+ `<text class="cm-open-label" x="${REPO_X}" y="${OPEN_Y + 29}" text-anchor="middle">Claude Code で開く</text>` +
2680
+ `</a>`;
2681
+ // 🔴 **箱と線は cm-repo より前に連結する。** connection-map.test.mjs の repoNode() は
2682
+ // 「cm-repo から文字列の末尾まで」を切って「図が外部から画像を読んでいないか」を見ており、
2683
+ // 後ろに置くと href の https://claude.ai がその検査に当たる(描画順は重ならないので自由)。
2684
+ //
2685
+ // 注釈は **SVG の外の HTML 1行**(#465)。SVG の <text> は折り返せず、図は 760px まで
2686
+ // 縮むので、中に入れると読めない大きさになる。**判定はしないので常に出す**——
2687
+ // 「追加済みか」は API で確かめられない(組織オーナー権限か App 自身の資格が要る)。
2688
+ return (`<svg class="cm-svg" viewBox="0 0 ${W} 402" xmlns="http://www.w3.org/2000/svg"` +
2689
+ // 🔴 **role="img" にしない。** 中に本物のリンク(cm-open)が入ったので、
2690
+ // 1枚の画像として名乗ると「キーボードでは押せるのに読み上げは何も言わない」になる
2691
+ ` role="group" aria-label="データソースとの接続マップ">` +
2707
2692
  links.join("") +
2708
2693
  nodes.join("") +
2694
+ openBox +
2709
2695
  repo +
2710
- `</svg>`);
2696
+ `</svg>` +
2697
+ `<p class="conn-map-note">${escapeHtml(CONN_MAP_NOTE)}</p>`);
2711
2698
  }
2712
2699
  /**
2713
2700
  * 「はじめに」セクション(Homeタブ下部)のチュートリアルデータをビルド時に生成する。
@@ -3517,24 +3504,6 @@ button.site-icon[aria-busy="true"] .site-icon-plus { opacity: 0; }
3517
3504
  @media (max-width: 600px) {
3518
3505
  .tut-actions { flex-direction: column; align-items: flex-start; }
3519
3506
  }
3520
- /* ---- Claude GitHub App の追加申請(#94)----
3521
- 接続マップの直後・「データソースと自動化」の前に出る。**印を持つ案件にしか出ない**ので、
3522
- 常設の節と同じ見た目にはしない(一度きりの手続き)。
3523
- 🔴 **枠は症状・行は一手。** 帯にしないのは、いま何かが壊れているわけではないから。 */
3524
- .claude-app { margin-top: 28px; }
3525
- .claude-app-head { font-size: 16px; font-weight: 700; color: var(--text); margin: 0 0 6px; }
3526
- .claude-app-lead { color: var(--text); font-size: 13.5px; line-height: 1.8; margin: 0 0 10px; }
3527
- /* 申請文。**選べる形で出す**(コピーボタンが使えない環境でも手で取れるように)。
3528
- 等幅にするのは、貼り先(Slack)で改行がそのまま効くことを見せるため */
3529
- .claude-app-text {
3530
- width: 100%; box-sizing: border-box; min-height: 130px; resize: vertical;
3531
- border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px;
3532
- background: var(--surface); color: var(--text);
3533
- font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
3534
- font-size: 12px; line-height: 1.7;
3535
- }
3536
- .claude-app-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 8px; }
3537
- .claude-app-done { color: #1a7f37; font-size: 13px; margin: 0; }
3538
3507
  /* ---- データソースと自動化 ---- */
3539
3508
  .datasources { margin-top: 34px; }
3540
3509
  .datasources-head { font-size: 18px; font-weight: 700; color: var(--text); margin: 0 0 4px; }
@@ -3666,6 +3635,22 @@ a.ds-name:hover { text-decoration: underline; }
3666
3635
  .cm-repo-label { font-size: 14px; font-weight: 700; fill: #1e3a8a; }
3667
3636
  /* GitHubロゴは fill="currentColor" なので、色は color で渡す(ラベルと同色に揃える) */
3668
3637
  .cm-repo-logo { color: #1e3a8a; }
3638
+ /* ---- 図の下の「Claude Code で開く」(#465)----
3639
+ リポジトリの箱と同じ幅・同じ中心で、実線1本でつなぐ。**流れ(animation)は付けない**
3640
+ ——動く点線は「いまデータが通っている」の合図で、ここは経路ではなく関係だから。 */
3641
+ /* a:hover の下線(HTML のリンクの規則)は SVG の中では二重の強調になるので外す
3642
+ ——押せることは箱の塗り替えで見せている */
3643
+ .cm-open, .cm-open:hover { cursor: pointer; text-decoration: none; }
3644
+ .cm-open rect { fill: var(--surface); stroke: var(--accent); stroke-width: 1.5; }
3645
+ .cm-open:hover rect { fill: var(--accent-weak); }
3646
+ /* 🔴 **outline: none を書かない。** ここは本物のリンクで、キーボードだけで使う人が
3647
+ 「いまどこにいるか」を失う。枠を太らせる強調は、その上に重ねる */
3648
+ .cm-open:focus rect, .cm-open:focus-visible rect { fill: var(--accent-weak); stroke-width: 2.5; }
3649
+ .cm-open-label { font-size: 14px; font-weight: 700; fill: var(--accent); }
3650
+ .cm-open-line { fill: none; stroke: var(--accent); stroke-width: 1.6; }
3651
+ /* 注釈は図の外(HTML)。**横スクロールしても居残らせる**(.conn-map は overflow-x: auto で、
3652
+ 図は min-width 760px。左端に貼っておかないと、右へスクロールした人の視界から消える) */
3653
+ .conn-map-note { position: sticky; left: 0; color: var(--muted); font-size: 12.5px; line-height: 1.7; margin: 2px 0 0; }
3669
3654
  /* 未使用(非活性)行: アダプターとして存在するが、この案件では設定されていないもの */
3670
3655
  .ds-card.ds-inactive, .ds-pipe.ds-inactive { background: #f8f9fa; border-style: dashed; box-shadow: none; }
3671
3656
  .ds-inactive .ds-icon { filter: grayscale(1); opacity: .55; }
@@ -4356,14 +4341,6 @@ ${secretStateFromNow.toString()}
4356
4341
  ${secretRegisteredAnywhere.toString()}
4357
4342
  ${sanitizeCapabilitiesNow.toString()}
4358
4343
  ${sanitizeGithubOwnersNow.toString()}
4359
- // 立ち上げの申請の印(#94)。**判定の実体はビルド時の定義**——画面側に書き写すと
4360
- // 語彙の許可リストが2枚になる。
4361
- // 🔴 この2つの定数行を落とすと、流し込んだ関数が未定義を参照して画面が丸ごと死ぬ
4362
- var CLAUDE_APP_STATES = ${JSON.stringify(CLAUDE_APP_STATES)};
4363
- var ONBOARDING_CAP = ${JSON.stringify(ONBOARDING_CAP)};
4364
- ${sanitizeOnboardingNow.toString()}
4365
- ${claudeAppRepoSlug.toString()}
4366
- ${claudeAppRequestText.toString()}
4367
4344
  ${splitNeededParts.toString()}
4368
4345
  ${ingestedAtOf.toString()}
4369
4346
  var TILE_CAPS = ${JSON.stringify(TILE_CAPS)};
@@ -4855,9 +4832,14 @@ ${calendarAddUrl.toString()}
4855
4832
  // 届いたときに走るので、**画面が畳まれた後に届く**ことがある(テストの window.close()・
4856
4833
  // 実機のタブを閉じた直後)。掴んだノード越しなら、その後でも例外を投げずに済む
4857
4834
  // ——投げると応答の後段(取り直しの停止など)が丸ごと落ちる
4835
+ // 🔴 **setAttribute で貼る(プロパティ代入にしない)。** 接続マップの中の
4836
+ // 「Claude Code で開く」(#465)は SVG の a で、SVG の href は読み取り専用
4837
+ // (SVGAnimatedString)——代入は例外も出さずに黙って無視され、**その箱だけが
4838
+ // ビルド時の値のまま**(登録した開発リポジトリが載らない)になる。
4839
+ // HTML の a では setAttribute と .href = は同じ結果になる。
4858
4840
  function syncAiLinks() {
4859
4841
  var as = app.ownerDocument.querySelectorAll("a[data-ai-prompt]");
4860
- for (var i = 0; i < as.length; i++) as[i].href = codeUrl(as[i].getAttribute("data-ai-prompt"));
4842
+ for (var i = 0; i < as.length; i++) as[i].setAttribute("href", codeUrl(as[i].getAttribute("data-ai-prompt")));
4861
4843
  }
4862
4844
  // editUrl(.../edit/<branch>/<path>)からリポジトリ相対のファイルパスを取り出す(プロンプトで対象を指し示すため)
4863
4845
  function repoPathOf(rec) {
@@ -7674,27 +7656,6 @@ ${calendarAddUrl.toString()}
7674
7656
  * 指すのか説明できなくなる(realtime を別に持っているのと同じ理由)。
7675
7657
  */
7676
7658
  var LIVE_REGISTERED = null;
7677
- /**
7678
- * 立ち上げの申請の印(op=state の registeredNow.onboarding・#94)。
7679
- * 中身は claudeGithubApp の1欄("unrequested" / "requested")か null。**取得前も null。**
7680
- *
7681
- * 🔴 **null と "requested" を区別しないこと**——どちらも「節を出さない」で正しい。
7682
- * 取得前に出す形にすると、印を持たない既存案件(34 件)で**一瞬だけ節が出る**。
7683
- *
7684
- * **LIVE_REGISTERED とは別に持つ。** あちらは能力ごとの行の配列で、こちらは
7685
- * 1つの状態。同じ入れ物に混ぜると、配列を期待している篩とマージがオブジェクトを踏む。
7686
- *
7687
- * 注: ここは埋め込みJSのテンプレートリテラル内。**コメントでもバッククォートを使わない**
7688
- */
7689
- var LIVE_ONBOARDING = null;
7690
- /**
7691
- * この画面で「申請済み」を押して、サーバが保存を確定したか(#94)。
7692
- *
7693
- * **メモリだけに持つ**(localStorage には書かない)。押した直後に取り直す正本が
7694
- * 遅れた・落ちた回に、**役目を終えた申請文とボタンが戻ってくる**のを止めるためだけの旗で、
7695
- * 再読み込みすれば案件側のファイル(正本)を読み直す——だから正本は2つにならない。
7696
- */
7697
- var CLAUDE_APP_DONE = false;
7698
7659
  /**
7699
7660
  * いまの LIVE_REGISTERED を取りに行った時刻(op=state の要求を出した時刻・#388)。
7700
7661
  * 仮置きの失効「保存より後の live が同じことを言っている」の根拠。届いた時刻ではなく
@@ -12366,12 +12327,10 @@ ${calendarAddUrl.toString()}
12366
12327
  * ⚠ **並べ替えてから送る。** サーバのキャッシュキーは caps を並べ替えて繋ぐが、
12367
12328
  * こちらも定数の並びを固定しておく(同じ集合が何通りにも増えない)。
12368
12329
  */
12369
- // 🔴 **立ち上げの印(#94)は STATE_CAP_NAMES には足さず、ここで足す。** あちらは
12370
- // 「能力名の対応表」で、capabilitiesNow の行・接続状態ストアの引き当てにも使われる
12371
- // ——足すと、能力でも何でもない onboarding の行が能力の一覧に生える。
12372
- // ここに足すのは**サーバへ「このファイルも読んで」と伝える**ためだけ(操作タブを
12373
- // 開いているときは常に要求される。要求は常に全量なので、載せれば必ず入る)。
12374
- var ALL_STATE_CAPS = Object.keys(STATE_CAP_NAMES).concat([ONBOARDING_CAP]).sort();
12330
+ // **要求は STATE_CAP_NAMES の能力だけ。** 立ち上げの印(onboarding・#94)は
12331
+ // ここに足していたが、その印を出す枠を画面ごと撤去した(#465)ので外した
12332
+ // ——要求しない caps はサーバも読みに行かない(planRegisteredReads)。
12333
+ var ALL_STATE_CAPS = Object.keys(STATE_CAP_NAMES).sort();
12375
12334
  /**
12376
12335
  * 一度でも開いた能力。**送る caps とは無関係**(送るのは常に全量)。
12377
12336
  *
@@ -12486,11 +12445,6 @@ ${calendarAddUrl.toString()}
12486
12445
  // (=立ち上げ直後=この仕組みが一番効く案件)は下で throw するので、
12487
12446
  // ここより後に置くと**その案件だけ素通り**する
12488
12447
  LIVE_REGISTERED = sanitizeRegisteredNow(j && j.registeredNow);
12489
- // 🔴 **立ち上げの印も fleet より先に読む(#94)。** 印を持つのは
12490
- // **新しく作った案件だけ**=まとめ表(fleet-status.json)をまだ持たない案件で、
12491
- // 下の「fleet が無ければ throw」の経路に入る。ここより後に置くと、
12492
- // **この節が一番効くはずの案件でだけ**印が出ない
12493
- LIVE_ONBOARDING = sanitizeOnboardingNow(j && j.registeredNow && j.registeredNow.onboarding);
12494
12448
  LIVE_REGISTERED_AT = startedAt;
12495
12449
  LIVE_STATE = sanitizeStateNow(j && j.stateNow);
12496
12450
  // **欄の有無をそのまま覚える。** sanitize を通した後では「読めて0件」が消える
@@ -12526,8 +12480,6 @@ ${calendarAddUrl.toString()}
12526
12480
  syncAiLinks();
12527
12481
  // Home を開いていれば「使用ツール」の状態を実測で描き直す(#275)
12528
12482
  syncHomeTools();
12529
- // 立ち上げの申請の印(#94)。**ライブが届いて初めて出る**(それまでは空の受け皿)
12530
- paintClaudeAppSection();
12531
12483
  // 取得中に保存が効いていたら、迂回付きでもう一度取りに行く
12532
12484
  if (freshWanted) loadLiveFleet();
12533
12485
  })
@@ -12559,173 +12511,15 @@ ${calendarAddUrl.toString()}
12559
12511
  // 🔴 **ここでも「使用ツール」を描き直す(#275)。** 実測(stateNow)は fleet より
12560
12512
  // 先に読んでいるので、まとめ表を持たない案件でもこの経路で状態が確定する
12561
12513
  syncHomeTools();
12562
- // 🔴 **ここでも印を描き直す(#94)。** まとめ表(fleet-status.json)をまだ
12563
- // 持たない案件は上で throw してここへ来る——**印を持つのはまさにその案件**なので、
12564
- // ここに置かないと、この節が一番効くはずの新規案件でだけ何も出ない。
12565
- // 通信そのものが失敗した回は LIVE_ONBOARDING が null のままで、何も出ない
12566
- paintClaudeAppSection();
12567
12514
  // 取得中に保存が効いていたら、迂回付きでもう一度取りに行く
12568
12515
  if (freshWanted) loadLiveFleet();
12569
12516
  });
12570
12517
  }
12571
12518
 
12572
- /* ---------------------------------------------------------------------------
12573
- * Claude GitHub App の追加申請(#94)
12574
- * ---------------------------------------------------------------------------
12575
- * 案件の自動セットアップが終わったあと、「Claude Code からこのリポジトリを使うには
12576
- * 追加申請が要る」ことを伝える節。承認の完了は機械で検知しないので、
12577
- * **本人が「申請済み」を押すまで出し続ける。**
12578
- *
12579
- * 🔴 **仮置き(notePendingOp / pending-ops / localStorage)は使わない。**
12580
- * 印の正本は**案件側のファイル**(Cortex/onboarding.json)で、そこへの push は
12581
- * Cortex/** の変更としてビューアの再ビルドを起こす——つまり静的側もいずれ追いつく。
12582
- * localStorage に持つと、
12583
- * - 別の端末・別の人が開いたときに印が戻る(正本が2つになる)
12584
- * - 案件側のファイルを手で直しても画面が追いつかない
12585
- * という、正本を1つにすることで避けているズレを自分で作ることになる。
12586
- * 押した直後の追いつきは loadLiveFleet(true)(据え置きの迂回)1本で足りる。
12587
- *
12588
- * 🔴 **出す条件はライブの unrequested だけ。** null(取得前・取得失敗・知らない値)と
12589
- * requested では帯も行も出さない——取得前に出すと、**印を持たない既存案件で
12590
- * 一瞬だけ節が出る**(そしてその案件では押しても 404 で断られる)。
12591
- *
12592
- * 注: ここは埋め込みJSのテンプレートリテラル内。**コメントでもバッククォートを使わない**
12593
- * (リテラルが閉じて構文エラーになる。実際にそれで壊した)。
12594
- * ------------------------------------------------------------------------- */
12595
-
12596
- /**
12597
- * 節の中身を作る。**呼ばれるのは unrequested のときだけ**(判定は paint 側)。
12598
- *
12599
- * 中身は「見出し・一手の1行・申請文・コピー・申請済み」だけ。
12600
- * **帯は症状・行は一手・繰り返さない**——申請文に書いてあることを行でもう一度言わない。
12601
- */
12602
- function buildClaudeAppSection(slug) {
12603
- var sec = el("section", { class: "claude-app" });
12604
- sec.appendChild(el("h2", { class: "claude-app-head", text: "Claude GitHub App" }));
12605
- sec.appendChild(el("p", {
12606
- class: "claude-app-lead",
12607
- text: "Claude Code からこのリポジトリを使うには、追加申請が必要です。",
12608
- }));
12609
- // 申請の宛先。**どこへ出すのかが無いと、文面だけあっても一手にならない**
12610
- sec.appendChild(el("p", {
12611
- class: "sec-how",
12612
- text: "Slack の #tf-github-classmethod で @g-cm-jsys-github-admin 宛てに、下の申請文を貼って依頼してください。",
12613
- }));
12614
- // 案件名は SITE.title(画面のタイトル=この案件の表示名)。承認する側が
12615
- // リポジトリ名だけでは案件を特定できないので、両方を載せる
12616
- var text = claudeAppRequestText(slug, SITE.title);
12617
- // 選んで手でコピーできる形にする(コピーボタンが使えない環境のため。ポータルの②と同じ作法)
12618
- var field = el("textarea", {
12619
- class: "claude-app-text",
12620
- readonly: "readonly",
12621
- spellcheck: "false",
12622
- rows: "7",
12623
- });
12624
- field.value = text;
12625
- sec.appendChild(field);
12626
- var msg = el("div", { class: "ds-toggle-msg" });
12627
- var copyBtn = el("button", { class: "ds-toggle-btn", type: "button", text: "コピー" });
12628
- copyBtn.addEventListener("click", function () {
12629
- // コピーの作法は realtimeSyncCard と同じ(clipboard API → 選択 → 手でコピーの案内)。
12630
- // **黙って何も起きないにしない**
12631
- var done = function () { msg.className = "ds-toggle-msg is-ok"; msg.textContent = "✓ コピーしました。"; };
12632
- var fallback = function () {
12633
- try {
12634
- field.focus();
12635
- field.select();
12636
- if (document.execCommand && document.execCommand("copy")) return done();
12637
- } catch (e) { /* 下の案内へ */ }
12638
- msg.className = "ds-toggle-msg is-error";
12639
- msg.textContent = "⚠ 自動コピーできませんでした。選択された文字列を手でコピーしてください。";
12640
- };
12641
- try {
12642
- if (navigator.clipboard && navigator.clipboard.writeText) {
12643
- navigator.clipboard.writeText(text).then(done, fallback);
12644
- } else fallback();
12645
- } catch (e) { fallback(); }
12646
- });
12647
- var doneBtn = el("button", { class: "ds-toggle-btn", type: "button", text: "申請済み" });
12648
- doneBtn.addEventListener("click", function () { markClaudeAppRequested(doneBtn, msg); });
12649
- sec.appendChild(el("div", { class: "claude-app-actions" }, [copyBtn, doneBtn, msg]));
12650
- return sec;
12651
- }
12652
-
12653
- /**
12654
- * 「申請済み」を押したとき。
12655
- *
12656
- * **sendConfig は通さない。** あちらは成功のたびに仮置き(notePendingOp)を書く
12657
- * 経路で、この機能は仮置きを持たないと決めてある(上の項)。使うのは同じ callIntake。
12658
- *
12659
- * 成功したら**その場で節を1行に差し替え**(押した手応えを返す)、
12660
- * loadLiveFleet(true) で正本を取り直す。失敗したら**サーバーが返した文をそのまま出す**
12661
- * ——ファイルを持たない案件の 404 の文面も、そのまま読めば何が無いのか分かる。
12662
- */
12663
- async function markClaudeAppRequested(btn, msg) {
12664
- if (!SITE.intake) return;
12665
- var endBusy = beginBusy(btn);
12666
- msg.className = "ds-toggle-msg";
12667
- msg.textContent = "保存中…";
12668
- try {
12669
- var res = await callIntake("config", { op: "claude-app-requested" });
12670
- var j = await res.json().catch(function () { return {}; });
12671
- if (!res.ok) {
12672
- msg.className = "ds-toggle-msg is-error";
12673
- msg.textContent = "✗ " + (j.message || "保存できませんでした。");
12674
- endBusy();
12675
- return;
12676
- }
12677
- // **節ごと1行に差し替える。** 申請文もボタンも役目を終えているので残さない。
12678
- // 旗を先に立てるのは、下の取り直しが届いたときの描き直しでも1行のままにするため
12679
- CLAUDE_APP_DONE = true;
12680
- paintClaudeAppSection();
12681
- // **正本を取り直す**(据え置きを迂回)。ここで印は requested になり、
12682
- // 次に描き直したときには節が出ない
12683
- loadLiveFleet(true);
12684
- } catch (e) {
12685
- msg.className = "ds-toggle-msg is-error";
12686
- msg.textContent = "✗ 通信に失敗しました。";
12687
- endBusy();
12688
- }
12689
- }
12690
-
12691
- /**
12692
- * 節を描き直す。**冪等**(何度呼んでも同じ状態に落ち着く)。
12693
- *
12694
- * ライブの応答は操作タブを描いたあとに届くので、loadLiveFleet の成功・失敗の
12695
- * どちらからも呼ぶ(届くまでは受け皿が空のまま=何も出ない)。
12696
- */
12697
- function paintClaudeAppSection() {
12698
- var slot = app.querySelector(".claude-app-slot");
12699
- if (!slot) return;
12700
- slot.textContent = "";
12701
- // 🔴 **正本が追いついたら旗を落とす。** 落とさないと「✓ 申請済みとして記録しました。」が
12702
- // リロードするまでセッション中ずっと残る——旗は「正本が届くまでのつなぎ」でしかない。
12703
- // 落とせば、押した経路も最初から requested だった経路も**同じ「何も出ない」**に落ち着く
12704
- if (LIVE_ONBOARDING && LIVE_ONBOARDING.claudeGithubApp === "requested") CLAUDE_APP_DONE = false;
12705
- // 押した直後(サーバが保存を確定した)は、正本が届く前でも1行だけ残す。
12706
- // **正本が requested を返せば、この旗が無くても同じ「何も出ない」に落ち着く**
12707
- if (CLAUDE_APP_DONE) {
12708
- slot.appendChild(el("section", { class: "claude-app" }, [
12709
- el("p", { class: "claude-app-done", text: "✓ 申請済みとして記録しました。" }),
12710
- ]));
12711
- return;
12712
- }
12713
- // 🔴 **ライブの unrequested のときだけ。** null・未取得・requested では何も出さない
12714
- if (!LIVE_ONBOARDING || LIVE_ONBOARDING.claudeGithubApp !== "unrequested") return;
12715
- // 申請文にリポジトリ名を埋められないなら、押しても宛先の分からない依頼になる
12716
- var slug = claudeAppRepoSlug(SITE.repoBaseUrl);
12717
- if (!slug) return;
12718
- // 押す先(intake)が無い構成では、押せないボタンだけを出さない
12719
- if (!SITE.intake) return;
12720
- slot.appendChild(buildClaudeAppSection(slug));
12721
- }
12722
-
12723
12519
  function renderOps() {
12724
12520
  renderConnMap();
12725
- // 🔴 **受け皿は接続マップの直後・「データソースと自動化」より前**(利用者が最初に見る位置)。
12726
- // 中身が入るのはライブの応答が届いてからで、それまでは**空の受け皿**=何も出ない
12727
- app.appendChild(el("div", { class: "claude-app-slot" }));
12728
- paintClaudeAppSection();
12521
+ // 追加申請の枠(#94)はここに出していたが、撤去した(#465)。
12522
+ // 一手は図の下の「Claude Code で開く」と、その直下の注釈 1 行(CONN_MAP_NOTE)に集約。
12729
12523
  // 🔴 **先読みが飛行中なら、いま作った図を確認中へ倒す(#264)。**
12730
12524
  // 先読み(起動時の loadLiveFleet)は操作タブを開く前に走るので、
12731
12525
  // 確認中へ倒す口(loadLiveFleet の connMapConfirmingShown の1回)は
@@ -13277,13 +13071,38 @@ ${calendarAddUrl.toString()}
13277
13071
  if (s) t.textContent = "最終同期 " + s;
13278
13072
  });
13279
13073
  // ノードclickで、下の該当詳細カード(アンカー)へスクロール
13280
- wrap.addEventListener("click", function (ev) {
13074
+ function jumpToNode(ev) {
13281
13075
  var g = ev.target && ev.target.closest ? ev.target.closest("[data-target]") : null;
13282
13076
  if (!g) return;
13283
13077
  var target = document.getElementById(g.getAttribute("data-target"));
13284
13078
  if (target && target.scrollIntoView) target.scrollIntoView({ behavior: "smooth", block: "center" });
13079
+ }
13080
+ wrap.addEventListener("click", jumpToNode);
13081
+ // 🔴 **キーボードでも押せるようにする(#465 の実装レビュー)。** 6つのノードは
13082
+ // 前から role="link" tabindex="0" を名乗っているのに、押す手はクリックだけだった。
13083
+ // 図の role を img から group に変えた(中に本物のリンクが入ったため)ので、
13084
+ // **この瞬間からノードが支援技術に「リンク」として読み上げられる**——名乗っている
13085
+ // 以上、Enter / Space で同じ場所へ飛べないと「押せないリンク」を配ることになる。
13086
+ wrap.addEventListener("keydown", function (ev) {
13087
+ if (ev.key !== "Enter" && ev.key !== " " && ev.key !== "Spacebar") return;
13088
+ var g = ev.target && ev.target.closest ? ev.target.closest("[data-target]") : null;
13089
+ if (!g) return;
13090
+ // Space の既定(ページのスクロール)を止める。Enter は既定を持たない
13091
+ ev.preventDefault();
13092
+ jumpToNode(ev);
13285
13093
  });
13286
13094
  app.appendChild(wrap);
13095
+ // 🔴 **横にはみ出す図は、中央(リポジトリと「Claude Code で開く」の列)を出しておく。**
13096
+ // 図は最小 760px で、狭い画面では左端=いちばん左の能力の箱が出る。押す箱は中央に
13097
+ // あるので、そのままだと**スクロールしないと見えない**。溢れていないときは 0 のまま
13098
+ if (wrap.scrollWidth > wrap.clientWidth) {
13099
+ wrap.scrollLeft = (wrap.scrollWidth - wrap.clientWidth) / 2;
13100
+ }
13101
+ // 🔴 **図の中の「Claude Code で開く」(#465)を、いまの値で貼り直す。**
13102
+ // 図はビルド時に焼いてあるので、**起動時の貼り替え(syncAiLinks)はこの箱が
13103
+ // まだ文書に無いときに終わっている**——ここで呼ばないと、仮置きのリポジトリを
13104
+ // 持つ人や応答が届かない案件で、**この箱だけがヘッダーと違う先**を開く
13105
+ syncAiLinks();
13287
13106
  updateConnMapAsOf();
13288
13107
  applyStateToConnMap();
13289
13108
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.41",
3
+ "version": "0.12.42",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,