@takagaki/cortex-decisions-viewer 0.12.114 → 0.12.116

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
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.PROJECT_ICON_EXTS = exports.MIRROR_RELS = void 0;
39
+ exports.DECISION_FIRMNESS = exports.PROJECT_ICON_EXTS = exports.MIRROR_RELS = void 0;
40
40
  exports.sanitizeBody = sanitizeBody;
41
41
  exports.findProjectIcon = findProjectIcon;
42
42
  exports.readFleetSources = readFleetSources;
@@ -55,6 +55,9 @@ exports.bodyFileSlug = bodyFileSlug;
55
55
  exports.bodyStampId = bodyStampId;
56
56
  exports.bodyStampComment = bodyStampComment;
57
57
  exports.build = build;
58
+ exports.decisionFirmnessOf = decisionFirmnessOf;
59
+ exports.decisionTopicOf = decisionTopicOf;
60
+ exports.decidedByOf = decidedByOf;
58
61
  exports.createRefResolver = createRefResolver;
59
62
  const node_crypto_1 = require("node:crypto");
60
63
  const node_fs_1 = require("node:fs");
@@ -1523,6 +1526,16 @@ async function buildEvidenceBook(repoRoot, targetUrls, externalSources = []) {
1523
1526
  if (key && name && !book.names.has(key))
1524
1527
  book.names.set(key, name);
1525
1528
  };
1529
+ // 片付けで移した議事録のリンク(URL の鍵 → 「定例名 日付」・cortex-engine #968 R-6・A-R6-3)。表が無い・読めない案件は今どおり
1530
+ // (種類の既定の名前)。資料より先に載せる(同じ鍵に資料の題名が先に付かないように)
1531
+ try {
1532
+ const table = JSON.parse(await node_fs_1.promises.readFile(path.join(repoRoot, evidence_names_1.MOVED_MEETINGS_FILE), "utf8"));
1533
+ for (const [url, name] of (0, evidence_names_1.movedMeetingNamesOf)(table))
1534
+ putName(foldKeyOf(url), name);
1535
+ }
1536
+ catch {
1537
+ // 表が無い(片付けていない案件)・壊れている
1538
+ }
1526
1539
  // 課題(キー → 「キー 題名」)・Backlog のドキュメントと Wiki(URL の鍵 → 題名)
1527
1540
  const backlogDir = await findBacklogDir(repoRoot, "課題管理");
