@takagaki/cortex-decisions-viewer 0.12.84 → 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/README.md CHANGED
@@ -40,7 +40,7 @@ cortex-decisions-viewer build [dir] [options]
40
40
  | --- | --- | --- |
41
41
  | `[dir]` | `Decisions` | Decisionsディレクトリ(リポジトリルートからの相対 or 絶対) |
42
42
  | `-o, --out <dir>` | `site` | 出力ディレクトリ |
43
- | `-t, --title <title>` | `Decisions` | サイトタイトル |
43
+ | `-t, --title <title>` | `Decisions` | サイトタイトル(`Cortex/Home.md` の frontmatter `project` が読めればそちらを優先。これは読めないときの控え) |
44
44
  | `--repo-url <url>` | (git remoteから解決) | GitHubリポジトリURL。CIで git remote が引けない場合に明示指定 |
45
45
  | `--branch <branch>` | `main`(環境変数 `AWS_BRANCH` があればそれ) | 編集リンク・参照リンクが指すブランチ |
46
46
  | `--exclude <files...>` | (なし) | 一覧から除外する追加ファイル名。既定で `README.md` / `template.md` は除外 |
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;
@@ -96,6 +98,8 @@ function takeProposals(filePath) {
96
98
  return hit;
97
99
  }
98
100
  const external_id_1 = require("./external-id");
101
+ const home_project_1 = require("./home-project");
102
+ const supersedes_1 = require("./supersedes");
99
103
  const render_1 = require("./render");
100
104
  /**
101
105
  * この画面を作ったパッケージの版。**package.json を唯一の出どころにする。**
@@ -112,6 +116,11 @@ const PKG_VERSION = (() => {
112
116
  }
113
117
  })();
114
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"]);
115
124
  /** frontmatterのrelationsを安全に正規化する */
