@takagaki/cortex-decisions-viewer 0.12.172 → 0.12.174

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/build.js CHANGED
@@ -82,6 +82,7 @@ const memory_evidence_1 = require("./memory-evidence");
82
82
  const hands_1 = require("./hands");
83
83
  const rederivation_1 = require("./rederivation");
84
84
  const external_id_1 = require("./external-id");
85
+ const member_origin_1 = require("./member-origin");
85
86
  const home_project_1 = require("./home-project");
86
87
  const supersedes_1 = require("./supersedes");
87
88
  const minify_1 = require("./minify");
@@ -2400,6 +2401,8 @@ async function buildSite(opts, views) {
2400
2401
  // 「記憶 v2 の案件か」は 1 本の栓 `memory_v2_writer` の値で決める(cortex-engine #1094 鎖 3 V10b)。橋の間は写しとの OR
2401
2402
  const memoryHands = await (0, hands_1.readMemoryHands)(repoRoot, ledgerDir);
2402
2403
  const memoryV2 = (0, hands_1.isMemoryV2)(opts.memoryV2Writer, memoryHands !== null);
2404
+ // 関係者の「つながっている ID」の出どころ(cortex-engine #1911 便 B4b)。`--view-dir` の中の平文を読むだけ。無い・読めない案件は何も付かない(今と 1 バイトも同じ)
2405
+ const memberOrigins = opts.viewDir ? await (0, member_origin_1.readMemberOrigins)(path.resolve(opts.viewDir)) : null;
2403
2406
  // 記録の id → リポジトリルート相対のパス(順位のファイルの鍵と突き合わせる。`repoRoot` 起点・cwd 起点にしない)
2404
2407
  const recordRelPaths = new Map();
2405
2408
  const relOf = (dir, fileName) => {
@@ -2463,6 +2466,15 @@ async function buildSite(opts, views) {
2463
2466
  // 旧い取り下げが普通に見えないよう、`retracted_by` を 1 本以上持つルールは一覧(と件数・つながり)から外す
2464
2467
  if (e.name === "Rules")
2465
2468
  records = records.filter((r) => !r.relations.some((x) => x.rel === "retracted_by"));
2469
+ // つながっている ID の出どころ(関係者だけ・`fm.identities` と同じ並びの別の表)
2470
+ if (memberOrigins)
2471
+ for (const r of records) {
2472
+ if (!(0, external_id_1.isMemberRecord)(r.rtype, r.fm.id, e.name))
2473
+ continue;
2474
+ const o = (0, member_origin_1.originsOfMember)(r.fm, String(r.fm.id ?? r.id), memberOrigins);
2475
+ if (o)
2476
+ r.idOrigins = o;
2477
+ }
2466
2478
  // 関連の資料(cortex-engine #1094 F8)。ルール・用語だけ・欄の無い記録には何も付かない
2467
2479
  attachRelatedMaterials(e.name, records, refs);
2468
2480
  // 根拠(references)。ルール・用語・関係者を決定と同じリンクにする(用語・関係者の移行前の source は先頭の 1 本)
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ORIGIN_WORDS = exports.MEMBER_ORIGIN_FILE = void 0;
7
+ exports.identityKeyOf = identityKeyOf;
8
+ exports.parseMemberOrigins = parseMemberOrigins;
9
+ exports.readMemberOrigins = readMemberOrigins;
10
+ exports.originsOfMember = originsOfMember;
11
+ /**
12
+ * 関係者の「つながっている ID」の出どころ(cortex-engine #1911 便 B4b・仕様 `member.identity-origin`)。
13
+ *
14
+ * engine の記憶側の晩の終わりの段 `origin`(`scripts/memory-v2/memory/post.mjs` の `stageMemberOrigin`)が、各 ID 行が「どうやって分かったか」を
15
+ * 保管庫の `gold/v1/c/view/member-identity-origin.json` に置く。`--view-dir` の作業場(`view-dir.ts`)に取り出された同じ名前のファイルを、ここは**読むだけ**。
16
+ * 形は `{ "v": 1, "members": { "<関係者の id>": { "<主張の鍵 pc:…>": "self|peer|exact|name|ai" } } }`(封筒・目次なし。名簿の要約と同じ平文)。
17
+ *
18
+ * ## 🔴 約束
19
+ *
20
+ * - **記録の `fm.identities[]` の要素には足さない。** 画面の編集の書き戻しで機械の行が人の行に固まらないよう、記録ごとに**並びが `fm.identities` と同じ別の表**
21
+ * (`idOrigins`・出どころの語か `null`)で持つ。画面は `idOrigins[i]` を `fm.identities[i]` の出どころとして読む
22
+ * - **ブラウザ側にハッシュを持ち込まない。** 鍵(`pc:<種類>:<sha256 先頭 32>`)の突き合わせはビルドでだけ行う
23
+ * - **読めない値は昇格させない。** ファイルが無い・壊れている・版が違う・語彙の外の値・鍵の形が違う行は無かったことにする(その行は空欄のまま)
24
+ * - 鍵の計算は engine の `identityKeyOf`(`scripts/memory-v2/spine/person-keys.mjs`)と**完全に同じ**: 小文字に畳むのはメールだけ(GitHub の login は畳まない)、
25
+ * 値は前後の空白を落とす。同じ固定ケース表(`test/fixtures/identity-origin-keys.json`)を `viewer/test/member-origin.test.mjs` が検査する
26
+ */
27
+ const node_crypto_1 = require("node:crypto");
28
+ const promises_1 = __importDefault(require("node:fs/promises"));
29
+ const node_path_1 = __importDefault(require("node:path"));
30
+ const external_id_1 = require("./external-id");
31
+ /** `--view-dir` の中の置き場(engine `ORIGIN_VIEW_NAME` + `.json`) */
32
+ exports.MEMBER_ORIGIN_FILE = "member-identity-origin.json";
33
+ /** 出どころの語彙(engine `ORIGIN_WORDS`・強い順) */
34
+ exports.ORIGIN_WORDS = ["self", "peer", "exact", "name", "ai"];
35
+ /** 主張の ID の種類(engine `PERSON_ID_KINDS`) */
36
+ const ID_KINDS = ["email", "git_author", "github_login", "slack_user", "google_chat_user", "figma_user", "backlog_user"];
37
+ const KEY_RE = /^pc:[a-z_]+:[0-9a-f]{32}$/;
38
+ const isWord = (w) => typeof w === "string" && exports.ORIGIN_WORDS.includes(w);
39
+ /** 主張の鍵(engine `identityKeyOf` と同じ)。形が違えば `null` */
40
+ function identityKeyOf(entry) {
41
+ const raw = entry !== null && typeof entry === "object" && !Array.isArray(entry) ? entry.id : entry;
42
+ const s = String(raw ?? "");
43
+ const i = s.indexOf(":");
44
+ if (i <= 0)
45
+ return null;
46
+ const kind = s.slice(0, i);
47
+ const id = s.slice(i + 1).trim();
48
+ if (!ID_KINDS.includes(kind) || !id)
49
+ return null;
50
+ let v = id;
51
+ if (kind === "email") {
52
+ v = id.toLowerCase();
53
+ if (!/^[^@\s]+@[^@\s]+$/.test(v))
54
+ return null;
55
+ }
56
+ return `pc:${kind}:${(0, node_crypto_1.createHash)("sha256").update(v, "utf8").digest("hex").slice(0, 32)}`;
57
+ }
58
+ /** 中身を読む(投げない)。形が違う・版が違う → `null`。語彙の外の値・鍵の形が違う行は捨てる */
59
+ function parseMemberOrigins(text) {
60
+ let data;
61
+ try {
62
+ data = JSON.parse(text);
63
+ }
64
+ catch {
65
+ return null;
66
+ }
67
+ if (!data || typeof data !== "object" || Array.isArray(data))
68
+ return null;
69
+ const o = data;
70
+ if (o.v !== 1 || !o.members || typeof o.members !== "object" || Array.isArray(o.members))
71
+ return null;
72
+ const out = new Map();
73
+ for (const [id, keys] of Object.entries(o.members)) {
74
+ if (!keys || typeof keys !== "object" || Array.isArray(keys))
75
+ continue;
76
+ const m = new Map();
77
+ for (const [k, w] of Object.entries(keys))
78
+ if (KEY_RE.test(k) && isWord(w))
79
+ m.set(k, w);
80
+ if (m.size)
81
+ out.set(id, m);
82
+ }
83
+ return out;
84
+ }
85
+ /** `--view-dir` から読む。無い → `null`(黙る)・壊れている → `null`(build のログに 1 行) */
86
+ async function readMemberOrigins(viewDir) {
87
+ let text;
88
+ try {
89
+ text = await promises_1.default.readFile(node_path_1.default.join(viewDir, exports.MEMBER_ORIGIN_FILE), "utf8");
90
+ }
91
+ catch {
92
+ return null;
93
+ }
94
+ const parsed = parseMemberOrigins(text);
95
+ if (!parsed)
96
+ console.warn(`${exports.MEMBER_ORIGIN_FILE} を読めませんでした(つながっている ID の「どうやって分かったか」は空欄で出します)`);
97
+ return parsed;
98
+ }
99
+ /**
100
+ * 関係者 1 人の `fm.identities` と同じ並びの出どころの表。1 行も当たらなければ `undefined`(キーごと載せない=出どころの無い案件は今と 1 バイトも同じ)。
101
+ * `fm.identities` の要素が文字列でも `{ id }` でも鍵は同じ(engine と同じ)
102
+ */
103
+ function originsOfMember(fm, memberId, origins) {
104
+ if (!origins)
105
+ return undefined;
106
+ const mine = origins.get(memberId);
107
+ const list = fm[external_id_1.FM_LINKED_IDS];
108
+ if (!mine || !Array.isArray(list))
109
+ return undefined;
110
+ const out = list.map((e) => {
111
+ const k = identityKeyOf(e);
112
+ return k ? mine.get(k) ?? null : null;
113
+ });
114
+ return out.some((w) => w !== null) ? out : undefined;
115
+ }
package/dist/render.js CHANGED
@@ -4923,6 +4923,10 @@ a:hover { text-decoration: underline; }
4923
4923
  .mface { position: relative; flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: 50%; overflow: hidden; background: var(--border, #e5e7eb); color: var(--muted, #6b7280); font-size: 13px; font-weight: 700; line-height: 1; text-decoration: none; box-sizing: border-box; }
4924
4924
  a.mface:hover { box-shadow: 0 0 0 2px var(--accent, #4f46e5); }
4925
4925
  .mface svg { width: 18px; height: 18px; display: block; }
4926
+ .mface-loading { animation: mface-pulse 1.4s ease-in-out infinite; }
4927
+ .mface-loading img { opacity: 0; }
4928
+ @keyframes mface-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .45; } }
4929
+ @media (prefers-reduced-motion: reduce) { .mface-loading { animation: none; } }
4926
4930
  .mface img { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
4927
4931
  .detail-namerow .mface { width: 36px; height: 36px; font-size: 16px; }
4928
4932
  .detail-namerow .mface svg { width: 22px; height: 22px; }
@@ -7741,36 +7745,82 @@ ${story_1.splitByNameLinks.toString()}
7741
7745
  var mfAsked = {};
7742
7746
  var mfWaiting = [];
7743
7747
  var mfTimer = null;
7748
+ /** 頼んでいて答え待ちの人 -> その人の丸(待っている間はスケルトン) */
7749
+ var mfInflight = {};
7750
+ var MF_ASK_TIMEOUT_MS = 8000;
7751
+ var MF_IMG_TIMEOUT_MS = 8000;
7752
+ /** 待ちの形から最後の形へ(loading を外す)。state: photo=写真が読めた/initial=頭文字 */
7753
+ function mfSettle(box, state) {
7754
+ if (box.getAttribute("data-state") !== "loading") return;
7755
+ box.classList.remove("mface-loading");
7756
+ box.setAttribute("data-state", state);
7757
+ if (state === "initial") {
7758
+ var img = box.querySelector("img");
7759
+ if (img && img.parentNode) img.parentNode.removeChild(img);
7760
+ box.appendChild(el("span", { class: "mface-initial", text: box.getAttribute("data-initial") || "" }));
7761
+ }
7762
+ }
7763
+ /** 取れた写真を読み込む。読めるまではスケルトンのまま・失敗か時間切れで頭文字(止まり続けない) */
7744
7764
  function mfAttach(box, path) {
7745
- if (!box.isConnected && !box.parentNode) return;
7765
+ if (box.getAttribute("data-state") !== "loading") return;
7746
7766
  var img = document.createElement("img");
7767
+ var timer = setTimeout(function () { mfSettle(box, "initial"); }, MF_IMG_TIMEOUT_MS);
7747
7768
  img.setAttribute("alt", "");
7748
7769
  img.setAttribute("referrerpolicy", "no-referrer");
7749
- img.setAttribute("loading", "lazy");
7750
- img.addEventListener("error", function () { if (img.parentNode) img.parentNode.removeChild(img); });
7770
+ img.addEventListener("load", function () { clearTimeout(timer); mfSettle(box, "photo"); });
7771
+ img.addEventListener("error", function () { clearTimeout(timer); mfSettle(box, "initial"); });
7751
7772
  img.setAttribute("src", PORTAL_ORIGIN + path);
7752
7773
  box.appendChild(img);
7753
7774
  }
7775
+ /** もう答えが分かっている人の丸を決める(写真のパスがあれば読みに行き、無ければ頭文字) */
7776
+ function mfResolve(box, id) {
7777
+ if (mfFaces[id]) mfAttach(box, mfFaces[id]);
7778
+ else mfSettle(box, "initial");
7779
+ }
7754
7780
  function mfFlush() {
7755
7781
  mfTimer = null;
7756
7782
  var waiting = mfWaiting;
7757
7783
  mfWaiting = [];
7758
- if (!waiting.length || !SITE.intake || !actorTicket()) return;
7784
+ if (!waiting.length) return;
7785
+ if (!SITE.intake || !actorTicket()) { waiting.forEach(function (w) { mfSettle(w.box, "initial"); }); return; }
7759
7786
  var ids = [];
7760
7787
  waiting.forEach(function (w) {
7761
- if (Object.prototype.hasOwnProperty.call(mfFaces, w.id)) { if (mfFaces[w.id]) mfAttach(w.box, mfFaces[w.id]); return; }
7762
- if (!Object.prototype.hasOwnProperty.call(mfAsked, w.id) && ids.indexOf(w.id) < 0 && ids.length < 200) ids.push(w.id);
7788
+ if (Object.prototype.hasOwnProperty.call(mfFaces, w.id)) { mfResolve(w.box, w.id); return; }
7789
+ var asked = Object.prototype.hasOwnProperty.call(mfAsked, w.id);
7790
+ if (!asked && ids.indexOf(w.id) < 0) {
7791
+ // 1 回に頼むのは 200 人まで。あふれた人は待たせず頭文字(スケルトンのまま止まらない)
7792
+ if (ids.length >= 200) { mfSettle(w.box, "initial"); return; }
7793
+ ids.push(w.id);
7794
+ }
7795
+ (mfInflight[w.id] = mfInflight[w.id] || []).push(w.box);
7763
7796
  });
7764
7797
  if (!ids.length) return;
7765
7798
  ids.forEach(function (id) { mfAsked[id] = 1; });
7799
+ var finished = false;
7800
+ // 答えが出た(または諦めた)人の待ち行列を空にして、全部の丸を最後の形へ
7801
+ var drain = function () {
7802
+ ids.forEach(function (id) {
7803
+ var boxes = mfInflight[id] || [];
7804
+ delete mfInflight[id];
7805
+ boxes.forEach(function (b) { mfResolve(b, id); });
7806
+ });
7807
+ };
7766
7808
  var give = function (j) {
7767
7809
  (j.people || []).forEach(function (one) {
7768
7810
  if (one && typeof one.userId === "string" && typeof one.avatarUrl === "string" && MF_AVATAR.test(one.avatarUrl)) mfFaces[one.userId] = one.avatarUrl;
7769
7811
  });
7770
7812
  ids.forEach(function (id) { if (!Object.prototype.hasOwnProperty.call(mfFaces, id)) mfFaces[id] = null; });
7771
- waiting.forEach(function (w) { if (mfFaces[w.id]) mfAttach(w.box, mfFaces[w.id]); });
7813
+ if (!finished) { finished = true; clearTimeout(giveUp); drain(); }
7814
+ };
7815
+ // 失敗・断り・時間切れは覚えない(あとで画面を開き直せば取り直す)。待っていた丸は頭文字に落とす
7816
+ var forget = function () {
7817
+ if (finished) return;
7818
+ finished = true;
7819
+ clearTimeout(giveUp);
7820
+ ids.forEach(function (id) { delete mfAsked[id]; });
7821
+ drain();
7772
7822
  };
7773
- var forget = function () { ids.forEach(function (id) { delete mfAsked[id]; }); };
7823
+ var giveUp = setTimeout(forget, MF_ASK_TIMEOUT_MS);
7774
7824
  intakeJson(callIntake("member-faces", { op: "member-faces", userIds: ids })).then(function (res) {
7775
7825
  var j = (res && res.j) || {};
7776
7826
  if (res && res.ok && j.ok === true && Array.isArray(j.people)) give(j);
@@ -7805,15 +7855,18 @@ ${story_1.splitByNameLinks.toString()}
7805
7855
  return plain;
7806
7856
  }
7807
7857
  var name = Array.from(humanTitle(rec.title))[0] || "";
7858
+ // 🔴 画像を取りに行っている間は頭文字を出さず、丸の形のスケルトン(確定した表示がすり替わって見えないように)。
7859
+ // 頭文字は「取れなかった・読めなかった・時間切れ」の最後の形にだけ出す。読み上げには「読み込み中」を足さない
7808
7860
  var a = el("a", {
7809
- class: "mface mface-portal",
7861
+ class: "mface mface-portal mface-loading",
7810
7862
  "data-face": "portal",
7863
+ "data-state": "loading",
7864
+ "data-initial": name,
7811
7865
  href: PORTAL_ORIGIN + "/people/" + encodeURIComponent(id),
7812
7866
  target: "_blank",
7813
7867
  rel: "noopener",
7814
7868
  title: "ポータルでこの人のページを開く",
7815
7869
  });
7816
- a.appendChild(el("span", { class: "mface-initial", text: name }));
7817
7870
  // 行ごと(カード・詳細)の click に伝えない(カードを押した扱いで詳細へ移らない)
7818
7871
  a.addEventListener("click", function (ev) { ev.stopPropagation(); });
7819
7872
  mfWant(id, a);
@@ -19366,7 +19419,9 @@ ${story_1.splitByNameLinks.toString()}
19366
19419
  // 名前一致/AI が判定。レコード情報のパネルも同じ表を引く(2 度書かない)
19367
19420
  var LI_BASIS = {
19368
19421
  verified: { text: "ポータルで確定", tone: "is-firm" },
19369
- declared: { text: "本人が申告", tone: "is-firm" },
19422
+ // 🔴 「本人が申告」は画面に出さない(cortex-engine #1911 便 B4b・リーダーの答え 6238278773 の 3)。旧い declared の行は人が書いた行と機械の行が
19423
+ // 混ざっていて、本人が申告した証拠ではない。本人の確定は出どころ(LI_ORIGIN の self)だけで言う。declared は空欄(読めない値と同じく何も言わない)
19424
+ declared: { text: "", tone: "is-plain" },
19370
19425
  exact: { text: "メール一致", tone: "is-firm" },
19371
19426
  name_match: { text: "名前一致", tone: "is-machine" },
19372
19427
  name_only: { text: "名前一致", tone: "is-machine" },
@@ -19379,6 +19434,15 @@ ${story_1.splitByNameLinks.toString()}
19379
19434
  var LI_BASIS_UNREADABLE = "(根拠を読み取れません)";
19380
19435
  // basis が無い行(出どころを言わない)。空欄で出す(「本人が申告」に倒さない)
19381
19436
  var LI_BASIS_NONE = { text: "", tone: "is-plain" };
19437
+ // 出どころ(cortex-engine #1911 便 B4b・仕様 member.portal-id-list の( )の言い方)。ビルドが記録ごとに持つ表 idOrigins[i](fm.identities と同じ並び)の語。
19438
+ // 🔴 語彙の外の値は無かったことにする(読めない値を昇格させない)。ハッシュ・鍵の突き合わせはビルドでだけ行う
19439
+ var LI_ORIGIN = {
19440
+ self: { text: "本人が確定", tone: "is-firm" },
19441
+ peer: { text: "周りの人が確定", tone: "is-firm" },
19442
+ exact: { text: "同じメール・同じコミット", tone: "is-firm" },
19443
+ name: { text: "名前の一致", tone: "is-machine" },
19444
+ ai: { text: "AI が材料から結んだ", tone: "is-machine" },
19445
+ };
19382
19446
  // 🔴 **5 列目の「場」(scope)は #704 で足した。** engine は綴りだけ予約していて、いまは
19383
19447
  // 人が手で書いたときだけ入る。無い行は素の空(「いつから」と同じ——空が正常)
19384
19448
  var LI_COLS = ["道具", "この人のページ", "どうやって分かったか", "いつから", "場"];
@@ -19425,6 +19489,19 @@ ${story_1.splitByNameLinks.toString()}
19425
19489
  * 🔴 **語彙の外はそのまま出して、中立の色に置く**——知らない根拠を、確かな側にも
19426
19490
  * 機械の側にも倒さない
19427
19491
  */
19492
+ /** 出どころの語 → 画面の言葉と色。語彙の外・無い → null(呼び手は basis に戻る) */
19493
+ function liOrigin(word) {
19494
+ return typeof word === "string" && Object.prototype.hasOwnProperty.call(LI_ORIGIN, word) ? LI_ORIGIN[word] : null;
19495
+ }
19496
+ /**
19497
+ * この行の「どうやって分かったか」。出どころの表がある記録(記憶 v2 の案件)は 5 つの言い方だけで、表に無い行は空欄
19498
+ * (取り下げた行・結び先が別の人の行に、旧い basis の言い方を出さない)。表が無い記録(出どころを渡されない案件)だけ、今までの basis の言い方
19499
+ */
19500
+ function liWhy(rec, index, ent) {
19501
+ var origins = rec && Array.isArray(rec.idOrigins) ? rec.idOrigins : null;
19502
+ if (origins) return liOrigin(origins[index]) || LI_BASIS_NONE;
19503
+ return liBasis(ent ? ent.basis : null);
19504
+ }
19428
19505
  function liBasis(raw) {
19429
19506
  // 🔴 **declared(いちばん強い段)に倒してよいのは「欄が無い/空」のときだけ。**
19430
19507
  // 以前は typeof raw === "string" でない値を一律に空扱いしていたので、
@@ -19507,6 +19584,43 @@ ${story_1.splitByNameLinks.toString()}
19507
19584
  }
19508
19585
  return host;
19509
19586
  }
19587
+ /**
19588
+ * Slack の行の「場」の列に出す言い方(リンクの行き先の判断には使わない・表示だけ)。
19589
+ *
19590
+ * 🔴 **内部の値(team_id・組織のドメインなど)を画面に出さない。**
19591
+ * 1. 場が接続済みワークスペースの team_id かドメインと一致し、操作画面で登録した名前(name)があれば、その名前
19592
+ * 2. 名前が引けないときは、登録した Slack チャンネルの URL のホスト(xxx.slack.com の形)をそのまま。
19593
+ * 場と結べる登録(workspace か URL のホストの頭が場)のホストが 1 つに決まるときだけ
19594
+ * 3. どちらも引けなければ場の値のまま(推し量らない)
19595
+ */
19596
+ function liScopeLabel(kind, scope) {
19597
+ if (kind !== "slack_user" || scope === "") return scope;
19598
+ var want = scope.toLowerCase();
19599
+ var list = slackWorkspaces() || [];
19600
+ var hit = null;
19601
+ for (var i = 0; i < list.length; i++) {
19602
+ var w = list[i];
19603
+ if (!w) continue;
19604
+ if (w.teamId === scope || (typeof w.domain === "string" && w.domain.toLowerCase() === want)) { hit = w; break; }
19605
+ }
19606
+ var named = hit && typeof hit.name === "string" ? hit.name.trim() : "";
19607
+ // 名前が引けなかったワークスペースは name に team_id が入る(空にはしない)=名前ではない
19608
+ if (named !== "" && named !== hit.teamId && named.toLowerCase() !== want && named.toLowerCase() !== String(hit.domain || "").toLowerCase()) return named;
19609
+ var srcs = (fleetVal("externalSources") || []).filter(function (x) {
19610
+ return x && x.type === "slack" && typeof x.url === "string";
19611
+ });
19612
+ var host = "";
19613
+ for (var j = 0; j < srcs.length; j++) {
19614
+ var m = LI_SLACK_CHANNEL_HOST.exec(srcs[j].url);
19615
+ if (!m) continue;
19616
+ var h = m[1].toLowerCase() + ".slack.com";
19617
+ var tied = srcs[j].workspace === scope || (hit && srcs[j].workspace === hit.teamId) || m[1].toLowerCase() === want;
19618
+ if (!tied) continue;
19619
+ if (host && host !== h) return scope;
19620
+ host = h;
19621
+ }
19622
+ return host || scope;
19623
+ }
19510
19624
  /**
19511
19625
  * Backlog のユーザーのページを組むスペースのドメイン(決められなければ空文字)。
19512
19626
  *
@@ -19576,7 +19690,7 @@ ${story_1.splitByNameLinks.toString()}
19576
19690
  var list = fm ? fm[FM_LINKED_IDS] : null;
19577
19691
  if (!Array.isArray(list)) return [];
19578
19692
  var out = [];
19579
- list.forEach(function (ent) {
19693
+ list.forEach(function (ent, index) {
19580
19694
  if (!ent || typeof ent !== "object" || Array.isArray(ent)) return;
19581
19695
  if (typeof ent.id !== "string") return;
19582
19696
  var whole = ent.id.trim();
@@ -19584,7 +19698,7 @@ ${story_1.splitByNameLinks.toString()}
19584
19698
  var typed = LI_TYPED_ID.exec(whole);
19585
19699
  var kind = typed ? typed[1] : "";
19586
19700
  var value = typed ? typed[2] : whole;
19587
- var basis = liBasis(ent.basis);
19701
+ var basis = liWhy(rec, index, ent);
19588
19702
  // 場は文字列のときだけ(数やオブジェクトを String() して出さない)
19589
19703
  var scope = typeof ent.scope === "string" ? ent.scope.trim() : "";
19590
19704
  out.push({
@@ -19595,7 +19709,7 @@ ${story_1.splitByNameLinks.toString()}
19595
19709
  tone: basis.tone,
19596
19710
  // 🔴 since が無い行の「いつから」は素の空。印(ダッシュ等)を足さない——空が正常
19597
19711
  since: typeof ent.since === "string" ? ent.since : "",
19598
- scope: scope,
19712
+ scope: liScopeLabel(kind, scope),
19599
19713
  });
19600
19714
  });
19601
19715
  return out;
@@ -19858,7 +19972,7 @@ ${story_1.splitByNameLinks.toString()}
19858
19972
  if (linked) app.appendChild(linked);
19859
19973
  var history = changeLogSection(rec);
19860
19974
  if (history) app.appendChild(history);
19861
- var fmp = renderFmPanel(rec.fm);
19975
+ var fmp = renderFmPanel(rec.fm, rec.idOrigins);
19862
19976
  if (fmp) app.appendChild(fmp);
19863
19977
  appendDeleteFoot(rec);
19864
19978
  // 開き直しても削除済みのまま見せる(印は rec が持っている)
@@ -21379,7 +21493,7 @@ ${story_1.splitByNameLinks.toString()}
21379
21493
  }
21380
21494
  return String(x);
21381
21495
  }
21382
- function renderFmPanel(fm) {
21496
+ function renderFmPanel(fm, idOrigins) {
21383
21497
  if (!fm || !Object.keys(fm).length) return null;
21384
21498
  var panel = el("details", { class: "fm-panel" });
21385
21499
  panel.appendChild(el("summary", { class: "fm-title", text: "レコード情報(frontmatter)" }));
@@ -21415,10 +21529,10 @@ ${story_1.splitByNameLinks.toString()}
21415
21529
  // 🔴 **欄自体は隠さない**(frontmatter のパネルは今までどおり全部そのまま出す)。
21416
21530
  // ただしこの欄はオブジェクトの配列なので、下の join に落とすと [object Object] になる
21417
21531
  // ——1行1件で id と根拠の日本語を出す(上の「つながっている ID」と同じ言葉で)
21418
- v.forEach(function (ent) {
21532
+ v.forEach(function (ent, index) {
21419
21533
  var line = el("div", { class: "fm-li" });
21420
21534
  if (ent && typeof ent === "object" && !Array.isArray(ent) && typeof ent.id === "string") {
21421
- var basisText = liBasis(ent.basis).text;
21535
+ var basisText = liWhy({ idOrigins: idOrigins }, index, ent).text;
21422
21536
  line.textContent = basisText ? ent.id + "(" + basisText + ")" : ent.id;
21423
21537
  } else {
21424
21538
  // 形が外れている要素も消さずに読める形で残す(文字列で書かれた欄を含む)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.172",
3
+ "version": "0.12.174",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,