@takagaki/cortex-decisions-viewer 0.12.106 → 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 +109 -22
- package/dist/canonical-evidence.js +196 -0
- package/dist/render.js +147 -0
- 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,10 +1578,54 @@ function extractBodyLinks(content) {
|
|
|
1577
1578
|
}
|
|
1578
1579
|
return [...out];
|
|
1579
1580
|
}
|
|
1580
|
-
/**
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1581
|
+
/**
|
|
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`)なので、見た目は変わらない
|
|
1589
|
+
*/
|
|
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;
|
|
1628
|
+
}
|
|
1584
1629
|
}
|
|
1585
1630
|
/**
|
|
1586
1631
|
* 全レコードのfrontmatter(relations / source / references)と本文リンクから関係グラフを構築する(engine #551・分類A 10/1)。
|
|
@@ -1640,6 +1685,34 @@ function buildGraph(decisions, home, sections, targetUrls, repoBaseUrl) {
|
|
|
1640
1685
|
// (leaderの実測で77件)を無くすのが今回の決定の本題なので、ここも揃えて飛び先を外す
|
|
1641
1686
|
// (`graph-gold-only.test.mjs` の「議事録を根拠にした決定」で固定)。
|
|
1642
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
|
+
};
|
|
1643
1716
|
// 参照先IDから層を推定してノード化する
|
|
1644
1717
|
const classifyTarget = (target) => {
|
|
1645
1718
|
if (nodes.has(target))
|
|
@@ -1651,7 +1724,11 @@ function buildGraph(decisions, home, sections, targetUrls, repoBaseUrl) {
|
|
|
1651
1724
|
return resolved;
|
|
1652
1725
|
let group = "phantom";
|
|
1653
1726
|
let label = target;
|
|
1654
|
-
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:") ||
|
|
1655
1732
|
target.startsWith("material:") ||
|
|
1656
1733
|
target.startsWith("design:") ||
|
|
1657
1734
|
target.startsWith("figma-comment:")) {
|
|
@@ -1662,15 +1739,20 @@ function buildGraph(decisions, home, sections, targetUrls, repoBaseUrl) {
|
|
|
1662
1739
|
return addNode({ id: target, label, group, url });
|
|
1663
1740
|
}
|
|
1664
1741
|
else if (/^[A-Z][A-Z0-9_]*-\d+$/.test(target)) {
|
|
1665
|
-
|
|
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;
|
|
1666
1749
|
const url = dropMirrorBlob(targetUrls.get(target));
|
|
1667
|
-
|
|
1668
|
-
|
|
1750
|
+
const nodeId = addNode(url ? { id: target, label, group, url } : { id: target, label, group });
|
|
1751
|
+
evidenceKeyToNodeId.set(foldKey, nodeId);
|
|
1752
|
+
return nodeId;
|
|
1669
1753
|
}
|
|
1670
1754
|
else if (/^https?:\/\//.test(target)) {
|
|
1671
|
-
|
|
1672
|
-
label = target.replace(/^https?:\/\//, "").slice(0, 60);
|
|
1673
|
-
return addNode({ id: target, label, group, url: dropMirrorBlob(target) });
|
|
1755
|
+
return addEvidenceUrlNode(target, target.replace(/^https?:\/\//, "").slice(0, 60));
|
|
1674
1756
|
}
|
|
1675
1757
|
else if (target.includes("/") || target.toLowerCase().endsWith(".md")) {
|
|
1676
1758
|
// リポジトリ内ファイルへの生のパス参照(同期ミラーの議事録・資料など)。
|
|
@@ -1696,13 +1778,11 @@ function buildGraph(decisions, home, sections, targetUrls, repoBaseUrl) {
|
|
|
1696
1778
|
const addReferenceEdges = (fromId, references) => {
|
|
1697
1779
|
for (const ref of references) {
|
|
1698
1780
|
if (ref.url) {
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
url,
|
|
1705
|
-
});
|
|
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);
|
|
1706
1786
|
edges.push({ from: fromId, to: id, rel: "reference" });
|
|
1707
1787
|
}
|
|
1708
1788
|
else {
|
|
@@ -1712,8 +1792,7 @@ function buildGraph(decisions, home, sections, targetUrls, repoBaseUrl) {
|
|
|
1712
1792
|
};
|
|
1713
1793
|
const addLinkEdges = (fromId, links) => {
|
|
1714
1794
|
for (const link of links) {
|
|
1715
|
-
|
|
1716
|
-
edges.push({ from: fromId, to: classifyTarget(key ?? link), rel: "link" });
|
|
1795
|
+
edges.push({ from: fromId, to: classifyTarget(link), rel: "link" });
|
|
1717
1796
|
}
|
|
1718
1797
|
};
|
|
1719
1798
|
for (const d of decisions) {
|
|
@@ -2445,6 +2524,9 @@ function createRefResolver(opts) {
|
|
|
2445
2524
|
const trimmed = String(ref ?? "").trim();
|
|
2446
2525
|
if (!trimmed)
|
|
2447
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 };
|
|
2448
2530
|
const mdLink = trimmed.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
|
|
2449
2531
|
if (mdLink) {
|
|
2450
2532
|
// 🔴 http(s) 以外(`javascript:` `data:` 等)はリンクにしない。ラベルだけ残す。
|
|
@@ -2455,8 +2537,13 @@ function createRefResolver(opts) {
|
|
|
2455
2537
|
return { label: mdLink[1], url: null };
|
|
2456
2538
|
return { label: mdLink[1], url: mdLink[2] };
|
|
2457
2539
|
}
|
|
2458
|
-
if (/^https?:\/\//.test(trimmed))
|
|
2459
|
-
|
|
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
|
+
}
|
|
2460
2547
|
// 正本のURL(課題キー・material:・design:・minute:)。**ここが「ツールへ飛ぶ」の本体**
|
|
2461
2548
|
const known = targetUrls.get(trimmed);
|
|
2462
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/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
|
// 行が無いファイル(まだ一度も同期していない)には何も出ない。
|
package/package.json
CHANGED