@takagaki/cortex-decisions-viewer 0.4.8 → 0.4.10

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.
package/dist/build.js CHANGED
@@ -589,6 +589,197 @@ function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl,
589
589
  });
590
590
  return { nodes: [...nodes.values()], edges: deduped };
591
591
  }
592
+ /** 反発(O(n²))。点が少ないうちはこれが最速。 */
593
+ function repulseNaiveLayout(ps) {
594
+ for (let i = 0; i < ps.length; i++) {
595
+ for (let j = i + 1; j < ps.length; j++) {
596
+ const a = ps[i], b = ps[j];
597
+ const dx = b.x - a.x, dy = b.y - a.y;
598
+ const d2 = dx * dx + dy * dy + 0.01;
599
+ const f = 1800 / d2;
600
+ const d = Math.sqrt(d2);
601
+ const fx = (dx / d) * f, fy = (dy / d) * f;
602
+ a.vx -= fx;
603
+ a.vy -= fy;
604
+ b.vx += fx;
605
+ b.vy += fy;
606
+ }
607
+ }
608
+ }
609
+ /** 反発(Barnes-Hut・O(n log n))。四分木で遠いノード群を重心1点に近似する。render.ts と同一アルゴリズム。 */
610
+ function repulseBHLayout(ps) {
611
+ const nn = ps.length;
612
+ if (nn < 2)
613
+ return;
614
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity, i;
615
+ for (i = 0; i < nn; i++) {
616
+ const p = ps[i];
617
+ if (p.x < minX)
618
+ minX = p.x;
619
+ if (p.y < minY)
620
+ minY = p.y;
621
+ if (p.x > maxX)
622
+ maxX = p.x;
623
+ if (p.y > maxY)
624
+ maxY = p.y;
625
+ }
626
+ const size = Math.max(maxX - minX, maxY - minY, 1) + 1;
627
+ const QX = [minX], QY = [minY], QS = [size], MASS = [0], CX = [0], CY = [0];
628
+ const C0 = [-1], C1 = [-1], C2 = [-1], C3 = [-1], BODY = [-1];
629
+ const nc = (x, y, s) => { QX.push(x); QY.push(y); QS.push(s); MASS.push(0); CX.push(0); CY.push(0); C0.push(-1); C1.push(-1); C2.push(-1); C3.push(-1); BODY.push(-1); return QX.length - 1; };
630
+ const childOf = (c, q) => q === 0 ? C0[c] : q === 1 ? C1[c] : q === 2 ? C2[c] : C3[c];
631
+ const setChild = (c, q, v) => { if (q === 0)
632
+ C0[c] = v;
633
+ else if (q === 1)
634
+ C1[c] = v;
635
+ else if (q === 2)
636
+ C2[c] = v;
637
+ else
638
+ C3[c] = v; };
639
+ const pushDown = (c, bi, depth) => {
640
+ const hx = QX[c] + QS[c] / 2, hy = QY[c] + QS[c] / 2;
641
+ const q = (ps[bi].x >= hx ? 1 : 0) + (ps[bi].y >= hy ? 2 : 0);
642
+ let ch = childOf(c, q);
643
+ if (ch === -1) {
644
+ const s = QS[c] / 2;
645
+ ch = nc(QX[c] + ((q & 1) ? s : 0), QY[c] + ((q & 2) ? s : 0), s);
646
+ setChild(c, q, ch);
647
+ }
648
+ insert(ch, bi, depth + 1);
649
+ };
650
+ const insert = (c, bi, depth) => {
651
+ const x = ps[bi].x, y = ps[bi].y;
652
+ if (MASS[c] === 0) {
653
+ CX[c] = x;
654
+ CY[c] = y;
655
+ }
656
+ else {
657
+ CX[c] = (CX[c] * MASS[c] + x) / (MASS[c] + 1);
658
+ CY[c] = (CY[c] * MASS[c] + y) / (MASS[c] + 1);
659
+ }
660
+ if (MASS[c] === 0 && BODY[c] === -1) {
661
+ BODY[c] = bi;
662
+ MASS[c] = 1;
663
+ return;
664
+ }
665
+ MASS[c]++;
666
+ if (depth >= 24)
667
+ return;
668
+ if (BODY[c] !== -2) {
669
+ const old = BODY[c];
670
+ BODY[c] = -2;
671
+ pushDown(c, old, depth);
672
+ }
673
+ pushDown(c, bi, depth);
674
+ };
675
+ for (i = 0; i < nn; i++)
676
+ insert(0, i, 0);
677
+ const theta2 = 1.0;
678
+ for (i = 0; i < nn; i++) {
679
+ const p = ps[i];
680
+ const stack = [0];
681
+ while (stack.length) {
682
+ const c = stack.pop();
683
+ if (MASS[c] === 0)
684
+ continue;
685
+ if (BODY[c] === i && MASS[c] === 1)
686
+ continue;
687
+ const dx = CX[c] - p.x, dy = CY[c] - p.y;
688
+ const d2 = dx * dx + dy * dy + 0.01;
689
+ if (BODY[c] !== -2 || QS[c] * QS[c] < theta2 * d2) {
690
+ const f = 1800 * MASS[c] / d2;
691
+ const d = Math.sqrt(d2);
692
+ p.vx -= dx / d * f;
693
+ p.vy -= dy / d * f;
694
+ }
695
+ else {
696
+ if (C0[c] !== -1)
697
+ stack.push(C0[c]);
698
+ if (C1[c] !== -1)
699
+ stack.push(C1[c]);
700
+ if (C2[c] !== -1)
701
+ stack.push(C2[c]);
702
+ if (C3[c] !== -1)
703
+ stack.push(C3[c]);
704
+ }
705
+ }
706
+ }
707
+ }
708
+ /**
709
+ * グラフのレイアウト座標をビルド時に1回だけ計算し、各ノードに x/y を書き込む。
710
+ * クライアント(render.ts startGraph)と同一の力学を回すので、ブラウザはこれを初期位置に使い
711
+ * ウォームアップを省略できる=開いた瞬間ゼロ待ち。描画対象(recordId or 辺に接続)のみ計算する。
712
+ */
713
+ function computeLayout(graph) {
714
+ const used = new Set();
715
+ for (const e of graph.edges) {
716
+ used.add(e.from);
717
+ used.add(e.to);
718
+ }
719
+ const ns = graph.nodes.filter((n) => n.recordId || used.has(n.id));
720
+ const N = ns.length;
721
+ if (N === 0)
722
+ return;
723
+ const ps = ns.map(() => ({ x: 0, y: 0, vx: 0, vy: 0 }));
724
+ const isRec = ns.map((n) => !!n.recordId);
725
+ const byId = new Map();
726
+ ns.forEach((n, i) => byId.set(n.id, i));
727
+ const RING = Math.max(200, 26 * Math.sqrt(N));
728
+ for (let i = 0; i < N; i++) {
729
+ const a = (i / N) * Math.PI * 2;
730
+ const r = isRec[i] ? 60 : RING;
731
+ ps[i].x = Math.cos(a) * r + (i % 7) * 3;
732
+ ps[i].y = Math.sin(a) * r + (i % 5) * 3;
733
+ }
734
+ const es = [];
735
+ for (const e of graph.edges) {
736
+ const a = byId.get(e.from), b = byId.get(e.to);
737
+ if (a !== undefined && b !== undefined && a !== b)
738
+ es.push([a, b]);
739
+ }
740
+ const big = N >= 900;
741
+ let alpha = 1;
742
+ for (let t = 0; t < 450; t++) {
743
+ if (big)
744
+ repulseBHLayout(ps);
745
+ else
746
+ repulseNaiveLayout(ps);
747
+ for (const [ai, bi] of es) {
748
+ const a = ps[ai], b = ps[bi];
749
+ const dx = b.x - a.x, dy = b.y - a.y;
750
+ const d = Math.sqrt(dx * dx + dy * dy) + 0.01;
751
+ const f = (d - 90) * 0.02;
752
+ const fx = (dx / d) * f, fy = (dy / d) * f;
753
+ a.vx += fx;
754
+ a.vy += fy;
755
+ b.vx -= fx;
756
+ b.vy -= fy;
757
+ }
758
+ for (let i = 0; i < N; i++) {
759
+ const p = ps[i];
760
+ if (isRec[i]) {
761
+ p.vx -= p.x * 0.012;
762
+ p.vy -= p.y * 0.012;
763
+ }
764
+ else {
765
+ const r = Math.sqrt(p.x * p.x + p.y * p.y) + 0.01;
766
+ const rf = (r - RING) * 0.012;
767
+ p.vx -= (p.x / r) * rf;
768
+ p.vy -= (p.y / r) * rf;
769
+ }
770
+ p.vx *= 0.85;
771
+ p.vy *= 0.85;
772
+ p.x += p.vx * alpha;
773
+ p.y += p.vy * alpha;
774
+ }
775
+ if (alpha > 0.003)
776
+ alpha *= 0.995;
777
+ }
778
+ for (let i = 0; i < N; i++) {
779
+ ns[i].x = Math.round(ps[i].x);
780
+ ns[i].y = Math.round(ps[i].y);
781
+ }
782
+ }
592
783
  /**
593
784
  * 各レコードの bodyHtml を out/bodies/<i>.html に書き出し、bodyRef にパスを設定して
594
785
  * inlineデータ上の bodyHtml を空にする(初期ロードのpayloadから本文を外す)。
@@ -672,6 +863,7 @@ async function build(opts) {
672
863
  : new Map();
673
864
  const contentEntries = await scanContent(repoRoot, knowledgeRoot ? path.basename(knowledgeRoot) : null, repo.baseUrl, repo.branch);
674
865
  const graph = buildGraph(decisions, home, sections, contentEntries, targetUrls, repo.baseUrl, repo.branch);
866
+ computeLayout(graph); // 各ノードに x/y を書き込む(クライアントはこれを初期位置に使いウォームアップ省略)
675
867
  const site = {
676
868
  title: opts.title,
677
869
  generatedAt: opts.generatedAt,
package/dist/render.js CHANGED
@@ -4,11 +4,29 @@ exports.renderSite = renderSite;
4
4
  /** SiteDataを単一の自己完結HTML(埋め込みデータ+CSS+JS)にレンダリングする */
