@takagaki/cortex-decisions-viewer 0.4.5 → 0.4.7

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
@@ -472,7 +472,9 @@ function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl,
472
472
  for (const sec of sections)
473
473
  for (const r of sec.records)
474
474
  fileNameToId.set(r.fileName, r.id);
475
- // 参照先IDから層を推定してノード化する
475
+ // 参照先IDから層を推定してノード化する。
476
+ // 外部リンク(http(s))はノード化せず null を返す(外部URLの点はグラフ肥大化=描画負荷の主因。
477
+ // リンク自体は各レコードの詳細・参照欄に残るため情報は失われない)。
476
478
  const classifyTarget = (target) => {
477
479
  if (nodes.has(target))
478
480
  return target;
@@ -496,9 +498,7 @@ function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl,
496
498
  return addNode({ id: target, label, group, url });
497
499
  }
498
500
  else if (/^https?:\/\//.test(target)) {
499
- group = "external";
500
- label = target.replace(/^https?:\/\//, "").slice(0, 60);
501
- return addNode({ id: target, label, group, url: target });
501
+ return null; // 外部リンクはノード化しない
502
502
  }
503
503
  else if (target.includes("/") || target.toLowerCase().endsWith(".md")) {
504
504
  group = "silver"; // リポジトリ内ファイルへのパス参照 → GitHubのコピーへ飛べるようにする
@@ -508,36 +508,45 @@ function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl,
508
508
  }
509
509
  return addNode({ id: target, label, group });
510
510
  };
511
+ // 解決先が null(外部リンク等で非ノード化)の辺は張らない
512
+ const pushEdge = (fromId, target, rel) => {
513
+ if (target)
514
+ edges.push({ from: fromId, to: target, rel });
515
+ };
516
+ // 外部URLノードを作る(source/references の出典用フォールバック)。
517
+ const externalNode = (url, label) => addNode({
518
+ id: url,
519
+ label: (label ?? url.replace(/^https?:\/\//, "")).slice(0, 60),
520
+ group: "external",
521
+ url,
522
+ });
523
+ // 出典(source/reference)の解決: Backlog課題URLは実ノード(bronze)へ、議事録ID等は該当ノードへ。
524
+ // 解決できない外部URL(Wiki・GitHub等)は外部ノードとして必ず繋ぐ(出典は少数で意味があるため残す。
525
+ // 大量の「本文リンク」の騒音は addLinkEdges 側でドロップしたまま)。
526
+ const provenanceTarget = (raw, label) => {
527
+ const v = raw.trim();
528
+ return classifyTarget(backlogViewKey(v) ?? v) ?? externalNode(v, label);
529
+ };
511
530
  const addRelationEdges = (fromId, relations) => {
512
- for (const r of relations) {
513
- edges.push({ from: fromId, to: classifyTarget(r.target), rel: r.rel });
514
- }
531
+ for (const r of relations)
532
+ pushEdge(fromId, classifyTarget(r.target), r.rel);
515
533
  };
516
534
  const addSourceEdge = (fromId, source) => {
517
535
  if (source && source.trim() !== "")
518
- edges.push({ from: fromId, to: classifyTarget(source.trim()), rel: "source" });
536
+ edges.push({ from: fromId, to: provenanceTarget(source), rel: "source" });
519
537
  };
520
538
  const addReferenceEdges = (fromId, references) => {
521
539
  for (const ref of references) {
522
- if (ref.url) {
523
- const id = addNode({
524
- id: ref.url,
525
- label: ref.label.slice(0, 60),
526
- group: "external",
527
- url: ref.url,
528
- });
529
- edges.push({ from: fromId, to: id, rel: "reference" });
530
- }
531
- else {
532
- edges.push({ from: fromId, to: classifyTarget(ref.label), rel: "reference" });
533
- }
540
+ const to = ref.url ? provenanceTarget(ref.url, ref.label) : classifyTarget(ref.label);
541
+ pushEdge(fromId, to, "reference");
534
542
  }
535
543
  };
544
+ // Gold層(Decision/用語/レポート/Home)の本文リンクは必ず繋ぐ(外部URLでも外部ノードとして接続)。
545
+ // =frontmatterを持つレコードは孤立させない。大量の騒音は「コンテキスト側の本文リンク」(content edges)
546
+ // のみドロップする(下の content ループは classifyTarget のまま)。
536
547
  const addLinkEdges = (fromId, links) => {
537
- for (const link of links) {
538
- const key = backlogViewKey(link);
539
- edges.push({ from: fromId, to: classifyTarget(key ?? link), rel: "link" });
540
- }
548
+ for (const link of links)
549
+ edges.push({ from: fromId, to: provenanceTarget(link), rel: "link" });
541
550
  };
542
551
  // Bronze/Silverのノードを先に登録する(Gold・Bronze/Silver双方からの参照が同一ノードに解決されるように)
543
552
  for (const c of content) {
@@ -573,7 +582,7 @@ function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl,
573
582
  }
574
583
  for (const c of content) {
575
584
  for (const e of c.edges) {
576
- edges.push({ from: c.id, to: classifyTarget(e.target), rel: e.rel });
585
+ pushEdge(c.id, classifyTarget(e.target), e.rel);
577
586
  }
578
587
  }
579
588
  // 自己ループと重複辺を除去する(同じURLが複数回貼られていても辺は1本)
@@ -587,7 +596,15 @@ function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl,
587
596
  seen.add(key);
588
597
  return true;
589
598
  });
