@takagaki/cortex-decisions-viewer 0.12.111 → 0.12.113
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 +313 -89
- package/dist/engagement.js +9 -1
- package/dist/evidence-names.js +100 -0
- package/dist/rederivation.js +31 -6
- package/dist/render.js +18 -11
- package/package.json +1 -1
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
|
-
//
|
|
1689
|
-
|
|
1690
|
-
|
|
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
|
-
*
|
|
1694
|
-
*
|
|
1919
|
+
* 根拠を、畳む鍵(`identifyEvidence`)で 1 つの点にする(#968 tools 1)。同じ課題・資料・会議を、
|
|
1920
|
+
* 課題キー・URL・コメントの URL・`material:`・写しのパスのどの形で指しても同じ点に集まる。
|
|
1921
|
+
* 先にできた点の名前が弱ければ(種類の既定・URL)、強い名前(帳の題名・人が付けたリンクの文字)で上書きし、
|
|
1922
|
+
* 飛び先が無ければ後から来た飛び先を付ける
|
|
1695
1923
|
*/
|
|
1696
|
-
const
|
|
1697
|
-
const
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
return
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
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
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
//
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
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
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
edges.push({ from: fromId, to:
|
|
1787
|
-
|
|
1788
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2524
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/engagement.js
CHANGED
|
@@ -50,7 +50,7 @@ exports.ENGAGEMENT_LEDGER_VERSION = 1;
|
|
|
50
50
|
/** 半減期(日)。engine `weights.mjs` の `HALF_LIFE_DAYS`(正本 6-B「半減期 90 日固定」) */
|
|
51
51
|
exports.HALF_LIFE_DAYS = 90;
|
|
52
52
|
/**
|
|
53
|
-
* 行為 1 件の重み。engine `weights.mjs` の `WEIGHTS` の写し(
|
|
53
|
+
* 行為 1 件の重み。engine `weights.mjs` の `WEIGHTS` の写し(31 値・**表に無い `kind` は 0**)。
|
|
54
54
|
*
|
|
55
55
|
* 🔴 契約の表(fixture)は `message.posted` しか使っていないので、**ここの写し間違いは表では
|
|
56
56
|
* 捕まらない**。重みの番人は `test/member-engagement-parity.test.mjs` の `[重み]` が持つ。
|
|
@@ -90,6 +90,14 @@ 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,
|
|
95
|
+
// リアクションの状態の行(cortex-engine #1046 便 1c)。人の行為ではない(`actor` は unknown・読み手の入口で外す)ので 0
|
|
96
|
+
"reaction.updated": 0,
|
|
97
|
+
// Slack の参加・退出・トピックなどの知らせ(cortex-engine #1046 便 1c)。今までは `message.posted`(1.0)に混ざって関与を水増ししていた。発言ではないので 0
|
|
98
|
+
"member.joined": 0,
|
|
99
|
+
"member.left": 0,
|
|
100
|
+
"venue.updated": 0,
|
|
93
101
|
});
|
|
94
102
|
const DAY_MS = 86400000;
|
|
95
103
|
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
|
+
}
|
package/dist/rederivation.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
@@ -4554,7 +4554,7 @@ const TUT_MEMBER_PROMPT = [
|
|
|
4554
4554
|
].join("\n");
|
|
4555
4555
|
const TUT_BACKLOG_PROMPT = [
|
|
4556
4556
|
"Backlogの課題に、わたしの名義でコメントを1件投稿してください。Backlogの画面を開かずにAIから記票する練習です。",
|
|
4557
|
-
"課題を取れる道具(Backlog のコネクタ)を道具の検索で探してください。同じ働きの道具が2
|
|
4557
|
+
"課題を取れる道具(Backlog のコネクタ)を道具の検索で探してください。同じ働きの道具が2つ以上あれば、Cortex/sources.json の backlog の接続の domain(このファイルがまだ無いプロジェクトは 課題管理/issues-config.json の targets の domain)と同じスペースを読むものを選びます。",
|
|
4558
4558
|
"課題はその道具で一覧して、どの課題に何を書くかをわたしに質問してください(課題管理/<プロジェクトキー>/issues/ に写しがあれば手がかりにしてかまいません)。",
|
|
4559
4559
|
"",
|
|
4560
4560
|
"投稿はその道具で行います(あなた本人の名義)。Backlog上の名義を先に伝えてください。",
|
|
@@ -4575,7 +4575,7 @@ const TUT_DESIGN_PROMPT = [
|
|
|
4575
4575
|
const TUT_SLACK_PROMPT = [
|
|
4576
4576
|
"Cortex のチュートリアルを完了したことを、このプロジェクトの Slack チャンネルに投稿してください。最後のステップです。",
|
|
4577
4577
|
"",
|
|
4578
|
-
"1. チャット/channels.json
|
|
4578
|
+
"1. Cortex/sources.json の slack の接続の channels(このファイルがまだ無いプロジェクトは チャット/channels.json)を読んで投稿先のチャンネルを選ぶ(複数あれば description を見て判断し、迷ったらわたしに聞く)",
|
|
4579
4579
|
"2. 次の雛形で本文を作る({名前} は Cortex/Members/records/ にあるわたしのレコードの表記に合わせる)",
|
|
4580
4580
|
"",
|
|
4581
4581
|
"🎉 {名前} さんが Cortex のチュートリアルを完了しました",
|
|
@@ -18871,7 +18871,7 @@ ${calendarAddUrl.toString()}
|
|
|
18871
18871
|
// 置いていたため、能力モーダルの上に2枚目が重なり、Escape が下の1枚だけを
|
|
18872
18872
|
// 閉じて上が残った。isModalOpen も app しか見ないので、開いている間の
|
|
18873
18873
|
// ライブ差し替えの見送りも効いていなかった
|
|
18874
|
-
var mk = makeModal(
|
|
18874
|
+
var mk = makeModal("会議の文字起こしを追加", false);
|
|
18875
18875
|
var overlay = mk.overlay;
|
|
18876
18876
|
var modal = mk.modal;
|
|
18877
18877
|
// 🔴 **資料の案内はカードだけでは届かない**(#663・設計レビュー🔴2)。makeModal は
|
|
@@ -18892,7 +18892,7 @@ ${calendarAddUrl.toString()}
|
|
|
18892
18892
|
var allowedExt = isT
|
|
18893
18893
|
? { txt: 1, md: 1, docx: 1, doc: 1, rtf: 1, pdf: 1, vtt: 1, srt: 1 }
|
|
18894
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 };
|
|
18895
|
-
var allowedLabel =
|
|
18895
|
+
var allowedLabel = "テキスト・Word・PDF・字幕(.vtt/.srt)";
|
|
18896
18896
|
function extOf(name) { var m = String(name).toLowerCase().match(/\.([a-z0-9]+)$/); return m ? m[1] : ""; }
|
|
18897
18897
|
var fileInput = el("input", { type: "file", multiple: "multiple", class: "modal-file-hidden" });
|
|
18898
18898
|
var fileListEl = el("div", { class: "dropzone-files" });
|
|
@@ -19209,7 +19209,7 @@ ${calendarAddUrl.toString()}
|
|
|
19209
19209
|
status.textContent = "";
|
|
19210
19210
|
status.appendChild(el("div", { class: "modal-ok", text: allDup
|
|
19211
19211
|
? "✓ 送信しました。保存されました(この日付には既に議事録があるため、議事録の自動生成は行われません)"
|
|
19212
|
-
: "✓
|
|
19212
|
+
: "✓ 送信しました。数分後に議事録が自動生成され反映されます。" }));
|
|
19213
19213
|
var paths = (sub.j && sub.j.committed) || [];
|
|
19214
19214
|
if (paths.length && sub.j.repo) {
|
|
19215
19215
|
var br = SITE.branch || "main";
|
|
@@ -20343,12 +20343,15 @@ ${calendarAddUrl.toString()}
|
|
|
20343
20343
|
reference: { color: "#d1d5db", label: "参照", kind: "evidence" },
|
|
20344
20344
|
source: { color: "#d1d5db", label: "出典", kind: "evidence" },
|
|
20345
20345
|
link: { color: "#e2e8f0", label: "リンク", kind: "evidence" },
|
|
20346
|
+
// 話題の線(決定・ルール・関係者 → 用語・engine #551 の about)。名前の一致で機械が付けた手がかりなので、
|
|
20347
|
+
// 根拠の線と同じく細く薄い「ゆるいつながり」として描き、凡例は日本語の 1 行(栓 about_relations の on の条件)
|
|
20348
|
+
about: { color: "#ddd6fe", label: "話題", kind: "topic" },
|
|
20346
20349
|
};
|
|
20347
20350
|
var WEAK_RELS = {};
|
|
20348
20351
|
var REL_COLOR = {};
|
|
20349
20352
|
Object.keys(REL_META).forEach(function (r) {
|
|
20350
20353
|
REL_COLOR[r] = REL_META[r].color;
|
|
20351
|
-
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;
|
|
20352
20355
|
});
|
|
20353
20356
|
var graphState = { showWeak: true, q: "" };
|
|
20354
20357
|
|
|
@@ -20381,7 +20384,7 @@ ${calendarAddUrl.toString()}
|
|
|
20381
20384
|
});
|
|
20382
20385
|
|
|
20383
20386
|
// 線(関係)の色凡例。グラフ内の有無に関わらず全種別を、Goldどうし/根拠の2群で表示する
|
|
20384
|
-
[["gold", "線(Goldどうし):"], ["evidence", "線(根拠):"]].forEach(function (pair) {
|
|
20387
|
+
[["gold", "線(Goldどうし):"], ["evidence", "線(根拠):"], ["topic", "線(話題):"]].forEach(function (pair) {
|
|
20385
20388
|
var kind = pair[0], heading = pair[1];
|
|
20386
20389
|
var keys = Object.keys(REL_META).filter(function (r) { return REL_META[r].kind === kind; });
|
|
20387
20390
|
if (!keys.length) return;
|
|
@@ -20398,7 +20401,7 @@ ${calendarAddUrl.toString()}
|
|
|
20398
20401
|
cb.checked = graphState.showWeak;
|
|
20399
20402
|
cb.addEventListener("change", function () { graphState.showWeak = cb.checked; renderList(); });
|
|
20400
20403
|
toggle.appendChild(cb);
|
|
20401
|
-
toggle.appendChild(document.createTextNode("
|
|
20404
|
+
toggle.appendChild(document.createTextNode("ゆるいつながり(参照・本文リンク・話題)も表示"));
|
|
20402
20405
|
legend.appendChild(toggle);
|
|
20403
20406
|
app.appendChild(legend);
|
|
20404
20407
|
|
|
@@ -20748,6 +20751,7 @@ ${calendarAddUrl.toString()}
|
|
|
20748
20751
|
based_on: "rgba(125,165,255,0.85)", derived_from: "rgba(94,234,212,0.75)",
|
|
20749
20752
|
relates_to: "rgba(148,163,184,0.6)", supersedes: "rgba(248,113,113,0.9)",
|
|
20750
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)",
|
|
20751
20755
|
};
|
|
20752
20756
|
edges.forEach(function (e) {
|
|
20753
20757
|
// ホバー中: リネージに入った辺だけ、波の順に「order の低い側→高い側」へ伸びながら金色に。
|
|
@@ -21055,6 +21059,7 @@ ${calendarAddUrl.toString()}
|
|
|
21055
21059
|
var REL_LABEL = {
|
|
21056
21060
|
based_on: "根拠", derived_from: "由来", relates_to: "関連",
|
|
21057
21061
|
supersedes: "置換", superseded_by: "置換先", reference: "参照", source: "出典", link: "リンク",
|
|
21062
|
+
about: "話題",
|
|
21058
21063
|
};
|
|
21059
21064
|
var explorerState = { folder: null, path: [], q: "", sx: 0, autoRight: false };
|
|
21060
21065
|
// 索引もグラフ本体と同じ材料(削除済みを除いたもの)から作る。別々に間引くと、
|
|
@@ -21067,7 +21072,7 @@ ${calendarAddUrl.toString()}
|
|
|
21067
21072
|
(adjDir[e.from] = adjDir[e.from] || []).push({ id: e.to, rel: e.rel, dir: "out" });
|
|
21068
21073
|
(adjDir[e.to] = adjDir[e.to] || []).push({ id: e.from, rel: e.rel, dir: "in" });
|
|
21069
21074
|
});
|
|
21070
|
-
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"];
|
|
21071
21076
|
|
|
21072
21077
|
function recordText(rec) {
|
|
21073
21078
|
if (!rec) return "";
|
|
@@ -21289,8 +21294,10 @@ ${calendarAddUrl.toString()}
|
|
|
21289
21294
|
path.setAttribute("d", "M " + x1 + " " + y1 + " C " + mx + " " + y1 + ", " + mx + " " + y2 + ", " + x2 + " " + y2);
|
|
21290
21295
|
path.setAttribute("fill", "none");
|
|
21291
21296
|
path.setAttribute("stroke", REL_COLOR[rows[j].getAttribute("data-rel")] || "#cbd5e1");
|
|
21292
|
-
|
|
21293
|
-
|
|
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");
|
|
21294
21301
|
lines.appendChild(path);
|
|
21295
21302
|
}
|
|
21296
21303
|
}
|
package/package.json
CHANGED