@takagaki/cortex-decisions-viewer 0.12.110 → 0.12.112

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
@@ -49,6 +49,7 @@ exports.buildHomeGrounding = buildHomeGrounding;
49
49
  exports.collectMeetings = collectMeetings;
50
50
  exports.findBacklogDir = findBacklogDir;
51
51
  exports.figmaUrlFromRef = figmaUrlFromRef;
52
+ exports.identifyEvidence = identifyEvidence;
52
53
  exports.bodyFileSlug = bodyFileSlug;
53
54
  exports.bodyStampId = bodyStampId;
54
55
  exports.bodyStampComment = bodyStampComment;
@@ -61,6 +62,7 @@ const gray_matter_1 = __importDefault(require("gray-matter"));
61
62
  const marked_1 = require("marked");
62
63
  const sanitize_html_1 = __importDefault(require("sanitize-html"));
63
64
  const canonical_evidence_1 = require("./canonical-evidence");
65
+ const evidence_names_1 = require("./evidence-names");
64
66
  const repo_1 = require("./repo");
65
67
  const engagement_1 = require("./engagement");
66
68
  const story_1 = require("./story");
@@ -1500,6 +1502,123 @@ async function buildTargetUrlMap(repoRoot, repoBaseUrl, branch) {
1500
1502
  }
1501
1503
  return map;
1502
1504
  }
