@takagaki/cortex-decisions-viewer 0.12.162 → 0.12.164

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
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.RELATED_MATERIALS_SECTIONS = exports.RELATED_URL_MAX = exports.RELATED_MATERIALS_FIELD = exports.DECISION_FIRMNESS = exports.PROJECT_ICON_EXTS = exports.MIRROR_RELS = void 0;
39
+ exports.RELATED_MATERIALS_SECTIONS = exports.RELATED_URL_MAX = exports.RELATED_MATERIALS_FIELD = exports.DECISION_FIRMNESS = exports.GRAPH_RELS = exports.GRAPH_RECORD_TYPES = exports.PROJECT_ICON_EXTS = exports.MIRROR_RELS = void 0;
40
40
  exports.sanitizeBody = sanitizeBody;
41
41
  exports.findProjectIcon = findProjectIcon;
42
42
  exports.readFleetSources = readFleetSources;
@@ -1025,7 +1025,15 @@ function sanitizeFm(v) {
1025
1025
  }
1026
1026
  // グラフの凡例で独立した分類(=色)を持つ type。Gold 4区画(decision/term/member/rule)は
1027
1027
  // それぞれ独立の分類にする(「その他レコード」に丸めない——2026-08-28 オーナー裁定)
1028
- const GOLD_TYPES = new Set(["decision", "term", "member", "rule", "report", "overview"]);
1028
+ /** つながり画面の点にする記録の種類(仕様 9 章 graph.records-and-evidence)。Home・レポート・その他は点にしない */
1029
+ exports.GRAPH_RECORD_TYPES = new Set(["decision", "term", "member", "rule"]);
1030
+ /** 区画(置き場)の名前 → 点の種類。`type` の無い記録の受け皿 */
1031
+ const GRAPH_SECTION_TYPES = { Decisions: "decision", Glossary: "term", Members: "member", Rules: "rule" };
1032
+ /**
1033
+ * つながり画面の線にする relations の種類(仕様 9 章・記録どうしの frontmatter のつながり)。
1034
+ * 根拠・生成元・関連・置換と、根拠への参照・出典だけ。話題(about)など、機械が名前の一致で付けた手がかりは線にしない
1035
+ */
1036
+ exports.GRAPH_RELS = new Set(["based_on", "derived_from", "relates_to", "supersedes", "reference", "source"]);
1029
1037
  /** ディレクトリ配下の .md を再帰的に列挙する(存在しなければ空) */