5
5
  function renderSite(site) {
6
6
  // </script> によるブレイクアウトを防ぐため < をエスケープして埋め込む
7
+ // Claude Code Web ディープリンク(この案件リポ+狙ったプロンプトで開く)。repoBaseUrl から slug を導出(追加入力なし)。
8
+ const repoSlug = site.repoBaseUrl
9
+ ? site.repoBaseUrl
10
+ .replace(/^https?:\/\/github\.com\//, "")
11
+ .replace(/\.git$/, "")
12
+ : "";
13
+ const codeUrl = (prompt) => repoSlug
14
+ ? `https://claude.ai/code?repo=${repoSlug}&prompt=${encodeURIComponent(prompt)}`
15
+ : "https://claude.ai/code/";
16
+ const askUrl = codeUrl("このプロジェクトについて、Cortex/Home.md と最近の Decision を読んだうえで、直近の状況・主要な決定・進行中の論点を教えてください。");
17
+ // 「操作」タブの取り込みアクション。repo が解決できた案件だけ出す(リポ書き込み権を持つ人=CM社員や招待済み顧客が使える)。
18
+ if (repoSlug) {
19
+ site.intake = {
20
+ transcript: codeUrl("会議の文字起こしを取り込みます。まず 会議/README.md(無ければ MTG/README.md 等の会議ディレクトリのREADME)と 会議/templates/minute-template.md を読んでください。次のメッセージで、ダウンロードした文字起こしファイル(.txt / .docx 等)を添付するので、その内容を読み、テンプレートの様式に従って議事録を生成し、文字起こし原本と議事録を会議ディレクトリの規約どおりの場所にコミットしてください。/post-meeting や /create-minute スラッシュコマンドが使えるならそれを使ってよいですが、使えなくても手動で作成して構いません。議事録に frontmatter は付けないでください。"),
21
+ material: codeUrl("共有資料を取り込みます。まず 共有資料/README.md を読んでください。次のメッセージで資料(テキストまたはファイル)を渡すので、それを Markdown 化して 共有資料/{資料名}/{資料名}.md に配置し、元ファイルも同じディレクトリに残してコミットしてください(PDF・PPT は中身を読み、図表も文章で説明して Markdown 化する)。/sync-materials スラッシュコマンドが使えるならそれを使ってよいですが、使えなくても手動で構いません。frontmatter は付けないでください。"),
22
+ };
23
+ }
7
24
  const dataJson = JSON.stringify(site).replace(/</g, "\\u003c");
8
25
  const safeTitle = escapeHtml(site.title);
9
26
  const githubBtn = site.repoBaseUrl
10
27
  ? `<a class="gh-btn" href="${escapeHtml(site.repoBaseUrl)}" target="_blank" rel="noopener" title="コンテキストリポジトリをGitHubで開く">${GITHUB_ICON}</a>`
11
28
  : "";
29
+ const askBtn = `<a class="ask-ai-btn" href="${escapeHtml(askUrl)}" target="_blank" rel="noopener" title="Claude Code Web でこの案件のリポジトリを開き、プロジェクトについて質問・調査できます">💬 プロジェクトについてAIに聞く</a>`;
12
30
  return `<!DOCTYPE html>
13
31
  <html lang="ja">
14
32
  <head>
@@ -25,8 +43,7 @@ function renderSite(site) {
25
43
  <header class="site-header">
26
44
  <div class="header-inner">
27
45
  <a class="site-title" href="?" id="home-link">${safeTitle}</a>
28
- <a class="ask-ai-btn" href="https://claude.ai/code/" target="_blank" rel="noopener"
29
- title="Claude Codeでこのプロジェクトのリポジトリを選択すると、全コンテキストに対してAIに質問・調査ができます">💬 プロジェクトについてAIに聞く</a>
46
+ ${askBtn}
30
47
  ${githubBtn}
31
48
  </div>
32
49
  </header>
@@ -313,6 +330,23 @@ a:hover { text-decoration: underline; }
313
330
  }
314
331
  .edit-btn:hover { border-color: var(--accent); color: var(--accent); text-decoration: none; }
315
332
  .section-label { font-size: 12px; font-weight: 700; color: var(--muted); letter-spacing: .04em; text-transform: uppercase; margin: 24px 0 8px; }
333
+ /* ---- 操作タブ ---- */
334
+ .ops-intro { color: var(--muted); font-size: 14px; line-height: 1.8; margin: 4px 0 8px; }
335
+ .ops-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 14px; }
336
+ .ops-card {
337
+ background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
338
+ padding: 18px; box-shadow: var(--shadow); display: flex; flex-direction: column; gap: 10px;
339
+ }
340
+ .ops-card-head { display: flex; align-items: center; gap: 10px; }
341
+ .ops-card-icon { font-size: 22px; line-height: 1; }
342
+ .ops-card-title { font-size: 15px; font-weight: 700; color: var(--text); margin: 0; }
343
+ .ops-card-desc { color: var(--muted); font-size: 13px; line-height: 1.7; margin: 0; flex: 1 1 auto; }
344
+ .ops-card-btn {
345
+ align-self: flex-start; display: inline-flex; align-items: center; gap: 4px;
346
+ padding: 8px 16px; border-radius: 999px; border: 1px solid var(--accent);
347
+ background: var(--accent); color: #fff; font-size: 13px; font-weight: 600;
348
+ }
349
+ .ops-card-btn:hover { background: #1d4ed8; text-decoration: none; }
316
350
  .summary-box { background: var(--accent-weak); border: 1px solid #dce6fb; border-radius: var(--radius); padding: 12px 16px; color: #1e3a8a; }
317
351
  .deciders { display: flex; flex-wrap: wrap; gap: 8px; }
318
352
  .refs { margin: 0; padding-left: 18px; }
@@ -380,6 +414,10 @@ const CLIENT_JS = `
380
414
  if (SITE.graph && SITE.graph.nodes.length > 0) {
381
415
  TABS.push({ key: "graph", label: "ナレッジグラフ" });
382
416
  }
417
+ // Cortex自体を操作するアクションの受け皿(取り込み等)。今後この種の機能が増えたらここに集約する。
418
+ if (SITE.intake) {
419
+ TABS.push({ key: "ops", label: "操作" });
420
+ }
383
421
 
384
422
  var app = document.getElementById("app");
385
423
  // ウィキリンク(本文中の [[用語]])クリックでSPA内遷移する
@@ -526,6 +564,7 @@ const CLIENT_JS = `
526
564
  if (listState.tab === "home") { renderHome(); return; }
527
565
  if (listState.tab === "decisions") { renderDecisionList(); return; }
528
566
  if (listState.tab === "graph") { renderKnowledge(); return; }
567
+ if (listState.tab === "ops") { renderOps(); return; }
529
568
  renderGenericList();
530
569
  }
531
570
 
@@ -561,6 +600,29 @@ const CLIENT_JS = `
561
600
  loadBody(h, homeBody);
562
601
  }
563
602
 
603
+ // ---------- 操作タブ(Cortex自体への操作。まずは取り込み。今後この受け皿に機能を足す) ----------
604
+ function renderOps() {
605
+ var intake = SITE.intake;
606
+ app.appendChild(el("p", { class: "ops-intro", text: "Cortex にコンテキストを取り込む操作です。ボタンを押すと Claude Code Web がこの案件のリポジトリで開くので、内容を貼る/渡すと議事録化・Markdown化してコミットまで行います。" }));
607
+ app.appendChild(el("div", { class: "section-label", text: "取り込み" }));
608
+ var grid = el("div", { class: "ops-grid" });
609
+ if (intake) {
610
+ grid.appendChild(opsCard("🗒️", "会議の文字起こしを追加", "顧客主催の会議など、自動取り込みできない会議の文字起こしファイル(.txt / .docx 等)を添付して議事録を生成します。", intake.transcript, "文字起こしを追加"));
611
+ grid.appendChild(opsCard("📎", "共有資料を追加", "提案書・仕様書・議事メモなどの資料を渡して Markdown 化し、共有資料に取り込みます。", intake.material, "資料を追加"));
612
+ }
613
+ app.appendChild(grid);
614
+ }
615
+ function opsCard(icon, title, desc, url, btnLabel) {
616
+ return el("div", { class: "ops-card" }, [
617
+ el("div", { class: "ops-card-head" }, [
618
+ el("span", { class: "ops-card-icon", text: icon }),
619
+ el("h3", { class: "ops-card-title", text: title }),
620
+ ]),
621
+ el("p", { class: "ops-card-desc", text: desc }),
622
+ el("a", { class: "ops-card-btn", href: url, target: "_blank", rel: "noopener", text: "+ " + btnLabel }),
623
+ ]);
624
+ }
625
+
564
626
  // ---------- Decisions一覧(フィルタ付き) ----------
565
627
  function renderDecisionList() {
566
628
  // 既存案件・移行案件は導入前/移行前の意思決定が揃わないため、その旨を最初に注記する
@@ -828,21 +890,27 @@ const CLIENT_JS = `
828
890
  // 弱い辺を消したとき、レコード以外の孤立ノードは隠す
829
891
  var used = {};
830
892
  allEdges.forEach(function (e) { used[e.from] = true; used[e.to] = true; });
893
+ // ビルド時に算出済みの座標(n.x/n.y)があればそれを使う=ウォームアップ不要で開いた瞬間に整列済み。
894
+ var preLaidOut = SITE.graph.nodes.some(function (n) { return typeof n.x === "number"; });
831
895
  var nodes = SITE.graph.nodes
832
896
  .filter(function (n) { return n.recordId || used[n.id]; })
833
897
  .map(function (n, i) {
834
898
  return { id: n.id, label: n.label, group: n.group, recordId: n.recordId, url: n.url,
835
- x: 0, y: 0, vx: 0, vy: 0, phase: i * 2.399 };
899
+ x: typeof n.x === "number" ? n.x : 0, y: typeof n.y === "number" ? n.y : 0,
900
+ vx: 0, vy: 0, phase: i * 2.399 };
836
901
  });
837
902
  // Gold層を中心に据え、グレーのコンテキストはその外周に配置する(ハブ&スポーク)
838
903
  var RING = Math.max(200, 26 * Math.sqrt(nodes.length));
839
904
  var byNode = {};
840
905
  nodes.forEach(function (n, i) {
841
906
  byNode[n.id] = n;
842
- var angle = (i / nodes.length) * Math.PI * 2;
843
- var r = n.recordId ? 60 : RING;
844
- n.x = Math.cos(angle) * r + (i % 7) * 3;
845
- n.y = Math.sin(angle) * r + (i % 5) * 3;
907
+ // 計算済み座標が無い場合のみ、円状の初期配置を与える(フォールバック)
908
+ if (!preLaidOut) {
909
+ var angle = (i / nodes.length) * Math.PI * 2;
910
+ var r = n.recordId ? 60 : RING;
911
+ n.x = Math.cos(angle) * r + (i % 7) * 3;
912
+ n.y = Math.sin(angle) * r + (i % 5) * 3;
913
+ }
846
914
  });
847
915
  var edges = allEdges
848
916
  .filter(function (e) { return byNode[e.from] && byNode[e.to]; })
@@ -1332,11 +1400,15 @@ const CLIENT_JS = `
1332
1400
  view.scale *= factor;
1333
1401
  }, { passive: false });
