@takagaki/cortex-decisions-viewer 0.12.115 → 0.12.117

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; }
@@ -6058,6 +6067,9 @@ button.ops-card-btn:disabled { opacity: .6; cursor: default; }
6058
6067
  .confirm-body-label { font-size: 13px; color: var(--muted); }
6059
6068
  .modal-ok { color: #047857; font-weight: 600; }
6060
6069
  .modal-status-error { color: #dc2626; font-weight: 600; }
6070
+ .modal-reopen { margin-top: 6px; font-weight: 400; }
6071
+ .modal-reopen-link { font-weight: 600; }
6072
+ .modal-reopen-note { color: var(--muted); font-size: 12.5px; margin-top: 2px; }
6061
6073
  .modal-paths { margin-top: 8px; display: flex; flex-direction: column; gap: 4px; }
6062
6074
  .modal-paths-label { font-size: 12px; color: var(--muted); }
6063
6075
  .modal-path { font-size: 12.5px; word-break: break-all; color: var(--accent); }
@@ -6171,20 +6183,48 @@ const CLIENT_JS = `
6171
6183
  // 券は document.cookie から**1回だけ**読んでメモリに持つ。site-data にも localStorage にも書かない
6172
6184
  // (ビルド時の値ではなく、開いた人ごとに違う値なので)。
6173
6185
  //
6174
- // **1回だけ読むのは仕様。** ポータルからログアウトしても、開いたままのタブは券の期限まで
6186
+ // **期限内の券は1回だけ読むのが仕様。** ポータルからログアウトしても、開いたままのタブは券の期限まで
6175
6187
  // 名乗り続ける——署名付き Cookie と同じ性質で、券の寿命がそのまま名乗りの寿命になる。
6188
+ //
6189
+ // 🔴 **持っている券の期限(exp)が過ぎたら、Cookie を読み直す**(券が無かったときも読み直す・engine #1037)。
6190
+ // 切れた券は intake が必ず ACTOR_INVALID で断るので、持ち続けて得るものは無い。持ち続けると、
6191
+ // 案内どおり別のタブでポータルから開き直して Cookie が新しくなっても、このタブは古い券を送り続け、
6192
+ // 入力したフォームの[送信]が何度押しても通らない(10/1 に 3 分で 10 回断られた形)。
6193
+ // exp を読めない券は「切れていない」として扱う(読み直さない=今までどおり)
6176
6194
  var ACTOR_COOKIE = "cortex-actor";
6177
6195
  var actorTicketCache;
6196
+ /**
6197
+ * intake が名乗りの理由で断った券は、期限内に見えても次の呼び出しで Cookie を読み直す
6198
+ * (鍵の入れ替えで kid が外れた・端末の時計が遅れている、のとき・#1037 の実装レビュー🟡)。
6199
+ * Cookie が同じ値のままなら同じ券を送り続ける(開き直すまでは何も変わらない)
6200
+ */
6201
+ var actorTicketRejected = false;
6202
+ function forgetActorTicket() { actorTicketRejected = true; }
6203
+ function actorTicketExpired(ticket) {
6204
+ try {
6205
+ var p = String(ticket).split(".")[1];
6206
+ if (!p) return false;
6207
+ p = p.replace(/-/g, "+").replace(/_/g, "/");
6208
+ while (p.length % 4) p += "=";
6209
+ var claims = JSON.parse(atob(p));
6210
+ return !!claims && typeof claims.exp === "number" && isFinite(claims.exp) && !(claims.exp * 1000 > Date.now());
6211
+ } catch (e) { return false; }
6212
+ }
6178
6213
  function actorTicket() {
6179
- if (actorTicketCache !== undefined) return actorTicketCache;
6180
- actorTicketCache = null;
6214
+ if (actorTicketCache && !actorTicketRejected && !actorTicketExpired(actorTicketCache)) return actorTicketCache;
6215
+ var found = null;
6181
6216
  try {
6182
6217
  var parts = String(document.cookie || "").split(";");
6183
6218
  for (var i = 0; i < parts.length; i++) {
6184
6219
  var kv = parts[i].trim();
6185
- if (kv.indexOf(ACTOR_COOKIE + "=") === 0) { actorTicketCache = kv.slice(ACTOR_COOKIE.length + 1) || null; break; }
6220
+ if (kv.indexOf(ACTOR_COOKIE + "=") === 0) { found = kv.slice(ACTOR_COOKIE.length + 1) || null; break; }
6186
6221
  }
6187
- } catch (e) { actorTicketCache = null; }
6222
+ } catch (e) { found = null; }
6223
+ // **別の券**を読めたら乗り換える(その券はまだ断られていない)。
6224
+ // 読み直して何も無ければ、持っている券を送り続ける(intake が断り、画面が開き直しを案内する)。
6225
+ // 券なしに変えると ACTOR_REQUIRED になるだけで、案内は同じ——名無しに変える理由が無い
6226
+ if (found && found !== actorTicketCache) { actorTicketCache = found; actorTicketRejected = false; }
6227
+ else if (!actorTicketCache) actorTicketCache = found;
6188
6228
  return actorTicketCache;
6189
6229
  }
6190
6230
  /**
@@ -7360,6 +7400,110 @@ ${calendarAddUrl.toString()}
7360
7400
  return box;
7361
7401
  }
7362
7402
 
7403
+ /* -------------------------------------------------------------------------
7404
+ * 論点・固さ・決めた人(記憶 v2・cortex-engine #1094)
7405
+ * -------------------------------------------------------------------------
7406
+ * 欄(topic / firmness / decidedBy)は**ビルド側で語彙・形を確かめてある**。欄の無い記録では
7407
+ * キーごと無いので、画面は有無だけを見る。🔴 **欄の無い記録だけの案件では要素を 1 つも作らない**
7408
+ * (絞り込み・「今」・固さの札・決めた人の節が出ない=今と同じ DOM)。
7409
+ * 「置き換えられた」の判定は isRetired(ビルドが supersedes.ts で組んだ答え)をそのまま使う。
7410
+ */
7411
+ function hasTopic(rec) {
7412
+ return !!(rec && typeof rec.topic === "string" && rec.topic);
7413
+ }
7414
+ /** 論点の札の一覧(札を持つ決定が無ければ空) */
7415
+ function topicOptions(items) {
7416
+ var seen = {};
7417
+ var out = [];
7418
+ (items || []).forEach(function (d) {
7419
+ if (!hasTopic(d) || Object.prototype.hasOwnProperty.call(seen, d.topic)) return;
7420
+ seen[d.topic] = true;
7421
+ out.push(d.topic);
7422
+ });
7423
+ return out.sort(function (a, b) { return a.localeCompare(b, "ja"); });
7424
+ }
7425
+ /** 新しい順(決定日の降順 → ID の降順)。日付の無いものは最後 */
7426
+ function newestDecisionFirst(a, b) {
7427
+ if (a.date !== b.date) {
7428
+ if (!a.date) return 1;
7429
+ if (!b.date) return -1;
7430
+ return a.date < b.date ? 1 : -1;
7431
+ }
7432
+ return a.id < b.id ? 1 : a.id > b.id ? -1 : 0;
7433
+ }
7434
+ /**
7435
+ * 札ごとの「今」の決定(一番新しく、置き換えられていない 1 件)の ID の集合。
7436
+ * 削除済みは数えない(一覧と同じ材料)
7437
+ */
7438
+ function nowDecisionIds() {
7439
+ var best = {};
7440
+ visibleRecords(SITE.decisions).forEach(function (d) {
7441
+ if (!hasTopic(d) || isRetired(d)) return;
7442
+ var cur = best[d.topic];
7443
+ if (!cur || newestDecisionFirst(d, cur) < 0) best[d.topic] = d;
7444
+ });
7445
+ var ids = {};
7446
+ Object.keys(best).forEach(function (t) { ids[best[t].id] = true; });
7447
+ return ids;
7448
+ }
7449
+ function isNowDecision(rec) {
7450
+ return !!(rec && rec._kind === "decision" && hasTopic(rec) && nowDecisionIds()[rec.id]);
7451
+ }
7452
+ function nowBadge(rec) {
7453
+ return isNowDecision(rec) ? el("span", { class: "badge now", title: "この論点で今有効な決定", text: "今" }) : null;
7454
+ }
7455
+ /** 論点の札(一覧の行)。札が無ければ null */
7456
+ function topicBadge(rec) {
7457
+ return hasTopic(rec) ? el("span", { class: "badge topic", title: "論点", text: rec.topic }) : null;
7458
+ }
7459
+ var FIRMNESS_CLASS = { "確定": "fixed", "仮": "tentative", "検討中": "open" };
7460
+ /** 固さの小札。語彙外は出さない(ビルド側でも捨てている・二重の網) */
7461
+ function firmnessBadge(rec) {
7462
+ var f = rec && rec.firmness;
7463
+ if (typeof f !== "string" || !Object.prototype.hasOwnProperty.call(FIRMNESS_CLASS, f)) return null;
7464
+ return el("span", { class: "badge firmness firmness-" + FIRMNESS_CLASS[f], title: "固さ", text: f });
7465
+ }
7466
+ /** 置き換えられた決定を薄く出すか(論点の札を持つ決定だけ。欄の無い記録の見た目は変えない) */
7467
+ function fadedClass(rec) {
7468
+ return hasTopic(rec) && isRetired(rec) ? " is-faded" : "";
7469
+ }
7470
+ /** 決めた人 1 人の札。関係者が名簿にあればそのページへ、{name, url} は url があれば元の URL へ */
7471
+ function decidedByChip(who) {
7472
+ if (!who || typeof who !== "object") return null;
7473
+ if (typeof who.member === "string") {
7474
+ var rec = byId[who.member];
7475
+ var fallback = who.member.slice("member:".length);
7476
+ if (rec && rec._kind === "generic") {
7477
+ var a = el("a", { class: "chip decided-by-member", href: "?id=" + encodeURIComponent(who.member), text: humanTitle(rec.title) || fallback });
7478
+ a.addEventListener("click", function (ev) {
7479
+ if (ev.defaultPrevented || ev.button !== 0 || ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey) return;
7480
+ ev.preventDefault();
7481
+ go(who.member);
7482
+ });
7483
+ return a;
7484
+ }
7485
+ return el("span", { class: "chip", text: fallback });
7486
+ }
7487
+ if (typeof who.name !== "string" || !who.name) return null;
7488
+ var raw = typeof who.url === "string" ? who.url.toLowerCase() : "";
7489
+ var url = raw.indexOf("http://") === 0 || raw.indexOf("https://") === 0 ? who.url : "";
7490
+ if (url) return el("a", { class: "chip decided-by-link", href: url, target: "_blank", rel: "noopener", text: who.name });
7491
+ return el("span", { class: "chip", text: who.name });
7492
+ }
7493
+ /** 検索に使う決めた人の表記(decidedBy があればそちら、無ければ deciders) */
7494
+ function deciderWords(d) {
7495
+ if (Array.isArray(d.decidedBy) && d.decidedBy.length) {
7496
+ return d.decidedBy.map(function (w) {
7497
+ if (w && typeof w.member === "string") {
7498
+ var r = byId[w.member];
7499
+ return r && r.title ? r.title : w.member.slice("member:".length);
7500
+ }
7501
+ return w && typeof w.name === "string" ? w.name : "";
7502
+ }).join(" ");
7503
+ }
7504
+ return (d.deciders || []).join(" ");
7505
+ }
7506
+
7363
7507
  // 未確認が1件も無い一覧では絞り込みを出さない
7364
7508
  function hasUnverified(items) {
7365
7509
  return (items || []).some(function (r) { return machineOwned(confirmPathOf(r), r.rstatus); });
@@ -8680,7 +8824,7 @@ ${calendarAddUrl.toString()}
8680
8824
  }
8681
8825
  }
8682
8826
 
8683
- var listState = { tab: DEFAULT_TAB, q: "", category: "", month: "", status: "", page: 1 };
8827
+ var listState = { tab: DEFAULT_TAB, q: "", category: "", month: "", status: "", topic: "", page: 1 };
8684
8828
 
8685
8829
  // 一覧のページネーション(描画コスト対策。1ページあたりの件数)
8686
8830
  var PAGE_SIZE = 30;
@@ -19224,6 +19368,28 @@ ${calendarAddUrl.toString()}
19224
19368
  submitIntake(kind, selectedFiles, nameInput.value, status, submitBtn, isT ? meetingValue() : null, allDup);
19225
19369
  });
19226
19370
  }
19371
+ /**
19372
+ * 名乗りの理由で断られたときに、送信の結果の下へ足す一手(engine #1037)。
19373
+ *
19374
+ * intake の文言は「ポータルに戻って、この案件を開き直してください」だが、それだけでは
19375
+ * **同じタブで開き直して入力を失う**か、**別のタブで開き直してこのタブで押し直し、また断られる**
19376
+ * (券を持ち続けていた頃)のどちらかになる。券は期限が切れると読み直す(actorTicket)ので、
19377
+ * 別のタブで開き直せば、このタブの入力のまま押し直して通る——それを 1 行で言う。
19378
+ * ポータル配下でないとき(Amplify・手元の確認)は開き直す先が無いので出さない
19379
+ */
19380
+ var ACTOR_REOPEN_CODES = ["ACTOR_INVALID", "ACTOR_REQUIRED", "ACTOR_PROJECT_MISMATCH"];
19381
+ function actorReopenNote(code) {
19382
+ // ACCESS_REVOKED(参加を確認できない)は入れない。参加は人で引くので、券を取り直しても
19383
+ // 同じ答えになる——「押し直せば通る」と言えない(intake の文言がポータルからの開き直しを案内する)
19384
+ if (ACTOR_REOPEN_CODES.indexOf(code) === -1) return null;
19385
+ var m = /^\\/p\\/[^/]+\\/[^/]+\\//.exec(location.pathname);
19386
+ if (!m) return null;
19387
+ var href = "/open?to=" + encodeURIComponent(m[0]);
19388
+ return el("div", { class: "modal-reopen" }, [
19389
+ el("a", { class: "modal-reopen-link", href: href, target: "_blank", rel: "noopener", text: "ポータルでこの案件を開き直す(別タブ)" }),
19390
+ el("div", { class: "modal-reopen-note", text: "開き直したら、このタブに戻ってもう一度[送信]を押してください。入力はそのまま使えます。" }),
19391
+ ]);
19392
+ }
19227
19393
  async function submitIntake(kind, entries, submitter, status, submitBtn, meeting, allDup) {
19228
19394
  var intake = SITE.intake;
19229
19395
  var isT = kind === "transcript";
@@ -19232,7 +19398,13 @@ ${calendarAddUrl.toString()}
19232
19398
  if (!entries || !entries.length) { setStatus("⚠ ファイルを選択してください。", true); return; }
19233
19399
  if (isT && !meeting) { setStatus("⚠ 会議名を選んでください。", true); return; }
19234
19400
  submitBtn.disabled = true;
19235
- function fail(msg) { setStatus("✗ " + msg, true); submitBtn.disabled = false; }
19401
+ function fail(msg, code) {
19402
+ if (ACTOR_REOPEN_CODES.indexOf(code) !== -1) forgetActorTicket();
19403
+ setStatus("✗ " + msg, true);
19404
+ var reopen = actorReopenNote(code);
19405
+ if (reopen) status.appendChild(reopen);
19406
+ submitBtn.disabled = false;
19407
+ }
19236
19408
  try {
19237
19409
  var uploaded = [];
19238
19410
  for (var i = 0; i < entries.length; i++) {
@@ -19243,7 +19415,7 @@ ${calendarAddUrl.toString()}
19243
19415
  pres = await callIntake("presign", { projectKey: intake.projectKey, filename: f.name, contentType: f.type || "application/octet-stream" })
19244
19416
  .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); });
19245
19417
  } catch (net) { return fail("サーバーに接続できませんでした。通信環境を確認して、もう一度お試しください。"); }
19246
- if (!pres.ok || !pres.j.url) return fail((pres.j && pres.j.message) || "アップロードURLの取得に失敗しました。対応していないファイル形式かもしれません。");
19418
+ if (!pres.ok || !pres.j.url) return fail((pres.j && pres.j.message) || "アップロードURLの取得に失敗しました。対応していないファイル形式かもしれません。", pres.j && pres.j.code);
19247
19419
  var put;
19248
19420
  try {
19249
19421
  put = await fetch(pres.j.url, { method: "PUT", headers: { "content-type": f.type || "application/octet-stream" }, body: f });
@@ -19262,7 +19434,7 @@ ${calendarAddUrl.toString()}
19262
19434
  sub = await callIntake("submit", payload)
19263
19435
  .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); });
19264
19436
  } catch (net3) { return fail("サーバーに接続できませんでした。もう一度お試しください。"); }
19265
- if (!sub.ok) return fail((sub.j && sub.j.message) || "取り込みに失敗しました。時間をおいて再度お試しください。");
19437
+ if (!sub.ok) return fail((sub.j && sub.j.message) || "取り込みに失敗しました。時間をおいて再度お試しください。", sub.j && sub.j.code);
19266
19438
  status.className = "modal-status";
19267
19439
  status.textContent = "";
19268
19440
  status.appendChild(el("div", { class: "modal-ok", text: allDup
@@ -19304,6 +19476,13 @@ ${calendarAddUrl.toString()}
19304
19476
  if (statusOptsD.length) {
19305
19477
  filters.appendChild(buildSelect("すべての状態", statusOptsD, listState.status, function (v) { listState.status = v; listState.page = 1; drawCards(); }, statusLabel));
19306
19478
  }
19479
+ // 論点の札(#1094)。札を持つ決定が 1 件も無い案件では出さない
19480
+ var topicOpts = topicOptions(visibleRecords(SITE.decisions));
19481
+ if (topicOpts.length) {
19482
+ var topicSel = buildSelect("すべての論点", topicOpts, listState.topic, function (v) { listState.topic = v; listState.page = 1; drawCards(); });
19483
+ topicSel.classList.add("topic-filter");
19484
+ filters.appendChild(topicSel);
19485
+ }
19307
19486
  app.appendChild(filters);
19308
19487
 
19309
19488
  var addBarD = aiAddBar("Decisions");
@@ -19335,16 +19514,25 @@ ${calendarAddUrl.toString()}
19335
19514
  var q = listState.q.trim().toLowerCase();
19336
19515
  // 削除済みは絞り込みの手前で落とす(分母も一緒に減るので「12/13件」が残らない)
19337
19516
  var all = visibleRecords(SITE.decisions);
19338
- return all.filter(function (d) {
19517
+ var out = all.filter(function (d) {
19339
19518
  if (listState.month && d.date.slice(0, 7) !== listState.month) return false;
19340
19519
  if (listState.category && d.category !== listState.category) return false;
19341
19520
  if (!matchesStatusFilter(d, !!SITE.decisionFollowup)) return false;
19521
+ if (!matchesTopicFilter(d)) return false;
19342
19522
  if (q) {
19343
- var hay = (d.title + " " + d.summary + " " + d.searchText + " " + d.deciders.join(" ")).toLowerCase();
19523
+ var hay = (d.title + " " + d.summary + " " + d.searchText + " " + deciderWords(d)).toLowerCase();
19344
19524
  if (hay.indexOf(q) === -1) return false;
19345
19525
  }
19346
19526
  return true;
19347
19527
  });
19528
+ // 同じ論点の中は新しい順(決定日の降順 → ID の降順)
19529
+ if (listState.topic) out.sort(newestDecisionFirst);
19530
+ return out;
19531
+ }
19532
+ /** 論点の絞り込み(#1094)。選んでいなければ全件、選んだら同じ札の決定だけ */
19533
+ function matchesTopicFilter(d) {
19534
+ if (!listState.topic) return true;
19535
+ return hasTopic(d) && d.topic === listState.topic;
19348
19536
  }
19349
19537
 
19350
19538
  function drawCards() {
@@ -19367,6 +19555,7 @@ ${calendarAddUrl.toString()}
19367
19555
 
19368
19556
  pg.slice.forEach(function (d) {
19369
19557
  var meta = el("div", { class: "card-meta" }, [
19558
+ nowBadge(d),
19370
19559
  draftBadge(d),
19371
19560
  proposalBadge(d),
19372
19561
  followupBadge(d),
@@ -19374,13 +19563,15 @@ ${calendarAddUrl.toString()}
19374
19563
  latestBadge(d),
19375
19564
  el("span", { class: "mono", text: d.date || "----" }),
19376
19565
  catBadge(d.category),
19566
+ firmnessBadge(d),
19567
+ topicBadge(d),
19377
19568
  ]);
19378
19569
  var eb = fmErrorBadge(d);
19379
19570
  if (eb) meta.appendChild(eb);
19380
19571
  var traceD = el("a", { class: "mc-trace", href: "?tab=graph", text: "🕸 つながり" });
19381
19572
  traceD.addEventListener("click", function (ev) { ev.preventDefault(); ev.stopPropagation(); openExplorerFor(d); });
19382
19573
  meta.appendChild(traceD);
19383
- var card = el("div", { class: "card", title: d.id, "data-record-id": d.id }, [
19574
+ var card = el("div", { class: "card" + fadedClass(d) + (isNowDecision(d) ? " is-now" : ""), title: d.id, "data-record-id": d.id }, [
19384
19575
  meta,
19385
19576
  // 消したレコードは一覧でも取り消し線(次のビルドまで一覧には残る)
19386
19577
  cardTitleLink(d, d.deleted ? " is-deleted" : ""),
@@ -20404,6 +20595,9 @@ ${calendarAddUrl.toString()}
20404
20595
  // 話題の線(決定・ルール・関係者 → 用語・engine #551 の about)。名前の一致で機械が付けた手がかりなので、
20405
20596
  // 根拠の線と同じく細く薄い「ゆるいつながり」として描き、凡例は日本語の 1 行(栓 about_relations の on の条件)
20406
20597
  about: { color: "#ddd6fe", label: "話題", kind: "topic" },
20598
+ // 決めた人の線(決定 → 関係者・#1094)。relations ではなく decided_by から引く。
20599
+ // 🔴 凡例には**この線がグラフにあるときだけ**出す(欄の無い案件の凡例を変えない)
20600
+ decided_by: { color: "#f472b6", label: "決めた人", kind: "gold", onlyWhenPresent: true },
20407
20601
  };
20408
20602
  var WEAK_RELS = {};
20409
20603
  var REL_COLOR = {};
@@ -20471,7 +20665,10 @@ ${calendarAddUrl.toString()}
20471
20665
  // 線(関係)の色凡例。グラフ内の有無に関わらず全種別を、Goldどうし/根拠の2群で表示する
20472
20666
  [["gold", "線(Goldどうし):"], ["evidence", "線(根拠):"], ["topic", "線(話題):"]].forEach(function (pair) {
20473
20667
  var kind = pair[0], heading = pair[1];
20474
- var keys = Object.keys(REL_META).filter(function (r) { return REL_META[r].kind === kind; });
20668
+ var keys = Object.keys(REL_META).filter(function (r) {
20669
+ if (REL_META[r].kind !== kind) return false;
20670
+ return !REL_META[r].onlyWhenPresent || graph.edges.some(function (e) { return e.rel === r; });
20671
+ });
20475
20672
  if (!keys.length) return;
20476
20673
  legend.appendChild(el("span", { class: "sep", text: heading }));
20477
20674
  keys.forEach(function (r) {
@@ -21019,6 +21216,7 @@ ${calendarAddUrl.toString()}
21019
21216
  relates_to: "rgba(148,163,184,0.6)", supersedes: "rgba(248,113,113,0.9)",
21020
21217
  reference: "rgba(180,196,230,0.32)", source: "rgba(180,196,230,0.32)", link: "rgba(180,196,230,0.22)",
21021
21218
  about: "rgba(196,181,253,0.22)",
21219
+ decided_by: "rgba(244,114,182,0.75)",
21022
21220
  };
21023
21221
  // 画面に入っている範囲(世界座標)。両端が同じ側の外にある線は描かない
21024
21222
  var em = 4 / view.scale; // 線の太さの分の余白(画面で 4px)
@@ -21270,12 +21468,14 @@ ${calendarAddUrl.toString()}
21270
21468
  app.appendChild(head);
21271
21469
 
21272
21470
  var meta = el("div", { class: "detail-meta" }, [
21471
+ nowBadge(d),
21273
21472
  draftBadge(d),
21274
21473
  proposalBadge(d),
21275
21474
  retiredBadge(d),
21276
21475
  latestBadge(d),
21277
21476
  el("span", { class: "mono", text: d.date || "----" }),
21278
21477
  catBadge(d.category),
21478
+ firmnessBadge(d),
21279
21479
  ]);
21280
21480
  meta.title = d.id;
21281
21481
  meta.setAttribute("data-record-id", d.id);
@@ -21295,7 +21495,18 @@ ${calendarAddUrl.toString()}
21295
21495
  app.appendChild(el("div", { class: "summary-box", text: d.summary }));
21296
21496
  }
21297
21497
 
21298
- if (d.deciders && d.deciders.length) {
21498
+ // 論点(#1094)。札の無い決定では何も作らない
21499
+ if (hasTopic(d)) {
21500
+ app.appendChild(el("div", { class: "section-label", text: "論点" }));
21501
+ app.appendChild(el("div", { class: "decision-topic" }, [el("span", { class: "chip", text: d.topic })]));
21502
+ }
21503
+
21504
+ // 決めた人(#1094)。decided_by に読めた人がいればそちらを出し、いなければ(欄が無い・空・全部崩れ)今どおり deciders
21505
+ var whoChips = Array.isArray(d.decidedBy) ? d.decidedBy.map(decidedByChip).filter(Boolean) : [];
21506
+ if (whoChips.length) {
21507
+ app.appendChild(el("div", { class: "section-label", text: "決めた人" }));
21508
+ app.appendChild(el("div", { class: "deciders decided-by" }, whoChips));
21509
+ } else if (d.deciders && d.deciders.length) {
21299
21510
  app.appendChild(el("div", { class: "section-label", text: "決定者" }));
21300
21511
  app.appendChild(el("div", { class: "deciders" }, d.deciders.map(function (p) {
21301
21512
  return el("span", { class: "chip", text: p });
@@ -21344,6 +21555,7 @@ ${calendarAddUrl.toString()}
21344
21555
  based_on: "根拠", derived_from: "由来", relates_to: "関連",
21345
21556
  supersedes: "置換", superseded_by: "置換先", reference: "参照", source: "出典", link: "リンク",
21346
21557
  about: "話題",
21558
+ decided_by: "決めた人",
21347
21559
  };
21348
21560
  var explorerState = { folder: null, path: [], q: "", sx: 0, autoRight: false };
21349
21561
  // 索引もグラフ本体と同じ材料(削除済みを除いたもの)から作る。別々に間引くと、
@@ -21356,7 +21568,7 @@ ${calendarAddUrl.toString()}
21356
21568
  (adjDir[e.from] = adjDir[e.from] || []).push({ id: e.to, rel: e.rel, dir: "out" });
21357
21569
  (adjDir[e.to] = adjDir[e.to] || []).push({ id: e.from, rel: e.rel, dir: "in" });
21358
21570
  });
21359
- var REL_ORDER = ["supersedes", "based_on", "derived_from", "relates_to", "source", "reference", "link", "about"];
21571
+ var REL_ORDER = ["supersedes", "based_on", "derived_from", "relates_to", "decided_by", "source", "reference", "link", "about"];
21360
21572
 
21361
21573
  function recordText(rec) {
21362
21574
  if (!rec) return "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.115",
3
+ "version": "0.12.117",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,