1528
1541
  for (const sub of BACKLOG_SECTIONS) {
@@ -2024,6 +2037,17 @@ function buildGraph(decisions, home, sections, targetUrls, repoBaseUrl, book = (
2024
2037
  addReferenceEdges(d.id, d.references);
2025
2038
  addLinkEdges(d.id, d.bodyLinks);
2026
2039
  }
2040
+ // 決めた人の線(決定 → 関係者・#1094)。relations には書かれないので `decided_by[].member` から引く。
2041
+ // 🔴 相手の関係者がグラフに無ければ引かない(宛先の無い点を作らない=classifyTarget を通さない)
2042
+ for (const d of decisions) {
2043
+ for (const who of d.decidedBy ?? []) {
2044
+ if (!("member" in who))
2045
+ continue;
2046
+ const node = nodes.get(who.member);
2047
+ if (node && node.group === "member")
2048
+ edges.push({ from: d.id, to: who.member, rel: "decided_by" });
2049
+ }
2050
+ }
2027
2051
  if (home) {
2028
2052
  addRelationEdges(home.id, home.relations);
2029
2053
  addSourceEdge(home.id, home.source);
@@ -2705,6 +2729,10 @@ async function parseDecision(raw, fileName, repoRelDir, repoBaseUrl, branch, ref
2705
2729
  const references = referencesRaw.map((r) => refs.resolve(r));
2706
2730
  const relations = toRelations(data.relations);
2707
2731
  const followupHuman = (0, rederivation_1.humanFollowupOf)(data.followup);
2732
+ // 記憶 v2 の任意の欄(#1094)。**欄が無い・語彙外の記録ではキーごと載せない**(埋め込みの形を今と同じに保つ)
2733
+ const firmness = decisionFirmnessOf(data.firmness);
2734
+ const topic = decisionTopicOf(data.topic);
2735
+ const decidedBy = decidedByOf(data.decided_by);
2708
2736
  // 本文先頭の H1 はタイトルと重複するため除去(詳細画面ではタイトルを別途表示する)
2709
2737
  const bodyHtml = await renderRecordBody(content);
2710
2738
  const filePath = repoRelDir ? `${repoRelDir}/${fileName}` : fileName;
@@ -2735,8 +2763,61 @@ async function parseDecision(raw, fileName, repoRelDir, repoBaseUrl, branch, ref
2735
2763
  fmErrors: validateFm(data),
2736
2764
  // 担当者の答え(#705)。**語彙外・形が違うものはキーごと載せない**
2737
2765
  ...(followupHuman ? { followupHuman: followupHuman } : {}),
2766
+ ...(firmness ? { firmness } : {}),
2767
+ ...(topic ? { topic } : {}),
2768
+ ...(decidedBy ? { decidedBy } : {}),
2738
2769
  };
2739
2770
  }
2771
+ /**
2772
+ * 記憶 v2 の決定の新しい欄の読み手(#1094)。
2773
+ *
2774
+ * 🔴 **写し。正本は cortex-engine `scripts/gold/decision-v2-fields.mjs` の `decisionV2FieldsOf`**。
2775
+ * 同じバイト列の表 `test/fixtures/decision-v2-fields-contract.json` を両リポに置き、
2776
+ * `test/supersedes-view.test.mjs` の「[契約] …」が全ケースを流す。規則を変えるときは engine の正本・この写し・表を同じ日に直す
2777
+ */
2778
+ /** 決定の固さの語彙(並びも正本と同じ) */
2779
+ exports.DECISION_FIRMNESS = ["確定", "仮", "検討中"];
2780
+ /** `decided_by` の要素が持ってよい鍵 */
2781
+ const DECIDED_BY_KEYS = ["member", "name", "url"];
2782
+ const MEMBER_ID_RE = /^member:\S/;
2783
+ const DECIDED_BY_URL_RE = /^https?:\/\/\S+$/;
2784
+ const isText = (v) => typeof v === "string" && v.trim() !== "";
2785
+ /** frontmatter の `firmness` → 語彙の 1 語。語彙外・文字列でないものは undefined(綴りはそのまま比べる) */
2786
+ function decisionFirmnessOf(value) {
2787
+ return exports.DECISION_FIRMNESS.includes(value) ? value : undefined;
2788
+ }
2789
+ /** frontmatter の `topic` → 論点の札の名前(前後の空白を落とす)。空・文字列でないものは undefined */
2790
+ function decisionTopicOf(value) {
2791
+ return isText(value) ? value.trim() : undefined;
2792
+ }
2793
+ /** `decided_by` の要素 1 つ。読めなければ null(文字列・知らない鍵・member と name/url の同居・member: で始まらない・空の name・http(s) でない url) */
2794
+ function decidedByEntryOf(entry) {
2795
+ if (entry == null || typeof entry !== "object" || Array.isArray(entry))
2796
+ return null;
2797
+ const o = entry;
2798
+ if (Object.keys(o).some((k) => !DECIDED_BY_KEYS.includes(k)))
2799
+ return null;
2800
+ const has = (k) => Object.prototype.hasOwnProperty.call(o, k);
2801
+ if (has("member")) {
2802
+ if (has("name") || has("url"))
2803
+ return null;
2804
+ return typeof o.member === "string" && MEMBER_ID_RE.test(o.member) ? { member: o.member } : null;
2805
+ }
2806
+ if (!has("name") || !isText(o.name))
2807
+ return null;
2808
+ if (o.url == null)
2809
+ return { name: o.name };
2810
+ return typeof o.url === "string" && DECIDED_BY_URL_RE.test(o.url) ? { name: o.name, url: o.url } : null;
2811
+ }
2812
+ /**
2813
+ * frontmatter の `decided_by` → 決めた人の並び。🔴 形の崩れた要素は**飛ばす**。
2814
+ * 欄がリストでなければ undefined(無いものとして扱い、旧い `deciders` を出す)。空のリストは []
2815
+ */
2816
+ function decidedByOf(value) {
2817
+ if (!Array.isArray(value))
2818
+ return undefined;
2819
+ return value.map(decidedByEntryOf).filter((e) => e !== null);
2820
+ }
2740
2821
  /**
2741
2822
  * 解決した参照 → 書かれていた値(#968 tools 1)。つながり画面は点の鍵をこの値から決める
2742
2823
  * (`site-data` に載せない——機械の語を画面の材料に増やさない)
@@ -13,9 +13,10 @@
13
13
  * 対応表で URL に引けるものは同じ鍵に寄せる——今の ID と新しい URL の根拠が混ざっても 1 つの点になる。
14
14
  */
15
15
  Object.defineProperty(exports, "__esModule", { value: true });
16
- exports.emptyEvidenceBook = void 0;
16
+ exports.MOVED_MEETINGS_FILE = exports.emptyEvidenceBook = void 0;
17
17
  exports.minuteFoldKey = minuteFoldKey;
18
18
  exports.minuteLabel = minuteLabel;
19
+ exports.movedMeetingNamesOf = movedMeetingNamesOf;
19
20
  exports.minuteRefOfPath = minuteRefOfPath;
20
21
  exports.slackChannelOfLabel = slackChannelOfLabel;
21
22
  exports.backlogTailOf = backlogTailOf;
@@ -55,6 +56,34 @@ function minuteLabel(id) {
55
56
  const d = m[2];
56
57
  return `${lastSegment(m[1])} ${d.slice(0, 4)}-${d.slice(4, 6)}-${d.slice(6, 8)}`;
57
58
  }
59
+ /**
60
+ * 片付けで移した会議の表(engine が目次と同じコミットで置く・cortex-engine #968 R-6・A-R6-3)。
61
+ * 写しを片付けた案件だけが持つ。無ければ今どおり(移した議事録のリンクは種類の既定の名前で出る)
62
+ */
63
+ exports.MOVED_MEETINGS_FILE = ".cortex/mirror-moved-meetings.json";
64
+ /**
65
+ * 移した会議の表 → `[移した URL, 会議の名前(「定例名 日付」)]` の並び。形が違う表・行は読まない(名前を当て推量しない)。
66
+ * 名前は `minuteLabel`(`minute:` の根拠と同じ 1 本)で作る——同じ会議が `minute:` で書かれても URL で書かれても同じ名前になる
67
+ */
68
+ function movedMeetingNamesOf(table) {
69
+ if (!table || typeof table !== "object")
70
+ return [];
71
+ const t = table;
72
+ if (t.version !== 1 || !Array.isArray(t.meetings))
73
+ return [];
74
+ const out = [];
75
+ for (const row of t.meetings) {
76
+ if (!row || typeof row !== "object")
77
+ continue;
78
+ const { url, ref } = row;
79
+ if (typeof url !== "string" || !/^https:\/\/\S+$/.test(url) || typeof ref !== "string")
80
+ continue;
81
+ const name = minuteLabel(ref);
82
+ if (name)
83
+ out.push([url, name]);
84
+ }
85
+ return out;
86
+ }
58
87
  /**
59
88
  * 議事録の写しのパス(`会議/{…}/{定例名}/{YYYYMMDD}/{…}minutes{…}.md`)→ `minute:{定例名}:{YYYYMMDD}`。
60
89
  * `buildTargetUrlMap` の `minute:` の組み方と同じ規約(定例名は日付の 1 つ上の段)
package/dist/render.js CHANGED
@@ -5314,6 +5314,15 @@ a.pager-btn:hover { text-decoration: none; }
5314
5314
  .followup-section { margin: 14px 0 4px; }
5315
5315
  /* 置き換え(#699 F3)。印は「その後」の置き換え済みと同じ色(同じ意味なので) */
5316
5316
  .badge.retired { background: #f5f5f4; color: #57534e; border: 1px solid #d6d3d1; }
5317
+ /* 論点・固さ・「今」(#1094)。欄の無い記録には付かない */
5318
+ .badge.now { background: #1d4ed8; color: #fff; }
5319
+ .card.is-now { border-left: 3px solid #1d4ed8; }
5320
+ .card.is-faded { opacity: .55; }
5321
+ .badge.topic { background: #f5f3ff; color: #5b21b6; border: 1px solid #ddd6fe; font-weight: 500; }
5322
+ .badge.firmness-fixed { background: #ecfdf5; color: #047857; border: 1px solid #a7f3d0; }
5323
+ .badge.firmness-tentative { background: #fffbeb; color: #92400e; border: 1px solid #fde68a; }
5324
+ .badge.firmness-open { background: #f8fafc; color: #475569; border: 1px dashed #cbd5e1; }
5325
+ .decision-topic { margin-bottom: 4px; }
5317
5326
  .supersede-section { margin: 14px 0 4px; }
5318
5327
  .supersede-list { list-style: none; margin: 0 0 8px; padding: 0; }
5319
5328
  .supersede-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: baseline; font-size: 13.5px; line-height: 1.7; padding: 2px 0; }
@@ -7360,6 +7369,110 @@ ${calendarAddUrl.toString()}
7360
7369
  return box;
7361
7370
  }
7362
7371
 
7372
+ /* -------------------------------------------------------------------------
7373
+ * 論点・固さ・決めた人(記憶 v2・cortex-engine #1094)
7374
+ * -------------------------------------------------------------------------
7375
+ * 欄(topic / firmness / decidedBy)は**ビルド側で語彙・形を確かめてある**。欄の無い記録では
7376
+ * キーごと無いので、画面は有無だけを見る。🔴 **欄の無い記録だけの案件では要素を 1 つも作らない**
7377
+ * (絞り込み・「今」・固さの札・決めた人の節が出ない=今と同じ DOM)。
7378
+ * 「置き換えられた」の判定は isRetired(ビルドが supersedes.ts で組んだ答え)をそのまま使う。
7379
+ */
7380
+ function hasTopic(rec) {
7381
+ return !!(rec && typeof rec.topic === "string" && rec.topic);
7382
+ }
7383
+ /** 論点の札の一覧(札を持つ決定が無ければ空) */
7384
+ function topicOptions(items) {
7385
+ var seen = {};
7386
+ var out = [];
7387
+ (items || []).forEach(function (d) {
7388
+ if (!hasTopic(d) || Object.prototype.hasOwnProperty.call(seen, d.topic)) return;
7389
+ seen[d.topic] = true;
7390
+ out.push(d.topic);
7391
+ });
7392
+ return out.sort(function (a, b) { return a.localeCompare(b, "ja"); });
7393
+ }
7394
+ /** 新しい順(決定日の降順 → ID の降順)。日付の無いものは最後 */
7395
+ function newestDecisionFirst(a, b) {
7396
+ if (a.date !== b.date) {
7397
+ if (!a.date) return 1;
7398
+ if (!b.date) return -1;
7399
+ return a.date < b.date ? 1 : -1;
7400
+ }
7401
+ return a.id < b.id ? 1 : a.id > b.id ? -1 : 0;
7402
+ }
7403
+ /**
7404
+ * 札ごとの「今」の決定(一番新しく、置き換えられていない 1 件)の ID の集合。
7405
+ * 削除済みは数えない(一覧と同じ材料)
7406
+ */
7407
+ function nowDecisionIds() {
7408
+ var best = {};
7409
+ visibleRecords(SITE.decisions).forEach(function (d) {
7410
+ if (!hasTopic(d) || isRetired(d)) return;
7411
+ var cur = best[d.topic];
7412
+ if (!cur || newestDecisionFirst(d, cur) < 0) best[d.topic] = d;
7413
+ });
7414
+ var ids = {};
7415
+ Object.keys(best).forEach(function (t) { ids[best[t].id] = true; });
7416
+ return ids;
7417
+ }
7418
+ function isNowDecision(rec) {
7419
+ return !!(rec && rec._kind === "decision" && hasTopic(rec) && nowDecisionIds()[rec.id]);
7420
+ }
7421
+ function nowBadge(rec) {
7422
+ return isNowDecision(rec) ? el("span", { class: "badge now", title: "この論点で今有効な決定", text: "今" }) : null;
7423
+ }
7424
+ /** 論点の札(一覧の行)。札が無ければ null */
7425
+ function topicBadge(rec) {
7426
+ return hasTopic(rec) ? el("span", { class: "badge topic", title: "論点", text: rec.topic }) : null;
7427
+ }
7428
+ var FIRMNESS_CLASS = { "確定": "fixed", "仮": "tentative", "検討中": "open" };
7429
+ /** 固さの小札。語彙外は出さない(ビルド側でも捨てている・二重の網) */
7430
+ function firmnessBadge(rec) {
7431
+ var f = rec && rec.firmness;
7432
+ if (typeof f !== "string" || !Object.prototype.hasOwnProperty.call(FIRMNESS_CLASS, f)) return null;
7433
+ return el("span", { class: "badge firmness firmness-" + FIRMNESS_CLASS[f], title: "固さ", text: f });
7434
+ }
7435
+ /** 置き換えられた決定を薄く出すか(論点の札を持つ決定だけ。欄の無い記録の見た目は変えない) */
7436
+ function fadedClass(rec) {
7437
+ return hasTopic(rec) && isRetired(rec) ? " is-faded" : "";
7438
+ }
7439
+ /** 決めた人 1 人の札。関係者が名簿にあればそのページへ、{name, url} は url があれば元の URL へ */
7440
+ function decidedByChip(who) {
7441
+ if (!who || typeof who !== "object") return null;
7442
+ if (typeof who.member === "string") {
7443
+ var rec = byId[who.member];
7444
+ var fallback = who.member.slice("member:".length);
7445
+ if (rec && rec._kind === "generic") {
7446
+ var a = el("a", { class: "chip decided-by-member", href: "?id=" + encodeURIComponent(who.member), text: humanTitle(rec.title) || fallback });
7447
+ a.addEventListener("click", function (ev) {
7448
+ if (ev.defaultPrevented || ev.button !== 0 || ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey) return;
7449
+ ev.preventDefault();
7450
+ go(who.member);
7451
+ });
7452
+ return a;
7453
+ }
7454
+ return el("span", { class: "chip", text: fallback });
7455
+ }
7456
+ if (typeof who.name !== "string" || !who.name) return null;
7457
+ var raw = typeof who.url === "string" ? who.url.toLowerCase() : "";
7458
+ var url = raw.indexOf("http://") === 0 || raw.indexOf("https://") === 0 ? who.url : "";
7459
+ if (url) return el("a", { class: "chip decided-by-link", href: url, target: "_blank", rel: "noopener", text: who.name });
7460
+ return el("span", { class: "chip", text: who.name });
7461
+ }
7462
+ /** 検索に使う決めた人の表記(decidedBy があればそちら、無ければ deciders) */
7463
+ function deciderWords(d) {
7464
+ if (Array.isArray(d.decidedBy) && d.decidedBy.length) {
7465
+ return d.decidedBy.map(function (w) {
7466
+ if (w && typeof w.member === "string") {
7467
+ var r = byId[w.member];
7468
+ return r && r.title ? r.title : w.member.slice("member:".length);
7469
+ }
7470
+ return w && typeof w.name === "string" ? w.name : "";
7471
+ }).join(" ");
7472
+ }
7473
+ return (d.deciders || []).join(" ");
7474
+ }
7475
+
7363
7476
  // 未確認が1件も無い一覧では絞り込みを出さない
7364
7477
  function hasUnverified(items) {
7365
7478
  return (items || []).some(function (r) { return machineOwned(confirmPathOf(r), r.rstatus); });
@@ -8680,7 +8793,7 @@ ${calendarAddUrl.toString()}
8680
8793
  }
8681
8794
  }
8682
8795
 
8683
- var listState = { tab: DEFAULT_TAB, q: "", category: "", month: "", status: "", page: 1 };
8796
+ var listState = { tab: DEFAULT_TAB, q: "", category: "", month: "", status: "", topic: "", page: 1 };
8684
8797
 
8685
8798
  // 一覧のページネーション(描画コスト対策。1ページあたりの件数)
8686
8799
  var PAGE_SIZE = 30;
@@ -19304,6 +19417,13 @@ ${calendarAddUrl.toString()}
19304
19417
  if (statusOptsD.length) {
19305
19418
  filters.appendChild(buildSelect("すべての状態", statusOptsD, listState.status, function (v) { listState.status = v; listState.page = 1; drawCards(); }, statusLabel));
19306
19419
  }
19420
+ // 論点の札(#1094)。札を持つ決定が 1 件も無い案件では出さない
19421
+ var topicOpts = topicOptions(visibleRecords(SITE.decisions));
19422
+ if (topicOpts.length) {
19423
+ var topicSel = buildSelect("すべての論点", topicOpts, listState.topic, function (v) { listState.topic = v; listState.page = 1; drawCards(); });
19424
+ topicSel.classList.add("topic-filter");
19425
+ filters.appendChild(topicSel);
19426
+ }
19307
19427
  app.appendChild(filters);
19308
19428
 
19309
19429
  var addBarD = aiAddBar("Decisions");
@@ -19335,16 +19455,25 @@ ${calendarAddUrl.toString()}
19335
19455
  var q = listState.q.trim().toLowerCase();
19336
19456
  // 削除済みは絞り込みの手前で落とす(分母も一緒に減るので「12/13件」が残らない)
19337
19457
  var all = visibleRecords(SITE.decisions);
19338
- return all.filter(function (d) {
19458
+ var out = all.filter(function (d) {
19339
19459
  if (listState.month && d.date.slice(0, 7) !== listState.month) return false;
19340
19460
  if (listState.category && d.category !== listState.category) return false;
19341
19461
  if (!matchesStatusFilter(d, !!SITE.decisionFollowup)) return false;
19462
+ if (!matchesTopicFilter(d)) return false;
19342
19463
  if (q) {
19343
- var hay = (d.title + " " + d.summary + " " + d.searchText + " " + d.deciders.join(" ")).toLowerCase();
19464
+ var hay = (d.title + " " + d.summary + " " + d.searchText + " " + deciderWords(d)).toLowerCase();
19344
19465
  if (hay.indexOf(q) === -1) return false;
19345
19466
  }
19346
19467
  return true;
19347
19468
  });
19469
+ // 同じ論点の中は新しい順(決定日の降順 → ID の降順)
19470
+ if (listState.topic) out.sort(newestDecisionFirst);
19471
+ return out;
19472
+ }
19473
+ /** 論点の絞り込み(#1094)。選んでいなければ全件、選んだら同じ札の決定だけ */
19474
+ function matchesTopicFilter(d) {
19475
+ if (!listState.topic) return true;
19476
+ return hasTopic(d) && d.topic === listState.topic;
19348
19477
  }
19349
19478
 
19350
19479
  function drawCards() {
@@ -19367,6 +19496,7 @@ ${calendarAddUrl.toString()}
19367
19496
 
19368
19497
  pg.slice.forEach(function (d) {
19369
19498
  var meta = el("div", { class: "card-meta" }, [
19499
+ nowBadge(d),
19370
19500
  draftBadge(d),
19371
19501
  proposalBadge(d),
19372
19502
  followupBadge(d),
@@ -19374,13 +19504,15 @@ ${calendarAddUrl.toString()}
19374
19504
  latestBadge(d),
19375
19505
  el("span", { class: "mono", text: d.date || "----" }),
19376
19506
  catBadge(d.category),
19507
+ firmnessBadge(d),
19508
+ topicBadge(d),
19377
19509
  ]);
19378
19510
  var eb = fmErrorBadge(d);
19379
19511
  if (eb) meta.appendChild(eb);
19380
19512
  var traceD = el("a", { class: "mc-trace", href: "?tab=graph", text: "🕸 つながり" });
19381
19513
  traceD.addEventListener("click", function (ev) { ev.preventDefault(); ev.stopPropagation(); openExplorerFor(d); });
19382
19514
  meta.appendChild(traceD);
19383
- var card = el("div", { class: "card", title: d.id, "data-record-id": d.id }, [
19515
+ var card = el("div", { class: "card" + fadedClass(d) + (isNowDecision(d) ? " is-now" : ""), title: d.id, "data-record-id": d.id }, [
19384
19516
  meta,
19385
19517
  // 消したレコードは一覧でも取り消し線(次のビルドまで一覧には残る)
19386
19518
  cardTitleLink(d, d.deleted ? " is-deleted" : ""),
@@ -20404,6 +20536,9 @@ ${calendarAddUrl.toString()}
20404
20536
  // 話題の線(決定・ルール・関係者 → 用語・engine #551 の about)。名前の一致で機械が付けた手がかりなので、
20405
20537
  // 根拠の線と同じく細く薄い「ゆるいつながり」として描き、凡例は日本語の 1 行(栓 about_relations の on の条件)
20406
20538
  about: { color: "#ddd6fe", label: "話題", kind: "topic" },
20539
+ // 決めた人の線(決定 → 関係者・#1094)。relations ではなく decided_by から引く。
20540
+ // 🔴 凡例には**この線がグラフにあるときだけ**出す(欄の無い案件の凡例を変えない)
20541
+ decided_by: { color: "#f472b6", label: "決めた人", kind: "gold", onlyWhenPresent: true },
20407
20542
  };
20408
20543
  var WEAK_RELS = {};
20409
20544
  var REL_COLOR = {};
@@ -20471,7 +20606,10 @@ ${calendarAddUrl.toString()}
20471
20606
  // 線(関係)の色凡例。グラフ内の有無に関わらず全種別を、Goldどうし/根拠の2群で表示する
20472
20607
  [["gold", "線(Goldどうし):"], ["evidence", "線(根拠):"], ["topic", "線(話題):"]].forEach(function (pair) {
20473
20608
  var kind = pair[0], heading = pair[1];
20474
- var keys = Object.keys(REL_META).filter(function (r) { return REL_META[r].kind === kind; });
20609
+ var keys = Object.keys(REL_META).filter(function (r) {
20610
+ if (REL_META[r].kind !== kind) return false;
20611
+ return !REL_META[r].onlyWhenPresent || graph.edges.some(function (e) { return e.rel === r; });
20612
+ });
20475
20613
  if (!keys.length) return;
20476
20614
  legend.appendChild(el("span", { class: "sep", text: heading }));
20477
20615
  keys.forEach(function (r) {
@@ -20603,6 +20741,12 @@ ${calendarAddUrl.toString()}
20603
20741
  (inAdj[e.b.id] = inAdj[e.b.id] || []).push(e.a.id);
20604
20742
  });
20605
20743
 
20744
+ // 描き直しの間引き(engine #551 のカクつき対策)。操作・検索・期間の切り替え・ホバーの波の間は毎フレーム描き、
20745
+ // それが済んで止まっている間は、星の瞬き(ゆっくり)のためだけに IDLE_FRAME_MS ごとに描く
20746
+ var IDLE_FRAME_MS = 100;
20747
+ var activeUntil = 0;
20748
+ function poke(ms) { activeUntil = Math.max(activeUntil, performance.now() + ms); }
20749
+
20606
20750
  // 検索: 一致ノード(matched)が光り、その隣接(lit)は残し、他は減光する
20607
20751
  var matched = {}, lit = {}, searching = false;
20608
20752
  // 期間(engine #551)を選んでいるときは、その期間に生まれた・変わった記録だけが一致になる(キーワードと両方なら両方を満たす記録)
@@ -20611,6 +20755,7 @@ ${calendarAddUrl.toString()}
20611
20755
  var range = graphPeriodRange(graphState.period.unit === "all" ? null : graphState.period);
20612
20756
  searching = q.length > 0 || !!range;
20613
20757
  matched = {}; lit = {};
20758
+ poke(250);
20614
20759
  if (!searching) {
20615
20760
  countEl.textContent = "点 " + nodes.length + " / つながり " + edges.length;
20616
20761
  return;
@@ -20659,6 +20804,8 @@ ${calendarAddUrl.toString()}
20659
20804
  var UPSTREAM_HOPS = 1; // 参照元(入辺)を辿る深さ
20660
20805
  var SWEEP_STEP_MS = 120; // 波が1段(1ホップ)進むごとの間隔
20661
20806
  var SWEEP_GROW_MS = 200; // 辺・点が灯り切るまでの時間
20807
+ // ホバーの波が流れ切るまで毎フレーム描く時間(参照元 1 段+参照先 2 段=順番 3 まで+灯り切る時間に余裕を見る)
20808
+ var HOVER_SWEEP_MS = (DOWNSTREAM_HOPS + UPSTREAM_HOPS + 2) * SWEEP_STEP_MS + SWEEP_GROW_MS + 300;
20662
20809
  function computeFitTarget() {
20663
20810
  if (!nodes.length) return null;
20664
20811
  var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
@@ -20686,7 +20833,10 @@ ${calendarAddUrl.toString()}
20686
20833
  view.x += (t.x - view.x) * 0.18;
20687
20834
  view.y += (t.y - view.y) * 0.18;
20688
20835
  }
20836
+ // まだ目に見えて動いているか(止まっていれば描き直しを間引ける)
20837
+ return Math.abs(t.scale - view.scale) > view.scale * 0.0005 || Math.abs(t.x - view.x) > 0.1 || Math.abs(t.y - view.y) > 0.1;
20689
20838
  }
20839
+ var fitMoving = true;
20690
20840
 
20691
20841
  // ロード時に1回だけ: Goldの中心から外へ「ふわっ」と光が広がるイントロ。
20692
20842
  // 金色(#fcd34d)はホバー連鎖・検索の専用色なので、イントロは寒色で別物として見せる。
@@ -20700,6 +20850,7 @@ ${calendarAddUrl.toString()}
20700
20850
  resetBtn.addEventListener("click", function () {
20701
20851
  autoFit = true; // 全ノードが収まる表示へ滑らかに戻す
20702
20852
  resize();
20853
+ poke(250);
20703
20854
  if (!reduceMotion) introStart = performance.now(); // 中心から光が広がるイントロを再生
20704
20855
  });
20705
20856
  canvas.parentNode.appendChild(resetBtn);
@@ -20834,6 +20985,96 @@ ${calendarAddUrl.toString()}
20834
20985
  return n.recordId && n.glow !== null ? 0.4 + 0.6 * n.glow : 1;
20835
20986
  }
20836
20987
 
20988
+ // 点の光(にじみ・shadowBlur)は、点ごとに毎フレーム描くと 700 点で 1 フレーム 30ms を超える(engine #551 のカクつき)。
20989
+ // にじみだけを小さな絵に前もって描いて使い回し、本体の円はその上に正確な半径で描く。
20990
+ // 絵は画面の画素(device px)で作る(shadowBlur は座標変換に影響されない=元の描き方と同じ幅)。
20991
+ // 半径は 0.25px・にじみの幅は小さいうちは 0.5 刻み、大きいと約 4% 刻みに丸めて、絵の数を抑える
20992
+ // (小さい点のにじみの明るさは円の面積に比例するので、1px 刻みでは引いた表示で明るさが ±数十% ずれる)
20993
+ var TAU = Math.PI * 2;
20994
+ // 使い回しの置き場は「最近使った順」の上限つき(上限を超えたら一番古いものから捨てる=ズームし続けても一度に作り直さない)
20995
+ function spriteStore(max) {
20996
+ var m = new Map();
20997
+ return {
20998
+ get: function (k) { var v = m.get(k); if (v) { m.delete(k); m.set(k, v); } return v; },
20999
+ set: function (k, v) { m.set(k, v); if (m.size > max) m.delete(m.keys().next().value); },
21000
+ };
21001
+ }
21002
+ var haloCache = spriteStore(800);
21003
+ // これより大きいにじみ(大きく寄ったとき・画面にわずかな点しか無い)は絵にせず直接描く(大きな絵を溜めない)
21004
+ var HALO_SPRITE_MAX = 512;
21005
+ function haloBlurStep(blur) {
21006
+ if (blur < 0.25) return 0;
21007
+ if (blur <= 12) return Math.round(blur * 2) / 2;
21008
+ return Math.round(12 * Math.pow(1.04, Math.round(Math.log(blur / 12) / Math.log(1.04))) * 2) / 2;
21009
+ }
21010
+ function haloSprite(color, R, blur) {
21011
+ var bq = haloBlurStep(blur);
21012
+ if (!bq) return null;
21013
+ var rq = Math.max(0.25, Math.round(R * 4) / 4);
21014
+ var half = Math.ceil(rq + bq * 1.5 + 2); // にじみ(σ=blur/2)が見える範囲 ≒ 3σ
21015
+ if (half * 2 > HALO_SPRITE_MAX) return false;
21016
+ var key = color + "|" + rq + "|" + bq;
21017
+ var sp = haloCache.get(key);
21018
+ if (sp) return sp;
21019
+ var size = half * 2;
21020
+ var c = document.createElement("canvas");
21021
+ c.width = size; c.height = size;
21022
+ var g = c.getContext("2d");
21023
+ if (!g) return null;
21024
+ // 円は絵の外(左へ size ずらす)に描き、影だけを絵の中央へ落とす=にじみだけが残る
21025
+ g.shadowColor = color;
21026
+ g.shadowBlur = bq;
21027
+ g.shadowOffsetX = size;
21028
+ g.fillStyle = color;
21029
+ g.beginPath();
21030
+ g.arc(half - size, half, rq, 0, TAU);
21031
+ g.fill();
21032
+ sp = { c: c, h: half };
21033
+ haloCache.set(key, sp);
21034
+ return sp;
21035
+ }
21036
+ // X・Y は画素の境目に揃えた値を渡す(半端な位置だと絵の引き伸ばしが入り、1 枚ごとに何倍も重くなる)
21037
+ function drawHalo(color, R, blur, X, Y) {
21038
+ var sp = haloSprite(color, R, blur);
21039
+ if (sp) { ctx.drawImage(sp.c, X - sp.h, Y - sp.h); return; }
21040
+ if (sp === false) {
21041
+ ctx.save();
21042
+ ctx.shadowColor = color;
21043
+ ctx.shadowBlur = blur;
21044
+ ctx.fillStyle = color;
21045
+ ctx.beginPath();
21046
+ ctx.arc(X, Y, R, 0, TAU);
21047
+ ctx.fill();
21048
+ ctx.restore();
21049
+ }
21050
+ }
21051
+ // ラベルも文字を絵にして使い回す(検索で数百のラベルを毎フレーム文字から描くと 1 フレーム 100ms を超える)。
21052
+ // 文字の大きさは画面の画素で一定(11px)なので、ズームしても同じ絵が使える
21053
+ var labelCache = spriteStore(3000);
21054
+ function labelFont(bold) { return (bold ? "600 " : "") + (11 * dpr) + "px 'Noto Sans JP', sans-serif"; }
21055
+ function drawLabel(text, color, bold, X, baseY) {
21056
+ var key = color + "|" + (bold ? 1 : 0) + "|" + text;
21057
+ var sp = labelCache.get(key);
21058
+ if (!sp) {
21059
+ var font = labelFont(bold);
21060
+ var c = document.createElement("canvas");
21061
+ var g = c.getContext("2d");
21062
+ if (!g) return;
21063
+ g.font = font;
21064
+ var asc = Math.ceil(13 * dpr), desc = Math.ceil(4 * dpr); // 11px の文字の上下+少しの余白
21065
+ c.width = Math.max(1, Math.ceil(g.measureText(text).width) + 2);
21066
+ c.height = asc + desc;
21067
+ g.font = font; // 大きさを変えると描き方の設定が戻るので、もう一度
21068
+ g.fillStyle = color;
21069
+ g.fillText(text, 0, asc);
21070
+ sp = { c: c, asc: asc };
21071
+ labelCache.set(key, sp);
21072
+ }
21073
+ ctx.drawImage(sp.c, Math.round(X), Math.round(baseY - sp.asc));
21074
+ }
21075
+ // ホバーの連鎖(参照先・参照元)は触った点が変わったときだけ辿り直す
21076
+ var linFor = null, linCache = null;
21077
+
20837
21078
  function draw() {
20838
21079
  var rect = canvas.getBoundingClientRect();
20839
21080
  var now = performance.now();
@@ -20862,7 +21103,7 @@ ${calendarAddUrl.toString()}
20862
21103
  ctx.fill();
20863
21104
  });
20864
21105
  ctx.globalAlpha = 1;
20865
- if (autoFit) applyFit(false); // 全ノードが収まる位置・倍率へ滑らかに追従
21106
+ fitMoving = autoFit ? !!applyFit(false) : false; // 全ノードが収まる位置・倍率へ滑らかに追従
20866
21107
  ctx.translate(view.x, view.y);
20867
21108
  ctx.scale(view.scale, view.scale);
20868
21109
 
@@ -20882,7 +21123,8 @@ ${calendarAddUrl.toString()}
20882
21123
  }
20883
21124
  return hop;
20884
21125
  }
20885
- if (hovered) {
21126
+ if (hovered && linFor === hovered) lin = linCache;
21127
+ else if (hovered) {
20886
21128
  var downHop = bfsDir(outAdj, DOWNSTREAM_HOPS); // 参照先
20887
21129
  var upHop = bfsDir(inAdj, UPSTREAM_HOPS); // 参照元
20888
21130
  var maxUp = 0;
@@ -20897,6 +21139,7 @@ ${calendarAddUrl.toString()}
20897
21139
  if (id === hovered.id || lin[id] !== undefined) return;
20898
21140
  lin[id] = { order: maxUp + downHop[id], hop: downHop[id], dir: "down" };
20899
21141
  });
21142
+ linFor = hovered; linCache = lin;
20900
21143
  }
20901
21144
  function hopAlpha(d) {
20902
21145
  return d <= 1 ? 1 : Math.max(0.6, 1 - d * 0.1);
@@ -20914,8 +21157,15 @@ ${calendarAddUrl.toString()}
20914
21157
  relates_to: "rgba(148,163,184,0.6)", supersedes: "rgba(248,113,113,0.9)",
20915
21158
  reference: "rgba(180,196,230,0.32)", source: "rgba(180,196,230,0.32)", link: "rgba(180,196,230,0.22)",
20916
21159
  about: "rgba(196,181,253,0.22)",
21160
+ decided_by: "rgba(244,114,182,0.75)",
20917
21161
  };
21162
+ // 画面に入っている範囲(世界座標)。両端が同じ側の外にある線は描かない
21163
+ var em = 4 / view.scale; // 線の太さの分の余白(画面で 4px)
21164
+ var wx0 = -view.x / view.scale - em, wy0 = -view.y / view.scale - em;
21165
+ var wx1 = (rect.width - view.x) / view.scale + em, wy1 = (rect.height - view.y) / view.scale + em;
20918
21166
  edges.forEach(function (e) {
21167
+ if ((e.a.x < wx0 && e.b.x < wx0) || (e.a.x > wx1 && e.b.x > wx1) ||
21168
+ (e.a.y < wy0 && e.b.y < wy0) || (e.a.y > wy1 && e.b.y > wy1)) return;
20919
21169
  // ホバー中: リネージに入った辺だけ、波の順に「order の低い側→高い側」へ伸びながら金色に。
20920
21170
  if (hovered) {
20921
21171
  var la = lin[e.a.id], lb = lin[e.b.id];
@@ -20955,6 +21205,10 @@ ${calendarAddUrl.toString()}
20955
21205
  ctx.stroke();
20956
21206
  });
20957
21207
 
21208
+ // 点は画面の画素の座標で描く(にじみの絵をそのまま置くため)
21209
+ var k = view.scale * dpr, ox = view.x * dpr, oy = view.y * dpr;
21210
+ var cw = canvas.width, ch = canvas.height;
21211
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
20958
21212
  nodes.forEach(function (n) {
20959
21213
  var style = GROUP_STYLE[n.group] || GROUP_STYLE.phantom;
20960
21214
  var alphaN = 1;
@@ -20977,46 +21231,45 @@ ${calendarAddUrl.toString()}
20977
21231
  }
20978
21232
  var dim = alphaN < 0.3;
20979
21233
  var isGold = !!n.recordId;
21234
+ var r0 = nodeRadius(n);
21235
+ // 画面の画素での位置と半径(ラベル・輪・にじみはすべて画素で描く)
21236
+ // 中心は画素の境目に揃える(にじみの絵と本体の円の中心を合わせる。dpr 2 なら 0.25px 以内の差)
21237
+ var X = Math.round(n.x * k + ox), Y = Math.round(n.y * k + oy), R = r0 * k;
21238
+ var inChain = hovered && n !== hovered && lin[n.id] !== undefined;
21239
+ var blur =
21240
+ (isGold ? (n.glow === null || matched[n.id] ? Math.max(16, n.glow === null ? 16 : 6 + 22 * n.glow) : 6 + 22 * n.glow) : 7) * (dim ? 0.3 : 1) * (inChain ? 1.7 : 1) * (1 + introFlash * 1.8);
21241
+ // ラベル: Gold層レコードはズームイン時のみ表示(引いて全体表示中は出さない)。
21242
+ // それ以外はホバーの連鎖に入ったときか検索ヒット時のみ(ズームに関係なく表示)。
21243
+ var showLabel = (n.recordId && view.scale >= LABEL_MIN_SCALE) || (hovered && lin[n.id] !== undefined && sweepRamp(lin[n.id]) > 0.15) || (searching && lit[n.id]);
21244
+ // 画面の外の点は描かない(にじみの届く幅・輪・右に出るラベルの幅を見込む)
21245
+ var reach = R + Math.max(blur, 18) * 1.5 + 6 * dpr;
21246
+ if (X + reach + (showLabel ? 300 * dpr : 0) < 0 || X - reach > cw || Y + reach < 0 || Y - reach > ch) return;
20980
21247
  // Gold以外の星はゆっくり瞬く
20981
21248
  var tw = isGold ? 1 : 0.72 + 0.28 * (0.5 + 0.5 * Math.sin(now * 0.0017 + n.phase));
20982
21249
  ctx.globalAlpha = alphaN * tw * glowAlpha(n);
20983
- var r0 = nodeRadius(n);
20984
21250
  var fill = isGold ? style.color : "#c9d6f2";
20985
21251
  // 検索一致ノードとホバー中のノードは金色のリングで発光させる
20986
21252
  if ((matched[n.id] || n === hovered) && !dim) {
20987
- ctx.save();
20988
- ctx.shadowColor = "#fcd34d";
20989
- ctx.shadowBlur = 18;
21253
+ drawHalo("#fcd34d", R + 2 * dpr, 18, X, Y);
20990
21254
  ctx.beginPath();
20991
- ctx.arc(n.x, n.y, r0 + 2 / view.scale, 0, Math.PI * 2);
21255
+ ctx.arc(X, Y, R + 2 * dpr, 0, TAU);
20992
21256
  ctx.fillStyle = fill;
20993
21257
  ctx.fill();
20994
- ctx.restore();
20995
21258
  ctx.strokeStyle = "#fcd34d";
20996
- ctx.lineWidth = 2.5 / view.scale;
21259
+ ctx.lineWidth = 2.5 * dpr;
20997
21260
  ctx.beginPath();
20998
- ctx.arc(n.x, n.y, r0 + 3.5 / view.scale, 0, Math.PI * 2);
21261
+ ctx.arc(X, Y, R + 3.5 * dpr, 0, TAU);
20999
21262
  ctx.stroke();
21000
21263
  }
21001
21264
  // 星本体(色のにじみ=グロー付き)
21002
- var inChain = hovered && n !== hovered && lin[n.id] !== undefined;
21003
- ctx.save();
21004
- ctx.shadowColor =
21005
- introFlash > 0.01 ? "#bcd0ff" : isGold ? style.color : "#9fb4e8";
21006
- ctx.shadowBlur =
21007
- (isGold ? (n.glow === null || matched[n.id] ? Math.max(16, n.glow === null ? 16 : 6 + 22 * n.glow) : 6 + 22 * n.glow) : 7) * (dim ? 0.3 : 1) * (inChain ? 1.7 : 1) * (1 + introFlash * 1.8);
21265
+ drawHalo(introFlash > 0.01 ? "#bcd0ff" : isGold ? style.color : "#9fb4e8", R, blur, X, Y);
21008
21266
  ctx.beginPath();
21009
- ctx.arc(n.x, n.y, r0, 0, Math.PI * 2);
21267
+ ctx.arc(X, Y, R, 0, TAU);
21010
21268
  ctx.fillStyle = fill;
21011
21269
  ctx.fill();
21012
- ctx.restore();
21013
- // ラベル: Gold層レコードはズームイン時のみ表示(引いて全体表示中は出さない)。
21014
- // それ以外はホバーの連鎖に入ったときか検索ヒット時のみ(ズームに関係なく表示)。
21015
- if ((n.recordId && view.scale >= LABEL_MIN_SCALE) || (hovered && lin[n.id] !== undefined && sweepRamp(lin[n.id]) > 0.15) || (searching && lit[n.id])) {
21016
- ctx.fillStyle = dim ? "#3d4763" : matched[n.id] ? "#fde68a" : isGold ? "#e7edfb" : "#9fb0d6";
21017
- ctx.font = (matched[n.id] ? "600 " : "") + (11 / view.scale) + "px 'Noto Sans JP', sans-serif";
21270
+ if (showLabel) {
21018
21271
  var label = n.label.length > 24 ? n.label.slice(0, 23) + "…" : n.label;
21019
- ctx.fillText(label, n.x + nodeRadius(n) + 4 / view.scale, n.y + 4 / view.scale);
21272
+ drawLabel(label, dim ? "#3d4763" : matched[n.id] ? "#fde68a" : isGold ? "#e7edfb" : "#9fb0d6", !!matched[n.id], X + R + 4 * dpr, Y + 4 * dpr);
21020
21273
  }
21021
21274
  });
21022
21275
  ctx.globalAlpha = 1;
@@ -21054,7 +21307,7 @@ ${calendarAddUrl.toString()}
21054
21307
  return;
21055
21308
  }
21056
21309
  var nh = nodeAt(ev.clientX, ev.clientY);
21057
- if (nh !== hovered) { hovered = nh; hoverStart = nh ? performance.now() : 0; } // 別ノードでスイープ再生
21310
+ if (nh !== hovered) { hovered = nh; hoverStart = nh ? performance.now() : 0; poke(HOVER_SWEEP_MS); } // 別ノードでスイープ再生
21058
21311
  canvas.style.cursor = hovered ? "pointer" : "grab";
21059
21312
  });
21060
21313
  canvas.addEventListener("mousedown", function (ev) {
@@ -21086,6 +21339,7 @@ ${calendarAddUrl.toString()}
21086
21339
  view.x = mx - (mx - view.x) * factor;
21087
21340
  view.y = my - (my - view.y) * factor;
21088
21341
  view.scale *= factor;
21342
+ poke(250);
21089
21343
  }, { passive: false });
21090
21344
 
21091
21345
  // ビルド時に座標計算済みなら、ウォームアップ(重い同期計算)を完全に省略する=開いた瞬間ゼロ待ち。
@@ -21104,10 +21358,17 @@ ${calendarAddUrl.toString()}
21104
21358
  if (!running || !document.body.contains(canvas)) { running = false; return; }
21105
21359
  // 収束後(alpha減衰しきり・無操作)は O(n²) の物理計算 tick を止める。これが「固まり続ける」主因。
21106
21360
  // ドラッグ/パン中は動かす必要があるので回す。描画(draw)はホバー演出・イントロのため毎フレーム続ける。
21107
- if (alpha > 0.004 || draggingNode || panning) tick();
21108
- draw();
21361
+ var moving = alpha > 0.004 || draggingNode || panning;
21362
+ if (moving) tick();
21363
+ // 動いている・操作の直後・イントロ・自動フレーミングの途中は毎フレーム描く。止まっている間は瞬きのためだけに間引いて描く(engine #551)
21364
+ var now = performance.now();
21365
+ if (moving || introStart || fitMoving || now < activeUntil || now - lastDrawAt >= IDLE_FRAME_MS) {
21366
+ lastDrawAt = now;
21367
+ draw();
21368
+ }
21109
21369
  requestAnimationFrame(loop);
21110
21370
  }
21371
+ var lastDrawAt = 0;
21111
21372
  loop();
21112
21373
  }
21113
21374
 
@@ -21148,12 +21409,14 @@ ${calendarAddUrl.toString()}
21148
21409
  app.appendChild(head);
21149
21410
 
21150
21411
  var meta = el("div", { class: "detail-meta" }, [
21412
+ nowBadge(d),
21151
21413
  draftBadge(d),
21152
21414
  proposalBadge(d),
21153
21415
  retiredBadge(d),
21154
21416
  latestBadge(d),
21155
21417
  el("span", { class: "mono", text: d.date || "----" }),
21156
21418
  catBadge(d.category),
21419
+ firmnessBadge(d),
21157
21420
  ]);
21158
21421
  meta.title = d.id;
21159
21422
  meta.setAttribute("data-record-id", d.id);
@@ -21173,7 +21436,18 @@ ${calendarAddUrl.toString()}
21173
21436
  app.appendChild(el("div", { class: "summary-box", text: d.summary }));
21174
21437
  }
21175
21438
 
21176
- if (d.deciders && d.deciders.length) {
21439
+ // 論点(#1094)。札の無い決定では何も作らない
21440
+ if (hasTopic(d)) {
21441
+ app.appendChild(el("div", { class: "section-label", text: "論点" }));
21442
+ app.appendChild(el("div", { class: "decision-topic" }, [el("span", { class: "chip", text: d.topic })]));
21443
+ }
21444
+
21445
+ // 決めた人(#1094)。decided_by に読めた人がいればそちらを出し、いなければ(欄が無い・空・全部崩れ)今どおり deciders
21446
+ var whoChips = Array.isArray(d.decidedBy) ? d.decidedBy.map(decidedByChip).filter(Boolean) : [];
21447
+ if (whoChips.length) {
21448
+ app.appendChild(el("div", { class: "section-label", text: "決めた人" }));
21449
+ app.appendChild(el("div", { class: "deciders decided-by" }, whoChips));
21450
+ } else if (d.deciders && d.deciders.length) {
21177
21451
  app.appendChild(el("div", { class: "section-label", text: "決定者" }));
21178
21452
  app.appendChild(el("div", { class: "deciders" }, d.deciders.map(function (p) {
21179
21453
  return el("span", { class: "chip", text: p });
@@ -21222,6 +21496,7 @@ ${calendarAddUrl.toString()}
21222
21496
  based_on: "根拠", derived_from: "由来", relates_to: "関連",
21223
21497
  supersedes: "置換", superseded_by: "置換先", reference: "参照", source: "出典", link: "リンク",
21224
21498
  about: "話題",
21499
+ decided_by: "決めた人",
21225
21500
  };
21226
21501
  var explorerState = { folder: null, path: [], q: "", sx: 0, autoRight: false };
21227
21502
  // 索引もグラフ本体と同じ材料(削除済みを除いたもの)から作る。別々に間引くと、
@@ -21234,7 +21509,7 @@ ${calendarAddUrl.toString()}
21234
21509
  (adjDir[e.from] = adjDir[e.from] || []).push({ id: e.to, rel: e.rel, dir: "out" });
21235
21510
  (adjDir[e.to] = adjDir[e.to] || []).push({ id: e.from, rel: e.rel, dir: "in" });
21236
21511
  });
21237
- var REL_ORDER = ["supersedes", "based_on", "derived_from", "relates_to", "source", "reference", "link", "about"];
21512
+ var REL_ORDER = ["supersedes", "based_on", "derived_from", "relates_to", "decided_by", "source", "reference", "link", "about"];
21238
21513
 
21239
21514
  function recordText(rec) {
21240
21515
  if (!rec) return "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.114",
3
+ "version": "0.12.116",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,