116
125
  function toRelations(value) {
117
126
  if (!Array.isArray(value))
@@ -794,6 +803,78 @@ function attachDecisionFollowup(decisions, recordsDir, repoRoot, ledger) {
794
803
  }
795
804
  return [...ledger.keys()].filter((k) => !matched.has(k.normalize("NFC")));
796
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
+ }
797
878
  /**
798
879
  * AIからの更新提案(`.cortex/gold-proposals.json`)を読む。
799
880
  *
@@ -1508,7 +1589,8 @@ async function scanContent(repoRoot, goldDirName, repoBaseUrl, branch) {
1508
1589
  }
1509
1590
  }
1510
1591
  for (const r of toRelations(data.relations))
1511
- 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 });
1512
1594
  }
1513
1595
  else {
1514
1596
  const keyMatch = raw.slice(0, 4000).match(/^- 課題キー: (\S+)$/m);
@@ -1617,6 +1699,9 @@ function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl,
1617
1699
  };
1618
1700
  const addRelationEdges = (fromId, relations) => {
1619
1701
  for (const r of relations) {
1702
+ // 🔴 `superseded_by` は描かない(#699 F3)。新しい側の `supersedes` の写しで、描くと同じ置き換えが 2 本になる
1703
+ if (exports.MIRROR_RELS.has(r.rel))
1704
+ continue;
1620
1705
  edges.push({ from: fromId, to: classifyTarget(r.target), rel: r.rel });
1621
1706
  }
1622
1707
  };
@@ -2062,11 +2147,18 @@ async function build(opts) {
2062
2147
  // **読めない・使えないなら `null`** で、関係者の並びは今までと 1 バイトも同じ
2063
2148
  const engagement = await (0, engagement_1.readEngagementLedger)(repoRoot);
2064
2149
  const decisions = [];
2150
+ const rawDates = new Map();
2065
2151
  for (const fileName of mdFiles) {
2066
2152
  const raw = await node_fs_1.promises.readFile(path.join(dirAbs, fileName), "utf8");
2067
2153
  const rec = await parseDecision(raw, fileName, repoRelDir, repo.baseUrl, repo.branch, refs);
2068
2154
  decisions.push(rec);
2155
+ // 置き換えの逆引きの決定日(書かれたままの文字列・SiteData には載せない)
2156
+ const rd = rawFrontmatterDate(raw);
2157
+ if (rd !== undefined)
2158
+ rawDates.set(rec, rd);
2069
2159
  }
2160
+ // 置き換えの逆引き(#699 F3)。**置き換えの無い案件では 1 件も付かない**
2161
+ attachSupersedes(decisions, (d) => rawDates.get(d) ?? d.fm.date);
2070
2162
  // 決定の「その後」(#705)。**帳簿を持たない案件では 1 件も付かず、絞り込みの印も立たない**
2071
2163
  // (`decisionFollowup` はキーごと無い=今までと 1 バイトも同じ画面)
2072
2164
  const followupLedger = await (0, rederivation_1.readDecisionFollowup)(repoRoot);
@@ -2081,12 +2173,15 @@ async function build(opts) {
2081
2173
  let home = null;
2082
2174
  let homeGrounding;
2083
2175
  let homeUpdatedAt;
2176
+ // Home.md の本文(ヘッダーの案件名を frontmatter の `project` から決めるため。無ければ null)
2177
+ let homeRaw = null;
2084
2178
  const sections = [];
2085
2179
  // グラフに渡す区画(**並べ替える前**の配列を持つ。上の sections とレコードの実体は同じ)
2086
2180
  const graphSections = [];
2087
2181
  if (knowledgeRoot) {
2088
2182
  try {
2089
2183
  const raw = await node_fs_1.promises.readFile(path.join(knowledgeRoot, "Home.md"), "utf8");
2184
+ homeRaw = raw;
2090
2185
  home = await parseGeneric(raw, "Home.md", toRepoRelative(knowledgeRoot), repo.baseUrl, repo.branch);
2091
2186
  // 自己記述の節を取り出し、**本文からは抜く**(最上部に出すので、残すと2度出る)。
2092
2187
  // 旧い使い方の説明もここで落ちる(説明はビューアが持つようになった)
@@ -2194,7 +2289,8 @@ async function build(opts) {
2194
2289
  // 案件アイコン(#348)。Cortex構成でなければ undefined(旧レイアウトには置き場が無い)
2195
2290
  const projectIcon = await findProjectIcon(knowledgeRoot);
2196
2291
  const site = {
2197
- title: opts.title,
2292
+ // 🔴 **案件名の正本は Home.md の `project`**(engine #699 F10)。`--title` は読めないときの控え
2293
+ title: (0, home_project_1.resolveSiteTitle)(homeRaw, opts.title),
2198
2294
  generatedAt: opts.generatedAt,
2199
2295
  // **package.json から読む。** 手で書くと、上げ忘れて嘘の版が出る
2200
2296
  viewerVersion: PKG_VERSION,
package/dist/cli.js CHANGED
@@ -12,7 +12,7 @@ program
12
12
  .description("Decisionsディレクトリを静的サイトにビルドする")
13
13
  .argument("[dir]", "Decisionsディレクトリ(未指定なら Cortex/Decisions/records → Cortex/Decisions → Decisions の順で自動検出)")
14
14
  .option("-o, --out <dir>", "出力ディレクトリ", "site")
15
- .option("-t, --title <title>", "サイトタイトル", "Decisions")
15
+ .option("-t, --title <title>", "サイトタイトル(Cortex/Home.md の project が読めればそちらを優先。これは読めないときの控え)", "Decisions")
16
16
  .option("--repo-url <url>", "GitHubリポジトリURL(未指定なら git remote から解決)")
17
17
  .option("--branch <branch>", "編集リンクが指すブランチ(既定: main / 環境変数 AWS_BRANCH)")
18
18
  .option("--exclude <files...>", "一覧から除外する追加ファイル名(既定で README.md / template.md は除外)", [])
@@ -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);
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ /**
3
+ * `Cortex/Home.md` の frontmatter `project`(案件の表示名)を読む(engine #699 F10)。
4
+ *
5
+ * ## なぜビューアが自分で読むのか
6
+ *
7
+ * 案件の表示名の**正本は Home.md の `project`**(`title` は頁の名前 "Home" で、表示名ではない)。
8
+ * これまでヘッダーの案件名は `--title`(engine の viewer-config.json の title、旧 Amplify は
9
+ * `viewerTitle ?? key`)の写しだけを見ていたので、Home.md で改名しても写しを直すまで
10
+ * ヘッダーが古い名前のままだった。build の時点で Home を読んでいるので、ここで正本を直接見る。
11
+ * 読めないとき(Home が無い・`project` が空・`{{プロジェクト名}}` のまま等)は `--title` に倒す。
12
+ *
13
+ * ## 読み方は infra の正本の写し
14
+ *
15
+ * 正本は `infra/scripts/lib/home-md.mjs` の `parseHomeProjectName`(名簿・Apps Script と同じ規則)。
16
+ * ビューアは npm で配る別パッケージなので import できず、写しを持つ。
17
+ * **規則を変えるときは両側を同じ便で直すこと。** 一致は `viewer/test/home-grounding.test.mjs` の
18
+ * parity の節が、infra の題材(`infra/test/fixtures/home-md-cases.mjs`・通る題材と弾く題材の両方)で検査する。
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.parseHomeProjectName = parseHomeProjectName;
22
+ exports.resolveSiteTitle = resolveSiteTitle;
23
+ /** 区切り行か(`---`。行末の空白は許す) */
24
+ const isDelimiter = (line) => /^---[ \t]*$/.test(line);
25
+ /** `project:` の右側を値にする(正本の `parseScalar` と同じ規則) */
26
+ function parseScalar(rest) {
27
+ const raw = rest.trim();
28
+ if (raw === "")
29
+ return { ok: false, reason: "EMPTY" };
30
+ const q = raw.charAt(0);
31
+ if (q === '"') {
32
+ let out = "";
33
+ for (let i = 1; i < raw.length; i += 1) {
34
+ const c = raw.charAt(i);
35
+ if (c === "\\") {
36
+ // `\"` と `\\` だけを剥がす(他のエスケープは原文のまま)
37
+ const n = raw.charAt(i + 1);
38
+ if (n === '"' || n === "\\") {
39
+ out += n;
40
+ i += 1;
41
+ continue;
42
+ }
43
+ out += c;
44
+ continue;
45
+ }
46
+ if (c === '"')
47
+ return { ok: true, value: out };
48
+ out += c;
49
+ }
50
+ return { ok: false, reason: "BROKEN_VALUE" };
51
+ }
52
+ if (q === "'") {
53
+ let out = "";
54
+ for (let i = 1; i < raw.length; i += 1) {
55
+ const c = raw.charAt(i);
56
+ if (c === "'") {
57
+ if (raw.charAt(i + 1) === "'") {
58
+ out += "'";
59
+ i += 1;
60
+ continue;
61
+ }
62
+ return { ok: true, value: out };
63
+ }
64
+ out += c;
65
+ }
66
+ return { ok: false, reason: "BROKEN_VALUE" };
67
+ }
68
+ // 裸の値: 空白の後ろの `#` から先はコメント。行頭の `#` は値が無いのと同じ
69
+ if (q === "#")
70
+ return { ok: false, reason: "EMPTY" };
71
+ const bare = (/^(.*?)(?:[ \t]+#.*)?$/.exec(raw) ?? [raw, raw])[1].trim();
72
+ // YAML の null は「無い」
73
+ if (bare === "null" || bare === "~")
74
+ return { ok: false, reason: "EMPTY" };
75
+ // 1 行の文字列以外(ブロックスカラー・flow・アンカー/エイリアス/タグ)は読まない
76
+ if (/^[|>][+-]?$/.test(bare) || /^[\[{&*!]/.test(bare))
77
+ return { ok: false, reason: "UNSUPPORTED" };
78
+ return { ok: true, value: bare };
79
+ }
80
+ /** Home.md の本文から `project` を読む(純関数。正本 `parseHomeProjectName` の写し) */
81
+ function parseHomeProjectName(markdown) {
82
+ const text = String(markdown ?? "").replace(/^/, "").replace(/\r\n?/g, "\n");
83
+ const lines = text.split("\n");
84
+ if (lines.length === 0 || !isDelimiter(lines[0]))
85
+ return { ok: false, reason: "NO_FRONTMATTER" };
86
+ let end = -1;
87
+ for (let i = 1; i < lines.length; i += 1) {
88
+ if (isDelimiter(lines[i])) {
89
+ end = i;
90
+ break;
91
+ }
92
+ }
93
+ if (end < 0)
94
+ return { ok: false, reason: "UNCLOSED_FRONTMATTER" };
95
+ for (let i = 1; i < end; i += 1) {
96
+ const m = /^project:(.*)$/.exec(lines[i]);
97
+ if (!m)
98
+ continue;
99
+ const scalar = parseScalar(m[1]);
100
+ if (!scalar.ok)
101
+ return scalar;
102
+ const value = scalar.value.trim();
103
+ if (value === "")
104
+ return { ok: false, reason: "EMPTY" };
105
+ if (/\{\{[\s\S]*\}\}/.test(value))
106
+ return { ok: false, reason: "PLACEHOLDER" };
107
+ return { ok: true, project: value };
108
+ }
109
+ return { ok: false, reason: "MISSING" };
110
+ }
111
+ /**
112
+ * ヘッダーに出す案件名を決める。Home.md の `project` が読めればそれ、読めなければ `--title`。
113
+ * @param homeRaw Home.md の本文(無ければ null)
114
+ * @param fallback `--title` の値
115
+ */
116
+ function resolveSiteTitle(homeRaw, fallback) {
117
+ if (homeRaw == null)
118
+ return fallback;
119
+ const parsed = parseHomeProjectName(homeRaw);
120
+ return parsed.ok ? parsed.project : fallback;
121
+ }
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.84",
3
+ "version": "0.12.86",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,