@takagaki/cortex-decisions-viewer 0.12.85 → 0.12.86

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,12 +36,14 @@ 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 = void 0;
39
+ exports.PROJECT_ICON_EXTS = exports.MIRROR_RELS = void 0;
40
40
  exports.sanitizeBody = sanitizeBody;
41
41
  exports.findProjectIcon = findProjectIcon;
42
42
  exports.readFleetSources = readFleetSources;
43
43
  exports.attachRederived = attachRederived;
44
44
  exports.attachDecisionFollowup = attachDecisionFollowup;
45
+ exports.rawFrontmatterDate = rawFrontmatterDate;
46
+ exports.attachSupersedes = attachSupersedes;
45
47
  exports.readGoldProposals = readGoldProposals;
46
48
  exports.buildHomeGrounding = buildHomeGrounding;
47
49
  exports.collectMeetings = collectMeetings;
@@ -97,6 +99,7 @@ function takeProposals(filePath) {
97
99
  }
98
100
  const external_id_1 = require("./external-id");
99
101
  const home_project_1 = require("./home-project");
102
+ const supersedes_1 = require("./supersedes");
100
103
  const render_1 = require("./render");
101
104
  /**
102
105
  * この画面を作ったパッケージの版。**package.json を唯一の出どころにする。**
@@ -113,6 +116,11 @@ const PKG_VERSION = (() => {
113
116
  }
114
117
  })();
115
118
  const DEFAULT_EXCLUDE = ["README.md", "template.md"];
119
+ /**
120
+ * グラフ・エクスプローラーに描かない関係(#699 F3)。`superseded_by` は engine の夜間の精製が古い側に書く
121
+ * `supersedes` の写しで、描くと同じ置き換えが逆向きにもう 1 本出る。frontmatter のパネルには今までどおり出る
122
+ */
123
+ exports.MIRROR_RELS = new Set(["superseded_by"]);
116
124
  /** frontmatterのrelationsを安全に正規化する */
117
125
  function toRelations(value) {
118
126
  if (!Array.isArray(value))
@@ -795,6 +803,78 @@ function attachDecisionFollowup(decisions, recordsDir, repoRoot, ledger) {
795
803
  }
796
804
  return [...ledger.keys()].filter((k) => !matched.has(k.normalize("NFC")));
797
805
  }