1334
1402
 
1335
- // 初回表示前にシミュレーションを裏で十分回し、整列済みの静かな状態で開く
1336
- // (ロード時のガタガタ揺れを無くす)。光のイントロは下で introStart を打ち直してから始める。
1337
- for (var warm = 0; warm < 320 && alpha > 0.06; warm++) tick();
1338
- alpha = Math.min(alpha, 0.05); // 残りの揺れも最小化(落ち着いた状態で開始)
1339
- applyFit(true); // 整定後の全ノードが収まるよう、開いた瞬間からフレーミングしておく
1403
+ // ビルド時に座標計算済みなら、ウォームアップ(重い同期計算)を完全に省略する=開いた瞬間ゼロ待ち。
1404
+ // 計算済みが無い場合のみ、従来どおり裏で320回回して整列済みの静かな状態で開く。
1405
+ if (preLaidOut) {
1406
+ alpha = 0.002; // 既に整定済み。ループ側の物理計算(tick)を即停止させる(ドラッグ時のみ再開)
1407
+ } else {
1408
+ for (var warm = 0; warm < 320 && alpha > 0.06; warm++) tick();
1409
+ alpha = Math.min(alpha, 0.05); // 残りの揺れも最小化(落ち着いた状態で開始)
1410
+ }
1411
+ applyFit(true); // 全ノードが収まるよう、開いた瞬間からフレーミングしておく
1340
1412
  if (!reduceMotion) introStart = performance.now(); // プリウォーム後を起点にして光の波を出す
1341
1413
 
1342
1414
  var running = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,