1505
+ /**
1506
+ * 根拠の**読める名前の帳**を組む(#968 tools 1・`evidence-names.ts`)。材料は写し(課題管理・共有資料・
1507
+ * デザイン)と、engine が置いた `fleet-status.json` のチャンネルの一覧。読めないファイル・無い置き場は黙って飛ばす(帳が薄くなるだけ)。
1508
+ *
1509
+ * 🔴 **`buildTargetUrlMap` は触らない**(移る間だけ残す表・詳細画面の飛び先の正本)。ここは名前と、
1510
+ * 写しのパス → 根拠の ID の対応だけを持つ。資料の題名を Drive の鍵にも載せるために、表の URL を読むだけ
1511
+ */
1512
+ async function buildEvidenceBook(repoRoot, targetUrls, externalSources = []) {
1513
+ const book = (0, evidence_names_1.emptyEvidenceBook)();
1514
+ const rel = (file) => path.relative(repoRoot, file).split(path.sep).join("/");
1515
+ const heading = (text) => text.match(/^#\s+(.+?)\s*$/m)?.[1] ?? null;
1516
+ const foldKeyOf = (url) => {
1517
+ const c = url ? (0, canonical_evidence_1.canonicalEvidence)(url) : null;
1518
+ return c && c.kind !== "url" ? c.parentKey ?? c.key : null;
1519
+ };
1520
+ const putName = (key, name) => {
1521
+ if (key && name && !book.names.has(key))
1522
+ book.names.set(key, name);
1523
+ };
1524
+ // 課題(キー → 「キー 題名」)・Backlog のドキュメントと Wiki(URL の鍵 → 題名)
1525
+ const backlogDir = await findBacklogDir(repoRoot, "課題管理");
1526
+ for (const sub of BACKLOG_SECTIONS) {
1527
+ for (const file of await walkMd(path.join(repoRoot, backlogDir, sub))) {
1528
+ try {
1529
+ const head = (await node_fs_1.promises.readFile(file, "utf8")).slice(0, 4000);
1530
+ const title = heading(head);
1531
+ const key = head.match(/^- 課題キー: (\S+)$/m)?.[1];
1532
+ const link = key ? null : head.match(/\[Backlog (?:Document|Wiki) Link\]\((https?:[^)]+)\)/)?.[1];
1533
+ const ref = key ?? link;
1534
+ if (!ref)
1535
+ continue;
1536
+ if (key)
1537
+ putName(key.toLowerCase(), title ? `${key} ${title}` : key);
1538
+ else
1539
+ putName(foldKeyOf(link), title);
1540
+ book.pathRefs.set(rel(file), ref);
1541
+ const tail = (0, evidence_names_1.backlogTailOf)(rel(file));
1542
+ if (tail && !book.backlogTailRefs.has(tail))
1543
+ book.backlogTailRefs.set(tail, ref);
1544
+ }
1545
+ catch {
1546
+ // 読めないファイルはスキップ
1547
+ }
1548
+ }
1549
+ }
1550
+ // 資料(`material:` と元の Drive の鍵 → 題名・写しのパス → `material:`)
1551
+ for (const file of await walkMd(path.join(repoRoot, MATERIALS_DIR))) {
1552
+ if (path.basename(file).toLowerCase() === "link.md")
1553
+ continue;
1554
+ try {
1555
+ const { data } = (0, gray_matter_1.default)(await node_fs_1.promises.readFile(file, "utf8"));
1556
+ const stem = path.basename(file).replace(/\.md$/i, "");
1557
+ const id = data.type === "material" && data.id ? String(data.id) : `material:${stem}`;
1558
+ const title = typeof data.title === "string" && data.title.trim() ? data.title.trim() : stem;
1559
+ putName(id, title);
1560
+ putName(foldKeyOf(targetUrls.get(id)), title);
1561
+ book.pathRefs.set(rel(file), id);
1562
+ }
1563
+ catch {
1564
+ // 読めないファイルはスキップ
1565
+ }
1566
+ }
1567
+ // デザイン(`design:` と Figma の鍵 → 画面・ファイルの名前・写しのパス → `design:`)。
1568
+ // 指摘の月ごとのファイルは、同じ置き場の README の `design:`(ファイル全体)を指す
1569
+ for (const sub of ["inventory", "指摘"]) {
1570
+ const readmeIdByDir = new Map();
1571
+ const monthFiles = [];
1572
+ for (const file of await walkMd(path.join(repoRoot, DESIGN_DIR, sub))) {
1573
+ try {
1574
+ const body = await node_fs_1.promises.readFile(file, "utf8");
1575
+ const id = body.match(/^- 参照ID: `(design:[^`]+)`\s*$/m)?.[1];
1576
+ if (!id) {
1577
+ if (/^\d{4}-\d{2}\.md$/.test(path.basename(file)))
1578
+ monthFiles.push(file);
1579
+ continue;
1580
+ }
1581
+ const title = heading(body);
1582
+ putName(id, title);
1583
+ putName(foldKeyOf(targetUrls.get(id) ?? figmaUrlFromRef(id)), title);
1584
+ book.pathRefs.set(rel(file), id);
1585
+ if (path.basename(file).toLowerCase() === "readme.md")
1586
+ readmeIdByDir.set(path.dirname(file), id);
1587
+ }
1588
+ catch {
1589
+ // 読めないファイルはスキップ
1590
+ }
1591
+ }
1592
+ for (const file of monthFiles) {
1593
+ const id = readmeIdByDir.get(path.dirname(file));
1594
+ if (id)
1595
+ book.pathRefs.set(rel(file), id);
1596
+ }
1597
+ }
1598
+ // チャット(チャンネルの鍵 → `#名前`・名前 → URL)。同じ名前に違う URL が 2 本なら名前からは引かない。
1599
+ // 🔴 **登録簿(`チャット/channels.json`)は読まない**(engine #275・`no-registry-read.test.mjs`)。engine が
1600
+ // 登録簿から算出して置いた `fleet-status.json` の `externalSources` を読む
1601
+ const ambiguous = new Set();
1602
+ for (const src of externalSources) {
1603
+ const url = src.url ?? "";
1604
+ const bare = src.name.trim().replace(/^#/, "");
1605
+ const key = foldKeyOf(url);
1606
+ if (!url || !bare || !key || !(key.startsWith("slack:") || key.startsWith("gchat:")))
1607
+ continue;
1608
+ putName(key, key.startsWith("slack:") ? `#${bare}` : bare);
1609
+ if (!key.startsWith("slack:"))
1610
+ continue;
1611
+ const k = bare.toLowerCase();
1612
+ const prev = book.slackUrlsByName.get(k);
1613
+ if (prev !== undefined && foldKeyOf(prev) !== key)
1614
+ ambiguous.add(k);
1615
+ else
1616
+ book.slackUrlsByName.set(k, url);
1617
+ }
1618
+ for (const k of ambiguous)
1619
+ book.slackUrlsByName.delete(k);
1620
+ return book;
1621
+ }
1503
1622
  /**
1504
1623
  * 指摘のスレッドから `figma-comment:{fileKey}:{commentId}` → 「Figmaでこの指摘を開く」の URL を拾う。
1505
1624
  *
@@ -1609,6 +1728,9 @@ const NO_ORIGINAL_TOOL_LABEL = "元のツールが無い根拠";
1609
1728
  function readableEvidenceLabel(kind, key, fallback) {
1610
1729
  switch (kind) {
1611
1730
  case "backlog":
1731
+ // Wiki・ドキュメントの鍵(`backlog:{host}:wiki:N`)は機械の語なので、種類の名前にする
1732
+ if (key.startsWith("backlog:"))
1733
+ return key.includes(":wiki:") ? "Backlog の Wiki" : "Backlog のドキュメント";
1612
1734
  return key.toUpperCase();
1613
1735
  case "github":
1614
1736
  return key.replace(/^gh:/, "");
@@ -1622,11 +1744,116 @@ function readableEvidenceLabel(kind, key, fallback) {
1622
1744
  case "slack":
1623
1745
  return "Slack のメッセージ";
1624
1746
  case "gchat":
1625
- return "Google Chat のメッセージ";
1747
+ return "Google Chat のスペース";
1626
1748
  default:
1627
1749
  return fallback;
1628
1750
  }
1629
1751
  }
1752
+ /** 課題キーだけの値(`buildGraph` の旧経路と同じ形) */
1753
+ const ISSUE_KEY_ONLY_RE = /^[A-Z][A-Z0-9_]*-\d+$/;
1754
+ /** `owner/repo#N` だけの値(`canonicalEvidence` の `WHOLE_GH_SHORT_RE` と同じ形) */
1755
+ const GH_SHORT_ONLY_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+#\d+$/;
1756
+ /**
1757
+ * 根拠の値(URL・`[ラベル](URL)`・課題キー・`owner/repo#N`・`minute:`/`material:`/`design:`/`figma-comment:`・
1758
+ * 写しのパス・`[slack] #名前 (…)`)から、点の鍵と読める名前を決める(#968 tools 1)。
1759
+ *
1760
+ * 🔴 **今の ID の根拠と新しい URL の根拠を同じ鍵に寄せる。** ID は対応表(`targetUrls`)で URL に引き、
1761
+ * `canonicalEvidence` の `key` にする(コメントは親の鍵)。写しのパスは帳で ID に引いてから同じ道を通す。
1762
+ * cortex の Gold は付け替えの途中で ID と URL が混ざった状態が数晩続く(便 3 の on の夜から)ので、
1763
+ * ここが割れると同じ課題・同じ資料の点が 2 つに分かれる。
1764
+ *
1765
+ * 根拠ではない値(Gold の ID・崩れた文字・帳に無いパス)は `null`——呼び出し側が今までどおり扱う。
1766
+ */
1767
+ function identifyEvidence(value, ctx, humanLabel) {
1768
+ const { book, targetUrls, dropLink } = ctx;
1769
+ let raw = String(value ?? "").trim();
1770
+ let human = humanLabel && humanLabel.trim() ? humanLabel.trim() : undefined;
1771
+ if (!raw)
1772
+ return null;
1773
+ // 解決器(`createRefResolver`)と同じ受け方(URL の中の空白も受ける)
1774
+ const md = raw.match(/^\[([^\]]+)\]\((https?:\/\/[^)]+)\)$/i);
1775
+ if (md) {
1776
+ human = human ?? md[1].trim();
1777
+ raw = md[2];
1778
+ }
1779
+ const channel = (0, evidence_names_1.slackChannelOfLabel)(raw);
1780
+ if (channel) {
1781
+ const url = book.slackUrlsByName.get(channel.toLowerCase());
1782
+ if (!url)
1783
+ return { id: `slack-name:${channel.toLowerCase()}`, label: `#${channel}`, rank: 2, group: "silver", kind: "slack" };
1784
+ human = human ?? `#${channel}`;
1785
+ raw = url;
1786
+ }
1787
+ // 自分のリポジトリの写しへの blob URL は、写しのパスに戻してから引く(同じ会議・課題の点に畳む)
1788
+ if (ctx.blobPrefix && raw.startsWith(ctx.blobPrefix)) {
1789
+ try {
1790
+ // `{branch}/{path}`(ブランチの段を外す)
1791
+ raw = decodeURIComponent(raw.slice(ctx.blobPrefix.length).replace(/[?#].*$/, "").replace(/^[^/]+\//, ""));
1792
+ }
1793
+ catch {
1794
+ // 崩れたエスケープはそのまま
1795
+ }
1796
+ }
1797
+ const tail = (0, evidence_names_1.backlogTailOf)(raw);
1798
+ raw = book.pathRefs.get(raw) ?? (tail ? book.backlogTailRefs.get(tail) : undefined) ?? (0, evidence_names_1.minuteRefOfPath)(raw) ?? raw;
1799
+ const rankOf = (named) => (named ? 3 : human ? 2 : 1);
1800
+ if (ISSUE_KEY_ONLY_RE.test(raw)) {
1801
+ const key = raw.toLowerCase();
1802
+ const name = book.names.get(key);
1803
+ return { id: key, label: name ?? raw, rank: name ? 3 : 1, group: "bronze", url: dropLink(targetUrls.get(raw)), kind: "backlog" };
1804
+ }
1805
+ if (raw.startsWith("minute:")) {
1806
+ const named = (0, evidence_names_1.minuteLabel)(raw);
1807
+ return {
1808
+ id: (0, evidence_names_1.minuteFoldKey)(raw) ?? raw,
1809
+ label: named ?? human ?? "会議の議事録",
1810
+ rank: named ? 3 : rankOf(false),
1811
+ group: "silver",
1812
+ url: dropLink(targetUrls.get(raw)),
1813
+ kind: "minute",
1814
+ };
1815
+ }
1816
+ let idRef = null;
1817
+ let url = null;
1818
+ if (/^(material|design|figma-comment):/.test(raw)) {
1819
+ idRef = raw;
1820
+ url = targetUrls.get(raw) ?? figmaUrlFromRef(raw);
1821
+ }
1822
+ else if (/^https?:\/\//i.test(raw) || GH_SHORT_ONLY_RE.test(raw)) {
1823
+ url = raw;
1824
+ }
1825
+ else {
1826
+ return null;
1827
+ }
1828
+ const canon = url ? (0, canonical_evidence_1.canonicalEvidence)(url) : null;
1829
+ if (!canon || canon.kind === "url") {
1830
+ if (idRef) {
1831
+ const name = book.names.get(idRef);
1832
+ return { id: idRef, label: name ?? human ?? (0, evidence_names_1.idFallbackLabel)(idRef), rank: rankOf(!!name), group: "silver", url: dropLink(url), kind: "id" };
1833
+ }
1834
+ // 対応表に無い種類の URL。今までどおり構文を正規化した URL を鍵にする
1835
+ const id = canon ? canon.url : raw;
1836
+ return { id, label: human ?? raw.replace(/^https?:\/\//i, "").slice(0, 60), rank: rankOf(false), group: "external", url: dropLink(id), kind: "url" };
1837
+ }
1838
+ const foldKey = canon.parentKey ?? canon.key;
1839
+ // 飛び先も親(コメント・返信の断片を外した課題・Issue)にそろえる——点は親なので
1840
+ const link = canon.parentKey ? canon.url.replace(/#.*$/, "") : canon.url;
1841
+ let name = book.names.get(foldKey) ?? (idRef ? book.names.get(idRef) : undefined);
1842
+ if (!name && canon.kind === "slack") {
1843
+ const ch = book.names.get(foldKey.replace(/:p\d+$/, ""));
1844
+ if (ch)
1845
+ name = foldKey.includes(":p") ? `${ch} のメッセージ` : ch;
1846
+ }
1847
+ const fallback = canon.kind === "slack" && !foldKey.includes(":p") ? "Slack のチャンネル" : readableEvidenceLabel(canon.kind, foldKey, raw);
1848
+ return {
1849
+ id: foldKey,
1850
+ label: name ?? human ?? fallback,
1851
+ rank: rankOf(!!name),
1852
+ group: EVIDENCE_GROUP[canon.kind] ?? "external",
1853
+ url: dropLink(link),
1854
+ kind: canon.kind,
1855
+ };
1856
+ }
1630
1857
  /**
1631
1858
  * 全レコードのfrontmatter(relations / source / references)と本文リンクから関係グラフを構築する(engine #551・分類A 10/1)。
1632
1859
  *
@@ -1644,7 +1871,7 @@ function readableEvidenceLabel(kind, key, fallback) {
1644
1871
  * ほうがよい(`createRefResolver` と同じ考え方)。崩れた文字(宛先に見えない参照)も同様に、
1645
1872
  * 点は作るが飛び先は付けない。
1646
1873
  */
1647
- function buildGraph(decisions, home, sections, targetUrls, repoBaseUrl) {
1874
+ function buildGraph(decisions, home, sections, targetUrls, repoBaseUrl, book = (0, evidence_names_1.emptyEvidenceBook)()) {
1648
1875
  const nodes = new Map();
1649
1876
  const edges = [];
1650
1877
  const addNode = (node) => {
@@ -1685,83 +1912,56 @@ function buildGraph(decisions, home, sections, targetUrls, repoBaseUrl) {
1685
1912
  // (leaderの実測で77件)を無くすのが今回の決定の本題なので、ここも揃えて飛び先を外す
1686
1913
  // (`graph-gold-only.test.mjs` の「議事録を根拠にした決定」で固定)。
1687
1914
  const dropMirrorBlob = (url) => url == null || (repoBaseUrl != null && url.startsWith(`${repoBaseUrl}/blob/`)) ? undefined : url;
1688
- // 課題・GitHub 等の鍵(`canonicalEvidence` の `key`、コメントは親の鍵)→ 先に作った点のID。
1689
- // 同じ課題を「課題キー」「URL」「コメントのURL」のどの形で指していても、1つの点に畳むために使う
1690
- // (#968 tools 1・設計v3「点を正規化の鍵で畳む」)
1691
- const evidenceKeyToNodeId = new Map();
1915
+ // 根拠の点の名前の強さ(`EvidenceIdentity.rank`)。同じ点に強い名前が後から来たら上書きする
1916
+ const labelRank = new Map();
1917
+ const identityCtx = { book, targetUrls, dropLink: dropMirrorBlob, blobPrefix: repoBaseUrl ? `${repoBaseUrl}/blob/` : undefined };
1692
1918
  /**
1693
- * 根拠の URL を、`canonicalEvidence` の鍵で重複を畳んで点にする(#968 tools 1)。
1694
- * 解けない(対応表でしか引けない・URLが2本以上混ざる等)URL は、今までどおり生のURLをIDにする。
1919
+ * 根拠を、畳む鍵(`identifyEvidence`)で 1 つの点にする(#968 tools 1)。同じ課題・資料・会議を、
1920
+ * 課題キー・URL・コメントの URL・`material:`・写しのパスのどの形で指しても同じ点に集まる。
1921
+ * 先にできた点の名前が弱ければ(種類の既定・URL)、強い名前(帳の題名・人が付けたリンクの文字)で上書きし、
1922
+ * 飛び先が無ければ後から来た飛び先を付ける
1695
1923
  */
1696
- const addEvidenceUrlNode = (rawUrl, fallbackLabel, preferFallbackLabel = false) => {
1697
- const canon = (0, canonical_evidence_1.canonicalEvidence)(rawUrl);
1698
- if (!canon)
1699
- return addNode({ id: rawUrl, label: fallbackLabel, group: "external", url: dropMirrorBlob(rawUrl) });
1700
- // コメント・返信は独立の点にせず、親(課題・Issue)の点に畳む
1701
- const foldKey = canon.parentKey ?? canon.key;
1702
- const existing = evidenceKeyToNodeId.get(foldKey);
1703
- if (existing && nodes.has(existing))
1704
- return existing;
1705
- const group = canon.kind === "url" ? "external" : EVIDENCE_GROUP[canon.kind] ?? "external";
1706
- // 対応表に無い種類(kind: "url")は、元から有効だった生URLに近い綴りをIDにする
1707
- // (既存の点のURL・1番目に見つかった記録の飛び先を変えない)
1708
- const id = canon.kind === "url" ? canon.url : foldKey;
1709
- // fallbackLabel が markdown リンクの表示文字列等(既に読める名前)なら、それを使う
1710
- // (例: 資料の題名)。生URLしか無いときだけ、根拠の種類から読める名前を補う
1711
- const label = canon.kind === "url" || preferFallbackLabel ? fallbackLabel : readableEvidenceLabel(canon.kind, foldKey, fallbackLabel);
1712
- const nodeId = addNode({ id, label, group, url: dropMirrorBlob(canon.url) });
1713
- evidenceKeyToNodeId.set(foldKey, nodeId);
1714
- return nodeId;
1924
+ const addEvidence = (ev) => {
1925
+ const existing = nodes.get(ev.id);
1926
+ // Gold の記録・宛先の引けない点と鍵がぶつかったときは、その点の名前・飛び先を書き換えない
1927
+ if (existing && !labelRank.has(ev.id))
1928
+ return ev.id;
1929
+ if (!existing) {
1930
+ addNode(ev.url ? { id: ev.id, label: ev.label, group: ev.group, url: ev.url } : { id: ev.id, label: ev.label, group: ev.group });
1931
+ labelRank.set(ev.id, ev.rank);
1932
+ return ev.id;
1933
+ }
1934
+ if (ev.rank > (labelRank.get(ev.id) ?? 0)) {
1935
+ existing.label = ev.label;
1936
+ labelRank.set(ev.id, ev.rank);
1937
+ }
1938
+ if (!existing.url && ev.url)
1939
+ existing.url = ev.url;
1940
+ return ev.id;
1715
1941
  };
1716
1942
  // 参照先IDから層を推定してノード化する
1717
- const classifyTarget = (target) => {
1718
- if (nodes.has(target))
1943
+ const classifyTarget = (target, humanLabel) => {
1944
+ if (nodes.has(target) && !labelRank.has(target))
1719
1945
  return target;
1720
1946
  // パス参照でも、末尾がレコードのファイル名ならレコードノードへ解決する
1721
1947
  const base = target.split("/").pop() ?? target;
1722
1948
  const resolved = fileNameToId.get(base);
1723
1949
  if (resolved && nodes.has(resolved))
1724
1950
  return resolved;
1725
- let group = "phantom";
1726
- let label = target;
1727
1951
  if (target.startsWith(NO_ORIGINAL_TOOL_PREFIX)) {
1728
1952
  // 元ツールが無い根拠(engine #968 の将来の語彙)。リンクにせず、機械の語(urn:…)を出さない
1729
1953
  return addNode({ id: target, label: NO_ORIGINAL_TOOL_LABEL, group: "phantom" });
1730
1954
  }
1731
- else if (target.startsWith("minute:") ||
1732
- target.startsWith("material:") ||
1733
- target.startsWith("design:") ||
1734
- target.startsWith("figma-comment:")) {
1735
- group = "silver";
1736
- // 表に無い design: / figma-comment: はキーから組む(詳細画面の resolve と同じ順)
1737
- const url = dropMirrorBlob(targetUrls.get(target) ?? figmaUrlFromRef(target));
1738
- if (url)
1739
- return addNode({ id: target, label, group, url });
1740
- }
1741
- else if (/^[A-Z][A-Z0-9_]*-\d+$/.test(target)) {
1742
- // 課題キー → 同期mdから解決した実Backlog URLに飛べるようにする(#968 便1までは1バイトも変えない表示)。
1743
- // 同じ課題が先にURLの形で現れていれば、その点に畳む
1744
- group = "bronze";
1745
- const foldKey = target.toLowerCase();
1746
- const existing = evidenceKeyToNodeId.get(foldKey);
1747
- if (existing && nodes.has(existing))
1748
- return existing;
1749
- const url = dropMirrorBlob(targetUrls.get(target));
1750
- const nodeId = addNode(url ? { id: target, label, group, url } : { id: target, label, group });
1751
- evidenceKeyToNodeId.set(foldKey, nodeId);
1752
- return nodeId;
1753
- }
1754
- else if (/^https?:\/\//.test(target)) {
1755
- return addEvidenceUrlNode(target, target.replace(/^https?:\/\//, "").slice(0, 60));
1756
- }
1757
- else if (target.includes("/") || target.toLowerCase().endsWith(".md")) {
1758
- // リポジトリ内ファイルへの生のパス参照(同期ミラーの議事録・資料など)。
1759
- // 元ツールのURLがまだ無い記録は、点にするが飛び先を付けない(#968 の書き換えを待つ)
1760
- group = "silver";
1761
- label = target.split("/").pop() ?? target;
1762
- return addNode({ id: target, label, group });
1763
- }
1764
- return addNode({ id: target, label, group });
1955
+ const ev = identifyEvidence(target, identityCtx, humanLabel);
1956
+ if (ev)
1957
+ return addEvidence(ev);
1958
+ if (target.includes("/") || target.toLowerCase().endsWith(".md")) {
1959
+ // リポジトリ内ファイルへの生のパス参照で、帳(写しの課題・資料・議事録)に当たらないもの。
1960
+ // 元ツールのURLがまだ無い記録は、点にするが飛び先を付けない(#968 の書き換えを待つ)。
1961
+ // 名前は最後の段から拡張子を外したもの(`.md` のようなファイルの語を出さない)
1962
+ return addNode({ id: target, label: (0, evidence_names_1.pathFallbackLabel)(target), group: "silver" });
1963
+ }
1964
+ return addNode({ id: target, label: target, group: "phantom" });
1765
1965
  };
1766
1966
  const addRelationEdges = (fromId, relations) => {
1767
1967
  for (const r of relations) {
@@ -1777,17 +1977,15 @@ function buildGraph(decisions, home, sections, targetUrls, repoBaseUrl) {
1777
1977
  };
1778
1978
  const addReferenceEdges = (fromId, references) => {
1779
1979
  for (const ref of references) {
1780
- if (ref.url) {
1781
- // ref.label はマークダウンリンクの表示文字列(既に読める名前のことが多い。例: 資料の題名)。
1782
- // 生URLそのものがラベルのとき(本文の裸URL等)だけ、根拠の種類から読める名前を補う
1783
- const hasHumanLabel = ref.label !== ref.url;
1784
- const fallback = hasHumanLabel ? ref.label.slice(0, 60) : ref.label.replace(/^https?:\/\//, "").slice(0, 60);
1785
- const id = addEvidenceUrlNode(ref.url, fallback, hasHumanLabel);
1786
- edges.push({ from: fromId, to: id, rel: "reference" });
1787
- }
1788
- else {
1789
- edges.push({ from: fromId, to: classifyTarget(ref.label), rel: "reference" });
1790
- }
1980
+ // 点の鍵は**書かれていた値**から決める(解決器の飛び先は blob の写しのこともあるので鍵にしない)。
1981
+ // マークダウンリンクの表示文字列(人が付けた名前)は、帳の題名が無いときの名前に使う
1982
+ // (`[名前](URL)` の名前は `identifyEvidence` が書かれていた値から拾う。解決器が付けた名前は
1983
+ // 種類の既定のこともあるので、人の名前としては渡さない)
1984
+ const raw = REF_RAW.get(ref);
1985
+ if (raw !== undefined)
1986
+ edges.push({ from: fromId, to: classifyTarget(raw), rel: "reference" });
1987
+ else
1988
+ edges.push({ from: fromId, to: classifyTarget(ref.url ?? ref.label, ref.url && ref.label !== ref.url ? ref.label : undefined), rel: "reference" });
1791
1989
  }
1792
1990
  };
1793
1991
  const addLinkEdges = (fromId, links) => {
@@ -2165,7 +2363,11 @@ async function build(opts) {
2165
2363
  const targetUrls = knowledgeRoot
2166
2364
  ? await buildTargetUrlMap(repoRoot, repo.baseUrl, repo.branch)
2167
2365
  : new Map();
2168
- const refs = createRefResolver({ repoRoot, targetUrls, repoBaseUrl: repo.baseUrl, branch: repo.branch });
2366
+ // fleet-status.json は 1 回だけ読む(読み手が警告を出すので、2 回読むと警告が 2 行になる)。
2367
+ // 名前の帳(チャンネル名)と、下の「データソースと自動化」で同じものを使う
2368
+ const fleet = await readFleetSources(repoRoot);
2369
+ const evidenceBook = knowledgeRoot ? await buildEvidenceBook(repoRoot, targetUrls, fleet.externalSources) : (0, evidence_names_1.emptyEvidenceBook)();
2370
+ const refs = createRefResolver({ repoRoot, targetUrls, repoBaseUrl: repo.baseUrl, branch: repo.branch, book: evidenceBook });
2169
2371
  // AIからの更新提案。**レコードを組み立てる前に読む**——各レコードが自分の分を
2170
2372
  // takeProposals で引き取るので、後から読んでも載せる先が既に閉じている。
2171
2373
  // 同一プロセスで2回ビルドしても前回の値が残らないよう、毎回入れ替える
@@ -2310,10 +2512,9 @@ async function build(opts) {
2310
2512
  console.warn(`gold-proposals.json: レコードに突き合わない提案が ${unmatched.length} 件ありました(表示しません): ` +
2311
2513
  unmatched.slice(0, 5).join(", ") + (unmatched.length > 5 ? " ほか" : ""));
2312
2514
  }
2313
- const graph = buildGraph(decisions, home, graphSections, targetUrls, repo.baseUrl);
2515
+ const graph = buildGraph(decisions, home, graphSections, targetUrls, repo.baseUrl, evidenceBook);
2314
2516
  computeLayout(graph); // 各ノードに x/y を書き込む(クライアントはこれを初期位置に使いウォームアップ省略)
2315
- // データソースと自動化(fleet-status.json 由来)。無ければ undefined でセクション非表示。
2316
- const fleet = await readFleetSources(repoRoot);
2517
+ // データソースと自動化(fleet-status.json 由来・上で 1 回だけ読んだもの)。無ければ undefined でセクション非表示。
2317
2518
  // すでにある会議(投入モーダルの「会議名」の選択肢)。intake の有無に関わらず載せる——
2318
2519
  // 出し分けは表示側の仕事で、ここで絞ると「投入の口を後から付けたら選択肢が空」になる
2319
2520
  const meetings = await collectMeetings(repoRoot);
@@ -2495,8 +2696,16 @@ async function parseDecision(raw, fileName, repoRelDir, repoBaseUrl, branch, ref
2495
2696
  ...(followupHuman ? { followupHuman: followupHuman } : {}),
2496
2697
  };
2497
2698
  }
2699
+ /**
2700
+ * 解決した参照 → 書かれていた値(#968 tools 1)。つながり画面は点の鍵をこの値から決める
2701
+ * (`site-data` に載せない——機械の語を画面の材料に増やさない)
2702
+ */
2703
+ const REF_RAW = new WeakMap();
2704
+ /** リポジトリのパス 1 本だけの値(`会議/…/x.md`・`tmp/a.txt`)。カンマ・読点・括弧の注記を含むものは当てない */
2705
+ const REPO_PATH_ONLY_RE = /^[^\s,、()()]+(?:[ _-][^,、()()/]*)*\/[^,、()()/]+\.[A-Za-z0-9]{1,5}$/;
2498
2706
  function createRefResolver(opts) {
2499
2707
  const { repoRoot, targetUrls, repoBaseUrl, branch } = opts;
2708
+ const book = opts.book ?? (0, evidence_names_1.emptyEvidenceBook)();
2500
2709
  // **他の走査結果を流用しない。** 全md一覧(Decisions走査や、かつてのグラフ走査 `scanContent`)は
2501
2710
  // `.md` しか歩かず一部のディレクトリを除外するので、実在する `.txt`(Geminiメモ)や
2502
2711
  // `tmp/` 配下のファイルを「実在しない」と誤判定して**いま動いているリンクを消す**。
@@ -2520,8 +2729,15 @@ function createRefResolver(opts) {
2520
2729
  seen.set(relPath, ok);
2521
2730
  return ok;
2522
2731
  };
2523
- const resolve = (ref) => {
2524
- const trimmed = String(ref ?? "").trim();
2732
+ /**
2733
+ * 書かれていた値の読める名前(#968 tools 1)。帳の題名・会議の定例名と日付・種類の既定の名前。
2734
+ * **飛び先は変えない**(ここは名前だけ)。根拠でない値・汎用の URL は `null`(今までどおりの名前)
2735
+ */
2736
+ const nameOf = (raw) => {
2737
+ const ev = identifyEvidence(raw, { book, targetUrls, dropLink: (u) => u ?? undefined });
2738
+ return ev && ev.kind !== "url" ? ev.label : null;
2739
+ };
2740
+ const resolveOne = (trimmed) => {
2525
2741
  if (!trimmed)
2526
2742
  return { label: trimmed, url: null };
2527
2743
  // 元ツールが無い根拠(engine #968 の将来の語彙)。リンクにせず、機械の語(urn:…)を出さない
@@ -2540,23 +2756,31 @@ function createRefResolver(opts) {
2540
2756
  if (/^https?:\/\//.test(trimmed)) {
2541
2757
  // 裸のURL(markdownリンクでラベルを添えていないもの)は、分かる種類なら読める名前にする
2542
2758
  // (#968 tools 1「URL → 読める名前」)。**url は変えない**(正規化で既存のリンクを壊さない)
2543
- const canon = (0, canonical_evidence_1.canonicalEvidence)(trimmed);
2544
- const label = canon && canon.kind !== "url" ? readableEvidenceLabel(canon.kind, canon.parentKey ?? canon.key, trimmed) : trimmed;
2545
- return { label, url: trimmed };
2759
+ return { label: nameOf(trimmed) ?? trimmed, url: trimmed };
2546
2760
  }
2547
2761
  // 正本のURL(課題キー・material:・design:・minute:)。**ここが「ツールへ飛ぶ」の本体**
2548
2762
  const known = targetUrls.get(trimmed);
2549
2763
  if (known)
2550
- return { label: trimmed, url: known };
2764
+ return { label: nameOf(trimmed) ?? trimmed, url: known };
2551
2765
  // inventory の無い案件(#727 K10)の design: / figma-comment: は、キーとノードから組む
2552
2766
  const figma = figmaUrlFromRef(trimmed);
2553
2767
  if (figma)
2554
- return { label: trimmed, url: figma };
2768
+ return { label: nameOf(trimmed) ?? trimmed, url: figma };
2769
+ // リポジトリのパス 1 本だけの値は、最後の段から拡張子を外した名前で出す(ファイルの語を出さない・#968 tools 1)。
2770
+ // 文の中のパス・パスに注記が付いた複合形は、書かれたまま(どれを指すか画面では決められない)
2771
+ const pathName = REPO_PATH_ONLY_RE.test(trimmed) ? (0, evidence_names_1.pathFallbackLabel)(trimmed) : trimmed;
2555
2772
  // 実在するファイルだけ blob URL にする
2556
2773
  if (repoBaseUrl && exists(trimmed)) {
2557
- return { label: trimmed, url: `${repoBaseUrl}/blob/${branch}/${encodePath(trimmed)}` };
2774
+ return { label: nameOf(trimmed) ?? pathName, url: `${repoBaseUrl}/blob/${branch}/${encodePath(trimmed)}` };
2558
2775
  }
2559
- return { label: trimmed, url: null };
2776
+ return { label: nameOf(trimmed) ?? pathName, url: null };
2777
+ };
2778
+ const resolve = (ref) => {
2779
+ const trimmed = String(ref ?? "").trim();
2780
+ const out = resolveOne(trimmed);
2781
+ if (trimmed)
2782
+ REF_RAW.set(out, trimmed);
2783
+ return out;
2560
2784
  };
2561
2785
  return { resolve, exists };
2562
2786
  }
@@ -90,6 +90,8 @@ exports.WEIGHTS = Object.freeze({
90
90
  // 人が承認して push した記憶の訂正(cortex-engine #935)。同じコミットから `record.edited` も起きるので、
91
91
  // 数えると二重になる。0
92
92
  "correction.stated": 0,
93
+ // 添付の原本の行(cortex-engine #1046 便 1)。本文は元の名前だけで、記憶のどの材料にも入れない。0
94
+ "file.attached": 0,
93
95
  });
94
96
  const DAY_MS = 86400000;
95
97
  const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ /**
3
+ * **根拠の読める名前の帳**(cortex-engine #968 tools 1 の続き・設計は #968 の状態コメント 2026-10-02)。
4
+ *
5
+ * つながり画面の点と、詳細画面の「参照」に出す名前を、URL や機械の ID(`minute:`・`material:`・
6
+ * `gfile:`・写しのファイル名)ではなく、人が呼ぶ名前(課題キー+題名・資料の題名・会議の定例名と
7
+ * 日付・チャンネル名)にするための材料。
8
+ *
9
+ * 帳は**ビルドのたびに写し(課題管理・共有資料・デザイン)と `fleet-status.json` のチャンネルの一覧から組む**。写しを
10
+ * 片付けた案件では帳が薄くなるだけで、点は今までどおり種類の既定の名前(「Google ドライブの資料」等)で出る。
11
+ *
12
+ * 鍵は `canonicalEvidence` の `key`(コメントは親の鍵)。ID の根拠(課題キー・`material:` 等)も、
13
+ * 対応表で URL に引けるものは同じ鍵に寄せる——今の ID と新しい URL の根拠が混ざっても 1 つの点になる。
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.emptyEvidenceBook = void 0;
17
+ exports.minuteFoldKey = minuteFoldKey;
18
+ exports.minuteLabel = minuteLabel;
19
+ exports.minuteRefOfPath = minuteRefOfPath;
20
+ exports.slackChannelOfLabel = slackChannelOfLabel;
21
+ exports.backlogTailOf = backlogTailOf;
22
+ exports.idFallbackLabel = idFallbackLabel;
23
+ exports.pathFallbackLabel = pathFallbackLabel;
24
+ const emptyEvidenceBook = () => ({
25
+ names: new Map(),
26
+ pathRefs: new Map(),
27
+ slackUrlsByName: new Map(),
28
+ backlogTailRefs: new Map(),
29
+ });
30
+ exports.emptyEvidenceBook = emptyEvidenceBook;
31
+ /** `minute:{定例名}:{YYYYMMDD}`(定例名は `Ph.1/全体定例` のようにフェーズの段を含むことがある) */
32
+ const MINUTE_ID_RE = /^minute:(.+):(\d{8})$/;
33
+ /** 定例名の最後の段(`Ph.1/全体定例` → `全体定例`)。画面の名前に使う */
34
+ const lastSegment = (s) => s.split("/").filter(Boolean).pop() ?? s;
35
+ /** フェーズの段(`Ph.1`・`Phase2`)。会議の置き場の上の段で、会議そのものを区別しない */
36
+ const PHASE_SEGMENT_RE = /^ph(?:ase)?\.?\s*\d+$/i;
37
+ /**
38
+ * `minute:` の ID を畳む鍵にする。**落とすのはフェーズの段だけ**(`Ph.1/全体定例` と `全体定例` を同じ会議に)。
39
+ * ほかの段(`顧客A/定例` と `顧客B/定例`)は別の会議なので残す——最後の段だけで畳むと別の会議が 1 点になる。
40
+ * 形が違えば `null`
41
+ */
42
+ function minuteFoldKey(id) {
43
+ const m = id.match(MINUTE_ID_RE);
44
+ if (!m)
45
+ return null;
46
+ const segs = m[1].split("/").filter(Boolean);
47
+ const kept = segs.filter((seg, i) => i === segs.length - 1 || !PHASE_SEGMENT_RE.test(seg));
48
+ return `minute:${kept.join("/")}:${m[2]}`;
49
+ }
50
+ /** `minute:` の ID → 「全体定例 2026-06-04」。形が違えば `null` */
51
+ function minuteLabel(id) {
52
+ const m = id.match(MINUTE_ID_RE);
53
+ if (!m)
54
+ return null;
55
+ const d = m[2];
56
+ return `${lastSegment(m[1])} ${d.slice(0, 4)}-${d.slice(4, 6)}-${d.slice(6, 8)}`;
57
+ }
58
+ /**
59
+ * 議事録の写しのパス(`会議/{…}/{定例名}/{YYYYMMDD}/{…}minutes{…}.md`)→ `minute:{定例名}:{YYYYMMDD}`。
60
+ * `buildTargetUrlMap` の `minute:` の組み方と同じ規約(定例名は日付の 1 つ上の段)
61
+ */
62
+ function minuteRefOfPath(p) {
63
+ const parts = p.split("/");
64
+ if (parts.length < 4 || parts[0] !== "会議")
65
+ return null;
66
+ const file = parts[parts.length - 1];
67
+ const date = parts[parts.length - 2];
68
+ const meeting = parts[parts.length - 3];
69
+ if (!/minutes/i.test(file) || !/\.md$/i.test(file) || !/^\d{8}$/.test(date) || !meeting)
70
+ return null;
71
+ return `minute:${meeting}:${date}`;
72
+ }
73
+ /**
74
+ * チャットの根拠の、チャンネル名だけで書かれた形 → `名前`。取り込みが書くラベル `[slack] #名前 (N messages since …)` と、
75
+ * AI が書いた `slack:{名前}:{YYYYMMDD}`。形が違えば `null`(チャンネル ID の `slack:C…` は鍵なのでここでは扱わない)
76
+ */
77
+ function slackChannelOfLabel(raw) {
78
+ const t = raw.trim();
79
+ const m = t.match(/^\[slack\]\s+#([^\s()]+)(?:\s+\(.*\))?$/i) ?? t.match(/^slack:#?([^:\s]+):\d{8}$/);
80
+ return m && !/^C[A-Z0-9]{6,}$/.test(m[1]) ? m[1] : null;
81
+ }
82
+ /** 課題管理の写しのパス → 区画から下(`issues/2026/x.md`)。写しの形でなければ `null` */
83
+ function backlogTailOf(p) {
84
+ const m = p.match(/^課題管理\/(?:[^/]+\/)?((?:issues|documents|wiki)\/.+)$/);
85
+ return m ? m[1] : null;
86
+ }
87
+ /** ID の根拠の既定の名前(題名が引けないとき)。機械の接頭辞(`material:` 等)を外す */
88
+ function idFallbackLabel(id) {
89
+ const m = id.match(/^(material|design|figma-comment):(.+)$/);
90
+ if (!m)
91
+ return id;
92
+ if (m[1] === "material")
93
+ return m[2];
94
+ return m[1] === "design" ? "Figma のデザイン" : "Figma のコメント";
95
+ }
96
+ /** 写しのパスの既定の名前: 最後の段から拡張子を外す(`.md` のようなファイルの語を出さない) */
97
+ function pathFallbackLabel(p) {
98
+ const base = p.split("/").filter(Boolean).pop() ?? p;
99
+ return base.replace(/\.[A-Za-z0-9]{1,5}$/, "") || base;
100
+ }
@@ -9,6 +9,7 @@ exports.parseLedger = parseLedger;
9
9
  exports.readRederivationLedgers = readRederivationLedgers;
10
10
  exports.parseInjectedNow = parseInjectedNow;
11
11
  exports.activityScore = activityScore;
12
+ exports.withoutFollowupHeadingText = withoutFollowupHeadingText;
12
13
  exports.comparableContent = comparableContent;
13
14
  exports.sameRederivedContent = sameRederivedContent;
14
15
  exports.contentShaOf = contentShaOf;
@@ -368,14 +369,15 @@ const isFollowupBegin = (line) => FOLLOWUP_BEGIN_RE.test(String(line ?? "").repl
368
369
  * 開始の印の行(最初の 1 つ・行末の `\r` は無視)から、終了の印**だけ**の行までを(両端を含めて)消す。終了の印が
369
370
  * 無ければ本文の終わりまで消す。塊の前後の空行も除き、両側に本文が残れば空行 1 行でつなぐ。そのあと末尾の空行を落とす。塊の無い本文は触らない。
370
371
  */
371
- function withoutBlock(body, isBegin, endMarker) {
372
+ function withoutBlock(body, isBegin, endMarker, { keepEnd = false } = {}) {
372
373
  const lines = body.split("\n");
373
374
  const start = lines.findIndex((line) => isBegin(line));
374
375
  if (start < 0)
375
376
  return body;
376
377
  // 行末の `\r` は見ない(CRLF の記録でも印の行として当てる。当てないと毎晩印が立つ)
377
378
  const endRel = lines.slice(start + 1).findIndex((line) => line.replace(/\r$/, "") === endMarker);
378
- const end = endRel < 0 ? lines.length : start + 1 + endRel + 1;
379
+ // `keepEnd`: 終わりの行(次の節の見出し)は残す(見出しで区切る「その後」の節・#1094 便 1)
380
+ const end = endRel < 0 ? lines.length : start + 1 + endRel + (keepEnd ? 0 : 1);
379
381
  const isBlank = (line) => line.trim() === "";
380
382
  // 塊の前後の空行も一緒に除き、両側に本文が残れば空行 1 行でつなぐ(engine の書き手と同じ規則・#702 実装レビュー R1)
381
383
  const before = lines.slice(0, start);
@@ -413,7 +415,28 @@ function stripMemberDecisions(body) {
413
415
  function withoutFollowup(body) {
414
416
  return withoutBlock(body, isFollowupBegin, FOLLOWUP_END);
415
417
  }
416
- function comparableContent(raw) {
418
+ /** 「その後」の節の見出し(印を外した後の形・#1094 便 1)。🔴 engine の `FOLLOWUP_HEADING_RE` の写し */
419
+ const FOLLOWUP_HEADING_RE = /^##\s+その後\s*$/;
420
+ /**
421
+ * 印の無い「その後」の節(`## その後` の行から、次の `## ` の行か本文の終わりまで)を除く(#1094 便 1・#551 設計 D2)。
422
+ * 規則は塊と同じ 1 本(`withoutBlock`・次の節の見出しは残す)。印の塊がある本文は塊の規則で先に除くので、
423
+ * ここは印を外した後の本文だけに効く。
424
+ *
425
+ * 🔴 **engine の写し(`scripts/rederive/content-sha.mjs` の `withoutFollowupHeadingText`)と 1 バイト一致・
426
+ * 番人は `test/followup-block-sha.test.mjs` の `[markerFree]`**(engine の `contentShaOf(raw, { markerFree: true })` を
427
+ * 実際に呼んで焼いた表 `test/fixtures/marker-free-sha-contract.json`)。変えたら両方を変えて表を焼き直す。
428
+ */
429
+ function withoutFollowupHeadingText(body) {
430
+ const text = String(body ?? "");
431
+ const lines = text.split("\n");
432
+ const start = lines.findIndex((line) => FOLLOWUP_HEADING_RE.test(line.replace(/\r$/, "")));
433
+ if (start < 0)
434
+ return text;
435
+ const nextRel = lines.slice(start + 1).findIndex((line) => /^##\s/.test(line));
436
+ const endLine = nextRel < 0 ? null : lines[start + 1 + nextRel].replace(/\r$/, "");
437
+ return withoutBlock(text, (line) => line === lines[start], endLine === null ? "\u0000" : endLine, { keepEnd: endLine !== null });
438
+ }
439
+ function comparableContent(raw, { markerFree = false } = {}) {
417
440
  let data = {};
418
441
  let content = String(raw ?? "");
419
442
  try {
@@ -428,7 +451,9 @@ function comparableContent(raw) {
428
451
  return {
429
452
  description: normalizeForCompare(String(data.description ?? "")),
430
453
  // engine と同じ順(目録の塊 → その後の塊)
431
- body: normalizeForCompare(withoutFollowup(stripMemberDecisions(content))),
454
+ body: normalizeForCompare(markerFree
455
+ ? withoutFollowupHeadingText(withoutFollowup(stripMemberDecisions(content)))
456
+ : withoutFollowup(stripMemberDecisions(content))),
432
457
  };
433
458
  }
434
459
  /** 2 つの版で「説明文と本文」が同じか(frontmatter の他の欄の差は見ない) */
@@ -448,8 +473,8 @@ function sameRederivedContent(a, b) {
448
473
  * engine の写し: `cortex-engine/scripts/rederive/content-sha.mjs`・一致の番人:
449
474
  * `cortex-engine/test/rederive-history.test.mjs` の #546 の節(ここを変えたら engine の `VIEWER_SHA` を計算し直す)
450
475
  */
451
- function contentShaOf(raw) {
452
- const c = comparableContent(raw);
476
+ function contentShaOf(raw, options = {}) {
477
+ const c = comparableContent(raw, options);
453
478
  return node_crypto_1.default.createHash("sha256").update(c.description + "\u0000" + c.body, "utf8").digest("hex");
454
479
  }
455
480
  /**
package/dist/render.js CHANGED
@@ -57,7 +57,6 @@ exports.nextMemoryReflectGoldAtMs = nextMemoryReflectGoldAtMs;
57
57
  exports.memoryReflectDoneAtMs = memoryReflectDoneAtMs;
58
58
  exports.memoryReflectTextOf = memoryReflectTextOf;
59
59
  exports.runLogUrlOf = runLogUrlOf;
60
- exports.lastSyncRunUrlOf = lastSyncRunUrlOf;
61
60
  exports.runSourceWordOf = runSourceWordOf;
62
61
  exports.fleetLastSyncOf = fleetLastSyncOf;
63
62
  exports.stateRefKey = stateRefKey;
@@ -2722,27 +2721,6 @@ function runLogUrlOf(row) {
2722
2721
  return "";
2723
2722
  return row.runUrl;
2724
2723
  }
2725
- /**
2726
- * 「最終同期: …」の横に出すリンク先(#560)。**その時刻を書いた run のときだけ。**
2727
- *
2728
- * 「最終同期」は `lastOkAt`(最後に取り込めた時刻)で、行の `runUrl` は**いまの status を
2729
- * 書いた run**のもの。いま `failed` の行の `lastOkAt` が最大なら、その `runUrl` は失敗した run
2730
- * を指すので出さない(間違った run へ送るより、無いほうがよい)。`ok` の行でも `lastOkAt` と
2731
- * `statusAt` が違う(履歴だけ先に進んだ)回は出さない。
2732
- *
2733
- * **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す)。
2734
- */
2735
- function lastSyncRunUrlOf(row) {
2736
- // **部分失敗の回も「取り込めた」(#634)。** ここを `status === "ok"` のままにすると、
2737
- // 「最終同期」の時刻は部分失敗の回のものになったのに、その時刻を書いた run へ行けない
2738
- // ——R9(その時刻を書いた run にだけリンクを出す)が、まさにこの便の対象の回で満たせない
2739
- if (!ingestSucceeded(row))
2740
- return "";
2741
- // 「最終同期」として出した時刻がこの行の `at` でないなら、リンク先の run は別の回のもの
2742
- if (row.lastOkAt && row.lastOkAt > row.at)
2743
- return "";
2744
- return runLogUrlOf(row);
2745
- }
2746
2724
  /**
2747
2725
  * `runUrl` の無い行に添える出どころの語(#560)。リンクが出る行・語の無い書き手は空文字。
2748
2726
  *
@@ -5716,10 +5694,6 @@ a.ds-tile:hover { text-decoration: none; }
5716
5694
  .ds-all-connected { color: var(--muted); }
5717
5695
  .ds-gate { display: inline-flex; align-items: center; gap: 5px; font-size: 12.5px; font-weight: 600; flex: 0 0 auto; }
5718
5696
  .ds-gate-at { font-size: 11px; color: var(--muted); flex: 0 0 auto; }
5719
- /* 実行ログへの導線(#560)。時刻の横の添え物なので .ds-gate-at と同じ弱さ。押せることだけ
5720
- 点線の下線で示す(目立たせない——案件リポジトリに入っていない人には開けない) */
5721
- .ds-run-link { font-size: 11px; color: var(--muted); flex: 0 0 auto; text-decoration: underline dotted; text-underline-offset: 2px; }
5722
- .ds-run-link:hover { color: var(--text); }
5723
5697
  /* run の無い書き手の行の出どころ(「中央の処理」「その場で確認」)。同じ弱さの文字 */
5724
5698
  .ds-run-src { font-size: 11px; color: var(--muted); flex: 0 0 auto; }
5725
5699
  /* 接続先1件ごとの「取り込み」(段C・2026-08-24)。**「最終同期」(.ds-sync・run単位)とは
@@ -6430,7 +6404,6 @@ ${nextMemoryReflectGoldAtMs.toString()}
6430
6404
  ${memoryReflectDoneAtMs.toString()}
6431
6405
  ${memoryReflectTextOf.toString()}
6432
6406
  ${runLogUrlOf.toString()}
6433
- ${lastSyncRunUrlOf.toString()}
6434
6407
  ${runSourceWordOf.toString()}
6435
6408
  var TILE_CAPS = ${JSON.stringify(TILE_CAPS)};
6436
6409
  var BAD_STATE_STATUSES = ${JSON.stringify(BAD_STATE_STATUSES)};
@@ -12901,12 +12874,11 @@ ${calendarAddUrl.toString()}
12901
12874
  var t = dsFmtTime(at || fleetSyncFallback(s) || "");
12902
12875
  if (!t) return null;
12903
12876
  var sync = el("span", { class: "ds-sync", text: "最終同期: " + t });
12904
- // その時刻を書いた run へ(#560)。**ストア由来の時刻のときだけ**——まとめ表の後詰めに
12905
- // run は無い。行は「ingestedAtOf」 と同じ選び方(「ingestedRowOf」)で取る。
12906
- // run の無い書き手(中央 Apps Script の会議・資料の取り込み)は出どころの語
12877
+ // run の無い書き手(中央 Apps Script の会議・資料の取り込み)は出どころの語を添える。
12878
+ // ストア由来の時刻のときだけ——まとめ表の後詰めには行が無い。行は「ingestedAtOf」 と
12879
+ // 同じ選び方(「ingestedRowOf」)で取る
12907
12880
  var row = at ? ingestedRowOf(LIVE_STATE, capId, refs) : null;
12908
- var url = lastSyncRunUrlOf(row);
12909
- var trail = url ? [runLogLink(url)] : row ? runTrailNodes(row, "") : [];
12881
+ var trail = row ? runTrailNodes(row) : [];
12910
12882
  return trail.length ? frag([sync].concat(trail)) : sync;
12911
12883
  }
12912
12884
 
@@ -14820,37 +14792,26 @@ ${calendarAddUrl.toString()}
14820
14792
  }
14821
14793
 
14822
14794
  /* -------------------------------------------------------------------------
14823
- * 実行ログへの導線(#560)
14795
+ * run の出どころの表示(#560・オーナー決定 2026-10-01: 「実行ログ ↗」は社内・お客様とも
14796
+ * 画面から外した)
14824
14797
  * -------------------------------------------------------------------------
14825
- * 行の 「runUrl」(その行を書いた Actions の run)があれば、時刻の横に小さな「実行ログ ↗」を
14826
- * 出す。run の無い書き手の行は、出どころを語で示す(「中央の処理」「その場で確認」)。
14827
- * どちらを出すかの判断は純関数(runLogUrlOf / runSourceWordOf)が持ち、ここは描くだけ。
14798
+ * run の無い書き手の行は、出どころを語で示す(「中央の処理」「その場で確認」)。run のある
14799
+ * 行(ingest / gold / nightly)は何も添えない——リンク先が実行用リポや案件リポの GitHub
14800
+ * Actions で、多くの利用者には開けないため。出すかどうかの判断は純関数
14801
+ * (runLogUrlOf / runSourceWordOf)が持ち、ここは描くだけ。
14828
14802
  * ----------------------------------------------------------------------- */
14829
14803
 
14830
- /** リンクの title。案件リポジトリに入っていない人には開けないので、押す前に分かるようにする */
14831
- var RUN_LINK_TITLE = "GitHub Actions の実行ログを開きます(プロジェクトリポジトリへのアクセスが要ります)";
14832
- /** 一覧(開始直後・run の番号がまだ無い)の title。飛び先が違うので文言も分ける */
14833
- var RUN_LIST_TITLE = "GitHub Actions の実行の一覧を開きます(プロジェクトリポジトリへのアクセスが要ります)";
14834
- var RUN_LIST_TEXT = "実行の一覧 ↗";
14835
-
14836
- /** 「実行ログ ↗」(新しいタブ・title つき)。一覧のときは文言と title を一覧のものにする */
14837
- function runLogLink(url, text) {
14838
- var list = text === RUN_LIST_TEXT;
14839
- return el("a", { class: "ds-run-link", href: url, target: "_blank", rel: "noopener", title: list ? RUN_LIST_TITLE : RUN_LINK_TITLE, text: text || "実行ログ ↗" });
14840
- }
14841
-
14842
14804
  /**
14843
- * 行の時刻の横に添えるもの(リンクか出どころの語)。**無ければ空配列。**
14805
+ * 行の時刻の横に添える出どころの語。**無ければ空配列。**
14844
14806
  * 「.ds-gate-at」「.ds-ingest」 の**兄弟**として置く(子にすると、文言の検査と
14845
14807
  * 「いま測った結果」への差し替えが崩れる)。
14846
14808
  *
14847
14809
  * @param kind どの時刻に添えたか(「gate」=「◯分前に確認」)。「いま測った結果」への差し替えは
14848
- * **この印のものだけ**消す——「取り込み: 〜に成功」の横のリンク(別の行の run)は残す
14810
+ * **この印のものだけ**消す——「取り込み: 〜に成功」の横の語(別の行のもの)は残す
14849
14811
  */
14850
- function runTrailNodes(row, url, kind) {
14851
- var u = url === undefined ? runLogUrlOf(row) : url;
14852
- var word = u ? "" : runSourceWordOf(row);
14853
- var node = u ? runLogLink(u) : word ? el("span", { class: "ds-run-src", text: word }) : null;
14812
+ function runTrailNodes(row, kind) {
14813
+ var word = runSourceWordOf(row);
14814
+ var node = word ? el("span", { class: "ds-run-src", text: word }) : null;
14854
14815
  if (!node) return [];
14855
14816
  if (kind) node.setAttribute("data-trail", kind);
14856
14817
  return [node];
@@ -14864,11 +14825,11 @@ ${calendarAddUrl.toString()}
14864
14825
  }
14865
14826
 
14866
14827
  /**
14867
- * 「いま測った結果」に差し替えるとき、**「◯分前に確認」に添えた**古い run の導線を外す
14868
- * (同じ行が 2 つのことを言わない)。「取り込み: 〜に成功」の横のリンクは別の行の run なので残す
14828
+ * 「いま測った結果」に差し替えるとき、**「◯分前に確認」に添えた**古い出どころの語を外す
14829
+ * (同じ行が 2 つのことを言わない)。「取り込み: 〜に成功」の横の語は別の行のものなので残す
14869
14830
  */
14870
14831
  function removeRunTrail(scope) {
14871
- var old = scope.querySelectorAll('.ds-run-link[data-trail="gate"], .ds-run-src[data-trail="gate"]');
14832
+ var old = scope.querySelectorAll('.ds-run-src[data-trail="gate"]');
14872
14833
  for (var i = 0; i < old.length; i++) old[i].remove();
14873
14834
  }
14874
14835
 
@@ -14907,7 +14868,7 @@ ${calendarAddUrl.toString()}
14907
14868
  class: "ds-ingest" + (r.status === "failed" && !(f && f.partial) ? " ds-ingest-failed" : ""),
14908
14869
  text: text,
14909
14870
  });
14910
- // その行を書いた run へ(#560)。成功でも失敗でも、この時刻を書いた run のログ
14871
+ // run の無い書き手は出どころの語を添える(#560)。成功でも失敗でも同じ
14911
14872
  return frag([span].concat(runTrailNodes(r)));
14912
14873
  }
14913
14874
 
@@ -14966,8 +14927,8 @@ ${calendarAddUrl.toString()}
14966
14927
  row.appendChild(el("span", { class: "ds-gate " + gate.cls, text: gate.text }));
14967
14928
  var when = rtAgo(r.at);
14968
14929
  if (when) row.appendChild(el("span", { class: "ds-gate-at", text: when + "に確認" }));
14969
- // その行を書いた run へ(#560)。run の無い書き手は出どころの語
14970
- runTrailNodes(r, undefined, "gate").forEach(function (n) { row.appendChild(n); });
14930
+ // run の無い書き手は出どころの語を添える(#560)
14931
+ runTrailNodes(r, "gate").forEach(function (n) { row.appendChild(n); });
14971
14932
  return row;
14972
14933
  }
14973
14934
 
@@ -15116,28 +15077,15 @@ ${calendarAddUrl.toString()}
15116
15077
  at: (before && before.at) || "",
15117
15078
  startedAt: Date.now(),
15118
15079
  message: (j && j.message) || "",
15119
- // 起こした run(特定できた回)と一覧(#560)。描き直しでもリンクが消えないよう控える
15120
- runUrl: (j && RUN_URL_RE.test(String(j.runUrl || ""))) ? String(j.runUrl) : "",
15121
- runsUrl: (j && /^https:\\/\\/github\\.com\\/[^\\s"'<>]+$/.test(String(j.runsUrl || ""))) ? String(j.runsUrl) : "",
15122
15080
  };
15123
15081
  probeWaitNote(note, PROBE_WAITS[key]);
15124
15082
  // 待っているあいだは取り直しを回す(届いた行で一覧が描き直される)
15125
15083
  refreshStateTimer();
15126
15084
  }
15127
15085
 
15128
- /**
15129
- * 待ちの 1 文と、その横の導線(#560)。**その run を特定できていればそれ、無ければ一覧。**
15130
- * 開始直後は run の番号が無いことが多いので一覧へ。行が更新されて待ちが解けたら、
15131
- * 行側の「実行ログ ↗」(その run)に置き換わる。run の無い経路(固定 IP・中央)は何も出ない
15132
- */
15086
+ /** 待ちの 1 文(#548)。「同期を開始しました」等、サーバの文言をそのまま出す */
15133
15087
  function probeWaitNote(note, wait) {
15134
15088
  note.textContent = (wait && wait.message) || "";
15135
- if (!wait) return;
15136
- var link = wait.runUrl ? runLogLink(wait.runUrl) : wait.runsUrl ? runLogLink(wait.runsUrl, RUN_LIST_TEXT) : null;
15137
- if (link) {
15138
- note.appendChild(document.createTextNode(" "));
15139
- note.appendChild(link);
15140
- }
15141
15089
  }
15142
15090
 
15143
15091
  /**
@@ -15216,12 +15164,7 @@ ${calendarAddUrl.toString()}
15216
15164
  var phase = probeWaitPhaseOf(capId, ref);
15217
15165
  var wait = probeWaitOf(capId, ref);
15218
15166
  if (phase === "waiting") probeWaitNote(note, wait);
15219
- else if (phase === "timeout") {
15220
- // 15 分待って届かないなら見に行く番。**行き先を添える**(#560)——一覧でも run でも
15221
- note.textContent = PROBE_WAIT_TIMEOUT_TEXT;
15222
- var to = wait && (wait.runUrl || wait.runsUrl);
15223
- if (to) { note.appendChild(document.createTextNode(" ")); note.appendChild(runLogLink(to, wait.runUrl ? "実行ログ ↗" : RUN_LIST_TEXT)); }
15224
- }
15167
+ else if (phase === "timeout") note.textContent = PROBE_WAIT_TIMEOUT_TEXT;
15225
15168
  else note.hidden = true;
15226
15169
 
15227
15170
  checkBtn.addEventListener("click", async function () {
@@ -16545,7 +16488,7 @@ ${calendarAddUrl.toString()}
16545
16488
  // 起動できなかった案件(権限・スタブ未配布)ではリンクを出さない——押しても何も無い
16546
16489
  if (j.runsUrl) {
16547
16490
  msg.appendChild(document.createTextNode(" "));
16548
- msg.appendChild(el("a", { href: j.runUrl || j.runsUrl, target: "_blank", rel: "noopener", title: RUN_LINK_TITLE, text: "実行状況を見る →" })); // その run を特定できていればそれ(#560)、無ければ一覧
16491
+ msg.appendChild(el("a", { href: j.runUrl || j.runsUrl, target: "_blank", rel: "noopener", title: "GitHub Actions の実行ログを開きます(プロジェクトリポジトリへのアクセスが要ります)", text: "実行状況を見る →" })); // その run を特定できていればそれ、無ければ一覧
16549
16492
  }
16550
16493
  // **「反映の状況 →」のリンクは無くなった(#221・2026-08-25)。** 指していたのは
16551
16494
  // 「画面を追いつかせるための fleet-status の実行」で、その起動ごと撤去した
@@ -17131,8 +17074,8 @@ ${calendarAddUrl.toString()}
17131
17074
  var gateAtEl = s.gateAt
17132
17075
  ? el("span", { class: "ds-gate-at", text: rtAgo(s.gateAt) + "に確認" })
17133
17076
  : null;
17134
- // その行を書いた run へ(#560)。overlay が写した 「gateRunUrl」 / 「gateBy」 から決める
17135
- var gateTrail = s.gateAt ? runTrailNodes({ runUrl: s.gateRunUrl || "", by: s.gateBy || "" }, undefined, "gate") : [];
17077
+ // run の無い書き手は出どころの語を添える(#560)。overlay が写した 「gateRunUrl」 / 「gateBy」 から決める
17078
+ var gateTrail = s.gateAt ? runTrailNodes({ runUrl: s.gateRunUrl || "", by: s.gateBy || "" }, "gate") : [];
17136
17079
  var card = el("div", { class: "ds-card" }, [
17137
17080
  el("div", { class: "ds-card-main" }, [
17138
17081
  el("span", { class: "ds-icon", text: meta.icon }),
@@ -18424,7 +18367,7 @@ ${calendarAddUrl.toString()}
18424
18367
  // 起動できたときだけ、実行状況への導線を出す(鍵を入れた直後に効いたか確かめられる)
18425
18368
  if (j.runsUrl) {
18426
18369
  msg.appendChild(document.createTextNode(" "));
18427
- msg.appendChild(el("a", { href: j.runUrl || j.runsUrl, target: "_blank", rel: "noopener", title: RUN_LINK_TITLE, text: "実行状況を見る →" })); // その run を特定できていればそれ(#560)、無ければ一覧
18370
+ msg.appendChild(el("a", { href: j.runUrl || j.runsUrl, target: "_blank", rel: "noopener", title: "GitHub Actions の実行ログを開きます(プロジェクトリポジトリへのアクセスが要ります)", text: "実行状況を見る →" })); // その run を特定できていればそれ、無ければ一覧
18428
18371
  }
18429
18372
  // 入れた値は画面に残さない(肩越しに見えるのを防ぐ)
18430
18373
  input.value = "";
@@ -18928,7 +18871,7 @@ ${calendarAddUrl.toString()}
18928
18871
  // 置いていたため、能力モーダルの上に2枚目が重なり、Escape が下の1枚だけを
18929
18872
  // 閉じて上が残った。isModalOpen も app しか見ないので、開いている間の
18930
18873
  // ライブ差し替えの見送りも効いていなかった
18931
- var mk = makeModal(isT ? "会議の文字起こしを追加" : "共有資料を追加", false);
18874
+ var mk = makeModal("会議の文字起こしを追加", false);
18932
18875
  var overlay = mk.overlay;
18933
18876
  var modal = mk.modal;
18934
18877
  // 🔴 **資料の案内はカードだけでは届かない**(#663・設計レビュー🔴2)。makeModal は
@@ -18949,7 +18892,7 @@ ${calendarAddUrl.toString()}
18949
18892
  var allowedExt = isT
18950
18893
  ? { txt: 1, md: 1, docx: 1, doc: 1, rtf: 1, pdf: 1, vtt: 1, srt: 1 }
18951
18894
  : { pdf: 1, docx: 1, doc: 1, pptx: 1, ppt: 1, xlsx: 1, xls: 1, csv: 1, txt: 1, md: 1, rtf: 1, png: 1, jpg: 1, jpeg: 1, gif: 1, webp: 1, svg: 1, bmp: 1, heic: 1, tiff: 1 };
18952
- var allowedLabel = isT ? "テキスト・Word・PDF・字幕(.vtt/.srt)" : "PDF・Word・PowerPoint・Excel・テキスト・画像";
18895
+ var allowedLabel = "テキスト・Word・PDF・字幕(.vtt/.srt)";
18953
18896
  function extOf(name) { var m = String(name).toLowerCase().match(/\.([a-z0-9]+)$/); return m ? m[1] : ""; }
18954
18897
  var fileInput = el("input", { type: "file", multiple: "multiple", class: "modal-file-hidden" });
18955
18898
  var fileListEl = el("div", { class: "dropzone-files" });
@@ -19266,7 +19209,7 @@ ${calendarAddUrl.toString()}
19266
19209
  status.textContent = "";
19267
19210
  status.appendChild(el("div", { class: "modal-ok", text: allDup
19268
19211
  ? "✓ 送信しました。保存されました(この日付には既に議事録があるため、議事録の自動生成は行われません)"
19269
- : "✓ 送信しました。数分後に" + (isT ? "議事録が自動生成され" : "内容がMarkdownに変換されて") + "反映されます。" }));
19212
+ : "✓ 送信しました。数分後に議事録が自動生成され反映されます。" }));
19270
19213
  var paths = (sub.j && sub.j.committed) || [];
19271
19214
  if (paths.length && sub.j.repo) {
19272
19215
  var br = SITE.branch || "main";
@@ -20400,12 +20343,15 @@ ${calendarAddUrl.toString()}
20400
20343
  reference: { color: "#d1d5db", label: "参照", kind: "evidence" },
20401
20344
  source: { color: "#d1d5db", label: "出典", kind: "evidence" },
20402
20345
  link: { color: "#e2e8f0", label: "リンク", kind: "evidence" },
20346
+ // 話題の線(決定・ルール・関係者 → 用語・engine #551 の about)。名前の一致で機械が付けた手がかりなので、
20347
+ // 根拠の線と同じく細く薄い「ゆるいつながり」として描き、凡例は日本語の 1 行(栓 about_relations の on の条件)
20348
+ about: { color: "#ddd6fe", label: "話題", kind: "topic" },
20403
20349
  };
20404
20350
  var WEAK_RELS = {};
20405
20351
  var REL_COLOR = {};
20406
20352
  Object.keys(REL_META).forEach(function (r) {
20407
20353
  REL_COLOR[r] = REL_META[r].color;
20408
- if (REL_META[r].kind === "evidence") WEAK_RELS[r] = true;
20354
+ if (REL_META[r].kind === "evidence" || REL_META[r].kind === "topic") WEAK_RELS[r] = true;
20409
20355
  });
20410
20356
  var graphState = { showWeak: true, q: "" };
20411
20357
 
@@ -20438,7 +20384,7 @@ ${calendarAddUrl.toString()}
20438
20384
  });
20439
20385
 
20440
20386
  // 線(関係)の色凡例。グラフ内の有無に関わらず全種別を、Goldどうし/根拠の2群で表示する
20441
- [["gold", "線(Goldどうし):"], ["evidence", "線(根拠):"]].forEach(function (pair) {
20387
+ [["gold", "線(Goldどうし):"], ["evidence", "線(根拠):"], ["topic", "線(話題):"]].forEach(function (pair) {
20442
20388
  var kind = pair[0], heading = pair[1];
20443
20389
  var keys = Object.keys(REL_META).filter(function (r) { return REL_META[r].kind === kind; });
20444
20390
  if (!keys.length) return;
@@ -20455,7 +20401,7 @@ ${calendarAddUrl.toString()}
20455
20401
  cb.checked = graphState.showWeak;
20456
20402
  cb.addEventListener("change", function () { graphState.showWeak = cb.checked; renderList(); });
20457
20403
  toggle.appendChild(cb);
20458
- toggle.appendChild(document.createTextNode("ゆるいつながり(参照・本文リンク)も表示"));
20404
+ toggle.appendChild(document.createTextNode("ゆるいつながり(参照・本文リンク・話題)も表示"));
20459
20405
  legend.appendChild(toggle);
20460
20406
  app.appendChild(legend);
20461
20407
 
@@ -20805,6 +20751,7 @@ ${calendarAddUrl.toString()}
20805
20751
  based_on: "rgba(125,165,255,0.85)", derived_from: "rgba(94,234,212,0.75)",
20806
20752
  relates_to: "rgba(148,163,184,0.6)", supersedes: "rgba(248,113,113,0.9)",
20807
20753
  reference: "rgba(180,196,230,0.32)", source: "rgba(180,196,230,0.32)", link: "rgba(180,196,230,0.22)",
20754
+ about: "rgba(196,181,253,0.22)",
20808
20755
  };
20809
20756
  edges.forEach(function (e) {
20810
20757
  // ホバー中: リネージに入った辺だけ、波の順に「order の低い側→高い側」へ伸びながら金色に。
@@ -21112,6 +21059,7 @@ ${calendarAddUrl.toString()}
21112
21059
  var REL_LABEL = {
21113
21060
  based_on: "根拠", derived_from: "由来", relates_to: "関連",
21114
21061
  supersedes: "置換", superseded_by: "置換先", reference: "参照", source: "出典", link: "リンク",
21062
+ about: "話題",
21115
21063
  };
21116
21064
  var explorerState = { folder: null, path: [], q: "", sx: 0, autoRight: false };
21117
21065
  // 索引もグラフ本体と同じ材料(削除済みを除いたもの)から作る。別々に間引くと、
@@ -21124,7 +21072,7 @@ ${calendarAddUrl.toString()}
21124
21072
  (adjDir[e.from] = adjDir[e.from] || []).push({ id: e.to, rel: e.rel, dir: "out" });
21125
21073
  (adjDir[e.to] = adjDir[e.to] || []).push({ id: e.from, rel: e.rel, dir: "in" });
21126
21074
  });
21127
- var REL_ORDER = ["supersedes", "based_on", "derived_from", "relates_to", "source", "reference", "link"];
21075
+ var REL_ORDER = ["supersedes", "based_on", "derived_from", "relates_to", "source", "reference", "link", "about"];
21128
21076
 
21129
21077
  function recordText(rec) {
21130
21078
  if (!rec) return "";
@@ -21346,8 +21294,10 @@ ${calendarAddUrl.toString()}
21346
21294
  path.setAttribute("d", "M " + x1 + " " + y1 + " C " + mx + " " + y1 + ", " + mx + " " + y2 + ", " + x2 + " " + y2);
21347
21295
  path.setAttribute("fill", "none");
21348
21296
  path.setAttribute("stroke", REL_COLOR[rows[j].getAttribute("data-rel")] || "#cbd5e1");
21349
- path.setAttribute("stroke-width", "1.5");
21350
- path.setAttribute("opacity", "0.85");
21297
+ // 話題の線は細く薄く(名前の一致の手がかり・根拠の線より弱い)
21298
+ var weakLine = rows[j].getAttribute("data-rel") === "about";
21299
+ path.setAttribute("stroke-width", weakLine ? "1" : "1.5");
21300
+ path.setAttribute("opacity", weakLine ? "0.5" : "0.85");
21351
21301
  lines.appendChild(path);
21352
21302
  }
21353
21303
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.110",
3
+ "version": "0.12.112",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,