@takagaki/cortex-decisions-viewer 0.12.106 → 0.12.108
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 +326 -16
- 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
|
*
|
|
@@ -4798,14 +4913,21 @@ button.site-icon[aria-busy="true"] .site-icon-plus { opacity: 0; }
|
|
|
4798
4913
|
}
|
|
4799
4914
|
.card:hover { border-color: var(--accent); }
|
|
4800
4915
|
.card-meta { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; font-size: 12.5px; color: var(--muted); margin-bottom: 6px; }
|
|
4801
|
-
|
|
4916
|
+
/* card-title はカードの見出し(#1 新しいタブでも開けるよう <a> にした)。見た目は元の <div> と揃える */
|
|
4917
|
+
.card-title { display: block; font-size: 16px; font-weight: 650; margin: 2px 0 6px; color: var(--text); }
|
|
4802
4918
|
.card-summary { color: #3b424c; font-size: 14px; margin: 0; }
|
|
4803
4919
|
.empty { color: var(--muted); text-align: center; padding: 48px 0; }
|
|
4804
4920
|
.pager { display: flex; align-items: center; justify-content: center; gap: 14px; padding: 20px 0 4px; }
|
|
4805
|
-
.pager-btn { padding: 6px 14px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); color: inherit; cursor: pointer; font: inherit; }
|
|
4921
|
+
.pager-btn { display: inline-block; padding: 6px 14px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); color: inherit; cursor: pointer; font: inherit; }
|
|
4806
4922
|
.pager-btn:hover:not(:disabled) { border-color: var(--muted); }
|
|
4807
4923
|
.pager-btn:disabled { opacity: .4; cursor: default; }
|
|
4924
|
+
/* ボタンの見た目の <a> に下線は要らない(.trace-btn 等、押しボタン型のリンクと同じ扱い) */
|
|
4925
|
+
a.pager-btn:hover { text-decoration: none; }
|
|
4926
|
+
/* 前へ/次へ(#2)の端は <a> ではなく <span>(キーボードでも踏めないように)。このクラスで見た目をそろえる */
|
|
4927
|
+
.pager-btn.is-disabled { opacity: .4; cursor: default; pointer-events: none; }
|
|
4808
4928
|
.pager-ind { color: var(--muted); font-size: 13px; min-width: 72px; text-align: center; }
|
|
4929
|
+
/* 詳細の「前へ/次へ」は本文の上(戻るリンクのすぐ下)に置くので、一覧の下の余白は要らない */
|
|
4930
|
+
.detail-pager { justify-content: flex-start; padding: 0 0 16px; }
|
|
4809
4931
|
|
|
4810
4932
|
/* ---- badges / chips ---- */
|
|
4811
4933
|
.badge {
|
|
@@ -5601,6 +5723,10 @@ a.ds-tile:hover { text-decoration: none; }
|
|
|
5601
5723
|
どちらが重いのか読めない) */
|
|
5602
5724
|
.ds-ingest { font-size: 11px; color: var(--muted); flex: 0 0 auto; font-variant-numeric: tabular-nums; }
|
|
5603
5725
|
.ds-ingest-failed { color: #b45309; font-weight: 600; }
|
|
5726
|
+
/* 「記憶に反映」(cortex-engine #968 条件4・T2)。「取り込み」(.ds-ingest)の下に1行。
|
|
5727
|
+
**必ず自分の行を占める**(flex: 1 1 100%)——.ds-gate-help と同じ理由(チップの横に
|
|
5728
|
+
押し込むと省略される)。強さは .ds-ingest と同じ弱さ(添え物として読ませる) */
|
|
5729
|
+
.ds-reflect { flex: 1 1 100%; margin: 2px 0 0; font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
5604
5730
|
.ds-gate::before { content: ""; width: 8px; height: 8px; border-radius: 999px; background: currentColor; }
|
|
5605
5731
|
.ds-gate-ok { color: #1f7a45; }
|
|
5606
5732
|
.ds-gate-warn { color: #b45309; }
|
|
@@ -6290,6 +6416,14 @@ ${ingestFacts.toString()}
|
|
|
6290
6416
|
${ingestSucceeded.toString()}
|
|
6291
6417
|
${ingestedAtOf.toString()}
|
|
6292
6418
|
${ingestedRowOf.toString()}
|
|
6419
|
+
// 「記憶に反映」(#968 条件 4・T2)。🔴 **ingestedEl より前に置く。** 落とすと行の描画が
|
|
6420
|
+
// 未定義を参照して画面のスクリプトが丸ごと死ぬ
|
|
6421
|
+
${reflectedStateOf.toString()}
|
|
6422
|
+
${jstNightLabel.toString()}
|
|
6423
|
+
${jstCalendarDay.toString()}
|
|
6424
|
+
${nextMemoryReflectGoldAtMs.toString()}
|
|
6425
|
+
${memoryReflectDoneAtMs.toString()}
|
|
6426
|
+
${memoryReflectTextOf.toString()}
|
|
6293
6427
|
${runLogUrlOf.toString()}
|
|
6294
6428
|
${lastSyncRunUrlOf.toString()}
|
|
6295
6429
|
${runSourceWordOf.toString()}
|
|
@@ -8450,11 +8584,37 @@ ${calendarAddUrl.toString()}
|
|
|
8450
8584
|
});
|
|
8451
8585
|
return a;
|
|
8452
8586
|
}
|
|
8587
|
+
// 一覧 ⇄ 詳細のスクロール位置(#1 一覧→詳細→戻るで毎回スクロールが消える問題)。
|
|
8588
|
+
//
|
|
8589
|
+
// ⚠ ブラウザ既定の自動復元(scrollRestoration: "auto")に任せない。** この画面は
|
|
8590
|
+
// 一覧も詳細も同じ #app を毎回 innerHTML="" で作り直す静的サイトなので、戻ったときの
|
|
8591
|
+
// 高さが自動復元の計算と噛み合わず当てにできない。代わりに「一覧を離れた瞬間のY」を
|
|
8592
|
+
// 自分で1つだけ覚え、一覧へ戻るときに明示的に scrollTo する(分類B 2026-10-01)。
|
|
8593
|
+
//
|
|
8594
|
+
// ⚠ 詳細→詳細(後述の前へ/次へ)では更新しない。** 更新すると、何件か読み進めてから
|
|
8595
|
+
// 一覧へ戻ったときに「最後に見ていた詳細でのスクロール位置」に飛んでしまう。
|
|
8596
|
+
// 一覧に戻る行き先は常に「一覧を離れたときの位置」でなければならない
|
|
8597
|
+
var savedListScrollY = 0;
|
|
8598
|
+
// どのタブの一覧で覚えた位置か(実装レビュー②)。** route() は詳細を描くとき、その記録の
|
|
8599
|
+
// 種類で listState.tab を書き換える(例: 用語集の一覧からウィキリンクで決定の詳細へ移ると
|
|
8600
|
+
// listState.tab は "decisions" になる)。そこで「一覧へ戻る」を押すと、戻る先はその
|
|
8601
|
+
// 書き換わった後のタブになるので、離れたときのタブと戻る先のタブが違うことがある。
|
|
8602
|
+
// 違うタブに離れたときのYを当てはめない(合わなければ先頭へ)
|
|
8603
|
+
var savedListTab = null;
|
|
8604
|
+
// 直前に描いたのが詳細(not foundも含む)か一覧か。popstate(ブラウザの戻る/進む)は
|
|
8605
|
+
// go() を通らないので、この印で「詳細→一覧」の遷移だけを見分けて復元する
|
|
8606
|
+
var lastWasDetail = false;
|
|
8607
|
+
if ("scrollRestoration" in history) history.scrollRestoration = "manual";
|
|
8608
|
+
|
|
8453
8609
|
function go(id) {
|
|
8610
|
+
// 一覧から詳細へ移るときだけ、今の一覧のスクロール位置とタブを覚える
|
|
8611
|
+
if (id && !lastWasDetail) { savedListScrollY = window.scrollY; savedListTab = listState.tab; }
|
|
8454
8612
|
var url = id ? "?id=" + encodeURIComponent(id) : tabUrl(listState.tab);
|
|
8455
8613
|
history.pushState({}, "", url);
|
|
8456
8614
|
route();
|
|
8457
|
-
|
|
8615
|
+
// 詳細は常に先頭から。一覧は、離れたときと同じタブに戻るときだけ元の位置へ
|
|
8616
|
+
if (id) window.scrollTo(0, 0);
|
|
8617
|
+
else window.scrollTo(0, listState.tab === savedListTab ? savedListScrollY : 0);
|
|
8458
8618
|
}
|
|
8459
8619
|
|
|
8460
8620
|
var listState = { tab: DEFAULT_TAB, q: "", category: "", month: "", status: "", page: 1 };
|
|
@@ -8482,6 +8642,38 @@ ${calendarAddUrl.toString()}
|
|
|
8482
8642
|
return el("div", { class: "pager" }, [prev, ind, next]);
|
|
8483
8643
|
}
|
|
8484
8644
|
|
|
8645
|
+
/** 新しいタブ・新しいウィンドウ等を求めるクリックではない(左クリック・修飾キー無し)か */
|
|
8646
|
+
function isPlainClick(ev) {
|
|
8647
|
+
return !ev.defaultPrevented && ev.button === 0 && !ev.metaKey && !ev.ctrlKey && !ev.shiftKey && !ev.altKey;
|
|
8648
|
+
}
|
|
8649
|
+
|
|
8650
|
+
/**
|
|
8651
|
+
* 一覧カードの見出し(#1)。普通の a href="?id=..." にして、中クリック・
|
|
8652
|
+
* Ctrl/Cmd クリックで新しいタブでも開けるようにする(opsTabLink と同じ型)。
|
|
8653
|
+
* 素のクリックは今までどおり SPA 内遷移(go)のまま——1 件ずつ確かめるときに
|
|
8654
|
+
* ページ全体の再読み込みを挟まない。
|
|
8655
|
+
*/
|
|
8656
|
+
function cardTitleLink(rec, extraClass) {
|
|
8657
|
+
var a = el("a", {
|
|
8658
|
+
class: "card-title" + (extraClass || ""),
|
|
8659
|
+
href: "?id=" + encodeURIComponent(rec.id),
|
|
8660
|
+
text: humanTitle(rec.title),
|
|
8661
|
+
});
|
|
8662
|
+
a.addEventListener("click", function (ev) {
|
|
8663
|
+
if (ev.defaultPrevented) return;
|
|
8664
|
+
if (!isPlainClick(ev)) {
|
|
8665
|
+
// 新しいタブ・新しいウィンドウ等はブラウザの既定の動作に任せる。
|
|
8666
|
+
// ここで止めないと、外側のカードの click にも伝わって SPA 内遷移も重ねて走ってしまう
|
|
8667
|
+
ev.stopPropagation();
|
|
8668
|
+
return;
|
|
8669
|
+
}
|
|
8670
|
+
ev.preventDefault();
|
|
8671
|
+
ev.stopPropagation();
|
|
8672
|
+
go(rec.id);
|
|
8673
|
+
});
|
|
8674
|
+
return a;
|
|
8675
|
+
}
|
|
8676
|
+
|
|
8485
8677
|
function monthLabel(m) {
|
|
8486
8678
|
var p = m.split("-");
|
|
8487
8679
|
return p[0] + "年" + parseInt(p[1], 10) + "月";
|
|
@@ -8550,6 +8742,7 @@ ${calendarAddUrl.toString()}
|
|
|
8550
8742
|
|
|
8551
8743
|
// ---------- 一覧 ----------
|
|
8552
8744
|
function renderList() {
|
|
8745
|
+
lastWasDetail = false;
|
|
8553
8746
|
app.innerHTML = "";
|
|
8554
8747
|
renderTabs();
|
|
8555
8748
|
renderInfoBar();
|
|
@@ -14636,6 +14829,22 @@ ${calendarAddUrl.toString()}
|
|
|
14636
14829
|
return frag([span].concat(runTrailNodes(r)));
|
|
14637
14830
|
}
|
|
14638
14831
|
|
|
14832
|
+
/**
|
|
14833
|
+
* 「記憶に反映」の行(cortex-engine #968 条件 4・T2)。**「取り込み」の行の下に 1 行**。
|
|
14834
|
+
* 材料は「reflectedStateOf」(R)と、同じ能力の「ingested」の行(N=「lastNewAt」)。
|
|
14835
|
+
* **R が無ければ何も出さない**(栓「memory_reflect_row」が off の今の姿。「ingestedEl」 と同じ原則)。
|
|
14836
|
+
*
|
|
14837
|
+
* @param r その能力の「ingested」の行(「ingestedEl」に渡したのと同じ行。無くても R だけで判定する)
|
|
14838
|
+
* @param capId 接続状態ストアの能力名(issues / design 等。「ingestedEl」 の「cap」は画面の
|
|
14839
|
+
* ラベルなので使えない——「keyMissingText」用の別物)
|
|
14840
|
+
*/
|
|
14841
|
+
function memoryReflectEl(r, capId) {
|
|
14842
|
+
var reflected = reflectedStateOf(LIVE_STATE, capId);
|
|
14843
|
+
var text = memoryReflectTextOf(reflected, r, Date.now());
|
|
14844
|
+
if (!text) return null;
|
|
14845
|
+
return el("p", { class: "ds-reflect", text: text });
|
|
14846
|
+
}
|
|
14847
|
+
|
|
14639
14848
|
/**
|
|
14640
14849
|
* その接続先の「届くか」の行(接続状態ストア・#523 S1)。**無ければ null。**
|
|
14641
14850
|
*
|
|
@@ -15133,6 +15342,8 @@ ${calendarAddUrl.toString()}
|
|
|
15133
15342
|
pendingBadgeEl(t),
|
|
15134
15343
|
// この接続先の取り込みが最後にどうなったか(段C)。測っていなければ null
|
|
15135
15344
|
ingestedEl(ingestedStateOf(t.domain, t.projectKey), "課題管理"),
|
|
15345
|
+
// 「記憶に反映」(cortex-engine #968 条件4・T2)。R が無ければ null(栓offの今の姿)
|
|
15346
|
+
memoryReflectEl(ingestedStateOf(t.domain, t.projectKey), "issues"),
|
|
15136
15347
|
]);
|
|
15137
15348
|
// この接続先に届くか(#523 S1)。**「取り込み」とは別の問い**なので併記する
|
|
15138
15349
|
// ——鍵はあるが 403 でプロジェクトが見えない、は取り込みの失敗より前の話。
|
|
@@ -15772,6 +15983,8 @@ ${calendarAddUrl.toString()}
|
|
|
15772
15983
|
pendingBadgeEl(f),
|
|
15773
15984
|
// このファイルの取り込みが最後にどうなったか(段C)。測っていなければ null
|
|
15774
15985
|
ingestedEl(designIngestedStateOf(f.key), "デザイン"),
|
|
15986
|
+
// 「記憶に反映」(cortex-engine #968 条件4・T2)。R が無ければ null(栓offの今の姿)
|
|
15987
|
+
memoryReflectEl(designIngestedStateOf(f.key), "design"),
|
|
15775
15988
|
]);
|
|
15776
15989
|
// このファイルに届くか(#523 S1)。書き手は engine の同期(#523 S4・engine #329)。
|
|
15777
15990
|
// 行が無いファイル(まだ一度も同期していない)には何も出ない。
|
|
@@ -19030,11 +19243,16 @@ ${calendarAddUrl.toString()}
|
|
|
19030
19243
|
return sel;
|
|
19031
19244
|
}
|
|
19032
19245
|
|
|
19033
|
-
|
|
19246
|
+
/**
|
|
19247
|
+
* 今の絞り込み(検索語・期間・カテゴリー・状態)を当てた決定の一覧(並びは
|
|
19248
|
+
* SITE.decisions の順=新しい順のまま)。drawCards と、詳細の「前へ/次へ」
|
|
19249
|
+
* (#2)の両方がここを通る——一覧の並びと前後移動の並びがずれないように、口を1つにする。
|
|
19250
|
+
*/
|
|
19251
|
+
function filteredDecisions() {
|
|
19034
19252
|
var q = listState.q.trim().toLowerCase();
|
|
19035
19253
|
// 削除済みは絞り込みの手前で落とす(分母も一緒に減るので「12/13件」が残らない)
|
|
19036
19254
|
var all = visibleRecords(SITE.decisions);
|
|
19037
|
-
|
|
19255
|
+
return all.filter(function (d) {
|
|
19038
19256
|
if (listState.month && d.date.slice(0, 7) !== listState.month) return false;
|
|
19039
19257
|
if (listState.category && d.category !== listState.category) return false;
|
|
19040
19258
|
if (!matchesStatusFilter(d, !!SITE.decisionFollowup)) return false;
|
|
@@ -19044,6 +19262,11 @@ ${calendarAddUrl.toString()}
|
|
|
19044
19262
|
}
|
|
19045
19263
|
return true;
|
|
19046
19264
|
});
|
|
19265
|
+
}
|
|
19266
|
+
|
|
19267
|
+
function drawCards() {
|
|
19268
|
+
var all = visibleRecords(SITE.decisions);
|
|
19269
|
+
var items = filteredDecisions();
|
|
19047
19270
|
|
|
19048
19271
|
var wrap = document.getElementById("card-list");
|
|
19049
19272
|
wrap.innerHTML = "";
|
|
@@ -19077,7 +19300,7 @@ ${calendarAddUrl.toString()}
|
|
|
19077
19300
|
var card = el("div", { class: "card", title: d.id, "data-record-id": d.id }, [
|
|
19078
19301
|
meta,
|
|
19079
19302
|
// 消したレコードは一覧でも取り消し線(次のビルドまで一覧には残る)
|
|
19080
|
-
|
|
19303
|
+
cardTitleLink(d, d.deleted ? " is-deleted" : ""),
|
|
19081
19304
|
d.summary ? el("p", { class: "card-summary", text: d.summary }) : null,
|
|
19082
19305
|
]);
|
|
19083
19306
|
card.addEventListener("click", function () { go(d.id); });
|
|
@@ -19090,8 +19313,7 @@ ${calendarAddUrl.toString()}
|
|
|
19090
19313
|
|
|
19091
19314
|
// ---------- 汎用一覧(records/ を持つディレクトリ) ----------
|
|
19092
19315
|
function renderGenericList() {
|
|
19093
|
-
var tab =
|
|
19094
|
-
TABS.forEach(function (t) { if (t.key === listState.tab) tab = t; });
|
|
19316
|
+
var tab = findTab(listState.tab);
|
|
19095
19317
|
// 削除済みは描画の手前で落とす(件数の分母・絞り込みの選択肢もこれ1本で揃う)
|
|
19096
19318
|
var items = visibleRecords(tab && tab.items ? tab.items : []);
|
|
19097
19319
|
|
|
@@ -19112,12 +19334,7 @@ ${calendarAddUrl.toString()}
|
|
|
19112
19334
|
app.appendChild(listWrap);
|
|
19113
19335
|
|
|
19114
19336
|
function draw() {
|
|
19115
|
-
var
|
|
19116
|
-
var shown = items.filter(function (r) {
|
|
19117
|
-
if (!matchesStatusFilter(r)) return false;
|
|
19118
|
-
if (!q) return true;
|
|
19119
|
-
return (r.title + " " + r.summary + " " + r.searchText).toLowerCase().indexOf(q) !== -1;
|
|
19120
|
-
});
|
|
19337
|
+
var shown = filteredGenericItems(tab);
|
|
19121
19338
|
listWrap.innerHTML = "";
|
|
19122
19339
|
if (!shown.length) {
|
|
19123
19340
|
countEl.textContent = "0 / " + items.length + " 件";
|
|
@@ -19146,7 +19363,7 @@ ${calendarAddUrl.toString()}
|
|
|
19146
19363
|
meta.appendChild(traceR);
|
|
19147
19364
|
var card = el("div", { class: "card", title: r.id, "data-record-id": r.id }, [
|
|
19148
19365
|
meta,
|
|
19149
|
-
|
|
19366
|
+
cardTitleLink(r, r.deleted ? " is-deleted" : ""),
|
|
19150
19367
|
r.summary ? el("p", { class: "card-summary", text: r.summary }) : null,
|
|
19151
19368
|
]);
|
|
19152
19369
|
card.addEventListener("click", function () { go(r.id); });
|
|
@@ -19916,6 +20133,68 @@ ${calendarAddUrl.toString()}
|
|
|
19916
20133
|
function visibleRecords(items) {
|
|
19917
20134
|
return (items || []).filter(function (r) { return !r.deleted; });
|
|
19918
20135
|
}
|
|
20136
|
+
|
|
20137
|
+
/** タブkeyからTABSの該当行を探す(見つからなければ null) */
|
|
20138
|
+
function findTab(key) {
|
|
20139
|
+
var found = null;
|
|
20140
|
+
TABS.forEach(function (t) { if (t.key === key) found = t; });
|
|
20141
|
+
return found;
|
|
20142
|
+
}
|
|
20143
|
+
|
|
20144
|
+
/**
|
|
20145
|
+
* 今の絞り込み(検索語・状態)を当てた汎用一覧(用語集・関係者・ルール等)。
|
|
20146
|
+
* renderGenericList の draw と、詳細の「前へ/次へ」(#2)の両方がここを通る
|
|
20147
|
+
*/
|
|
20148
|
+
function filteredGenericItems(tab) {
|
|
20149
|
+
var q = listState.q.trim().toLowerCase();
|
|
20150
|
+
var items = visibleRecords(tab && tab.items ? tab.items : []);
|
|
20151
|
+
return items.filter(function (r) {
|
|
20152
|
+
if (!matchesStatusFilter(r)) return false;
|
|
20153
|
+
if (!q) return true;
|
|
20154
|
+
return (r.title + " " + r.summary + " " + r.searchText).toLowerCase().indexOf(q) !== -1;
|
|
20155
|
+
});
|
|
20156
|
+
}
|
|
20157
|
+
|
|
20158
|
+
/**
|
|
20159
|
+
* 絞り込んだ並び(filtered)に今の記録が無ければ、絞り込み前の既定の並び(fallback。
|
|
20160
|
+
* 削除済みを除いた新しい順)を使う(#2 一覧を経ずに開いたとき・絞り込みに
|
|
20161
|
+
* 合わない記録を開いたときは既定の順、の受け皿)。
|
|
20162
|
+
*/
|
|
20163
|
+
function pagerOrder(filtered, fallback, id) {
|
|
20164
|
+
for (var i = 0; i < filtered.length; i++) { if (filtered[i].id === id) return filtered; }
|
|
20165
|
+
return fallback;
|
|
20166
|
+
}
|
|
20167
|
+
|
|
20168
|
+
/**
|
|
20169
|
+
* 詳細画面の「前へ/次へ」(#2)。ordered の中で id の前後の記録へ移る。
|
|
20170
|
+
* 端(先頭・末尾)では押せない。ordered に id が無ければ何も出さない
|
|
20171
|
+
* (削除済みを直リンクで開いた等、並びそのものが無いとき)
|
|
20172
|
+
*/
|
|
20173
|
+
function buildDetailPager(ordered, id) {
|
|
20174
|
+
var idx = -1;
|
|
20175
|
+
for (var i = 0; i < ordered.length; i++) { if (ordered[i].id === id) { idx = i; break; } }
|
|
20176
|
+
if (idx === -1) return null;
|
|
20177
|
+
var prevRec = idx > 0 ? ordered[idx - 1] : null;
|
|
20178
|
+
var nextRec = idx < ordered.length - 1 ? ordered[idx + 1] : null;
|
|
20179
|
+
function navBtn(rec, cls, text) {
|
|
20180
|
+
// 端では押せない。<a href="#"> のままだとポインタは止められても、Tab→Enter では
|
|
20181
|
+
// 踏めてしまう(#へ飛んで popstate が走り、詳細を描き直してしまう)ので、
|
|
20182
|
+
// 端では href を持たない <span> にする(実装レビュー④)
|
|
20183
|
+
if (!rec) return el("span", { class: "pager-btn is-disabled " + cls, "aria-disabled": "true", text: text });
|
|
20184
|
+
var a = el("a", { class: "pager-btn " + cls, href: "?id=" + encodeURIComponent(rec.id), text: text });
|
|
20185
|
+
a.addEventListener("click", function (ev) {
|
|
20186
|
+
if (!isPlainClick(ev)) { ev.stopPropagation(); return; }
|
|
20187
|
+
ev.preventDefault();
|
|
20188
|
+
ev.stopPropagation();
|
|
20189
|
+
go(rec.id);
|
|
20190
|
+
});
|
|
20191
|
+
return a;
|
|
20192
|
+
}
|
|
20193
|
+
return el("div", { class: "pager detail-pager" }, [
|
|
20194
|
+
navBtn(prevRec, "detail-pager-prev", "← 前へ"),
|
|
20195
|
+
navBtn(nextRec, "detail-pager-next", "次へ →"),
|
|
20196
|
+
]);
|
|
20197
|
+
}
|
|
19919
20198
|
/**
|
|
19920
20199
|
* 削除済みを除いたグラフ。**消費側で1行ずつ摘まむのではなく、ここ1箇所で作る**
|
|
19921
20200
|
* ——グラフ・エクスプローラー・件数表示が別々に間引くと、片方だけ消え残る。
|
|
@@ -19940,6 +20219,7 @@ ${calendarAddUrl.toString()}
|
|
|
19940
20219
|
|
|
19941
20220
|
// ---------- 汎用詳細 ----------
|
|
19942
20221
|
function renderGenericDetail(r) {
|
|
20222
|
+
lastWasDetail = true;
|
|
19943
20223
|
app.innerHTML = "";
|
|
19944
20224
|
countEl.textContent = "";
|
|
19945
20225
|
|
|
@@ -19947,6 +20227,12 @@ ${calendarAddUrl.toString()}
|
|
|
19947
20227
|
back.addEventListener("click", function (ev) { ev.preventDefault(); go(null); });
|
|
19948
20228
|
app.appendChild(back);
|
|
19949
20229
|
|
|
20230
|
+
// 前へ/次へ(#2)。一覧の今の絞り込み・並びの順。一覧を経ずに開いたときは既定の順
|
|
20231
|
+
var pagerTabG = findTab("dir:" + r._section);
|
|
20232
|
+
var orderedG = pagerOrder(filteredGenericItems(pagerTabG), visibleRecords(pagerTabG ? pagerTabG.items : []), r.id);
|
|
20233
|
+
var detailPagerG = buildDetailPager(orderedG, r.id);
|
|
20234
|
+
if (detailPagerG) app.appendChild(detailPagerG);
|
|
20235
|
+
|
|
19950
20236
|
var head = el("div", { class: "detail-head" });
|
|
19951
20237
|
head.appendChild(el("div", {}, [ el("h1", { class: "detail-title", text: humanTitle(r.title) }) ]));
|
|
19952
20238
|
var traceBtn2 = el("a", { class: "trace-btn", href: "?tab=graph", text: "🕸 つながり" });
|
|
@@ -20632,6 +20918,7 @@ ${calendarAddUrl.toString()}
|
|
|
20632
20918
|
|
|
20633
20919
|
// ---------- Decision詳細 ----------
|
|
20634
20920
|
function renderDetail(d) {
|
|
20921
|
+
lastWasDetail = true;
|
|
20635
20922
|
app.innerHTML = "";
|
|
20636
20923
|
countEl.textContent = "";
|
|
20637
20924
|
|
|
@@ -20639,6 +20926,11 @@ ${calendarAddUrl.toString()}
|
|
|
20639
20926
|
back.addEventListener("click", function (ev) { ev.preventDefault(); go(null); });
|
|
20640
20927
|
app.appendChild(back);
|
|
20641
20928
|
|
|
20929
|
+
// 前へ/次へ(#2)。一覧の今の絞り込み・並びの順。一覧を経ずに開いたときは既定の順(新しい順)
|
|
20930
|
+
var orderedD = pagerOrder(filteredDecisions(), visibleRecords(SITE.decisions), d.id);
|
|
20931
|
+
var detailPagerD = buildDetailPager(orderedD, d.id);
|
|
20932
|
+
if (detailPagerD) app.appendChild(detailPagerD);
|
|
20933
|
+
|
|
20642
20934
|
var head = el("div", { class: "detail-head" });
|
|
20643
20935
|
var headLeft = el("div", {}, [ el("h1", { class: "detail-title", text: humanTitle(d.title) }) ]);
|
|
20644
20936
|
head.appendChild(headLeft);
|
|
@@ -20717,6 +21009,7 @@ ${calendarAddUrl.toString()}
|
|
|
20717
21009
|
}
|
|
20718
21010
|
|
|
20719
21011
|
function renderNotFound(id) {
|
|
21012
|
+
lastWasDetail = true;
|
|
20720
21013
|
app.innerHTML = "";
|
|
20721
21014
|
var back = el("a", { class: "detail-back", href: "?", text: "← 一覧へ戻る" });
|
|
20722
21015
|
back.addEventListener("click", function (ev) { ev.preventDefault(); go(null); });
|
|
@@ -21201,7 +21494,24 @@ ${calendarAddUrl.toString()}
|
|
|
21201
21494
|
}
|
|
21202
21495
|
})();
|
|
21203
21496
|
|
|
21204
|
-
|
|
21497
|
+
// ブラウザの「戻る」「進む」。go() を通らないので、ここで一覧⇄詳細のスクロールだけ別に見る
|
|
21498
|
+
// (#1 ブラウザの戻るでも一覧のスクロール位置を保つ)
|
|
21499
|
+
window.addEventListener("popstate", function () {
|
|
21500
|
+
var goingToId = getParam("id");
|
|
21501
|
+
var cameFromDetail = lastWasDetail;
|
|
21502
|
+
// 「進む」で一覧→詳細に戻ることもある(戻る→スクロール→進む、で位置が古くなる実装レビュー③)。
|
|
21503
|
+
// go() と同じ条件で、そのときの一覧の位置とタブを覚え直す
|
|
21504
|
+
if (goingToId && !cameFromDetail) { savedListScrollY = window.scrollY; savedListTab = listState.tab; }
|
|
21505
|
+
route();
|
|
21506
|
+
if (!goingToId && cameFromDetail) {
|
|
21507
|
+
// 一覧へ戻る。離れたときと同じタブのときだけ元の位置へ(違えば先頭)
|
|
21508
|
+
window.scrollTo(0, listState.tab === savedListTab ? savedListScrollY : 0);
|
|
21509
|
+
} else if (goingToId) {
|
|
21510
|
+
// 詳細はブラウザの戻る/進むでも常に先頭から
|
|
21511
|
+
// (scrollRestoration を manual にしたので、ここで明示しないとブラウザがどこへも戻さない)
|
|
21512
|
+
window.scrollTo(0, 0);
|
|
21513
|
+
}
|
|
21514
|
+
});
|
|
21205
21515
|
/**
|
|
21206
21516
|
* ヘッダーの案件名(#379)。**ポータル配下(/p/<key>/ の下)で開いているときだけ**、
|
|
21207
21517
|
* ポータルの案件一覧(同一オリジンの /)へ戻るリンクにする。
|
package/package.json
CHANGED