806
+ /**
807
+ * 決定に置き換えの逆引き(置き換え先・置き換えた決定)を載せる(cortex-engine #699 F3・A-1')。
808
+ *
809
+ * 🔴 **逆引きは `supersedes.ts`(engine `scripts/gold/supersedes-index.mjs` の写し)だけで組む**。
810
+ * 材料は新しい側の `relations[supersedes]` だけで、古い側の `superseded_by`(夜間の精製が書く写し)は
811
+ * 読まない。確認済み=新しい側の `status` が `active`。
812
+ *
813
+ * 置き換えが無い決定には**キーごと付けない**(今までと同じ SiteData)
814
+ */
815
+ /**
816
+ * frontmatter に**書かれたままの** `date` の値(トップレベルの `date:` の行・引用符と行末のコメントを外す)。
817
+ * 無い・frontmatter が無ければ undefined。置き換えの逆引きの決定日だけに使う(engine と同じ日付にするため)
818
+ */
819
+ function rawFrontmatterDate(raw) {
820
+ const m = String(raw ?? "").replace(/^\uFEFF/, "").match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/);
821
+ if (!m)
822
+ return undefined;
823
+ const line = m[1].match(/^date:[ \t]*(.*)$/m);
824
+ if (!line)
825
+ return undefined;
826
+ let v = line[1].replace(/\r$/, "").trim();
827
+ const q = v.match(/^(["'])(.*)\1(?:\s+#.*)?$/);
828
+ if (q)
829
+ return q[2];
830
+ v = v.replace(/\s+#.*$/, "").trim();
831
+ return v;
832
+ }
833
+ function attachSupersedes(decisions, rawDateOf = (d) => d.fm.date) {
834
+ const input = decisions.map((d) => ({
835
+ id: d.id,
836
+ // 🔴 **frontmatter に書かれたままの date の文字列**(`rawFrontmatterDate`)。gray-matter は時刻・時差つきの
837
+ // date を Date に読み、sanitizeFm は UTC の日付にするので、`2026-07-01T01:00:00+09:00` が 6/30 にずれる。
838
+ // engine は日付を Date にしない読み方(CORE_SCHEMA)で書かれた文字列の先頭 8 桁を使う。
839
+ // 無ければ写しが ID の先頭 8 桁から取る
840
+ date: rawDateOf(d),
841
+ status: d.fm.status,
842
+ supersedes: d.relations.filter((r) => r.rel === "supersedes").map((r) => r.target),
843
+ }));
844
+ const index = (0, supersedes_1.supersedesIndex)(input);
845
+ const idOfKey = new Map();
846
+ for (const d of decisions) {
847
+ const k = (0, supersedes_1.decisionKeyOf)(d.id);
848
+ if (!idOfKey.has(k))
849
+ idOfKey.set(k, d.id);
850
+ }
851
+ for (const d of decisions) {
852
+ const key = (0, supersedes_1.decisionKeyOf)(d.id);
853
+ const entry = index.get(key);
854
+ if (entry && entry.by.length) {
855
+ d.supersededBy = {
856
+ by: entry.by.map((b) => ({ id: b.id, date: b.date, confirmed: b.confirmed })),
857
+ retired: entry.retired,
858
+ };
859
+ }
860
+ const replaced = [];
861
+ for (const r of d.relations) {
862
+ if (r.rel !== "supersedes")
863
+ continue;
864
+ const k = (0, supersedes_1.decisionKeyOf)(r.target);
865
+ // engine の規則と同じく、自分自身・実在しない決定は数えない
866
+ if (!k || k === key)
867
+ continue;
868
+ const id = idOfKey.get(k);
869
+ if (id === undefined)
870
+ continue;
871
+ if (!replaced.includes(id))
872
+ replaced.push(id);
873
+ }
874
+ if (replaced.length)
875
+ d.supersedesIds = replaced;
876
+ }
877
+ }
798
878
  /**
799
879
  * AIからの更新提案(`.cortex/gold-proposals.json`)を読む。
800
880
  *
@@ -1509,7 +1589,8 @@ async function scanContent(repoRoot, goldDirName, repoBaseUrl, branch) {
1509
1589
  }
1510
1590
  }
1511
1591
  for (const r of toRelations(data.relations))
1512
- edges.push({ target: r.target, rel: r.rel });
1592
+ if (!exports.MIRROR_RELS.has(r.rel))
1593
+ edges.push({ target: r.target, rel: r.rel });
1513
1594
  }
1514
1595
  else {
1515
1596
  const keyMatch = raw.slice(0, 4000).match(/^- 課題キー: (\S+)$/m);
@@ -1618,6 +1699,9 @@ function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl,
1618
1699
  };
1619
1700
  const addRelationEdges = (fromId, relations) => {
1620
1701
  for (const r of relations) {
1702
+ // 🔴 `superseded_by` は描かない(#699 F3)。新しい側の `supersedes` の写しで、描くと同じ置き換えが 2 本になる
1703
+ if (exports.MIRROR_RELS.has(r.rel))
1704
+ continue;
1621
1705
  edges.push({ from: fromId, to: classifyTarget(r.target), rel: r.rel });
1622
1706
  }
1623
1707
  };
@@ -2063,11 +2147,18 @@ async function build(opts) {
2063
2147
  // **読めない・使えないなら `null`** で、関係者の並びは今までと 1 バイトも同じ
2064
2148
  const engagement = await (0, engagement_1.readEngagementLedger)(repoRoot);
2065
2149
  const decisions = [];
2150
+ const rawDates = new Map();
2066
2151
  for (const fileName of mdFiles) {
2067
2152
  const raw = await node_fs_1.promises.readFile(path.join(dirAbs, fileName), "utf8");
2068
2153
  const rec = await parseDecision(raw, fileName, repoRelDir, repo.baseUrl, repo.branch, refs);
2069
2154
  decisions.push(rec);
2155
+ // 置き換えの逆引きの決定日(書かれたままの文字列・SiteData には載せない)
2156
+ const rd = rawFrontmatterDate(raw);
2157
+ if (rd !== undefined)
2158
+ rawDates.set(rec, rd);
2070
2159
  }
2160
+ // 置き換えの逆引き(#699 F3)。**置き換えの無い案件では 1 件も付かない**
2161
+ attachSupersedes(decisions, (d) => rawDates.get(d) ?? d.fm.date);
2071
2162
  // 決定の「その後」(#705)。**帳簿を持たない案件では 1 件も付かず、絞り込みの印も立たない**
2072
2163
  // (`decisionFollowup` はキーごと無い=今までと 1 バイトも同じ画面)
2073
2164
  const followupLedger = await (0, rederivation_1.readDecisionFollowup)(repoRoot);
@@ -50,7 +50,7 @@ exports.ENGAGEMENT_LEDGER_VERSION = 1;
50
50
  /** 半減期(日)。engine `weights.mjs` の `HALF_LIFE_DAYS`(正本 6-B「半減期 90 日固定」) */
51
51
  exports.HALF_LIFE_DAYS = 90;
52
52
  /**
53
- * 行為 1 件の重み。engine `weights.mjs` の `WEIGHTS` の写し(22 値・**表に無い `kind` は 0**)。
53
+ * 行為 1 件の重み。engine `weights.mjs` の `WEIGHTS` の写し(24 値・**表に無い `kind` は 0**)。
54
54
  *
55
55
  * 🔴 契約の表(fixture)は `message.posted` しか使っていないので、**ここの写し間違いは表では
56
56
  * 捕まらない**。重みの番人は `test/member-engagement-parity.test.mjs` の `[重み]` が持つ。
@@ -80,6 +80,9 @@ exports.WEIGHTS = Object.freeze({
80
80
  "record.deleted": 0,
81
81
  // 本文を取り込んだ(engine #695)。人の行為ではないので 0(話した人は `utterance.spoken` で数える)
82
82
  "body.captured": 0,
83
+ // 人の宣言の指紋(cortex-engine #699 F1)。宣言の取り込みが書く行で、関与には数えない(0)。
84
+ // 場は宣言専用(`decl:<owner/repo>`)で帳簿にもともと入らないが、網羅のため写す
85
+ "decl.recorded": 0,
83
86
  });
84
87
  const DAY_MS = 86400000;
85
88
  const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
package/dist/render.js CHANGED
@@ -4983,6 +4983,13 @@ button.site-icon[aria-busy="true"] .site-icon-plus { opacity: 0; }
4983
4983
  .badge.followup-open { background: #eff6ff; color: #1e40af; border-color: #bfdbfe; }
4984
4984
  .badge.followup-superseded, .badge.followup-dropped { background: #f5f5f4; color: #57534e; border-color: #d6d3d1; }
4985
4985
  .followup-section { margin: 14px 0 4px; }
4986
+ /* 置き換え(#699 F3)。印は「その後」の置き換え済みと同じ色(同じ意味なので) */
4987
+ .badge.retired { background: #f5f5f4; color: #57534e; border: 1px solid #d6d3d1; }
4988
+ .supersede-section { margin: 14px 0 4px; }
4989
+ .supersede-list { list-style: none; margin: 0 0 8px; padding: 0; }
4990
+ .supersede-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: baseline; font-size: 13.5px; line-height: 1.7; padding: 2px 0; }
4991
+ .supersede-id { font-size: 12.5px; color: var(--muted); word-break: break-all; }
4992
+ .supersede-unconfirmed { font-size: 12px; color: #92400e; background: #fffbeb; border: 1px solid #fde68a; border-radius: 4px; padding: 0 6px; }
4986
4993
  .followup-human { font-size: 13.5px; margin: 0 0 6px; color: #166534; }
4987
4994
  .followup-list, .followup-evidence { list-style: none; margin: 0 0 8px; padding: 0; }
4988
4995
  .followup-row, .followup-evidence-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: baseline; font-size: 13.5px; line-height: 1.7; padding: 2px 0; }
@@ -6556,6 +6563,7 @@ ${calendarAddUrl.toString()}
6556
6563
  var FOLLOWUP_FILTER_LABEL = { "followup-done": "その後: 完了", "followup-open": "その後: 進行中", "followup-none": "その後: 記録なし" };
6557
6564
  function statusLabel(v) {
6558
6565
  if (Object.prototype.hasOwnProperty.call(FOLLOWUP_FILTER_LABEL, v)) return FOLLOWUP_FILTER_LABEL[v];
6566
+ if (v === RETIRED_FILTER) return "置き換え済み";
6559
6567
  return v === "draft" ? "未確認のみ" : v === "proposed" ? "提案ありのみ" : v;
6560
6568
  }
6561
6569
  /**
@@ -6568,6 +6576,8 @@ ${calendarAddUrl.toString()}
6568
6576
  var opts = [];
6569
6577
  if (hasUnverified(items)) opts.push("draft");
6570
6578
  if ((items || []).some(hasProposals)) opts.push("proposed");
6579
+ // 置き換え済み(#699 F3)。決定にしか付かない印なので、決定の一覧でだけ選択肢になる
6580
+ if ((items || []).some(isRetired)) opts.push(RETIRED_FILTER);
6571
6581
  if (withFollowup) {
6572
6582
  ["followup-done", "followup-open", "followup-none"].forEach(function (v) {
6573
6583
  if ((items || []).some(function (r) { return matchesFollowupFilter(r, v); })) opts.push(v);
@@ -6585,6 +6595,8 @@ ${calendarAddUrl.toString()}
6585
6595
  function matchesStatusFilter(rec, withFollowup) {
6586
6596
  if (!listState.status) return true;
6587
6597
  if (listState.status === "proposed") return hasProposals(rec);
6598
+ // 決定以外の一覧(タブを移っても絞り込みは残る)では絞らない——全件が消えないように
6599
+ if (listState.status === RETIRED_FILTER) return rec && rec._kind === "decision" ? isRetired(rec) : true;
6588
6600
  if (Object.prototype.hasOwnProperty.call(FOLLOWUP_FILTER_LABEL, listState.status)) return withFollowup ? matchesFollowupFilter(rec, listState.status) : true;
6589
6601
  return rec.rstatus === listState.status;
6590
6602
  }
@@ -6879,6 +6891,66 @@ ${calendarAddUrl.toString()}
6879
6891
  return false;
6880
6892
  }
6881
6893
 
6894
+ /* -------------------------------------------------------------------------
6895
+ * 置き換え(#699 F3・オーナー決定 A-1')
6896
+ * -------------------------------------------------------------------------
6897
+ * **逆引きはビルド側が組んである**(supersedes.ts=engine supersedes-index.mjs の写し)。
6898
+ * rec.supersededBy(置き換え先・新しい順)と rec.supersedesIds(置き換えた決定)は、無ければキーごと無い。
6899
+ * 🔴 古い側の superseded_by(夜間の精製が書く写し)は画面でも読まない。
6900
+ */
6901
+ var RETIRED_FILTER = "retired";
6902
+ /** もう有効でない(確認済みの置き換えが 1 本以上) */
6903
+ function isRetired(rec) {
6904
+ return !!(rec && rec.supersededBy && rec.supersededBy.retired);
6905
+ }
6906
+ /** 一覧・詳細の印。「その後」の印が同じ「置き換え済み」を出しているときは並べない */
6907
+ function retiredBadge(rec) {
6908
+ if (!isRetired(rec) || followupMarkOf(rec) === "superseded") return null;
6909
+ return el("span", { class: "badge retired", text: "置き換え済み" });
6910
+ }
6911
+ /** 決定 1 件の行。SITE にあれば題名で詳細へのリンク、無ければ ID だけ */
6912
+ function supersedeRow(id, date, unconfirmed) {
6913
+ var rec = byId[id];
6914
+ var li = el("li", { class: "supersede-row" });
6915
+ if (rec && rec._kind === "decision") {
6916
+ var a = el("a", { href: "?id=" + encodeURIComponent(id), text: rec.title });
6917
+ a.addEventListener("click", function (ev) {
6918
+ if (ev.defaultPrevented || ev.button !== 0 || ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey) return;
6919
+ ev.preventDefault();
6920
+ go(id);
6921
+ });
6922
+ li.appendChild(a);
6923
+ }
6924
+ li.appendChild(el("span", { class: "supersede-id mono", text: (date ? date + " · " : "") + id }));
6925
+ if (unconfirmed) {
6926
+ li.appendChild(el("span", { class: "supersede-unconfirmed", title: "置き換えた側の決定が、まだ人の確認を経ていません", text: "未確認" }));
6927
+ }
6928
+ return li;
6929
+ }
6930
+ /** 詳細の節「置き換え先の決定」「置き換えた決定」。どちらも無ければ null(要素を作らない) */
6931
+ function supersedeSection(d) {
6932
+ var by = (d && d.supersededBy && d.supersededBy.by) || [];
6933
+ var reps = (d && d.supersedesIds) || [];
6934
+ if (!by.length && !reps.length) return null;
6935
+ var box = el("div", { class: "supersede-section" });
6936
+ if (by.length) {
6937
+ box.appendChild(el("div", { class: "section-label", text: "置き換え先の決定" }));
6938
+ var ul = el("ul", { class: "supersede-list supersede-by" });
6939
+ by.forEach(function (b) { ul.appendChild(supersedeRow(b.id, b.date, !b.confirmed)); });
6940
+ box.appendChild(ul);
6941
+ }
6942
+ if (reps.length) {
6943
+ box.appendChild(el("div", { class: "section-label", text: "置き換えた決定" }));
6944
+ var ul2 = el("ul", { class: "supersede-list supersede-replaces" });
6945
+ reps.forEach(function (id) {
6946
+ var r = byId[id];
6947
+ ul2.appendChild(supersedeRow(id, r && r._kind === "decision" ? r.date : "", false));
6948
+ });
6949
+ box.appendChild(ul2);
6950
+ }
6951
+ return box;
6952
+ }
6953
+
6882
6954
  // 未確認が1件も無い一覧では絞り込みを出さない
6883
6955
  function hasUnverified(items) {
6884
6956
  return (items || []).some(function (r) { return r.rstatus === "draft"; });
@@ -18033,6 +18105,7 @@ ${calendarAddUrl.toString()}
18033
18105
  draftBadge(d),
18034
18106
  proposalBadge(d),
18035
18107
  followupBadge(d),
18108
+ retiredBadge(d),
18036
18109
  latestBadge(d),
18037
18110
  el("span", { class: "mono", text: d.date || "----" }),
18038
18111
  catBadge(d.category),
@@ -19614,6 +19687,7 @@ ${calendarAddUrl.toString()}
19614
19687
  var meta = el("div", { class: "detail-meta" }, [
19615
19688
  draftBadge(d),
19616
19689
  proposalBadge(d),
19690
+ retiredBadge(d),
19617
19691
  latestBadge(d),
19618
19692
  el("span", { class: "mono", text: d.date || "----" }),
19619
19693
  catBadge(d.category),
@@ -19659,6 +19733,10 @@ ${calendarAddUrl.toString()}
19659
19733
  app.appendChild(ul);
19660
19734
  }
19661
19735
 
19736
+ // 置き換え(#699 F3)。本文・参照の後、「その後の記録」の前。置き換えの無い決定では何も作らない
19737
+ var sup = supersedeSection(d);
19738
+ if (sup) app.appendChild(sup);
19739
+
19662
19740
  appendRecordFooter(d);
19663
19741
  }
19664
19742
 
@@ -19671,9 +19749,11 @@ ${calendarAddUrl.toString()}
19671
19749
  }
19672
19750
 
19673
19751
  // ---------- エクスプローラー(Miller columns: Gold層から関係を辿るカラム表示) ----------
19752
+ // superseded_by は**辺としては描かない**(ビルド側の MIRROR_RELS)。ここに置くのは
19753
+ // レコード情報(frontmatter)のパネルが英語の語のまま出さないためだけ(凡例の REL_LABEL には足さない)
19674
19754
  var REL_LABEL = {
19675
19755
  based_on: "根拠", derived_from: "由来", relates_to: "関連",
19676
- supersedes: "置換", reference: "参照", source: "出典", link: "リンク",
19756
+ supersedes: "置換", superseded_by: "置換先", reference: "参照", source: "出典", link: "リンク",
19677
19757
  };
19678
19758
  var explorerState = { folder: null, path: [], q: "", sx: 0, autoRight: false };
19679
19759
  // 索引もグラフ本体と同じ材料(削除済みを除いたもの)から作る。別々に間引くと、
@@ -20000,6 +20080,10 @@ ${calendarAddUrl.toString()}
20000
20080
  badge.style.borderColor = REL_COLOR[r.rel] || "#cbd5e1";
20001
20081
  row.appendChild(badge);
20002
20082
  row.appendChild(fmTargetLink(String(r.target)));
20083
+ // 夜間処理が古い側に書く置換先の写しは、置き換えた側が未確認なら unconfirmed: true を持つ(#699 F3)
20084
+ if (r.rel === "superseded_by" && r.unconfirmed === true) {
20085
+ row.appendChild(el("span", { class: "supersede-unconfirmed", title: "置き換えた側の決定が、まだ人の確認を経ていません", text: "未確認" }));
20086
+ }
20003
20087
  cell.appendChild(row);
20004
20088
  });
20005
20089
  } else if (k === FM_LINKED_IDS && Array.isArray(v)) {
@@ -0,0 +1,112 @@
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.CONFIRMED_STATUS = void 0;
7
+ exports.decisionKeyOf = decisionKeyOf;
8
+ exports.decisionDateOf = decisionDateOf;
9
+ exports.supersedesIndex = supersedesIndex;
10
+ exports.supersedesOf = supersedesOf;
11
+ exports.retiredIdsOf = retiredIdsOf;
12
+ /**
13
+ * 決定の置き換え(`supersedes`)の逆引き(cortex-engine #699 F3・オーナー決定 A-1'・A-1'')。
14
+ *
15
+ * ## 🔴 これは写しで、正本は engine にある
16
+ *
17
+ * 正本: cortex-engine `scripts/gold/supersedes-index.mjs`(`supersedesIndex`・`decisionKeyOf`・
18
+ * `decisionDateOf`)。**題材で一致を確かめる**: 両リポに同じ `supersedes-index-contract.json`
19
+ * (tools は `test/fixtures/`)を置き、`test/supersedes-index-parity.test.mjs` が全ケースを流す。
20
+ * 規則を変えるときは engine の正本・この写し・題材を同じ日に直す(**tools 側で規則を足さない**)。
21
+ *
22
+ * ## 規則(A-1')
23
+ *
24
+ * 1. 置き換えの正本は**新しい側の `relations[supersedes]` だけ**。古い側の `superseded_by`
25
+ * (engine の夜間の精製が書く写し)は逆引きの材料にしない
26
+ * 2. **確認済み=新しい側の `status` が `active`**。空・`draft`・語彙の外は「未確認」
27
+ * 3. **置き換えられた(もう有効でない・retired)=確認済みの置き換えが 1 本以上**
28
+ * 4. 置き換え先は**全部・新しい順**(決定日の降順 → ID の降順・同じ ID は 1 回)
29
+ * 5. 自分自身を指す・実在しない ID は数えない。古い側の `status` は問わない
30
+ */
31
+ const node_path_1 = __importDefault(require("node:path"));
32
+ /** 確認済みとみなす新しい側の status(これだけ) */
33
+ exports.CONFIRMED_STATUS = "active";
34
+ /**
35
+ * 決定を指す文字列を突き合わせの鍵にする(`decision:` の頭・パス・`.md` を落とす)。
36
+ * engine は `path.basename`(Linux で走る=posix)なので、ここも posix で固定する
37
+ */
38
+ function decisionKeyOf(ref) {
39
+ return node_path_1.default.posix.basename(String(ref ?? "").trim().replace(/^decision:/, ""), ".md");
40
+ }
41
+ /** `YYYY-MM-DD`。frontmatter の `date`(`20260910` / `2026-09-10`)→ ID の先頭 8 桁 → 無ければ空 */
42
+ function decisionDateOf(date, id) {
43
+ const raw = String(date ?? "").trim();
44
+ const ymd = raw.match(/^(\d{4})-?(\d{2})-?(\d{2})/) || decisionKeyOf(id).match(/^(\d{4})(\d{2})(\d{2})/);
45
+ return ymd ? `${ymd[1]}-${ymd[2]}-${ymd[3]}` : "";
46
+ }
47
+ function normalize(d) {
48
+ if (!d || typeof d !== "object")
49
+ return null;
50
+ const r = d;
51
+ const id = String(r.id ?? "").trim();
52
+ if (!id)
53
+ return null;
54
+ return {
55
+ id,
56
+ key: decisionKeyOf(id),
57
+ date: decisionDateOf(r.date, id),
58
+ status: typeof r.status === "string" ? r.status.trim() : "",
59
+ supersedes: (Array.isArray(r.supersedes) ? r.supersedes : []).map(decisionKeyOf).filter(Boolean),
60
+ };
61
+ }
62
+ /** 新しい順(決定日の降順 → ID の降順)。日付の無いものは最後 */
63
+ function newestFirst(a, b) {
64
+ if (a.date !== b.date) {
65
+ if (!a.date)
66
+ return 1;
67
+ if (!b.date)
68
+ return -1;
69
+ return a.date < b.date ? 1 : -1;
70
+ }
71
+ return a.id < b.id ? 1 : a.id > b.id ? -1 : 0;
72
+ }
73
+ /**
74
+ * 置き換えの逆引き。
75
+ *
76
+ * @returns `Map<古い決定の鍵, SupersedesEntry>`。置き換えが 1 本も無い決定は入らない
77
+ */
78
+ function supersedesIndex(decisions) {
79
+ const src = decisions instanceof Map ? decisions.values() : decisions || [];
80
+ const list = [...src].map(normalize).filter((d) => d !== null);
81
+ const byKey = new Map();
82
+ for (const d of list)
83
+ if (!byKey.has(d.key))
84
+ byKey.set(d.key, d);
85
+ const out = new Map();
86
+ for (const d of list) {
87
+ for (const target of new Set(d.supersedes)) {
88
+ const old = byKey.get(target);
89
+ if (!old || old.key === d.key)
90
+ continue; // 実在しない・自分自身
91
+ const entry = out.get(old.key) || { id: old.id, by: [], retired: false };
92
+ if (entry.by.some((x) => decisionKeyOf(x.id) === d.key))
93
+ continue;
94
+ const confirmed = d.status === exports.CONFIRMED_STATUS;
95
+ entry.by.push({ id: d.id, date: d.date, status: d.status, confirmed });
96
+ if (confirmed)
97
+ entry.retired = true;
98
+ out.set(old.key, entry);
99
+ }
100
+ }
101
+ for (const entry of out.values())
102
+ entry.by.sort(newestFirst);
103
+ return out;
104
+ }
105
+ /** その決定の逆引き(無ければ空の形) */
106
+ function supersedesOf(index, id) {
107
+ return index.get(decisionKeyOf(id)) || { id: String(id ?? ""), by: [], retired: false };
108
+ }
109
+ /** 置き換えられた(確認済みの置き換えがある)決定の ID の集合 */
110
+ function retiredIdsOf(index) {
111
+ return new Set([...index.values()].filter((e) => e.retired).map((e) => e.id));
112
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.85",
3
+ "version": "0.12.86",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,