590
- return { nodes: [...nodes.values()], edges: deduped };
599
+ // 描画対象は「レコード(Gold) or 辺に接続のあるノード」だけ。外部リンク除去で孤立した
600
+ // 非レコードノードは描画もされないため、ペイロードからも落としておく(初期ロードを軽くする)。
601
+ const usedIds = new Set();
602
+ for (const e of deduped) {
603
+ usedIds.add(e.from);
604
+ usedIds.add(e.to);
605
+ }
606
+ const prunedNodes = [...nodes.values()].filter((n) => n.recordId || usedIds.has(n.id));
607
+ return { nodes: prunedNodes, edges: deduped };
591
608
  }
592
609
  /**
593
610
  * 各レコードの bodyHtml を out/bodies/<i>.html に書き出し、bodyRef にパスを設定して
package/dist/render.js CHANGED
@@ -979,8 +979,8 @@ const CLIENT_JS = `
979
979
  }
980
980
  resize();
981
981
 
982
- function tick() {
983
- // 反発
982
+ // 反発(O()・全ペア総当たり)。点が少ないうちはこれが最速。
983
+ function repulseNaive() {
984
984
  for (var i = 0; i < nodes.length; i++) {
985
985
  for (var j = i + 1; j < nodes.length; j++) {
986
986
  var a = nodes[i], b = nodes[j];
@@ -992,6 +992,60 @@ const CLIENT_JS = `
992
992
  a.vx -= fx; a.vy -= fy; b.vx += fx; b.vy += fy;
993
993
  }
994
994
  }
995
+ }
996
+ // 反発(Barnes-Hut・O(n log n))。四分木で遠いノード群を重心1点に近似する。
997
+ // 点が多い案件(フリート拡大時)でも tick が重くならない=点を削らずに固まりを防ぐための土台。
998
+ function repulseBH() {
999
+ var nn = nodes.length, i;
1000
+ if (nn < 2) return;
1001
+ var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1002
+ for (i = 0; i < nn; i++) { var p0 = nodes[i]; if (p0.x < minX) minX = p0.x; if (p0.y < minY) minY = p0.y; if (p0.x > maxX) maxX = p0.x; if (p0.y > maxY) maxY = p0.y; }
1003
+ var size = Math.max(maxX - minX, maxY - minY, 1) + 1;
1004
+ // 配列ベースの四分木。BODY: -1=空 / >=0=葉(そのノードindex) / -2=内部ノード
1005
+ var QX = [minX], QY = [minY], QS = [size], MASS = [0], CX = [0], CY = [0];
1006
+ var C0 = [-1], C1 = [-1], C2 = [-1], C3 = [-1], BODY = [-1];
1007
+ function 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; }
1008
+ function childOf(c, q) { return q === 0 ? C0[c] : q === 1 ? C1[c] : q === 2 ? C2[c] : C3[c]; }
1009
+ function setChild(c, q, v) { if (q === 0) C0[c] = v; else if (q === 1) C1[c] = v; else if (q === 2) C2[c] = v; else C3[c] = v; }
1010
+ function pushDown(c, bi, depth) {
1011
+ var hx = QX[c] + QS[c] / 2, hy = QY[c] + QS[c] / 2;
1012
+ var q = (nodes[bi].x >= hx ? 1 : 0) + (nodes[bi].y >= hy ? 2 : 0);
1013
+ var ch = childOf(c, q);
1014
+ if (ch === -1) { var s = QS[c] / 2; ch = nc(QX[c] + ((q & 1) ? s : 0), QY[c] + ((q & 2) ? s : 0), s); setChild(c, q, ch); }
1015
+ insert(ch, bi, depth + 1);
1016
+ }
1017
+ function insert(c, bi, depth) {
1018
+ var x = nodes[bi].x, y = nodes[bi].y;
1019
+ if (MASS[c] === 0) { CX[c] = x; CY[c] = y; } else { CX[c] = (CX[c] * MASS[c] + x) / (MASS[c] + 1); CY[c] = (CY[c] * MASS[c] + y) / (MASS[c] + 1); }
1020
+ if (MASS[c] === 0 && BODY[c] === -1) { BODY[c] = bi; MASS[c] = 1; return; }
1021
+ MASS[c]++;
1022
+ if (depth >= 24) return; // 深さ上限(ほぼ同一座標): これ以上分割せず集約
1023
+ if (BODY[c] !== -2) { var old = BODY[c]; BODY[c] = -2; pushDown(c, old, depth); }
1024
+ pushDown(c, bi, depth);
1025
+ }
1026
+ for (i = 0; i < nn; i++) insert(0, i, 0);
1027
+ var theta2 = 1.0; // 受容角θ=1.0 の二乗(セルサイズ/距離 < θ なら重心1点で近似)
1028
+ for (i = 0; i < nn; i++) {
1029
+ var p = nodes[i]; var stack = [0];
1030
+ while (stack.length) {
1031
+ var c = stack.pop();
1032
+ if (MASS[c] === 0) continue;
1033
+ if (BODY[c] === i && MASS[c] === 1) continue; // 自分自身の葉
1034
+ var dx = CX[c] - p.x, dy = CY[c] - p.y; var d2 = dx * dx + dy * dy + 0.01;
1035
+ if (BODY[c] !== -2 || QS[c] * QS[c] < theta2 * d2) {
1036
+ var f = 1800 * MASS[c] / d2; var d = Math.sqrt(d2);
1037
+ p.vx -= dx / d * f; p.vy -= dy / d * f;
1038
+ } else {
1039
+ if (C0[c] !== -1) stack.push(C0[c]); if (C1[c] !== -1) stack.push(C1[c]);
1040
+ if (C2[c] !== -1) stack.push(C2[c]); if (C3[c] !== -1) stack.push(C3[c]);
1041
+ }
1042
+ }
1043
+ }
1044
+ }
1045
+ // 約900点を境に切替: 小規模=naive(オーバーヘッド無しで最速) / 大規模=Barnes-Hut(固まらない)。
1046
+ var REPULSE_NAIVE_MAX = 900;
1047
+ function tick() {
1048
+ if (nodes.length < REPULSE_NAIVE_MAX) repulseNaive(); else repulseBH();
995
1049
  // ばね(辺)
996
1050
  edges.forEach(function (e) {
997
1051
  var dx = e.b.x - e.a.x, dy = e.b.y - e.a.y;
@@ -1000,20 +1054,22 @@ const CLIENT_JS = `
1000
1054
  var fx = (dx / d) * f, fy = (dy / d) * f;
1001
1055
  e.a.vx += fx; e.a.vy += fy; e.b.vx -= fx; e.b.vy -= fy;
1002
1056
  });
1003
- // Goldは中心への重力、グレーは外周リングへの引力 + 減衰
1057
+ // 全ノードを中心へ向ける等方的な引力(Goldは強め=中央に、コンテキストは弱め=その周り)。
1058
+ // さらに半径 RING の円で外周を押し戻して封じ込める。これで対角の伸び・飛び出したスポークが
1059
+ // 抑えられ、全体が円形のディスクに収まる(force-directed本来の「丸める力の不在」を補う)。
1004
1060
  nodes.forEach(function (n) {
1005
- if (n.recordId) {
1006
- n.vx -= n.x * 0.012; n.vy -= n.y * 0.012;
1007
- } else {
1008
- var r = Math.sqrt(n.x * n.x + n.y * n.y) + 0.01;
1009
- var rf = (r - RING) * 0.012;
1061
+ var g = n.recordId ? 0.012 : 0.005;
1062
+ n.vx -= n.x * g; n.vy -= n.y * g;
1063
+ var r = Math.sqrt(n.x * n.x + n.y * n.y) + 0.01;
1064
+ if (r > RING) {
1065
+ var rf = (r - RING) * 0.05; // 円の外に出た分だけ内側へ強めに戻す(円形の境界)
1010
1066
  n.vx -= (n.x / r) * rf; n.vy -= (n.y / r) * rf;
1011
1067
  }
1012
1068
  if (n === draggingNode) { n.vx = 0; n.vy = 0; return; }
1013
1069
  n.vx *= 0.85; n.vy *= 0.85;
1014
1070
  n.x += n.vx * alpha; n.y += n.vy * alpha;
1015
1071
  });
1016
- if (alpha > 0.05) alpha *= 0.995;
1072
+ if (alpha > 0.0015) alpha *= 0.97; // 収束まで減衰しきる(下げ止まりさせず、整定後はループ側で物理計算を止める)
1017
1073
  }
1018
1074
 
1019
1075
  function nodeRadius(n) {
@@ -1257,6 +1313,7 @@ const CLIENT_JS = `
1257
1313
  else if (n.url) window.open(n.url, "_blank", "noopener");
1258
1314
  }
1259
1315
  }
1316
+ if (draggingNode) alpha = Math.max(alpha, 0.2); // 動かした点の周りを少し整え直してから止まる
1260
1317
  draggingNode = null;
1261
1318
  panning = null;
1262
1319
  canvas.classList.remove("dragging");
@@ -1282,7 +1339,9 @@ const CLIENT_JS = `
1282
1339
  var running = true;
1283
1340
  function loop() {
1284
1341
  if (!running || !document.body.contains(canvas)) { running = false; return; }
1285
- tick();
1342
+ // 収束後(alpha減衰しきり・無操作)は O() の物理計算 tick を止める。これが「固まり続ける」主因。
1343
+ // ドラッグ/パン中は動かす必要があるので回す。描画(draw)はホバー演出・イントロのため毎フレーム続ける。
1344
+ if (alpha > 0.004 || draggingNode || panning) tick();
1286
1345
  draw();
1287
1346
  requestAnimationFrame(loop);
1288
1347
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,