1030
1038
  async function walkMd(dirAbs) {
1031
1039
  const out = [];
@@ -1676,7 +1684,7 @@ function attachGlow(nodes, glow, recordRelPaths) {
1676
1684
  * ほうがよい(`createRefResolver` と同じ考え方)。崩れた文字(宛先に見えない参照)も同様に、
1677
1685
  * 点は作るが飛び先は付けない。
1678
1686
  */
1679
- function buildGraph(decisions, home, sections, targetUrls, repoBaseUrl, book = (0, evidence_names_1.emptyEvidenceBook)()) {
1687
+ function buildGraph(decisions, sections, targetUrls, repoBaseUrl, book = (0, evidence_names_1.emptyEvidenceBook)()) {
1680
1688
  const nodes = new Map();
1681
1689
  const edges = [];
1682
1690
  const addNode = (node) => {
@@ -1688,11 +1696,14 @@ function buildGraph(decisions, home, sections, targetUrls, repoBaseUrl, book = (
1688
1696
  for (const d of decisions) {
1689
1697
  addNode({ id: d.id, label: d.title, group: "decision", recordId: d.id });
1690
1698
  }
1691
- if (home)
1692
- addNode({ id: home.id, label: home.title, group: "overview", recordId: home.id });
1699
+ // 🔴 点は決定・ルール・用語・関係者の記録と、その根拠だけ(仕様 graph.question / graph.records-and-evidence)。
1700
+ // Home・レポート・その他レコードは点にしない
1693
1701
  for (const sec of sections) {
1694
1702
  for (const r of sec.records) {
1695
- const group = GOLD_TYPES.has(r.rtype) ? r.rtype : "record";
1703
+ // 種類は type で決め、type の無い記録は置き場(区画)で決める。どちらでも 4 種類に当たらないものは点にしない
1704
+ const group = exports.GRAPH_RECORD_TYPES.has(r.rtype) ? r.rtype : GRAPH_SECTION_TYPES[sec.name];
1705
+ if (!group)
1706
+ continue;
1696
1707
  addNode({ id: r.id, label: r.title, group, recordId: r.id });
1697
1708
  }
1698
1709
  }
@@ -1700,11 +1711,10 @@ function buildGraph(decisions, home, sections, targetUrls, repoBaseUrl, book = (
1700
1711
  const fileNameToId = new Map();
1701
1712
  for (const d of decisions)
1702
1713
  fileNameToId.set(d.fileName, d.id);
1703
- if (home)
1704
- fileNameToId.set(home.fileName, home.id);
1705
1714
  for (const sec of sections)
1706
1715
  for (const r of sec.records)
1707
- fileNameToId.set(r.fileName, r.id);
1716
+ if (nodes.has(r.id))
1717
+ fileNameToId.set(r.fileName, r.id);
1708
1718
  // 🔴 **GitHubの写し(blob)へ飛ぶ点は作らない**(2026-10-01 オーナー決定・分類B)。
1709
1719
  // `targetUrls`(minute:/material:)や詳細画面と共用の解決器(RefResolver)は、元ツールの
1710
1720
  // URLが無いとき自分のリポジトリの blob URL に倒すことがある(詳細画面ではそれでよい設計)。
@@ -1745,6 +1755,8 @@ function buildGraph(decisions, home, sections, targetUrls, repoBaseUrl, book = (
1745
1755
  return ev.id;
1746
1756
  };
1747
1757
  // 参照先IDから層を推定してノード化する
1758
+ // 🔴 引けない宛先(記録でも根拠でもないもの)は点を作らない。数だけ数えて、ビルドのログに出す(黙って捨てない)
1759
+ let unresolvedTargets = 0;
1748
1760
  const classifyTarget = (target, humanLabel) => {
1749
1761
  if (nodes.has(target) && !labelRank.has(target))
1750
1762
  return target;
@@ -1755,30 +1767,44 @@ function buildGraph(decisions, home, sections, targetUrls, repoBaseUrl, book = (
1755
1767
  return resolved;
1756
1768
  if (target.startsWith(NO_ORIGINAL_TOOL_PREFIX)) {
1757
1769
  // 元ツールが無い根拠(engine #968 の将来の語彙)。リンクにせず、機械の語(urn:…)を出さない
1758
- return addNode({ id: target, label: NO_ORIGINAL_TOOL_LABEL, group: "phantom" });
1770
+ return addNode({ id: target, label: NO_ORIGINAL_TOOL_LABEL, group: "silver" });
1759
1771
  }
1760
1772
  const ev = identifyEvidence(target, identityCtx, humanLabel);
1761
1773
  if (ev)
1762
1774
  return addEvidence(ev);
1775
+ // 点にしない記録(Home・レポート・その他)へのパス参照は、根拠ではない。点を作らず数える
1776
+ if (/(^|\/)Cortex\/(Home\.md|[^/]+\/records\/)/.test(target)) {
1777
+ unresolvedTargets++;
1778
+ return null;
1779
+ }
1763
1780
  if (target.includes("/") || target.toLowerCase().endsWith(".md")) {
1764
1781
  // リポジトリ内ファイルへの生のパス参照で、帳(写しの課題・資料・議事録)に当たらないもの。
1765
1782
  // 元ツールのURLがまだ無い記録は、点にするが飛び先を付けない(#968 の書き換えを待つ)。
1766
1783
  // 名前は最後の段から拡張子を外したもの(`.md` のようなファイルの語を出さない)
1767
1784
  return addNode({ id: target, label: (0, evidence_names_1.pathFallbackLabel)(target), group: "silver" });
1768
1785
  }
1769
- return addNode({ id: target, label: target, group: "phantom" });
1786
+ unresolvedTargets++;
1787
+ return null;
1770
1788
  };
1771
1789
  const addRelationEdges = (fromId, relations) => {
1772
1790
  for (const r of relations) {
1773
1791
  // 🔴 `superseded_by` は描かない(#699 F3)。新しい側の `supersedes` の写しで、描くと同じ置き換えが 2 本になる
1774
1792
  if (exports.MIRROR_RELS.has(r.rel))
1775
1793
  continue;
1776
- edges.push({ from: fromId, to: classifyTarget(r.target), rel: r.rel });
1794
+ // 🔴 線にするのは記録どうしの根拠・生成元・関連・置換だけ(話題 about などは引かない)
1795
+ if (!exports.GRAPH_RELS.has(r.rel))
1796
+ continue;
1797
+ const to = classifyTarget(r.target);
1798
+ if (to)
1799
+ edges.push({ from: fromId, to, rel: r.rel });
1777
1800
  }
1778
1801
  };
1779
1802
  const addSourceEdge = (fromId, source) => {
1780
- if (source && source.trim() !== "")
1781
- edges.push({ from: fromId, to: classifyTarget(source.trim()), rel: "source" });
1803
+ if (source && source.trim() !== "") {
1804
+ const to = classifyTarget(source.trim());
1805
+ if (to)
1806
+ edges.push({ from: fromId, to, rel: "source" });
1807
+ }
1782
1808
  };
1783
1809
  const addReferenceEdges = (fromId, references) => {
1784
1810
  for (const ref of references) {
@@ -1787,21 +1813,16 @@ function buildGraph(decisions, home, sections, targetUrls, repoBaseUrl, book = (
1787
1813
  // (`[名前](URL)` の名前は `identifyEvidence` が書かれていた値から拾う。解決器が付けた名前は
1788
1814
  // 種類の既定のこともあるので、人の名前としては渡さない)
1789
1815
  const raw = REF_RAW.get(ref);
1790
- if (raw !== undefined)
1791
- edges.push({ from: fromId, to: classifyTarget(raw), rel: "reference" });
1792
- else
1793
- edges.push({ from: fromId, to: classifyTarget(ref.url ?? ref.label, ref.url && ref.label !== ref.url ? ref.label : undefined), rel: "reference" });
1794
- }
1795
- };
1796
- const addLinkEdges = (fromId, links) => {
1797
- for (const link of links) {
1798
- edges.push({ from: fromId, to: classifyTarget(link), rel: "link" });
1816
+ const to = raw !== undefined
1817
+ ? classifyTarget(raw)
1818
+ : classifyTarget(ref.url ?? ref.label, ref.url && ref.label !== ref.url ? ref.label : undefined);
1819
+ if (to)
1820
+ edges.push({ from: fromId, to, rel: "reference" });
1799
1821
  }
1800
1822
  };
1801
1823
  for (const d of decisions) {
1802
1824
  addRelationEdges(d.id, d.relations);
1803
1825
  addReferenceEdges(d.id, d.references);
1804
- addLinkEdges(d.id, d.bodyLinks);
1805
1826
  }
1806
1827
  // 決めた人の線(決定 → 関係者・#1094)。relations には書かれないので `decided_by[].member` から引く。
1807
1828
  // 🔴 相手の関係者がグラフに無ければ引かない(宛先の無い点を作らない=classifyTarget を通さない)
@@ -1814,16 +1835,12 @@ function buildGraph(decisions, home, sections, targetUrls, repoBaseUrl, book = (
1814
1835
  edges.push({ from: d.id, to: who.member, rel: "decided_by" });
1815
1836
  }
1816
1837
  }
1817
- if (home) {
1818
- addRelationEdges(home.id, home.relations);
1819
- addSourceEdge(home.id, home.source);
1820
- addLinkEdges(home.id, home.bodyLinks);
1821
- }
1822
1838
  for (const sec of sections) {
1823
1839
  for (const r of sec.records) {
1840
+ if (!nodes.has(r.id))
1841
+ continue; // 点にしない種類(Home・レポート・その他)の線は引かない
1824
1842
  addRelationEdges(r.id, r.relations);
1825
1843
  addSourceEdge(r.id, r.source);
1826
- addLinkEdges(r.id, r.bodyLinks);
1827
1844
  }
1828
1845
  }
1829
1846
  // 自己ループと重複辺を除去する(同じURLが複数回貼られていても辺は1本)
@@ -1837,6 +1854,9 @@ function buildGraph(decisions, home, sections, targetUrls, repoBaseUrl, book = (
1837
1854
  seen.add(key);
1838
1855
  return true;
1839
1856
  });
1857
+ if (unresolvedTargets > 0) {
1858
+ console.log(` つながり: 引けない根拠 ${unresolvedTargets} 件は点にしませんでした(記録でも元のツールの根拠でもない宛先)`);
1859
+ }
1840
1860
  return { nodes: [...nodes.values()], edges: deduped };
1841
1861
  }
1842
1862
  /** 反発(O(n²))。点が少ないうちはこれが最速。 */
@@ -2332,7 +2352,7 @@ async function buildSite(opts, views) {
2332
2352
  const rankedDecisions = (0, ranking_1.orderByRanking)(decisions, (d) => recordRelPaths.get(d.id), memoryRanking?.Decisions ?? null);
2333
2353
  // 期間フィルタ用の月(YYYY-MM)を日付から導出し、降順(新しい順)にする
2334
2354
  const months = uniqueSorted(decisions.map((d) => d.date.slice(0, 7)).filter((m) => /^\d{4}-\d{2}$/.test(m))).reverse();
2335
- const graph = buildGraph(decisions, home, graphSections, targetUrls, repo.baseUrl, evidenceBook);
2355
+ const graph = buildGraph(decisions, graphSections, targetUrls, repo.baseUrl, evidenceBook);
2336
2356
  computeLayout(graph); // 各ノードに x/y を書き込む(クライアントはこれを初期位置に使いウォームアップ省略)
2337
2357
  // 点の明るさと大きさ(engine #551)。順位のファイルが読めない案件では 1 点も付かない(今までと同じ画面)
2338
2358
  if (memoryRanking)
@@ -2347,11 +2367,18 @@ async function buildSite(opts, views) {
2347
2367
  // ストーリー(#901 V2-5)の日本時間の今日。日・週・月の3つで同じ日にそろえるため1回だけ計算する
2348
2368
  const storyToday = (0, story_1.todayJst)(new Date(opts.generatedAt));
2349
2369
  // ストーリーの日々の動き(#901 V2-5)。帳簿で on の日のファイルが無ければ null(キーごと出さない)
2370
+ const shownIds = new Set([...decisions.map((d) => d.id), ...sections.flatMap((s) => s.records.map((r) => r.id))]);
2371
+ const idByRepoPath = new Map();
2372
+ for (const [id, rel] of recordRelPaths)
2373
+ if (shownIds.has(id))
2374
+ idByRepoPath.set(rel, id);
2350
2375
  const story = await (0, story_1.readStory)(repoRoot, renderGroundingSection, (p) => repo.baseUrl ? `${repo.baseUrl}/edit/${repo.branch}/${encodePath(p)}` : null,
2351
2376
  // 動きのない日を埋める終点=ビルドの時刻の日本時間の日付(時計を注入できる)
2352
2377
  storyToday, ledgerDir,
2353
2378
  // 名前のリンク(story.screen-name-links): 日の relations の関係者・用語・ルールを、画面にある記録の表示名・別名・同義語で本文に当てる
2354
- (targets) => (0, story_1.storyNameLinks)(targets, storyNameSources(sections)));
2379
+ (targets) => (0, story_1.storyNameLinks)(targets, storyNameSources(sections)),
2380
+ // 本文の相対リンク(決定の記録など)の飛び先: 画面に載る記録(決定・各区画)のパス → id
2381
+ (repoPath) => idByRepoPath.get(repoPath));
2355
2382
  // 週・月のあらすじ(#901)。週・月のファイルが無ければ null(キーごと出さない=今までと同じ画面)
2356
2383
  const storyDigest = await (0, story_1.readStoryDigest)(repoRoot, renderGroundingSection, (p) => repo.baseUrl ? `${repo.baseUrl}/edit/${repo.branch}/${encodePath(p)}` : null, storyToday, ledgerDir);
2357
2384
  // 案件アイコン(#348)。Cortex構成でなければ undefined(旧レイアウトには置き場が無い)
@@ -31,7 +31,7 @@ exports.memberSearchTerms = memberSearchTerms;
31
31
  /**
32
32
  * このリポジトリの内側の呼び名。外の ID と見なさない。
33
33
  *
34
- * 前半は記録の id の種類(`build.ts` の `GOLD_TYPES` と同じ)、後半は Silver の参照
34
+ * 前半は記録の id の種類(記録の種類の一覧と同じ)、後半は Silver の参照
35
35
  * (`build.ts` の `classifyTarget` が正本の URL に解決する 3 つ)。
36
36
  */
37
37
  const INSIDE_KINDS = new Set([
package/dist/render.js CHANGED
@@ -4790,14 +4790,6 @@ a:hover { text-decoration: underline; }
4790
4790
  box-shadow: var(--shadow); cursor: grab; display: block;
4791
4791
  }
4792
4792
  .graph-canvas.dragging { cursor: grabbing; }
4793
- .graph-legend {
4794
- display: flex; flex-wrap: wrap; gap: 12px; align-items: center;
4795
- margin: 10px 2px 0; font-size: 12.5px; color: var(--muted);
4796
- }
4797
- .graph-legend .dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 4px; vertical-align: -1px; }
4798
- .graph-legend .line { display: inline-block; width: 16px; height: 0; border-top: 3px solid; margin-right: 4px; vertical-align: 4px; }
4799
- .graph-legend .sep { color: var(--muted); margin-left: 8px; padding-left: 8px; border-left: 1px solid var(--border); }
4800
- .graph-legend label { display: inline-flex; align-items: center; gap: 4px; cursor: pointer; color: var(--text); }
4801
4793
 
4802
4794
  .graph-reset {
4803
4795
  position: absolute; top: 12px; right: 12px; z-index: 2;
@@ -8182,10 +8174,10 @@ ${story_1.splitByNameLinks.toString()}
8182
8174
  btnUrl: base ? base + "/actions/workflows/update-gold.yml" : null,
8183
8175
  },
8184
8176
  "graph": {
8185
- text: "決定・用語・レポートと、その根拠になった課題・議事録・資料のつながりを自動でつないだ地図です。点をクリックすると、そのページや実際のシステム(Backlog・GitHub等)に移動できます。",
8177
+ text: "決定・ルール・用語・関係者と、その根拠(元のツール)のつながりの地図です。点をクリックすると、そのページや元のツール(Backlog・GitHub等)に移動できます。",
8186
8178
  },
8187
8179
  "explorer": {
8188
- text: "決定・用語・レポートを起点に、つながり(根拠・出典・リンク)を1列ずつ辿る表示です。行をクリックすると右に次の列が開きます。",
8180
+ text: "決定・ルール・用語・関係者を起点に、つながり(根拠・出典)を1列ずつ辿る表示です。行をクリックすると右に次の列が開きます。",
8189
8181
  },
8190
8182
  "story": {
8191
8183
  text: "このプロジェクトの日々の動きを、日次・週次・月次に分けてまとめた記録です。直したいことや足したいことは、各カードの「GitHubで編集」か「AIで編集」から書けます。",
@@ -19940,42 +19932,30 @@ ${story_1.splitByNameLinks.toString()}
19940
19932
  term: { color: "#4ade80", label: "用語集" },
19941
19933
  member: { color: "#f472b6", label: "関係者" },
19942
19934
  rule: { color: "#fde047", label: "ルール" },
19943
- report: { color: "#22d3ee", label: "レポート" },
19944
- overview: { color: "#fb923c", label: "Home" },
19945
- record: { color: "#0e7490", label: "その他レコード" },
19946
19935
  silver: { color: "#9ca3af", label: "根拠(課題・議事録・資料・リンク等)" },
19947
19936
  bronze: { color: "#9ca3af", label: "根拠(課題・議事録・資料・リンク等)" },
19948
19937
  external: { color: "#9ca3af", label: "根拠(課題・議事録・資料・リンク等)" },
19949
- phantom: { color: "#d4d9e0", label: "参照先が見つからないもの" },
19950
19938
  };
19939
+ /* 点の色は区画を見分けるためだけに残す。凡例(分類の一覧)は出さない(仕様 graph.content-only) */
19940
+ var GROUP_FALLBACK = { color: "#9ca3af" };
19951
19941
  /* 線(関係)の種類ごとの色・ラベルと、「Goldどうし」か「根拠」かの分類(engine #551・分類A 10/1)。
19952
19942
  * **一覧(凡例)はこの表から組む**ので、将来 about(決定・ルール→用語)や is_a/part_of、
19953
19943
  * 人→用語・人→人の線を足すときは、ここに1行足すだけでよい——凡例もトグルも自動で追従し、
19954
19944
  * 既存の行・色は変わらない(語彙が増えても凡例が崩れない作り)。
19955
19945
  * kind: "gold"=Goldのレコードどうしを結ぶ線/"evidence"=Goldがその根拠を指す線(弱いつながり)。 */
19956
19946
  var REL_META = {
19957
- based_on: { color: "#2563eb", label: "根拠", kind: "gold" },
19958
- derived_from: { color: "#0d9488", label: "生成元", kind: "gold" },
19959
- relates_to: { color: "#94a3b8", label: "関連", kind: "gold" },
19960
- supersedes: { color: "#dc2626", label: "置換", kind: "gold" },
19961
- reference: { color: "#d1d5db", label: "参照", kind: "evidence" },
19962
- source: { color: "#d1d5db", label: "出典", kind: "evidence" },
19963
- link: { color: "#e2e8f0", label: "リンク", kind: "evidence" },
19964
- // 話題の線(決定・ルール・関係者 → 用語・engine #551 の about)。名前の一致で機械が付けた手がかりなので、
19965
- // 根拠の線と同じく細く薄い「ゆるいつながり」として描き、凡例は日本語の 1 行(栓 about_relations の on の条件)
19966
- about: { color: "#ddd6fe", label: "話題", kind: "topic" },
19967
- // 決めた人の線(決定 → 関係者・#1094)。relations ではなく decided_by から引く。
19968
- // 🔴 凡例には**この線がグラフにあるときだけ**出す(欄の無い案件の凡例を変えない)
19969
- decided_by: { color: "#f472b6", label: "決めた人", kind: "gold", onlyWhenPresent: true },
19947
+ based_on: { color: "#2563eb", label: "根拠" },
19948
+ derived_from: { color: "#0d9488", label: "生成元" },
19949
+ relates_to: { color: "#94a3b8", label: "関連" },
19950
+ supersedes: { color: "#dc2626", label: "置換" },
19951
+ reference: { color: "#d1d5db", label: "参照" },
19952
+ source: { color: "#d1d5db", label: "出典" },
19953
+ decided_by: { color: "#f472b6", label: "決めた人" },
19970
19954
  };
19971
- var WEAK_RELS = {};
19972
19955
  var REL_COLOR = {};
19973
- Object.keys(REL_META).forEach(function (r) {
19974
- REL_COLOR[r] = REL_META[r].color;
19975
- if (REL_META[r].kind === "evidence" || REL_META[r].kind === "topic") WEAK_RELS[r] = true;
19976
- });
19956
+ Object.keys(REL_META).forEach(function (r) { REL_COLOR[r] = REL_META[r].color; });
19977
19957
  // period: 期間で光らせる(engine #551)。unit は all(光らせない)/day/week、day は基準の日(YYYY-MM-DD)
19978
- var graphState = { showWeak: true, q: "", period: { unit: "all", day: null } };
19958
+ var graphState = { q: "", period: { unit: "all", day: null } };
19979
19959
 
19980
19960
  /** 期間の範囲(両端を含む YYYY-MM-DD)。「すべて」・日付が無いときは null。週は月曜〜日曜(ストーリーの週と同じ区切り) */
19981
19961
  function graphPeriodRange(period) {
@@ -20007,7 +19987,7 @@ ${story_1.splitByNameLinks.toString()}
20007
19987
  countEl.textContent = "点 " + graph.nodes.length + " / つながり " + graph.edges.length;
20008
19988
 
20009
19989
  var filters = el("div", { class: "filters" });
20010
- var search = el("input", { type: "search", placeholder: "キーワードで探す(決定・用語・レポートは本文の中まで探します。見つかった点とそのつながりが光ります)" });
19990
+ var search = el("input", { type: "search", placeholder: "キーワードで探す(決定・ルール・用語・関係者は本文の中まで探します。見つかった点とそのつながりが光ります)" });
20011
19991
  search.value = graphState.q;
20012
19992
  filters.appendChild(search);
20013
19993
  app.appendChild(filters);
@@ -20019,43 +19999,7 @@ ${story_1.splitByNameLinks.toString()}
20019
19999
  wrap.appendChild(canvas);
20020
20000
  app.appendChild(wrap);
20021
20001
 
20022
- // 凡例 + 弱い辺のトグル
20023
- var legend = el("div", { class: "graph-legend" });
20024
- var legendShown = {};
20025
- Object.keys(GROUP_STYLE).forEach(function (g) {
20026
- var anyNode = graph.nodes.some(function (n) { return n.group === g; });
20027
- if (!anyNode || legendShown[GROUP_STYLE[g].label]) return;
20028
- legendShown[GROUP_STYLE[g].label] = true;
20029
- var item = el("span", {}, [el("span", { class: "dot" }), document.createTextNode(GROUP_STYLE[g].label)]);
20030
- item.querySelector(".dot").style.background = GROUP_STYLE[g].color;
20031
- legend.appendChild(item);
20032
- });
20033
-
20034
- // 線(関係)の色凡例。グラフ内の有無に関わらず全種別を、Goldどうし/根拠の2群で表示する
20035
- [["gold", "線(Goldどうし):"], ["evidence", "線(根拠):"], ["topic", "線(話題):"]].forEach(function (pair) {
20036
- var kind = pair[0], heading = pair[1];
20037
- var keys = Object.keys(REL_META).filter(function (r) {
20038
- if (REL_META[r].kind !== kind) return false;
20039
- return !REL_META[r].onlyWhenPresent || graph.edges.some(function (e) { return e.rel === r; });
20040
- });
20041
- if (!keys.length) return;
20042
- legend.appendChild(el("span", { class: "sep", text: heading }));
20043
- keys.forEach(function (r) {
20044
- var li = el("span", {}, [el("span", { class: "line" }), document.createTextNode(REL_META[r].label)]);
20045
- li.querySelector(".line").style.borderTopColor = REL_META[r].color;
20046
- legend.appendChild(li);
20047
- });
20048
- });
20049
-
20050
- var toggle = el("label", {});
20051
- var cb = el("input", { type: "checkbox" });
20052
- cb.checked = graphState.showWeak;
20053
- cb.addEventListener("change", function () { graphState.showWeak = cb.checked; renderList(); });
20054
- toggle.appendChild(cb);
20055
- toggle.appendChild(document.createTextNode("ゆるいつながり(参照・本文リンク・話題)も表示"));
20056
- legend.appendChild(toggle);
20057
- app.appendChild(legend);
20058
-
20002
+ // 🔴 凡例(分類・線の種類の一覧)と、根拠の点を隠す切り替えは持たない(仕様 graph.content-only / graph.evidence-always)
20059
20003
  startGraph(canvas, search, graph, periodCtl);
20060
20004
  }
20061
20005
 
@@ -20117,12 +20061,9 @@ ${story_1.splitByNameLinks.toString()}
20117
20061
  }
20118
20062
 
20119
20063
  function startGraph(canvas, searchInput, graph, periodCtl) {
20120
- var allEdges = graph.edges.filter(function (e) {
20121
- return graphState.showWeak || !WEAK_RELS[e.rel];
20122
- });
20123
- // レコード以外の点は、**ゆるいつながりを外す前**の線(消した記録の線は graphWithoutDeleted が落としてある)が 1 本でもあれば出す。
20124
- // 🔴 **表示を絞る切り替えで根拠の点を隠さない**(仕様 graph.evidence-always)——切り替えで変わるのは線だけ。
20125
- // group では見分けない(元のツールが無い根拠も group phantom で作られる)。消した記録だけが指していた点は線が無いので出ない
20064
+ var allEdges = graph.edges;
20065
+ // レコード以外の点(根拠)は、線が 1 本でもあれば出す。🔴 根拠の点を隠す切り替えは持たない(仕様 graph.evidence-always)。
20066
+ // 消した記録だけが指していた点は線が無いので出ない
20126
20067
  var used = {};
20127
20068
  graph.edges.forEach(function (e) { used[e.from] = true; used[e.to] = true; });
20128
20069
  // ビルド時に算出済みの座標(n.x/n.y)があればそれを使う=ウォームアップ不要で開いた瞬間に整列済み。
@@ -20587,8 +20528,7 @@ ${story_1.splitByNameLinks.toString()}
20587
20528
  var EDGE_DARK = {
20588
20529
  based_on: "rgba(125,165,255,0.85)", derived_from: "rgba(94,234,212,0.75)",
20589
20530
  relates_to: "rgba(148,163,184,0.6)", supersedes: "rgba(248,113,113,0.9)",
20590
- reference: "rgba(180,196,230,0.32)", source: "rgba(180,196,230,0.32)", link: "rgba(180,196,230,0.22)",
20591
- about: "rgba(196,181,253,0.22)",
20531
+ reference: "rgba(180,196,230,0.6)", source: "rgba(180,196,230,0.6)",
20592
20532
  decided_by: "rgba(244,114,182,0.75)",
20593
20533
  };
20594
20534
  // 画面に入っている範囲(世界座標)。両端が同じ側の外にある線は描かない
@@ -20608,9 +20548,9 @@ ${story_1.splitByNameLinks.toString()}
20608
20548
  var span = Math.max(highO - lowO, 1) * SWEEP_STEP_MS;
20609
20549
  var prog = Math.max(0, Math.min(1, (sweepT - lowO * SWEEP_STEP_MS) / span));
20610
20550
  if (prog > 0) {
20611
- ctx.globalAlpha = WEAK_RELS[e.rel] ? 0.7 : 0.95;
20551
+ ctx.globalAlpha = 0.95;
20612
20552
  ctx.strokeStyle = "#fcd34d";
20613
- ctx.lineWidth = (e.rel === "supersedes" ? 2 : WEAK_RELS[e.rel] ? 1 : 1.6) / view.scale;
20553
+ ctx.lineWidth = (e.rel === "supersedes" ? 2 : 1.6) / view.scale;
20614
20554
  ctx.beginPath();
20615
20555
  ctx.moveTo(lowN.x, lowN.y);
20616
20556
  ctx.lineTo(lowN.x + (highN.x - lowN.x) * prog, lowN.y + (highN.y - lowN.y) * prog);
@@ -20619,18 +20559,18 @@ ${story_1.splitByNameLinks.toString()}
20619
20559
  } else {
20620
20560
  ctx.globalAlpha = 0.06; // リネージ外は沈める
20621
20561
  ctx.strokeStyle = EDGE_DARK[e.rel] || "rgba(180,196,230,0.3)";
20622
- ctx.lineWidth = (WEAK_RELS[e.rel] ? 1 : 1.6) / view.scale;
20562
+ ctx.lineWidth = 1.6 / view.scale;
20623
20563
  ctx.beginPath(); ctx.moveTo(e.a.x, e.a.y); ctx.lineTo(e.b.x, e.b.y); ctx.stroke();
20624
20564
  }
20625
20565
  return;
20626
20566
  }
20627
20567
  // 通常 / 検索
20628
20568
  var hot = searching && (matched[e.a.id] || matched[e.b.id]);
20629
- var a = hot ? 0.95 : (WEAK_RELS[e.rel] ? 0.5 : 0.85);
20569
+ var a = hot ? 0.95 : 0.85;
20630
20570
  if (searching && !hot) a = 0.06;
20631
20571
  ctx.globalAlpha = a;
20632
20572
  ctx.strokeStyle = hot ? "#fcd34d" : (EDGE_DARK[e.rel] || "rgba(180,196,230,0.3)");
20633
- ctx.lineWidth = (hot ? 1.8 : e.rel === "supersedes" ? 2 : WEAK_RELS[e.rel] ? 1 : 1.6) / view.scale;
20573
+ ctx.lineWidth = (hot ? 1.8 : e.rel === "supersedes" ? 2 : 1.6) / view.scale;
20634
20574
  ctx.beginPath();
20635
20575
  ctx.moveTo(e.a.x, e.a.y);
20636
20576
  ctx.lineTo(e.b.x, e.b.y);
@@ -20642,7 +20582,7 @@ ${story_1.splitByNameLinks.toString()}
20642
20582
  var cw = canvas.width, ch = canvas.height;
20643
20583
  ctx.setTransform(1, 0, 0, 1, 0, 0);
20644
20584
  nodes.forEach(function (n) {
20645
- var style = GROUP_STYLE[n.group] || GROUP_STYLE.phantom;
20585
+ var style = GROUP_STYLE[n.group] || GROUP_FALLBACK;
20646
20586
  var alphaN = 1;
20647
20587
  if (searching && !lit[n.id]) alphaN = 0.12;
20648
20588
  if (hovered) {
@@ -20923,7 +20863,7 @@ ${story_1.splitByNameLinks.toString()}
20923
20863
  (adjDir[e.from] = adjDir[e.from] || []).push({ id: e.to, rel: e.rel, dir: "out" });
20924
20864
  (adjDir[e.to] = adjDir[e.to] || []).push({ id: e.from, rel: e.rel, dir: "in" });
20925
20865
  });
20926
- var REL_ORDER = ["supersedes", "based_on", "derived_from", "relates_to", "decided_by", "source", "reference", "link", "about"];
20866
+ var REL_ORDER = ["supersedes", "based_on", "derived_from", "relates_to", "decided_by", "source", "reference"];
20927
20867
 
20928
20868
  function recordText(rec) {
20929
20869
  if (!rec) return "";
@@ -20935,11 +20875,17 @@ ${story_1.splitByNameLinks.toString()}
20935
20875
 
20936
20876
  // Gold層のフォルダ(1列目)。検索ヒット表示と使い分ける
20937
20877
  var goldGroups = [];
20938
- if (SITE.home) goldGroups.push({ name: "Home", ids: [SITE.home.id] });
20939
- // 削除済みは列にも件数にも出さない(一覧・グラフと同じ扱いに揃える)
20940
- goldGroups.push({ name: "Decisions", ids: visibleRecords(SITE.decisions).map(function (d) { return d.id; }) });
20878
+ // 削除済みは列にも件数にも出さない(一覧・グラフと同じ扱いに揃える)。
20879
+ // 🔴 点にならない記録(Home・レポート・その他)は出さない——グラフの点(nodeById)にある記録だけ
20880
+ var inGraph = {};
20881
+ (explorerGraph.nodes || []).forEach(function (n) { if (n.recordId) inGraph[n.recordId] = true; });
20882
+ function graphIds(list) {
20883
+ return visibleRecords(list).map(function (r) { return r.id; }).filter(function (id) { return inGraph[id]; });
20884
+ }
20885
+ goldGroups.push({ name: "Decisions", ids: graphIds(SITE.decisions) });
20941
20886
  (SITE.sections || []).forEach(function (sec) {
20942
- goldGroups.push({ name: sec.name, ids: visibleRecords(sec.records).map(function (r) { return r.id; }) });
20887
+ var ids = graphIds(sec.records);
20888
+ if (ids.length) goldGroups.push({ name: sec.name, ids: ids });
20943
20889
  });
20944
20890
 
20945
20891
  function renderExplorer() {
@@ -20981,7 +20927,7 @@ ${story_1.splitByNameLinks.toString()}
20981
20927
  app.appendChild(wrap);
20982
20928
 
20983
20929
  function nodeDot(n) {
20984
- var style = GROUP_STYLE[n.group] || GROUP_STYLE.phantom;
20930
+ var style = GROUP_STYLE[n.group] || GROUP_FALLBACK;
20985
20931
  var dot = el("span", { class: "mc-dot" });
20986
20932
  dot.style.background = style.color;
20987
20933
  return dot;
@@ -21145,10 +21091,8 @@ ${story_1.splitByNameLinks.toString()}
21145
21091
  path.setAttribute("d", "M " + x1 + " " + y1 + " C " + mx + " " + y1 + ", " + mx + " " + y2 + ", " + x2 + " " + y2);
21146
21092
  path.setAttribute("fill", "none");
21147
21093
  path.setAttribute("stroke", REL_COLOR[rows[j].getAttribute("data-rel")] || "#cbd5e1");
21148
- // 話題の線は細く薄く(名前の一致の手がかり・根拠の線より弱い)
21149
- var weakLine = rows[j].getAttribute("data-rel") === "about";
21150
- path.setAttribute("stroke-width", weakLine ? "1" : "1.5");
21151
- path.setAttribute("opacity", weakLine ? "0.5" : "0.85");
21094
+ path.setAttribute("stroke-width", "1.5");
21095
+ path.setAttribute("opacity", "0.85");
21152
21096
  lines.appendChild(path);
21153
21097
  }
21154
21098
  }
package/dist/story.js CHANGED
@@ -19,6 +19,7 @@ exports.isEngineQuietDay = isEngineQuietDay;
19
19
  exports.todayJst = todayJst;
20
20
  exports.quietStoryRow = quietStoryRow;
21
21
  exports.withQuietDays = withQuietDays;
22
+ exports.linkRecordRefs = linkRecordRefs;
22
23
  exports.readStory = readStory;
23
24
  exports.storyWeekHeading = storyWeekHeading;
24
25
  exports.storyMonthHeading = storyMonthHeading;
@@ -75,19 +76,29 @@ const MACHINE_WORD_RE = /urn:|cortex-engine|sha=|<!--|-->|\.md\b|Days\/|story-da
75
76
  * 門に掛ける前にリンクの文字だけに剝がす。外の URL(`https://…`・`<https://…>`)は対象外(そのまま残す)。
76
77
  * 本文に素で書かれた `foo.md` はここでは剝がさず、`MACHINE_WORD_RE` が行ごと伏せる
77
78
  */
78
- const RELATIVE_MD_LINK_RE = /\[([^\]\n]*)\]\(\s*<?(?![a-zA-Z][a-zA-Z0-9+.-]*:|\/\/|#)[^)\s<>]*\.md(?:#[^)\s<>]*)?>?\s*\)/g;
79
+ const RELATIVE_MD_LINK_RE = /\[([^\]\n]*)\]\(\s*<?(?![a-zA-Z][a-zA-Z0-9+.-]*:|\/\/|#)([^)\s<>]*\.md)(?:#[^)\s<>]*)?>?\s*\)/g;
79
80
  const LINK_OPEN_RE = /\[[^\]\n]*\]\([^)\s]*/g;
80
81
  /**
81
82
  * 相対 `.md` のリンクを文字に剝がす。剝がした結果、剝がす前の行に無かった `](` のリンクが現れる行(入れ子のリンクなど)は、
82
83
  * 剝がさず元のまま返す(`.md` が残るので `MACHINE_WORD_RE` が行ごと伏せる)。後ろの関所に頼らない
83
84
  */
84
- const unlinkRelativeMd = (line) => {
85
+ const unlinkRelativeMd = (line, keepRecordRefs = false) => {
85
86
  const out = line.replace(RELATIVE_MD_LINK_RE, "$1");
86
87
  if (out === line)
87
88
  return line;
88
89
  const before = new Set(line.match(LINK_OPEN_RE) ?? []);
89
- return (out.match(LINK_OPEN_RE) ?? []).some((m) => !before.has(m)) ? line : out;
90
+ if ((out.match(LINK_OPEN_RE) ?? []).some((m) => !before.has(m)))
91
+ return line;
92
+ if (!keepRecordRefs)
93
+ return out;
94
+ // 記録(`…/records/<名前>.md`)を指すものは、`.md` を落とした印のリンク `#rec:<相対パス>` にして残す(門の `.md` の伏せを通すため)。
95
+ // 画面の中の飛び先への差し替えは `linkRecordRefs`(build が記録の表を握って渡す)。それ以外の相対リンクは今までどおり文字に剝がす
96
+ return line.replace(RELATIVE_MD_LINK_RE, (_m, text, href) => RECORD_HREF_RE.test(href) ? `[${text}](${RECORD_REF_PREFIX}${href.slice(0, -3)})` : text);
90
97
  };
98
+ /** 記録の相対パス(`…/records/<名前>.md`) */
99
+ const RECORD_HREF_RE = /(?:^|\/)records\/[^/]+\.md$/;
100
+ /** 記録を指すリンクの印。門(`.md` の伏せ)と関所(相対 URL を許す)を通り、`linkRecordRefs` が飛び先に差し替える */
101
+ const RECORD_REF_PREFIX = "#rec:";
91
102
  /** 太字だけの 1 行(筋の名前の行) */
92
103
  const BOLD_ONLY_RE = /^\*\*[^*]+\*\*$/;
93
104
  /** 見出しの行(`##`〜`######`) */
@@ -346,7 +357,7 @@ function storyDayMarkdown(content, day) {
346
357
  }
347
358
  // 機械の行: 印と機械の語の行を伏せる
348
359
  // 決定の記録へのリンクは、門に掛ける前に文字だけに剝がす(行ごと伏せない)
349
- const clean = (ls) => ls.map(unlinkRelativeMd).filter((l) => !STORY_MARKER_RE.test(l.trim()) && !MACHINE_WORD_RE.test(l));
360
+ const clean = (ls) => ls.map((l) => unlinkRelativeMd(l, true)).filter((l) => !STORY_MARKER_RE.test(l.trim()) && !MACHINE_WORD_RE.test(l));
350
361
  // 🔴 人の追記: 落とすのは**印の行だけ**(人が書いた行を画面が黙って消さない・実装レビュー 🟠2)
351
362
  const humanKept = human.filter((l) => !STORY_MARKER_RE.test(l.trim()));
352
363
  const out = withoutEmptySections(withoutEmptyBoldHeads([...clean(main), ...clean(lateAfter)]));
@@ -466,6 +477,35 @@ async function loadStoryLedger(repoRoot, warn, ledgerDir) {
466
477
  }
467
478
  return ledger;
468
479
  }
480
+ /**
481
+ * 本文(関所を通した HTML)の相対リンクのうち、記録(`Cortex/<区画>/records/*.md`)を指すものを
482
+ * 画面の中のその記録への飛び先(人名・用語のリンクと同じ `a.wikilink[data-go]`)にする。
483
+ *
484
+ * - 相対パスはその日のファイルの置き場から解く(`../../../Decisions/records/x.md` → `Cortex/Decisions/records/x.md`)
485
+ * - 🔴 記録が見つからない(`idOf` が undefined)相対リンクは `<a>` を外して文字のまま残す(壊れたリンク・行き止まりを作らない)
486
+ * - 外の URL(http・https・mailto など `scheme:`・`//`)とページ内の `#…` は触らない
487
+ *
488
+ * 呼び手は**サニタイズ後**の HTML を渡す(属性値・文字はエスケープ済み。ここで新しい HTML は混ぜず、data-go の値だけ & と " を逃がす)
489
+ */
490
+ function linkRecordRefs(html, dayRepoPath, idOf) {
491
+ const baseDir = node_path_1.default.posix.dirname(dayRepoPath);
492
+ return html.replace(/<a href="([^"]*)">([\s\S]*?)<\/a>/g, (whole, href, inner) => {
493
+ const isRef = href.startsWith(RECORD_REF_PREFIX);
494
+ if (!isRef && (/^[a-z][a-z0-9+.-]*:/i.test(href) || href.startsWith("//") || href.startsWith("#")))
495
+ return whole;
496
+ let target = isRef ? href.slice(RECORD_REF_PREFIX.length).replace(/&amp;/g, "&") + ".md" : href.replace(/&amp;/g, "&").replace(/[?#].*$/, "");
497
+ try {
498
+ target = decodeURIComponent(target);
499
+ }
500
+ catch { /* 解けない綴りはそのまま照合して見つからなければ文字に落とす */ }
501
+ const resolved = target.startsWith("/") ? node_path_1.default.posix.normalize(target.slice(1)) : node_path_1.default.posix.join(baseDir, target);
502
+ const id = /^Cortex\/[^/]+\/records\/[^/]+\.md$/.test(resolved) ? idOf(resolved) : undefined;
503
+ if (!id)
504
+ return inner;
505
+ const attr = id.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
506
+ return `<a class="wikilink" data-go="${attr}">${inner}</a>`;
507
+ });
508
+ }
469
509
  /**
470
510
  * 案件のストーリーを読む。**載せる日が無ければ `null`**(タブも器も出さない)。
471
511
  *
@@ -479,7 +519,9 @@ today = todayJst(),
479
519
  // 帳簿の置き場(既定は `<repoRoot>/.cortex`)
480
520
  ledgerDir,
481
521
  // 日の `relations` の target から名前のリンクの表を作る口(build が記録の表を握って渡す。無ければリンクは付けない)
482
- linksOf = () => []) {
522
+ linksOf = () => [],
523
+ // 本文の相対リンクが指す記録(リポジトリ相対パス)の id。画面にある記録だけ返す(無ければリンクは文字に落とす)
524
+ recordIdOf = () => undefined) {
483
525
  const ledger = await loadStoryLedger(repoRoot, true, ledgerDir);
484
526
  if (!ledger)
485
527
  return null;
@@ -543,7 +585,7 @@ linksOf = () => []) {
543
585
  const links = linksOf(dayRelationTargets(fm));
544
586
  const partialHour = dayPartialHour(fm);
545
587
  out.push({
546
- date: f.day, heading: heading, bodyHtml: await renderMarkdown(markdown),
588
+ date: f.day, heading: heading, bodyHtml: linkRecordRefs(await renderMarkdown(markdown), f.rel, recordIdOf),
547
589
  ...(editUrl ? { editUrl: editUrl } : {}),
548
590
  ...(partialHour !== null ? { partialHour: partialHour } : {}),
549
591
  ...(links.length ? { links: links } : {}),
@@ -613,7 +655,7 @@ function storyDigestParts(content) {
613
655
  const end = body.findIndex((l) => DIGEST_END_RE.test(l.trim()));
614
656
  const at = body.findIndex((l, i) => i > end && l.trim() === HUMAN_HEADING);
615
657
  // 機械の部分の `#` の見出しの行は落とす(流れは段落だけの約束。見出しが紛れると後ろの文が別の節に見える)
616
- const main = withoutHiddenBoldGroups((at < 0 ? body : body.slice(0, at)).map(unlinkRelativeMd).filter((l) => !STORY_MARKER_RE.test(l.trim()) && !/^\s{0,3}#{1,6}(\s|$)/.test(l)))
658
+ const main = withoutHiddenBoldGroups((at < 0 ? body : body.slice(0, at)).map((l) => unlinkRelativeMd(l)).filter((l) => !STORY_MARKER_RE.test(l.trim()) && !/^\s{0,3}#{1,6}(\s|$)/.test(l)))
617
659
  .filter((l) => !MACHINE_WORD_RE.test(l));
618
660
  const human = (at < 0 ? [] : body.slice(at + 1)).filter((l) => !STORY_MARKER_RE.test(l.trim()));
619
661
  let phase = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.162",
3
+ "version": "0.12.164",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,