@takagaki/cortex-decisions-viewer 0.12.105 → 0.12.107
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 +144 -174
- package/dist/canonical-evidence.js +196 -0
- package/dist/engagement.js +8 -3
- package/dist/render.js +185 -21
- package/package.json +1 -1
package/dist/build.js
CHANGED
|
@@ -60,6 +60,7 @@ const path = __importStar(require("node:path"));
|
|
|
60
60
|
const gray_matter_1 = __importDefault(require("gray-matter"));
|
|
61
61
|
const marked_1 = require("marked");
|
|
62
62
|
const sanitize_html_1 = __importDefault(require("sanitize-html"));
|
|
63
|
+
const canonical_evidence_1 = require("./canonical-evidence");
|
|
63
64
|
const repo_1 = require("./repo");
|
|
64
65
|
const engagement_1 = require("./engagement");
|
|
65
66
|
const story_1 = require("./story");
|
|
@@ -1577,135 +1578,73 @@ function extractBodyLinks(content) {
|
|
|
1577
1578
|
}
|
|
1578
1579
|
return [...out];
|
|
1579
1580
|
}
|
|
1580
|
-
/** Backlogの課題URL(…/view/KEY-123 や #comment付き)なら課題キーに正規化する */
|
|
1581
|
-
function backlogViewKey(url) {
|
|
1582
|
-
const m = url.match(/\/view\/([A-Z][A-Z0-9_]*-\d+)(?:[#?]|$)/);
|
|
1583
|
-
return m ? m[1] : null;
|
|
1584
|
-
}
|
|
1585
|
-
const SCAN_SKIP_DIRS = new Set(["tmp", "node_modules", "templates", "resources", "src", "Decisions"]);
|
|
1586
|
-
const SCAN_SKIP_FILES = new Set(["readme.md", "template.md", "link.md"]);
|
|
1587
1581
|
/**
|
|
1588
|
-
*
|
|
1589
|
-
*
|
|
1590
|
-
*
|
|
1591
|
-
*
|
|
1592
|
-
*
|
|
1593
|
-
*
|
|
1582
|
+
* 根拠の URL の種類(`canonicalEvidence` の `kind`)→ グラフの層(#968 tools 1)。
|
|
1583
|
+
*
|
|
1584
|
+
* 🔴 **全部 `external` に潰さない。** Backlog の課題キーは今までどおり `bronze`、Google
|
|
1585
|
+
* ドキュメント/ドライブ・Figma・Slack・Google Chat は資料・デザイン・チャットの層として
|
|
1586
|
+
* `silver` に寄せる——根拠が同期ミラーの ID から元ツールの URL に書き換わっていっても
|
|
1587
|
+
* (engine #968 便 3)、課題・資料・会議の層の色が消えないようにするため。`bronze`/`silver`/
|
|
1588
|
+
* `external` は今は同じ色・同じ凡例行(`GROUP_STYLE`)なので、見た目は変わらない
|
|
1594
1589
|
*/
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
console.warn(` ⚠ frontmatterのパースに失敗したため、グラフ走査からスキップ: ${rel}` +
|
|
1634
|
-
` (${e instanceof Error ? e.message.split("\n")[0] : String(e)})`);
|
|
1635
|
-
continue;
|
|
1636
|
-
}
|
|
1637
|
-
// ノードの素性: frontmatterのID → 課題キー → リポジトリ相対パス の順で決める
|
|
1638
|
-
let id = rel;
|
|
1639
|
-
let label = path.basename(rel);
|
|
1640
|
-
let group = "bronze";
|
|
1641
|
-
let url = blobUrl;
|
|
1642
|
-
const edges = [];
|
|
1643
|
-
let thumb;
|
|
1644
|
-
if (data != null && data.id != null && data.type != null) {
|
|
1645
|
-
id = String(data.id);
|
|
1646
|
-
label = data.title != null ? String(data.title) : id;
|
|
1647
|
-
group = data.type === "issue" ? "bronze" : "silver";
|
|
1648
|
-
if (data.thumbnail != null)
|
|
1649
|
-
thumb = String(data.thumbnail);
|
|
1650
|
-
const source = data.source != null ? String(data.source).trim() : "";
|
|
1651
|
-
if (source) {
|
|
1652
|
-
if (/^https?:\/\//.test(source)) {
|
|
1653
|
-
if (data.type === "material" || data.type === "design")
|
|
1654
|
-
url = source; // 正本(Drive・Figma等)に飛べるようにする
|
|
1655
|
-
edges.push({ target: source, rel: "source" });
|
|
1656
|
-
}
|
|
1657
|
-
else {
|
|
1658
|
-
// 同ディレクトリの文字起こし等への相対参照は、リポジトリ相対パスに解決して繋ぐ
|
|
1659
|
-
const cand = path.join(path.dirname(file), source);
|
|
1660
|
-
try {
|
|
1661
|
-
await node_fs_1.promises.stat(cand);
|
|
1662
|
-
edges.push({ target: path.relative(repoRoot, cand).split(path.sep).join("/"), rel: "source" });
|
|
1663
|
-
}
|
|
1664
|
-
catch {
|
|
1665
|
-
edges.push({ target: source, rel: "source" });
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
|
-
}
|
|
1669
|
-
for (const r of toRelations(data.relations))
|
|
1670
|
-
if (!exports.MIRROR_RELS.has(r.rel))
|
|
1671
|
-
edges.push({ target: r.target, rel: r.rel });
|
|
1672
|
-
}
|
|
1673
|
-
else {
|
|
1674
|
-
const keyMatch = raw.slice(0, 4000).match(/^- 課題キー: (\S+)$/m);
|
|
1675
|
-
const h1 = content.match(/^#\s+(.+)$/m);
|
|
1676
|
-
if (keyMatch) {
|
|
1677
|
-
id = keyMatch[1];
|
|
1678
|
-
label = h1 ? `${id} ${h1[1].trim()}`.slice(0, 60) : id;
|
|
1679
|
-
const urlMatch = raw.match(/\[Backlog Issue Link\]\((https?:[^)]+)\)/);
|
|
1680
|
-
if (urlMatch)
|
|
1681
|
-
url = urlMatch[1];
|
|
1682
|
-
}
|
|
1683
|
-
else {
|
|
1684
|
-
if (h1)
|
|
1685
|
-
label = h1[1].trim().slice(0, 60);
|
|
1686
|
-
}
|
|
1687
|
-
}
|
|
1688
|
-
for (const link of extractBodyLinks(content)) {
|
|
1689
|
-
if (url && link.split("#")[0] === url.split("#")[0])
|
|
1690
|
-
continue; // 自分自身の正本リンクは張らない
|
|
1691
|
-
const key = backlogViewKey(link);
|
|
1692
|
-
edges.push({ target: key ?? link, rel: "link" });
|
|
1693
|
-
}
|
|
1694
|
-
const filtered = edges.filter((e) => e.target !== id);
|
|
1695
|
-
if (filtered.length === 0)
|
|
1696
|
-
continue; // どこにも繋がらないファイルはノイズになるため載せない
|
|
1697
|
-
entries.push({ id, label, group, url, thumb, edges: filtered });
|
|
1698
|
-
}
|
|
1590
|
+
const EVIDENCE_GROUP = {
|
|
1591
|
+
backlog: "bronze",
|
|
1592
|
+
gdoc: "silver",
|
|
1593
|
+
drive: "silver",
|
|
1594
|
+
"drive-folder": "silver",
|
|
1595
|
+
figma: "silver",
|
|
1596
|
+
slack: "silver",
|
|
1597
|
+
gchat: "silver",
|
|
1598
|
+
};
|
|
1599
|
+
/** 元ツールが無い根拠(engine #968 設計v3・将来の語彙)。リンクにせず、機械の語(urn:…)を画面に出さない */
|
|
1600
|
+
const NO_ORIGINAL_TOOL_PREFIX = "urn:cortex:obs:";
|
|
1601
|
+
const NO_ORIGINAL_TOOL_LABEL = "元のツールが無い根拠";
|
|
1602
|
+
/**
|
|
1603
|
+
* `canonicalEvidence` の `kind` → 画面に出す読める名前(題名が引けない種類の既定ラベル)。
|
|
1604
|
+
*
|
|
1605
|
+
* 🔴 **`key` は常に畳んだ後の鍵(`canon.parentKey ?? canon.key`)。** コメント・返信の
|
|
1606
|
+
* `#comment-N` / `#issuecomment-N` は呼び出し側で既に外れているので、ここで二重に取り除かない
|
|
1607
|
+
* ——GitHub の `#12`(Issue 番号そのもの)まで一緒に削ってしまう事故になる
|
|
1608
|
+
*/
|
|
1609
|
+
function readableEvidenceLabel(kind, key, fallback) {
|
|
1610
|
+
switch (kind) {
|
|
1611
|
+
case "backlog":
|
|
1612
|
+
return key.toUpperCase();
|
|
1613
|
+
case "github":
|
|
1614
|
+
return key.replace(/^gh:/, "");
|
|
1615
|
+
case "gdoc":
|
|
1616
|
+
return "Google ドキュメント";
|
|
1617
|
+
case "drive":
|
|
1618
|
+
case "drive-folder":
|
|
1619
|
+
return "Google ドライブの資料";
|
|
1620
|
+
case "figma":
|
|
1621
|
+
return "Figma のデザイン";
|
|
1622
|
+
case "slack":
|
|
1623
|
+
return "Slack のメッセージ";
|
|
1624
|
+
case "gchat":
|
|
1625
|
+
return "Google Chat のメッセージ";
|
|
1626
|
+
default:
|
|
1627
|
+
return fallback;
|
|
1699
1628
|
}
|
|
1700
|
-
return entries;
|
|
1701
1629
|
}
|
|
1702
1630
|
/**
|
|
1703
|
-
* 全レコードのfrontmatter(relations / source / references
|
|
1704
|
-
*
|
|
1705
|
-
*
|
|
1631
|
+
* 全レコードのfrontmatter(relations / source / references)と本文リンクから関係グラフを構築する(engine #551・分類A 10/1)。
|
|
1632
|
+
*
|
|
1633
|
+
* 🔴 **同期ミラー(会議/・課題管理/・共有資料/・デザイン/等)のファイルを走査して点にしない。**
|
|
1634
|
+
* 点にするのは Gold(決定・用語・ルール・関係者・Home)と、Gold の relations/source/references/
|
|
1635
|
+
* 本文リンクが指す先だけ。かつてはリポジトリ内の全mdを走査して無関係なファイルまで点にしていたが
|
|
1636
|
+
* (旧 `scanContent`)、Gold と繋がらない点・GitHubの写し(blob)へ飛ぶだけの点が大半を占め、
|
|
1637
|
+
* 「つながり画面」の名が指すつながりそのもの(Goldと根拠)を見えなくしていた。
|
|
1638
|
+
*
|
|
1639
|
+
* 参照先がリポジトリ内のレコードでないIDは、層を推定したノードとして表示する
|
|
1706
1640
|
* (minute:/material:→Silver、課題キー・外部URL→Bronze、Goldの未解決ID→phantom)。
|
|
1641
|
+
* **根拠の点の飛び先は元ツールのURLだけ**(Backlog・Slack・Drive・Figma・GitHub等)。根拠がまだ
|
|
1642
|
+
* 同期ミラーのパスのまま(元ツールのURLに書き換わっていない)記録は、点にするが飛び先を付けない
|
|
1643
|
+
* (分類B。URLへの書き換えはengine #968で別便が設計中)——押せそうで404のリンクより、押せない
|
|
1644
|
+
* ほうがよい(`createRefResolver` と同じ考え方)。崩れた文字(宛先に見えない参照)も同様に、
|
|
1645
|
+
* 点は作るが飛び先は付けない。
|
|
1707
1646
|
*/
|
|
1708
|
-
function buildGraph(decisions, home, sections,
|
|
1647
|
+
function buildGraph(decisions, home, sections, targetUrls, repoBaseUrl) {
|
|
1709
1648
|
const nodes = new Map();
|
|
1710
1649
|
const edges = [];
|
|
1711
1650
|
const addNode = (node) => {
|
|
@@ -1734,6 +1673,46 @@ function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl,
|
|
|
1734
1673
|
for (const sec of sections)
|
|
1735
1674
|
for (const r of sec.records)
|
|
1736
1675
|
fileNameToId.set(r.fileName, r.id);
|
|
1676
|
+
// 🔴 **GitHubの写し(blob)へ飛ぶ点は作らない**(2026-10-01 オーナー決定・分類B)。
|
|
1677
|
+
// `targetUrls`(minute:/material:)や詳細画面と共用の解決器(RefResolver)は、元ツールの
|
|
1678
|
+
// URLが無いとき自分のリポジトリの blob URL に倒すことがある(詳細画面ではそれでよい設計)。
|
|
1679
|
+
// グラフの点としては、それは元ツールのURLではないので飛び先にしない——点は作るが、
|
|
1680
|
+
// 元ツールのURLに書き換わる(engine #968)まで押せなくする。
|
|
1681
|
+
//
|
|
1682
|
+
// 🔴 **`minute:` も例外にしない。** `buildTargetUrlMap` は議事録の正本URLを持たず、
|
|
1683
|
+
// `minute:{定例名}:{日付}` の飛び先はいつも自分の blob URL(詳細画面ではそれを「正本」として
|
|
1684
|
+
// 扱う設計・1バイトも変えない)。だがグラフとしては、まさにその「GitHubの写しへ飛ぶ点」
|
|
1685
|
+
// (leaderの実測で77件)を無くすのが今回の決定の本題なので、ここも揃えて飛び先を外す
|
|
1686
|
+
// (`graph-gold-only.test.mjs` の「議事録を根拠にした決定」で固定)。
|
|
1687
|
+
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();
|
|
1692
|
+
/**
|
|
1693
|
+
* 根拠の URL を、`canonicalEvidence` の鍵で重複を畳んで点にする(#968 tools 1)。
|
|
1694
|
+
* 解けない(対応表でしか引けない・URLが2本以上混ざる等)URL は、今までどおり生のURLをIDにする。
|
|
1695
|
+
*/
|
|
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;
|
|
1715
|
+
};
|
|
1737
1716
|
// 参照先IDから層を推定してノード化する
|
|
1738
1717
|
const classifyTarget = (target) => {
|
|
1739
1718
|
if (nodes.has(target))
|
|
@@ -1745,37 +1724,42 @@ function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl,
|
|
|
1745
1724
|
return resolved;
|
|
1746
1725
|
let group = "phantom";
|
|
1747
1726
|
let label = target;
|
|
1748
|
-
if (target.startsWith(
|
|
1727
|
+
if (target.startsWith(NO_ORIGINAL_TOOL_PREFIX)) {
|
|
1728
|
+
// 元ツールが無い根拠(engine #968 の将来の語彙)。リンクにせず、機械の語(urn:…)を出さない
|
|
1729
|
+
return addNode({ id: target, label: NO_ORIGINAL_TOOL_LABEL, group: "phantom" });
|
|
1730
|
+
}
|
|
1731
|
+
else if (target.startsWith("minute:") ||
|
|
1749
1732
|
target.startsWith("material:") ||
|
|
1750
1733
|
target.startsWith("design:") ||
|
|
1751
1734
|
target.startsWith("figma-comment:")) {
|
|
1752
1735
|
group = "silver";
|
|
1753
1736
|
// 表に無い design: / figma-comment: はキーから組む(詳細画面の resolve と同じ順)
|
|
1754
|
-
const url = targetUrls.get(target) ?? figmaUrlFromRef(target);
|
|
1737
|
+
const url = dropMirrorBlob(targetUrls.get(target) ?? figmaUrlFromRef(target));
|
|
1755
1738
|
if (url)
|
|
1756
1739
|
return addNode({ id: target, label, group, url });
|
|
1757
1740
|
}
|
|
1758
1741
|
else if (/^[A-Z][A-Z0-9_]*-\d+$/.test(target)) {
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
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;
|
|
1763
1753
|
}
|
|
1764
1754
|
else if (/^https?:\/\//.test(target)) {
|
|
1765
|
-
|
|
1766
|
-
label = target.replace(/^https?:\/\//, "").slice(0, 60);
|
|
1767
|
-
return addNode({ id: target, label, group, url: target });
|
|
1755
|
+
return addEvidenceUrlNode(target, target.replace(/^https?:\/\//, "").slice(0, 60));
|
|
1768
1756
|
}
|
|
1769
1757
|
else if (target.includes("/") || target.toLowerCase().endsWith(".md")) {
|
|
1770
|
-
|
|
1758
|
+
// リポジトリ内ファイルへの生のパス参照(同期ミラーの議事録・資料など)。
|
|
1759
|
+
// 元ツールのURLがまだ無い記録は、点にするが飛び先を付けない(#968 の書き換えを待つ)
|
|
1760
|
+
group = "silver";
|
|
1771
1761
|
label = target.split("/").pop() ?? target;
|
|
1772
|
-
|
|
1773
|
-
// ここだけ無条件に blob URL を作っていたため、同じ参照が「詳細ではテキスト・
|
|
1774
|
-
// グラフでは404リンク」に分かれていた(グラフのノードは window.open される)。
|
|
1775
|
-
const blobUrl = repoBaseUrl && refs.exists(target)
|
|
1776
|
-
? `${repoBaseUrl}/blob/${branch}/${encodePath(target)}`
|
|
1777
|
-
: undefined;
|
|
1778
|
-
return addNode({ id: target, label, group, url: blobUrl });
|
|
1762
|
+
return addNode({ id: target, label, group });
|
|
1779
1763
|
}
|
|
1780
1764
|
return addNode({ id: target, label, group });
|
|
1781
1765
|
};
|
|
@@ -1794,12 +1778,11 @@ function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl,
|
|
|
1794
1778
|
const addReferenceEdges = (fromId, references) => {
|
|
1795
1779
|
for (const ref of references) {
|
|
1796
1780
|
if (ref.url) {
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
});
|
|
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);
|
|
1803
1786
|
edges.push({ from: fromId, to: id, rel: "reference" });
|
|
1804
1787
|
}
|
|
1805
1788
|
else {
|
|
@@ -1809,25 +1792,9 @@ function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl,
|
|
|
1809
1792
|
};
|
|
1810
1793
|
const addLinkEdges = (fromId, links) => {
|
|
1811
1794
|
for (const link of links) {
|
|
1812
|
-
|
|
1813
|
-
edges.push({ from: fromId, to: classifyTarget(key ?? link), rel: "link" });
|
|
1795
|
+
edges.push({ from: fromId, to: classifyTarget(link), rel: "link" });
|
|
1814
1796
|
}
|
|
1815
1797
|
};
|
|
1816
|
-
// Bronze/Silverのノードを先に登録する(Gold・Bronze/Silver双方からの参照が同一ノードに解決されるように)
|
|
1817
|
-
for (const c of content) {
|
|
1818
|
-
const existing = nodes.get(c.id);
|
|
1819
|
-
if (!existing) {
|
|
1820
|
-
addNode({ id: c.id, label: c.label, group: c.group, url: c.url ?? targetUrls.get(c.id), thumb: c.thumb });
|
|
1821
|
-
}
|
|
1822
|
-
else {
|
|
1823
|
-
if (!existing.url && c.url)
|
|
1824
|
-
existing.url = c.url;
|
|
1825
|
-
if (existing.group === "phantom") {
|
|
1826
|
-
existing.group = c.group;
|
|
1827
|
-
existing.label = c.label;
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1830
|
-
}
|
|
1831
1798
|
for (const d of decisions) {
|
|
1832
1799
|
addRelationEdges(d.id, d.relations);
|
|
1833
1800
|
addReferenceEdges(d.id, d.references);
|
|
@@ -1845,11 +1812,6 @@ function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl,
|
|
|
1845
1812
|
addLinkEdges(r.id, r.bodyLinks);
|
|
1846
1813
|
}
|
|
1847
1814
|
}
|
|
1848
|
-
for (const c of content) {
|
|
1849
|
-
for (const e of c.edges) {
|
|
1850
|
-
edges.push({ from: c.id, to: classifyTarget(e.target), rel: e.rel });
|
|
1851
|
-
}
|
|
1852
|
-
}
|
|
1853
1815
|
// 自己ループと重複辺を除去する(同じURLが複数回貼られていても辺は1本)
|
|
1854
1816
|
const seen = new Set();
|
|
1855
1817
|
const deduped = edges.filter((e) => {
|
|
@@ -2348,8 +2310,7 @@ async function build(opts) {
|
|
|
2348
2310
|
console.warn(`gold-proposals.json: レコードに突き合わない提案が ${unmatched.length} 件ありました(表示しません): ` +
|
|
2349
2311
|
unmatched.slice(0, 5).join(", ") + (unmatched.length > 5 ? " ほか" : ""));
|
|
2350
2312
|
}
|
|
2351
|
-
const
|
|
2352
|
-
const graph = buildGraph(decisions, home, graphSections, contentEntries, targetUrls, repo.baseUrl, repo.branch, refs);
|
|
2313
|
+
const graph = buildGraph(decisions, home, graphSections, targetUrls, repo.baseUrl);
|
|
2353
2314
|
computeLayout(graph); // 各ノードに x/y を書き込む(クライアントはこれを初期位置に使いウォームアップ省略)
|
|
2354
2315
|
// データソースと自動化(fleet-status.json 由来)。無ければ undefined でセクション非表示。
|
|
2355
2316
|
const fleet = await readFleetSources(repoRoot);
|
|
@@ -2536,9 +2497,10 @@ async function parseDecision(raw, fileName, repoRelDir, repoBaseUrl, branch, ref
|
|
|
2536
2497
|
}
|
|
2537
2498
|
function createRefResolver(opts) {
|
|
2538
2499
|
const { repoRoot, targetUrls, repoBaseUrl, branch } = opts;
|
|
2539
|
-
//
|
|
2540
|
-
//
|
|
2541
|
-
//
|
|
2500
|
+
// **他の走査結果を流用しない。** 全md一覧(Decisions走査や、かつてのグラフ走査 `scanContent`)は
|
|
2501
|
+
// `.md` しか歩かず一部のディレクトリを除外するので、実在する `.txt`(Geminiメモ)や
|
|
2502
|
+
// `tmp/` 配下のファイルを「実在しない」と誤判定して**いま動いているリンクを消す**。
|
|
2503
|
+
// 参照は1リポジトリあたり数百件なので stat で十分。
|
|
2542
2504
|
const seen = new Map();
|
|
2543
2505
|
const exists = (relPath) => {
|
|
2544
2506
|
const hit = seen.get(relPath);
|
|
@@ -2562,6 +2524,9 @@ function createRefResolver(opts) {
|
|
|
2562
2524
|
const trimmed = String(ref ?? "").trim();
|
|
2563
2525
|
if (!trimmed)
|
|
2564
2526
|
return { label: trimmed, url: null };
|
|
2527
|
+
// 元ツールが無い根拠(engine #968 の将来の語彙)。リンクにせず、機械の語(urn:…)を出さない
|
|
2528
|
+
if (trimmed.startsWith(NO_ORIGINAL_TOOL_PREFIX))
|
|
2529
|
+
return { label: NO_ORIGINAL_TOOL_LABEL, url: null };
|
|
2565
2530
|
const mdLink = trimmed.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
|
|
2566
2531
|
if (mdLink) {
|
|
2567
2532
|
// 🔴 http(s) 以外(`javascript:` `data:` 等)はリンクにしない。ラベルだけ残す。
|
|
@@ -2572,8 +2537,13 @@ function createRefResolver(opts) {
|
|
|
2572
2537
|
return { label: mdLink[1], url: null };
|
|
2573
2538
|
return { label: mdLink[1], url: mdLink[2] };
|
|
2574
2539
|
}
|
|
2575
|
-
if (/^https?:\/\//.test(trimmed))
|
|
2576
|
-
|
|
2540
|
+
if (/^https?:\/\//.test(trimmed)) {
|
|
2541
|
+
// 裸のURL(markdownリンクでラベルを添えていないもの)は、分かる種類なら読める名前にする
|
|
2542
|
+
// (#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 };
|
|
2546
|
+
}
|
|
2577
2547
|
// 正本のURL(課題キー・material:・design:・minute:)。**ここが「ツールへ飛ぶ」の本体**
|
|
2578
2548
|
const known = targetUrls.get(trimmed);
|
|
2579
2549
|
if (known)
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* **根拠の名前の正規化**(cortex-engine #968 設計 v3 §1.4)。
|
|
4
|
+
*
|
|
5
|
+
* 正本は cortex-engine `scripts/gold/ref-tokens.mjs#canonicalEvidence`。ここはその TypeScript の写し
|
|
6
|
+
* (ビューアは engine のコードを import できないので、ロジックごと複製する)。
|
|
7
|
+
*
|
|
8
|
+
* 🔴 **写しを直したら、`test/fixtures/evidence-canonical-cases.json`(両リポに同じバイトで置く)で
|
|
9
|
+
* 両方の実装が同じ答えを返すことを確かめる。** 手で見比べて直すと、どちらかの枝だけ直って
|
|
10
|
+
* 静かに食い違う。
|
|
11
|
+
*
|
|
12
|
+
* 比べるのは `key`、画面がリンク先として使うのは `url`。課題と GitHub の `key` は engine の
|
|
13
|
+
* 既存の印(`refTokens`)と同じ綴り(小文字の課題キー・`gh:o/r#N`)——ビューアの他の場所
|
|
14
|
+
* (課題キーをそのまま ID にする `classifyTarget` の旧経路)と当たり方をそろえるため。
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.githubRefToken = void 0;
|
|
18
|
+
exports.syntaxNormalizedUrl = syntaxNormalizedUrl;
|
|
19
|
+
exports.canonicalEvidence = canonicalEvidence;
|
|
20
|
+
/** 課題キーだけの値(`PJ_CORTEX-12`)。engine の `ISSUE_KEY_RE` と同じ形の全体一致 */
|
|
21
|
+
const WHOLE_ISSUE_KEY_RE = /^([A-Z][A-Z0-9_]{1,20})-(\d+)$/;
|
|
22
|
+
/** `owner/repo#N` だけの値 */
|
|
23
|
+
const WHOLE_GH_SHORT_RE = /^([A-Za-z0-9._-]+)\/([A-Za-z0-9._-]+)#(\d+)$/;
|
|
24
|
+
/** スキーム・ホスト・残り(パス・クエリ・断片) */
|
|
25
|
+
const URL_PARTS_RE = /^(https?):\/\/([^/?#\s]+)([^\s]*)$/i;
|
|
26
|
+
/** GitHub の issue / PR を指す 2 つの綴りを同じ印にする(engine `githubRefToken` と同じ) */
|
|
27
|
+
const githubRefToken = (owner, repo, num) => `gh:${String(owner).toLowerCase()}/${String(repo).toLowerCase()}#${num}`;
|
|
28
|
+
exports.githubRefToken = githubRefToken;
|
|
29
|
+
/**
|
|
30
|
+
* URL の構文の正規化(RFC 3986 §6.2.2)。スキームとホストを小文字・既定のポートを落とす・`%xx` を大文字・
|
|
31
|
+
* 末尾の句読点と末尾の `/` を落とす。日本語はエンコードしない。URL でなければ `null`。
|
|
32
|
+
*/
|
|
33
|
+
function syntaxNormalizedUrl(raw) {
|
|
34
|
+
let t = String(raw ?? "").trim();
|
|
35
|
+
for (let prev = ""; prev !== t;) {
|
|
36
|
+
prev = t;
|
|
37
|
+
t = t.replace(/[).,;:、。]+$/, "").replace(/\/+$/, "");
|
|
38
|
+
}
|
|
39
|
+
const m = t.match(URL_PARTS_RE);
|
|
40
|
+
if (!m)
|
|
41
|
+
return null;
|
|
42
|
+
const scheme = m[1].toLowerCase();
|
|
43
|
+
let host = m[2].toLowerCase();
|
|
44
|
+
if ((scheme === "http" && host.endsWith(":80")) || (scheme === "https" && host.endsWith(":443"))) {
|
|
45
|
+
host = host.replace(/:\d+$/, "");
|
|
46
|
+
}
|
|
47
|
+
const rest = m[3].replace(/%[0-9a-f]{2}/gi, (x) => x.toUpperCase()).replace(/\/+(?=$|[?#])/, "");
|
|
48
|
+
return `${scheme}://${host}${rest}`;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 値全体が URL 1 本の形のときだけ、その URL を返す(素の URL・`[ラベル](URL)`・Slack の `<URL|ラベル>`)。
|
|
52
|
+
* パスに URL を添えた値・文の中に URL がある値は `null`。Markdown のエスケープ(`PJ\_CORTEX`)は外す
|
|
53
|
+
*/
|
|
54
|
+
function wholeUrlOf(value) {
|
|
55
|
+
const v = value.trim();
|
|
56
|
+
const m = v.match(/^(https?:\/\/\S+)$/i) ||
|
|
57
|
+
v.match(/^\[[^\]\n]*\]\((https?:\/\/[^)\s]+)\)$/i) ||
|
|
58
|
+
v.match(/^<(https?:\/\/[^|>\s]+)(?:\|[^>]*)?>$/i);
|
|
59
|
+
return m ? m[1].replace(/\\_/g, "_") : null;
|
|
60
|
+
}
|
|
61
|
+
function toMap(v) {
|
|
62
|
+
if (v instanceof Map)
|
|
63
|
+
return v;
|
|
64
|
+
return new Map(Object.entries(v ?? {}));
|
|
65
|
+
}
|
|
66
|
+
const queryOf = (rest) => {
|
|
67
|
+
const q = rest.indexOf("?");
|
|
68
|
+
if (q < 0)
|
|
69
|
+
return new URLSearchParams();
|
|
70
|
+
const h = rest.indexOf("#", q);
|
|
71
|
+
return new URLSearchParams(rest.slice(q + 1, h < 0 ? undefined : h));
|
|
72
|
+
};
|
|
73
|
+
const pathOf = (rest) => rest.replace(/[?#].*$/, "");
|
|
74
|
+
const fragmentOf = (rest) => {
|
|
75
|
+
const h = rest.indexOf("#");
|
|
76
|
+
return h < 0 ? "" : rest.slice(h + 1);
|
|
77
|
+
};
|
|
78
|
+
const GDOC_KINDS = new Set(["document", "spreadsheets", "presentation", "forms"]);
|
|
79
|
+
const BACKLOG_HOST_RE = /\.(?:backlog\.jp|backlog\.com|backlogtool\.com)$/;
|
|
80
|
+
/** 種類ごとの解き方(URL を構文正規化した後)。当たらなければ汎用の `url` 種に落ちる */
|
|
81
|
+
function canonicalOfUrl(url, ctx) {
|
|
82
|
+
const m = url.match(URL_PARTS_RE);
|
|
83
|
+
const host = m[2];
|
|
84
|
+
const rest = m[3];
|
|
85
|
+
const p = pathOf(rest);
|
|
86
|
+
const q = queryOf(rest);
|
|
87
|
+
if (host === "docs.google.com") {
|
|
88
|
+
const g = p.match(/^\/([a-z]+)\/(?:u\/\d+\/)?d\/([A-Za-z0-9_-]{10,})/);
|
|
89
|
+
if (g && GDOC_KINDS.has(g[1])) {
|
|
90
|
+
return { kind: "gdoc", key: `gfile:${g[2]}`, url: `https://docs.google.com/${g[1]}/d/${g[2]}` };
|
|
91
|
+
}
|
|
92
|
+
const fd = p.match(/^\/file\/d\/([A-Za-z0-9_-]{10,})/);
|
|
93
|
+
if (fd)
|
|
94
|
+
return { kind: "drive", key: `gfile:${fd[1]}`, url: `https://drive.google.com/file/d/${fd[1]}/view` };
|
|
95
|
+
}
|
|
96
|
+
if (host === "drive.google.com") {
|
|
97
|
+
const f = p.match(/^\/file\/(?:u\/\d+\/)?d\/([A-Za-z0-9_-]{10,})/);
|
|
98
|
+
const id = f ? f[1] : /^\/(?:open|uc)$/.test(p) && /^[A-Za-z0-9_-]{10,}$/.test(q.get("id") || "") ? q.get("id") : "";
|
|
99
|
+
if (id)
|
|
100
|
+
return { kind: "drive", key: `gfile:${id}`, url: `https://drive.google.com/file/d/${id}/view` };
|
|
101
|
+
const d = p.match(/^\/drive\/(?:u\/\d+\/)?folders\/([A-Za-z0-9_-]{10,})/);
|
|
102
|
+
if (d)
|
|
103
|
+
return { kind: "drive-folder", key: `gfolder:${d[1]}`, url: `https://drive.google.com/drive/folders/${d[1]}` };
|
|
104
|
+
}
|
|
105
|
+
if (host.endsWith(".slack.com")) {
|
|
106
|
+
const s = p.match(/^\/archives\/([A-Z0-9]+)(?:\/p(\d+))?$/);
|
|
107
|
+
if (s) {
|
|
108
|
+
const hosts = toMap(ctx.slackHosts);
|
|
109
|
+
const h = String(hosts.get(s[1]) || "").trim().toLowerCase() || host;
|
|
110
|
+
const thread = s[2] && /^\d+\.\d+$/.test(q.get("thread_ts") || "") ? q.get("thread_ts") : "";
|
|
111
|
+
const key = `slack:${s[1]}${s[2] ? `:p${s[2]}` : ""}`;
|
|
112
|
+
const tail = `${s[2] ? `/p${s[2]}` : ""}${thread ? `?thread_ts=${thread}` : ""}`;
|
|
113
|
+
return { kind: "slack", key, url: `https://${h}/archives/${s[1]}${tail}` };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (host === "chat.google.com" || host === "mail.google.com") {
|
|
117
|
+
const room = host === "chat.google.com" ? p.match(/^\/(?:u\/\d+\/)?room\/([A-Za-z0-9_-]+)/) : null;
|
|
118
|
+
const inMail = host === "mail.google.com" && /^\/chat(?:\/|$)/.test(p) ? fragmentOf(rest).match(/^chat\/space\/([A-Za-z0-9_-]+)/) : null;
|
|
119
|
+
const space = (room && room[1]) || (inMail && inMail[1]);
|
|
120
|
+
if (space)
|
|
121
|
+
return { kind: "gchat", key: `gchat:${space}`, url: `https://chat.google.com/room/${space}` };
|
|
122
|
+
}
|
|
123
|
+
if (BACKLOG_HOST_RE.test(host)) {
|
|
124
|
+
const v = p.match(/^\/view\/([A-Z][A-Z0-9_]{1,20}-\d+)$/);
|
|
125
|
+
if (v) {
|
|
126
|
+
const c = fragmentOf(rest).match(/^comment-(\d+)$/);
|
|
127
|
+
const issue = v[1].toLowerCase();
|
|
128
|
+
return {
|
|
129
|
+
kind: "backlog",
|
|
130
|
+
key: c ? `${issue}#comment-${c[1]}` : issue,
|
|
131
|
+
url: `https://${host}/view/${v[1]}${c ? `#comment-${c[1]}` : ""}`,
|
|
132
|
+
...(c ? { parentKey: issue } : {}),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const w = p.match(/^\/alias\/wiki\/(\d+)$/);
|
|
136
|
+
if (w)
|
|
137
|
+
return { kind: "backlog", key: `backlog:${host}:wiki:${w[1]}`, url: `https://${host}/alias/wiki/${w[1]}` };
|
|
138
|
+
const d = p.match(/^\/document\/([^/]+)\/([0-9a-f]{32})$/i);
|
|
139
|
+
if (d)
|
|
140
|
+
return { kind: "backlog", key: `backlog:${host}:doc:${d[2].toLowerCase()}`, url: `https://${host}/document/${d[1]}/${d[2].toLowerCase()}` };
|
|
141
|
+
}
|
|
142
|
+
if (host === "figma.com" || host === "www.figma.com") {
|
|
143
|
+
const f = p.match(/^\/(design|file|proto|board)\/([A-Za-z0-9]{10,})(?:\/branch\/([A-Za-z0-9]{10,}))?(?:\/|$)/);
|
|
144
|
+
if (f) {
|
|
145
|
+
const fileKey = f[3] || f[2];
|
|
146
|
+
const node = String(q.get("node-id") || "").trim().replace(/%3A/gi, ":");
|
|
147
|
+
if (node && !/^\d+[-:]\d+$/.test(node))
|
|
148
|
+
return { kind: "url", key: `url:${url}`, url };
|
|
149
|
+
const n = node ? node.replace(":", "-") : "";
|
|
150
|
+
const seg = f[1] === "board" ? "board" : "design";
|
|
151
|
+
return { kind: "figma", key: `figma:${fileKey}${n ? `:${n.replace("-", ":")}` : ""}`, url: `https://www.figma.com/${seg}/${fileKey}${n ? `?node-id=${n}` : ""}` };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (host === "github.com" || host === "www.github.com") {
|
|
155
|
+
const g = p.match(/^\/([^/]+)\/([^/]+)\/(?:issues|pull|discussions)\/(\d+)$/);
|
|
156
|
+
if (g) {
|
|
157
|
+
const [o, r] = [g[1].toLowerCase(), g[2].toLowerCase()];
|
|
158
|
+
const issue = (0, exports.githubRefToken)(o, r, g[3]);
|
|
159
|
+
const c = fragmentOf(rest).match(/^(issuecomment-\d+|discussioncomment-\d+|discussion_r\d+|pullrequestreview-\d+)$/);
|
|
160
|
+
return {
|
|
161
|
+
kind: "github",
|
|
162
|
+
key: c ? `${issue}#${c[1]}` : issue,
|
|
163
|
+
url: `https://github.com/${o}/${r}/issues/${g[3]}${c ? `#${c[1]}` : ""}`,
|
|
164
|
+
...(c ? { parentKey: issue } : {}),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { kind: "url", key: `url:${url}`, url };
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* 根拠の名前の正規化。解けない入力(Gold の ID・`minute:`・`material:`・`design:`・リポのパス・
|
|
172
|
+
* ホストの決まらない課題キー・URL が 2 本以上の値)は `null`。engine 版と 1:1 対応。
|
|
173
|
+
*/
|
|
174
|
+
function canonicalEvidence(value, ctx = {}) {
|
|
175
|
+
const raw = String(value ?? "").trim();
|
|
176
|
+
if (!raw)
|
|
177
|
+
return null;
|
|
178
|
+
const key = raw.match(WHOLE_ISSUE_KEY_RE);
|
|
179
|
+
if (key) {
|
|
180
|
+
const domains = toMap(ctx.backlogDomains);
|
|
181
|
+
const host = String(domains.get(key[1]) || "")
|
|
182
|
+
.trim()
|
|
183
|
+
.toLowerCase()
|
|
184
|
+
.replace(/^https?:\/\//, "")
|
|
185
|
+
.replace(/\/+$/, "");
|
|
186
|
+
return host ? canonicalOfUrl(`https://${host}/view/${raw}`, ctx) : null;
|
|
187
|
+
}
|
|
188
|
+
const gh = raw.match(WHOLE_GH_SHORT_RE);
|
|
189
|
+
if (gh)
|
|
190
|
+
return canonicalOfUrl(`https://github.com/${gh[1]}/${gh[2]}/issues/${gh[3]}`, ctx);
|
|
191
|
+
const url = wholeUrlOf(raw);
|
|
192
|
+
if (!url)
|
|
193
|
+
return null;
|
|
194
|
+
const normalized = syntaxNormalizedUrl(url);
|
|
195
|
+
return normalized ? canonicalOfUrl(normalized, ctx) : null;
|
|
196
|
+
}
|
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` の写し(26 値・**表に無い `kind` は 0**)。
|
|
54
54
|
*
|
|
55
55
|
* 🔴 契約の表(fixture)は `message.posted` しか使っていないので、**ここの写し間違いは表では
|
|
56
56
|
* 捕まらない**。重みの番人は `test/member-engagement-parity.test.mjs` の `[重み]` が持つ。
|
|
@@ -77,14 +77,19 @@ exports.WEIGHTS = Object.freeze({
|
|
|
77
77
|
"review.submitted": 1.5,
|
|
78
78
|
"utterance.spoken": 1.0,
|
|
79
79
|
"meeting.held": 1.0,
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
// 記憶の記録の確認・編集(cortex-engine #551 Q6)。0——記憶の手入れは「案件に関わっている」とは別物で、
|
|
81
|
+
// 手入れをする運用の人が案件の中心人物より上に並ばないように数えない。行は正本と同じく残す
|
|
82
|
+
"record.confirmed": 0,
|
|
83
|
+
"record.edited": 0,
|
|
82
84
|
"record.deleted": 0,
|
|
83
85
|
// 本文を取り込んだ(engine #695)。人の行為ではないので 0(話した人は `utterance.spoken` で数える)
|
|
84
86
|
"body.captured": 0,
|
|
85
87
|
// 人の宣言の指紋(cortex-engine #699 F1)。宣言の取り込みが書く行で、関与には数えない(0)。
|
|
86
88
|
// 場は宣言専用(`decl:<owner/repo>`)で帳簿にもともと入らないが、網羅のため写す
|
|
87
89
|
"decl.recorded": 0,
|
|
90
|
+
// 人が承認して push した記憶の訂正(cortex-engine #935)。同じコミットから `record.edited` も起きるので、
|
|
91
|
+
// 数えると二重になる。0
|
|
92
|
+
"correction.stated": 0,
|
|
88
93
|
});
|
|
89
94
|
const DAY_MS = 86400000;
|
|
90
95
|
const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
package/dist/render.js
CHANGED
|
@@ -50,6 +50,12 @@ exports.ingestFacts = ingestFacts;
|
|
|
50
50
|
exports.ingestSucceeded = ingestSucceeded;
|
|
51
51
|
exports.ingestedAtOf = ingestedAtOf;
|
|
52
52
|
exports.ingestedRowOf = ingestedRowOf;
|
|
53
|
+
exports.reflectedStateOf = reflectedStateOf;
|
|
54
|
+
exports.jstNightLabel = jstNightLabel;
|
|
55
|
+
exports.jstCalendarDay = jstCalendarDay;
|
|
56
|
+
exports.nextMemoryReflectGoldAtMs = nextMemoryReflectGoldAtMs;
|
|
57
|
+
exports.memoryReflectDoneAtMs = memoryReflectDoneAtMs;
|
|
58
|
+
exports.memoryReflectTextOf = memoryReflectTextOf;
|
|
53
59
|
exports.runLogUrlOf = runLogUrlOf;
|
|
54
60
|
exports.lastSyncRunUrlOf = lastSyncRunUrlOf;
|
|
55
61
|
exports.runSourceWordOf = runSourceWordOf;
|
|
@@ -2591,6 +2597,115 @@ function ingestedRowOf(rows, capId, refs) {
|
|
|
2591
2597
|
}
|
|
2592
2598
|
return bestRow;
|
|
2593
2599
|
}
|
|
2600
|
+
/**
|
|
2601
|
+
* `reflected` の行(能力ごとに 1 本・cortex-engine #968 条件 4・E3)を探す。ref は接続先の ID では
|
|
2602
|
+
* なく決まった語 `memory`(サーバの定数は `STATE_MEMORY_REF`。埋め込み関数は共有定数を参照できず
|
|
2603
|
+
* 綴りを写して持つ——`exemptFromRegistrySieve` と同じ判断。ずれは state-vocab-parity.test.mjs)。
|
|
2604
|
+
* 無ければ null(栓 `memory_reflect_row` が off の今の姿。T2 の行は何も出さない)。
|
|
2605
|
+
*
|
|
2606
|
+
* **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す)。
|
|
2607
|
+
*/
|
|
2608
|
+
function reflectedStateOf(rows, capId) {
|
|
2609
|
+
if (!Array.isArray(rows) || !capId)
|
|
2610
|
+
return null;
|
|
2611
|
+
for (const r of rows) {
|
|
2612
|
+
if (r && r.cap === capId && r.question === "reflected" && r.ref === "memory")
|
|
2613
|
+
return r;
|
|
2614
|
+
}
|
|
2615
|
+
return null;
|
|
2616
|
+
}
|
|
2617
|
+
/**
|
|
2618
|
+
* ISO 時刻を「Gold の夜の日」の表示(`M/D`)にする(cortex-engine #968 設計 v1 §3)。
|
|
2619
|
+
*
|
|
2620
|
+
* **engine の `nightDayOf`(−12 時間)と同じ切り方。** Gold は JST 22:00 に始まり日をまたいで
|
|
2621
|
+
* 終わるので、素の日付だと同じ晩の記録が「9/28」と「9/29」に分かれる。12 時間引いてから
|
|
2622
|
+
* JST の日付を読むと、22:00 に始まった晩はいつも開始した日の日付になる。
|
|
2623
|
+
*
|
|
2624
|
+
* **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す)。
|
|
2625
|
+
* @returns `M/D`(ゼロ埋めなし)。読めない値は null
|
|
2626
|
+
*/
|
|
2627
|
+
function jstNightLabel(iso) {
|
|
2628
|
+
const ms = typeof iso === "string" && iso ? Date.parse(iso) : NaN;
|
|
2629
|
+
if (!Number.isFinite(ms))
|
|
2630
|
+
return null;
|
|
2631
|
+
const JST_MS = 9 * 60 * 60 * 1000;
|
|
2632
|
+
const d = new Date(ms - 12 * 60 * 60 * 1000 + JST_MS);
|
|
2633
|
+
return (d.getUTCMonth() + 1) + "/" + d.getUTCDate();
|
|
2634
|
+
}
|
|
2635
|
+
/** JST の暦日(`YYYY-MM-DD`)。`jstNightLabel` と違い −12 時間をしない(素の日付の比較用) */
|
|
2636
|
+
function jstCalendarDay(ms) {
|
|
2637
|
+
const JST_MS = 9 * 60 * 60 * 1000;
|
|
2638
|
+
const d = new Date(ms + JST_MS);
|
|
2639
|
+
const p = (n) => (n < 10 ? "0" + n : String(n));
|
|
2640
|
+
return d.getUTCFullYear() + "-" + p(d.getUTCMonth() + 1) + "-" + p(d.getUTCDate());
|
|
2641
|
+
}
|
|
2642
|
+
/**
|
|
2643
|
+
* ある時刻(`ms`)の後、最初に来る JST 22:00(update-gold の cron・T2)。
|
|
2644
|
+
*
|
|
2645
|
+
* 🔴 **「22」はここ 1 か所だけに書く**(.github/workflows の `update-gold.yml` の cron
|
|
2646
|
+
* `0 13 * * *`(UTC)= JST 22:00 と番人(memory-reflect-line.test.mjs)が突き合わせる)。
|
|
2647
|
+
*
|
|
2648
|
+
* **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す)。
|
|
2649
|
+
*/
|
|
2650
|
+
function nextMemoryReflectGoldAtMs(ms) {
|
|
2651
|
+
const GOLD_HOUR_JST = 22;
|
|
2652
|
+
const JST_MS = 9 * 60 * 60 * 1000;
|
|
2653
|
+
const jst = new Date(ms + JST_MS);
|
|
2654
|
+
const goldUtcMs = Date.UTC(jst.getUTCFullYear(), jst.getUTCMonth(), jst.getUTCDate(), GOLD_HOUR_JST, 0, 0, 0) - JST_MS;
|
|
2655
|
+
return goldUtcMs > ms ? goldUtcMs : goldUtcMs + 24 * 60 * 60 * 1000;
|
|
2656
|
+
}
|
|
2657
|
+
/**
|
|
2658
|
+
* Gold の夜間 job が走り切っている(はずの)締切(T2)。
|
|
2659
|
+
*
|
|
2660
|
+
* 🔴 **「105」(Gold の最長実行時間・分)もここ 1 か所だけ。** 余白 30 分を足す
|
|
2661
|
+
* (番人が update-gold の実測の最長と突き合わせる)。
|
|
2662
|
+
*
|
|
2663
|
+
* **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す)。
|
|
2664
|
+
*/
|
|
2665
|
+
function memoryReflectDoneAtMs(goldAtMs) {
|
|
2666
|
+
const GOLD_MAX_MINUTES = 105;
|
|
2667
|
+
const MARGIN_MINUTES = 30;
|
|
2668
|
+
return goldAtMs + (GOLD_MAX_MINUTES + MARGIN_MINUTES) * 60000;
|
|
2669
|
+
}
|
|
2670
|
+
/**
|
|
2671
|
+
* 「記憶に反映」の文言(接続先 1 件・cortex-engine #968 条件 4・T2)。発注書の下書き
|
|
2672
|
+
* (#968 -5925683579)の表のとおり。**R(`reflected` の行)が無ければ null**(栓 off の今の姿。
|
|
2673
|
+
* 行は何も出さない)。
|
|
2674
|
+
*
|
|
2675
|
+
* @param reflected `reflectedStateOf` の答え(R)
|
|
2676
|
+
* @param ingestedRow その能力の `ingested` の行(N=`lastNewAt` を読む。なければ比べない)
|
|
2677
|
+
* @param nowMs 時計。**呼び手が渡す**(ここで `Date.now()` を読まない。日付爆弾を避ける)
|
|
2678
|
+
*
|
|
2679
|
+
* **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す)。
|
|
2680
|
+
*/
|
|
2681
|
+
function memoryReflectTextOf(reflected, ingestedRow, nowMs) {
|
|
2682
|
+
if (!reflected)
|
|
2683
|
+
return null;
|
|
2684
|
+
const lastOkAt = typeof reflected.lastOkAt === "string" ? reflected.lastOkAt : "";
|
|
2685
|
+
const night = jstNightLabel(lastOkAt);
|
|
2686
|
+
// R の読み口(`detail`)が bronze 以外(写し・API から作った)なら、N とは比べない
|
|
2687
|
+
// (写しと S3 の時刻を比べると嘘になるため)
|
|
2688
|
+
if (reflected.detail && reflected.detail !== "bronze") {
|
|
2689
|
+
return night ? "記憶の更新: " + night + " の夜" : null;
|
|
2690
|
+
}
|
|
2691
|
+
// 一度も反映していない(成功した lastOkAt がまだ無い)。言い切れる材料が無いので何も出さない
|
|
2692
|
+
if (!night)
|
|
2693
|
+
return null;
|
|
2694
|
+
const rMs = Date.parse(lastOkAt);
|
|
2695
|
+
// 門で外した接続先(`ingested: skipped`・書き手 gold)の `lastNewAt` は比べない(申し送り)
|
|
2696
|
+
const n = ingestedRow && ingestedRow.status !== "skipped" && typeof ingestedRow.lastNewAt === "string" ? ingestedRow.lastNewAt : "";
|
|
2697
|
+
const nMs = n ? Date.parse(n) : NaN;
|
|
2698
|
+
if (!n || !Number.isFinite(nMs) || nMs <= rMs)
|
|
2699
|
+
return "記憶に反映: " + night + " の夜まで";
|
|
2700
|
+
const goldAt = nextMemoryReflectGoldAtMs(nMs);
|
|
2701
|
+
if (nowMs < goldAt) {
|
|
2702
|
+
const tonight = jstCalendarDay(goldAt) === jstCalendarDay(nowMs);
|
|
2703
|
+
return "取り込み済み・" + (tonight ? "今夜" : "明日の夜") + " 22:00 に記憶へ反映";
|
|
2704
|
+
}
|
|
2705
|
+
if (nowMs < memoryReflectDoneAtMs(goldAt))
|
|
2706
|
+
return "取り込み済み・いま記憶へ反映しています";
|
|
2707
|
+
return "取り込み済みの原文がまだ記憶に入っていません → 次の定時に自動でやり直します";
|
|
2708
|
+
}
|
|
2594
2709
|
/**
|
|
2595
2710
|
* 行の横に出す「実行ログ」のリンク先(#560)。**無ければ空文字。**
|
|
2596
2711
|
*
|
|
@@ -5601,6 +5716,10 @@ a.ds-tile:hover { text-decoration: none; }
|
|
|
5601
5716
|
どちらが重いのか読めない) */
|
|
5602
5717
|
.ds-ingest { font-size: 11px; color: var(--muted); flex: 0 0 auto; font-variant-numeric: tabular-nums; }
|
|
5603
5718
|
.ds-ingest-failed { color: #b45309; font-weight: 600; }
|
|
5719
|
+
/* 「記憶に反映」(cortex-engine #968 条件4・T2)。「取り込み」(.ds-ingest)の下に1行。
|
|
5720
|
+
**必ず自分の行を占める**(flex: 1 1 100%)——.ds-gate-help と同じ理由(チップの横に
|
|
5721
|
+
押し込むと省略される)。強さは .ds-ingest と同じ弱さ(添え物として読ませる) */
|
|
5722
|
+
.ds-reflect { flex: 1 1 100%; margin: 2px 0 0; font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
5604
5723
|
.ds-gate::before { content: ""; width: 8px; height: 8px; border-radius: 999px; background: currentColor; }
|
|
5605
5724
|
.ds-gate-ok { color: #1f7a45; }
|
|
5606
5725
|
.ds-gate-warn { color: #b45309; }
|
|
@@ -6290,6 +6409,14 @@ ${ingestFacts.toString()}
|
|
|
6290
6409
|
${ingestSucceeded.toString()}
|
|
6291
6410
|
${ingestedAtOf.toString()}
|
|
6292
6411
|
${ingestedRowOf.toString()}
|
|
6412
|
+
// 「記憶に反映」(#968 条件 4・T2)。🔴 **ingestedEl より前に置く。** 落とすと行の描画が
|
|
6413
|
+
// 未定義を参照して画面のスクリプトが丸ごと死ぬ
|
|
6414
|
+
${reflectedStateOf.toString()}
|
|
6415
|
+
${jstNightLabel.toString()}
|
|
6416
|
+
${jstCalendarDay.toString()}
|
|
6417
|
+
${nextMemoryReflectGoldAtMs.toString()}
|
|
6418
|
+
${memoryReflectDoneAtMs.toString()}
|
|
6419
|
+
${memoryReflectTextOf.toString()}
|
|
6293
6420
|
${runLogUrlOf.toString()}
|
|
6294
6421
|
${lastSyncRunUrlOf.toString()}
|
|
6295
6422
|
${runSourceWordOf.toString()}
|
|
@@ -14636,6 +14763,22 @@ ${calendarAddUrl.toString()}
|
|
|
14636
14763
|
return frag([span].concat(runTrailNodes(r)));
|
|
14637
14764
|
}
|
|
14638
14765
|
|
|
14766
|
+
/**
|
|
14767
|
+
* 「記憶に反映」の行(cortex-engine #968 条件 4・T2)。**「取り込み」の行の下に 1 行**。
|
|
14768
|
+
* 材料は「reflectedStateOf」(R)と、同じ能力の「ingested」の行(N=「lastNewAt」)。
|
|
14769
|
+
* **R が無ければ何も出さない**(栓「memory_reflect_row」が off の今の姿。「ingestedEl」 と同じ原則)。
|
|
14770
|
+
*
|
|
14771
|
+
* @param r その能力の「ingested」の行(「ingestedEl」に渡したのと同じ行。無くても R だけで判定する)
|
|
14772
|
+
* @param capId 接続状態ストアの能力名(issues / design 等。「ingestedEl」 の「cap」は画面の
|
|
14773
|
+
* ラベルなので使えない——「keyMissingText」用の別物)
|
|
14774
|
+
*/
|
|
14775
|
+
function memoryReflectEl(r, capId) {
|
|
14776
|
+
var reflected = reflectedStateOf(LIVE_STATE, capId);
|
|
14777
|
+
var text = memoryReflectTextOf(reflected, r, Date.now());
|
|
14778
|
+
if (!text) return null;
|
|
14779
|
+
return el("p", { class: "ds-reflect", text: text });
|
|
14780
|
+
}
|
|
14781
|
+
|
|
14639
14782
|
/**
|
|
14640
14783
|
* その接続先の「届くか」の行(接続状態ストア・#523 S1)。**無ければ null。**
|
|
14641
14784
|
*
|
|
@@ -15133,6 +15276,8 @@ ${calendarAddUrl.toString()}
|
|
|
15133
15276
|
pendingBadgeEl(t),
|
|
15134
15277
|
// この接続先の取り込みが最後にどうなったか(段C)。測っていなければ null
|
|
15135
15278
|
ingestedEl(ingestedStateOf(t.domain, t.projectKey), "課題管理"),
|
|
15279
|
+
// 「記憶に反映」(cortex-engine #968 条件4・T2)。R が無ければ null(栓offの今の姿)
|
|
15280
|
+
memoryReflectEl(ingestedStateOf(t.domain, t.projectKey), "issues"),
|
|
15136
15281
|
]);
|
|
15137
15282
|
// この接続先に届くか(#523 S1)。**「取り込み」とは別の問い**なので併記する
|
|
15138
15283
|
// ——鍵はあるが 403 でプロジェクトが見えない、は取り込みの失敗より前の話。
|
|
@@ -15772,6 +15917,8 @@ ${calendarAddUrl.toString()}
|
|
|
15772
15917
|
pendingBadgeEl(f),
|
|
15773
15918
|
// このファイルの取り込みが最後にどうなったか(段C)。測っていなければ null
|
|
15774
15919
|
ingestedEl(designIngestedStateOf(f.key), "デザイン"),
|
|
15920
|
+
// 「記憶に反映」(cortex-engine #968 条件4・T2)。R が無ければ null(栓offの今の姿)
|
|
15921
|
+
memoryReflectEl(designIngestedStateOf(f.key), "design"),
|
|
15775
15922
|
]);
|
|
15776
15923
|
// このファイルに届くか(#523 S1)。書き手は engine の同期(#523 S4・engine #329)。
|
|
15777
15924
|
// 行が無いファイル(まだ一度も同期していない)には何も出ない。
|
|
@@ -19604,9 +19751,10 @@ ${calendarAddUrl.toString()}
|
|
|
19604
19751
|
// 出し分けはサーバの DELETABLE_SECTIONS と同じ4区画。ここは「押せないボタンを
|
|
19605
19752
|
// 出さない」ためのもので、**守りはサーバ側**(promote と同じ関係)。
|
|
19606
19753
|
var DELETABLE_PATH = /^Cortex\\/(Decisions|Glossary|Members|Rules)\\/records\\/[^/]+\\.md$/;
|
|
19607
|
-
//
|
|
19608
|
-
//
|
|
19609
|
-
|
|
19754
|
+
// 削除で除外リストにも載る区画(サーバの DELETABLE_SECTIONS で値が null でない区画の写し)。
|
|
19755
|
+
// Decisions は cortex-engine #551 Q7 で足した——消した下書きの決定の題名が README の「除外決定」に載り、
|
|
19756
|
+
// 夜間の AI は同じ題の決定を二度と作らない(engine が「除外決定」を読むようになった)
|
|
19757
|
+
var DELETE_EXCLUDES = { Decisions: true, Glossary: true, Members: true, Rules: true };
|
|
19610
19758
|
function deletableRecordPath(rec) {
|
|
19611
19759
|
if (!SITE.intake) return "";
|
|
19612
19760
|
var p = repoPathOf(rec);
|
|
@@ -19615,7 +19763,7 @@ ${calendarAddUrl.toString()}
|
|
|
19615
19763
|
var base = p.slice(p.lastIndexOf("/") + 1).toLowerCase();
|
|
19616
19764
|
return base === "readme.md" || base === "template.md" ? "" : p;
|
|
19617
19765
|
}
|
|
19618
|
-
/**
|
|
19766
|
+
/** 削除で除外リストに載る区画なら、その区画の表示名。載らない区画(今は無い)は空文字 */
|
|
19619
19767
|
function deleteExclusionLabel(p) {
|
|
19620
19768
|
var m = /^Cortex\\/([^/]+)\\//.exec(p);
|
|
19621
19769
|
var dir = m ? m[1] : "";
|
|
@@ -20009,16 +20157,31 @@ ${calendarAddUrl.toString()}
|
|
|
20009
20157
|
report: { color: "#22d3ee", label: "レポート" },
|
|
20010
20158
|
overview: { color: "#fb923c", label: "Home" },
|
|
20011
20159
|
record: { color: "#0e7490", label: "その他レコード" },
|
|
20012
|
-
silver: { color: "#9ca3af", label: "
|
|
20013
|
-
bronze: { color: "#9ca3af", label: "
|
|
20014
|
-
external: { color: "#9ca3af", label: "
|
|
20160
|
+
silver: { color: "#9ca3af", label: "根拠(課題・議事録・資料・リンク等)" },
|
|
20161
|
+
bronze: { color: "#9ca3af", label: "根拠(課題・議事録・資料・リンク等)" },
|
|
20162
|
+
external: { color: "#9ca3af", label: "根拠(課題・議事録・資料・リンク等)" },
|
|
20015
20163
|
phantom: { color: "#d4d9e0", label: "参照先が見つからないもの" },
|
|
20016
20164
|
};
|
|
20017
|
-
|
|
20018
|
-
|
|
20019
|
-
|
|
20020
|
-
|
|
20165
|
+
/* 線(関係)の種類ごとの色・ラベルと、「Goldどうし」か「根拠」かの分類(engine #551・分類A 10/1)。
|
|
20166
|
+
* **一覧(凡例)はこの表から組む**ので、将来 about(決定・ルール→用語)や is_a/part_of、
|
|
20167
|
+
* 人→用語・人→人の線を足すときは、ここに1行足すだけでよい——凡例もトグルも自動で追従し、
|
|
20168
|
+
* 既存の行・色は変わらない(語彙が増えても凡例が崩れない作り)。
|
|
20169
|
+
* kind: "gold"=Goldのレコードどうしを結ぶ線/"evidence"=Goldがその根拠を指す線(弱いつながり)。 */
|
|
20170
|
+
var REL_META = {
|
|
20171
|
+
based_on: { color: "#2563eb", label: "根拠", kind: "gold" },
|
|
20172
|
+
derived_from: { color: "#0d9488", label: "生成元", kind: "gold" },
|
|
20173
|
+
relates_to: { color: "#94a3b8", label: "関連", kind: "gold" },
|
|
20174
|
+
supersedes: { color: "#dc2626", label: "置換", kind: "gold" },
|
|
20175
|
+
reference: { color: "#d1d5db", label: "参照", kind: "evidence" },
|
|
20176
|
+
source: { color: "#d1d5db", label: "出典", kind: "evidence" },
|
|
20177
|
+
link: { color: "#e2e8f0", label: "リンク", kind: "evidence" },
|
|
20021
20178
|
};
|
|
20179
|
+
var WEAK_RELS = {};
|
|
20180
|
+
var REL_COLOR = {};
|
|
20181
|
+
Object.keys(REL_META).forEach(function (r) {
|
|
20182
|
+
REL_COLOR[r] = REL_META[r].color;
|
|
20183
|
+
if (REL_META[r].kind === "evidence") WEAK_RELS[r] = true;
|
|
20184
|
+
});
|
|
20022
20185
|
var graphState = { showWeak: true, q: "" };
|
|
20023
20186
|
|
|
20024
20187
|
function renderGraph() {
|
|
@@ -20049,16 +20212,17 @@ ${calendarAddUrl.toString()}
|
|
|
20049
20212
|
legend.appendChild(item);
|
|
20050
20213
|
});
|
|
20051
20214
|
|
|
20052
|
-
//
|
|
20053
|
-
|
|
20054
|
-
|
|
20055
|
-
|
|
20056
|
-
|
|
20057
|
-
|
|
20058
|
-
|
|
20059
|
-
|
|
20060
|
-
|
|
20061
|
-
|
|
20215
|
+
// 線(関係)の色凡例。グラフ内の有無に関わらず全種別を、Goldどうし/根拠の2群で表示する
|
|
20216
|
+
[["gold", "線(Goldどうし):"], ["evidence", "線(根拠):"]].forEach(function (pair) {
|
|
20217
|
+
var kind = pair[0], heading = pair[1];
|
|
20218
|
+
var keys = Object.keys(REL_META).filter(function (r) { return REL_META[r].kind === kind; });
|
|
20219
|
+
if (!keys.length) return;
|
|
20220
|
+
legend.appendChild(el("span", { class: "sep", text: heading }));
|
|
20221
|
+
keys.forEach(function (r) {
|
|
20222
|
+
var li = el("span", {}, [el("span", { class: "line" }), document.createTextNode(REL_META[r].label)]);
|
|
20223
|
+
li.querySelector(".line").style.borderTopColor = REL_META[r].color;
|
|
20224
|
+
legend.appendChild(li);
|
|
20225
|
+
});
|
|
20062
20226
|
});
|
|
20063
20227
|
|
|
20064
20228
|
var toggle = el("label", {});
|
package/package.json
CHANGED