@takagaki/cortex-decisions-viewer 0.12.13 → 0.12.14

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.
Files changed (2) hide show
  1. package/dist/render.js +260 -52
  2. package/package.json +1 -1
package/dist/render.js CHANGED
@@ -2080,9 +2080,15 @@ a:hover { text-decoration: underline; }
2080
2080
  background: #fffbeb; border: 1px solid #fcd34d; border-radius: 8px;
2081
2081
  padding: 10px 14px; margin: 12px 0 4px; font-size: 13.5px; color: #92400e; line-height: 1.7;
2082
2082
  }
2083
+ /* 警告帯に並ぶ二択(確認済みにする/削除)。AIが起票した記録に人がやることは
2084
+ 「引き取る」か「要らないと言う」のどちらかなので、同じ場所に並べる */
2085
+ .draft-note-actions {
2086
+ flex: 0 0 auto; align-self: center; margin-left: auto;
2087
+ display: flex; flex-wrap: wrap; gap: 10px; align-items: center;
2088
+ }
2083
2089
  /* 警告帯に並べる「確認済みにする」。帯と同じ系統の色で、押すものだと分かる濃さにする */
2084
2090
  .promote-btn {
2085
- flex: 0 0 auto; align-self: center; margin-left: auto; cursor: pointer; white-space: nowrap;
2091
+ flex: 0 0 auto; cursor: pointer; white-space: nowrap;
2086
2092
  padding: 6px 14px; border-radius: 999px; border: 1px solid #b45309;
2087
2093
  background: #b45309; color: #fff; font-size: 13px; font-weight: 600;
2088
2094
  }
@@ -2133,23 +2139,30 @@ a:hover { text-decoration: underline; }
2133
2139
  font-size: 12.5px; line-height: 1.7; white-space: pre-wrap; word-break: break-word;
2134
2140
  }
2135
2141
  /* ---- レコードの削除(?op=delete-record)----
2136
- **控えめに置く。** 取り消せない操作なので、上の操作列(AIで編集・GitHubで編集)の
2137
- 隣には並べない。押したあとの見た目は「取り消し線+無効化」で、一覧からは消さない
2138
- (次のビルドまで SiteData は古いまま) */
2139
- .record-delete { margin-top: 28px; padding-top: 14px; border-top: 1px solid var(--border); }
2142
+ 置き場は2つ(2026-08-28 のオーナー裁定)。**未確認の記録は警告帯の中**で
2143
+ 「確認済みにする」と二択に、**警告帯が出ない記録は上の操作列の末尾**に置く。
2144
+ 取り消せない操作なので、どちらでも枠を持たない控えめな文字のボタンにして、
2145
+ 隣の実体のあるボタンと重さを分ける。押したあとの見た目は「取り消し線+無効化」 */
2140
2146
  .record-delete-btn {
2141
- background: none; border: 0; padding: 0; cursor: pointer;
2147
+ background: none; border: 0; padding: 0; cursor: pointer; white-space: nowrap;
2142
2148
  font-size: 12.5px; color: var(--muted); text-decoration: underline;
2143
2149
  }
2150
+ /* 警告帯の中では帯と同じ系統の色に寄せる(帯の外では既定の控えめな灰) */
2151
+ .draft-note .record-delete-btn { color: #92400e; }
2144
2152
  .record-delete-btn:hover { color: #b91c1c; }
2145
2153
  .record-delete-btn:disabled { cursor: default; text-decoration: none; opacity: 0.7; }
2146
- .record-delete-msg { font-size: 13px; margin-top: 6px; }
2154
+ .record-delete-msg { font-size: 13px; }
2155
+ .record-delete-msg:empty { display: none; }
2147
2156
  .record-delete-msg.is-error { color: #b91c1c; }
2157
+ /* 帯の中では1行を占める(ボタンの並びの下へ回り込ませる)。操作列では右寄せのまま畳む */
2158
+ .draft-note > .record-delete-msg { flex: 1 0 100%; }
2159
+ .detail-actions .record-delete-msg { max-width: 320px; text-align: right; }
2148
2160
  .deleted-note {
2149
- display: flex; gap: 8px; align-items: flex-start; margin-top: 10px;
2150
- background: #f8f9fa; border: 1px solid var(--border); border-radius: 8px;
2161
+ display: flex; gap: 8px; align-items: flex-start; margin: 4px 0 16px;
2162
+ background: #fff; border: 1px solid var(--border); border-radius: 8px;
2151
2163
  padding: 10px 12px; font-size: 13px; line-height: 1.8; color: var(--muted);
2152
2164
  }
2165
+ .draft-note > .deleted-note { flex: 1 0 100%; margin-bottom: 0; }
2153
2166
  /* 消したものは、消えたと分かる形で残す(一覧・詳細の両方) */
2154
2167
  .is-deleted { text-decoration: line-through; color: var(--muted); }
2155
2168
  /* 消したレコードでは、直す口(AIで編集・GitHubで編集)を押させない */
@@ -3078,6 +3091,11 @@ ${deriveCapTile.toString()}
3078
3091
  // **確認済みにできる記録には、その場で上げるボタンを並べる**(下の promotableRecordPath)。
3079
3092
  // ボタンが出せないとき(受け口が未設定・パスが解決できない)は、これまでどおり
3080
3093
  // 「frontmatter を直してください」の案内に倒す——押せない画面で押す話をしない
3094
+ //
3095
+ // **削除もここに並べる(2026-08-28 のオーナー裁定)。** AIが起票した記録に対して
3096
+ // 人がやることは「引き取る(確認済みにする)」か「要らないと言う(削除)」の二択で、
3097
+ // 同じ場面の同じ判断なので、口を離さない。**この帯は draft にしか出ない**ので、
3098
+ // 人が確認済みにした記録には削除の口ごと現れない(守りはサーバ側にもある)。
3081
3099
  function draftNote(rec) {
3082
3100
  if (!rec || rec.rstatus !== "draft") return null;
3083
3101
  var path = promotableRecordPath(rec);
@@ -3087,13 +3105,19 @@ ${deriveCapTile.toString()}
3087
3105
  ? "この内容はAIが自動生成したものです(人間未確認)。内容を確認し、直すところが無ければ「確認済みにする」を押してください。直したいところがあれば「AIで編集」から修正してください。"
3088
3106
  : "この内容はAIが自動生成したものです(人間未確認)。必ず内容を確認し、適切な内容に直したうえで frontmatter の status を active に変更してください。" }),
3089
3107
  ]);
3108
+ var actions = el("div", { class: "draft-note-actions" });
3090
3109
  if (path) {
3091
3110
  var btn = el("button", { class: "promote-btn", type: "button", text: "確認済みにする" });
3092
3111
  btn.addEventListener("click", function () {
3093
3112
  openPromoteModal(path, function () { markPromoted(rec, note); });
3094
3113
  });
3095
- note.appendChild(btn);
3114
+ actions.appendChild(btn);
3096
3115
  }
3116
+ var del = buildDeleteControls(rec);
3117
+ if (del) actions.appendChild(del.btn);
3118
+ if (actions.children.length) note.appendChild(actions);
3119
+ // 断られた理由・削除した旨は帯の中に出す(口と結果を離さない)
3120
+ if (del) note.appendChild(del.msg);
3097
3121
  return note;
3098
3122
  }
3099
3123
 
@@ -10161,7 +10185,7 @@ ${deriveCapTile.toString()}
10161
10185
  filters.appendChild(buildSelect("すべての期間", SITE.months, listState.month, function (v) { listState.month = v; listState.page = 1; drawCards(); }, monthLabel));
10162
10186
  }
10163
10187
  filters.appendChild(buildSelect("すべてのカテゴリー", SITE.categories, listState.category, function (v) { listState.category = v; listState.page = 1; drawCards(); }));
10164
- var statusOptsD = statusFilterOptions(SITE.decisions);
10188
+ var statusOptsD = statusFilterOptions(visibleRecords(SITE.decisions));
10165
10189
  if (statusOptsD.length) {
10166
10190
  filters.appendChild(buildSelect("すべての状態", statusOptsD, listState.status, function (v) { listState.status = v; listState.page = 1; drawCards(); }, statusLabel));
10167
10191
  }
@@ -10189,7 +10213,9 @@ ${deriveCapTile.toString()}
10189
10213
 
10190
10214
  function drawCards() {
10191
10215
  var q = listState.q.trim().toLowerCase();
10192
- var items = SITE.decisions.filter(function (d) {
10216
+ // 削除済みは絞り込みの手前で落とす(分母も一緒に減るので「12/13件」が残らない)
10217
+ var all = visibleRecords(SITE.decisions);
10218
+ var items = all.filter(function (d) {
10193
10219
  if (listState.month && d.date.slice(0, 7) !== listState.month) return false;
10194
10220
  if (listState.category && d.category !== listState.category) return false;
10195
10221
  if (!matchesStatusFilter(d)) return false;
@@ -10204,14 +10230,14 @@ ${deriveCapTile.toString()}
10204
10230
  wrap.innerHTML = "";
10205
10231
 
10206
10232
  if (!items.length) {
10207
- countEl.textContent = "0 / " + SITE.decisions.length + " 件";
10233
+ countEl.textContent = "0 / " + all.length + " 件";
10208
10234
  wrap.appendChild(el("div", { class: "empty", text: "該当する決定はありません" }));
10209
10235
  return;
10210
10236
  }
10211
10237
 
10212
10238
  var pg = paginate(items);
10213
10239
  countEl.textContent =
10214
- items.length + " / " + SITE.decisions.length + " 件" +
10240
+ items.length + " / " + all.length + " 件" +
10215
10241
  (pg.pages > 1 ? "(" + pg.page + "/" + pg.pages + "ページ)" : "");
10216
10242
 
10217
10243
  pg.slice.forEach(function (d) {
@@ -10246,7 +10272,8 @@ ${deriveCapTile.toString()}
10246
10272
  function renderGenericList() {
10247
10273
  var tab = null;
10248
10274
  TABS.forEach(function (t) { if (t.key === listState.tab) tab = t; });
10249
- var items = tab && tab.items ? tab.items : [];
10275
+ // 削除済みは描画の手前で落とす(件数の分母・絞り込みの選択肢もこれ1本で揃う)
10276
+ var items = visibleRecords(tab && tab.items ? tab.items : []);
10250
10277
 
10251
10278
  var filters = el("div", { class: "filters" });
10252
10279
  var search = el("input", { type: "search", placeholder: "キーワード検索" });
@@ -10490,9 +10517,10 @@ ${deriveCapTile.toString()}
10490
10517
  /**
10491
10518
  * 確認の文面。**何が起きるかを具体に書く**(消えるだけなのか、除外リストにも載るのか)。
10492
10519
  *
10493
- * 🔴 **status による重み付けはここ1箇所に閉じる。** 2026-08-28 のオーナー裁定は
10494
- * 「確認済み(active)の記録も消せる。ただし確認を一段重くする」。裁定が
10495
- * 「active は消せない」へ振れたときに直すのは、この関数と出し分けだけで済ませる。
10520
+ * **status による出し分けはもう無い。** 2026-08-28 のオーナー裁定は最終的に
10521
+ * 「消せるのは draft だけ」へ落ち着いた(確認済みの記録に重い警告を出して消させる、
10522
+ * ではなく、口ごと出さない)。判定は draftNote の出し分けとサーバの門にあるので、
10523
+ * ここは「何が起きるか」だけを書く。
10496
10524
  */
10497
10525
  function deleteConfirmLines(rec, path) {
10498
10526
  var lines = ["このレコードを削除しますか?", rec.title];
@@ -10500,10 +10528,9 @@ ${deriveCapTile.toString()}
10500
10528
  if (label) {
10501
10529
  lines.push("削除すると、夜間のAIが同じ内容を再登録しないよう、" + label + "の除外リストにも追記されます。");
10502
10530
  }
10503
- if (rec.rstatus === "active") lines.push("これは人が確認済みにした記録です。");
10504
10531
  return lines;
10505
10532
  }
10506
- async function sendDeleteRecord(path, msg, btn, onOk) {
10533
+ async function sendDeleteRecord(rec, path, msg, btn, onOk) {
10507
10534
  var intake = SITE.intake;
10508
10535
  if (!intake) return;
10509
10536
  function setError(text) {
@@ -10534,6 +10561,8 @@ ${deriveCapTile.toString()}
10534
10561
  // **なっている**ので、失敗として見せると「押しても消えない」堂々巡りになる。
10535
10562
  // **符号で見る**(promote の ALREADY_ACTIVE と同じ理由)
10536
10563
  if (res.ok || (res.status === 409 && j.code === "ALREADY_DELETED")) {
10564
+ // このブラウザでは即座に消えたことにする(次のビルドまで SiteData は古い)
10565
+ rememberDeleted(rec, path);
10537
10566
  endBusy();
10538
10567
  onOk();
10539
10568
  return;
@@ -10559,52 +10588,219 @@ ${deriveCapTile.toString()}
10559
10588
  rec.deleted = true;
10560
10589
  var title = app.querySelector(".detail-title");
10561
10590
  if (title) title.classList.add("is-deleted");
10562
- var btn = app.querySelector(".record-delete-btn");
10563
- if (btn) { btn.disabled = true; btn.textContent = "削除しました"; }
10591
+ // **置き場は2つある**(draft の警告帯の中/上の操作列)。どちらに出ていても止める
10592
+ var btns = app.querySelectorAll(".record-delete-btn");
10593
+ for (var b = 0; b < btns.length; b++) { btns[b].disabled = true; btns[b].textContent = "削除しました"; }
10564
10594
  // もう無い記録を直す口は押させない(AIで編集・GitHubで編集・確認済みにする)
10565
10595
  var links = app.querySelectorAll(".detail-actions a.ai-edit-btn, .detail-actions a.edit-btn");
10566
10596
  for (var i = 0; i < links.length; i++) disableDetailLink(links[i]);
10567
10597
  var promote = app.querySelector(".draft-note .promote-btn");
10568
10598
  if (promote) promote.disabled = true;
10569
- var host = app.querySelector(".record-delete");
10570
- if (host && !host.querySelector(".deleted-note")) {
10571
- host.appendChild(el("div", { class: "deleted-note" }, [
10572
- el("span", { text: "🗑" }),
10573
- el("span", { text: "削除しました。次のビルド(数分後)でサイトから消えます。" }),
10574
- ]));
10575
- }
10599
+ if (app.querySelector(".deleted-note")) return;
10600
+ var note = el("div", { class: "deleted-note" }, [
10601
+ el("span", { text: "🗑" }),
10602
+ el("span", { text: "削除しました。次のビルド(数分後)でサイトから消えます。" }),
10603
+ ]);
10604
+ // 押した口の近くに出す。警告帯があるならその中、無ければ見出しの直下
10605
+ var banner = app.querySelector(".draft-note");
10606
+ if (banner) { banner.appendChild(note); return; }
10607
+ var meta = app.querySelector(".detail-meta");
10608
+ if (meta && meta.parentNode) meta.parentNode.insertBefore(note, meta.nextSibling);
10576
10609
  }
10577
10610
  /**
10578
- * 詳細ページ最下部の削除の口。**上の操作列には並べない**(分類B・2026-08-28)——
10579
- * 高頻度の操作(AIで編集・GitHubで編集)の隣に、取り消せない操作を置かない。
10611
+ * 人の確認印が付いた記録か。**3値の意味は infra の HUMAN_CONFIRMED_STATUSES と同じ**
10612
+ * (active = 人が確認済みにした印/superseded = active を経た記録の差し替え後/
10613
+ * inactive = 人が退場を記した印)。ここは「押しても断られるボタンを出さない」ための
10614
+ * 出し分けで、**守りはサーバ側**(promote の PROMOTABLE_PATH と同じ関係)。
10580
10615
  */
10581
- function buildDeleteSection(rec) {
10616
+ function humanConfirmedStatus(rec) {
10617
+ var s = rec && rec.rstatus;
10618
+ return s === "active" || s === "superseded" || s === "inactive";
10619
+ }
10620
+ /**
10621
+ * 削除の口(ボタンと、その結果を出す行)。**置き場は呼び出し側が決める。**
10622
+ *
10623
+ * 出さないのは**人の確認印が付いた記録だけ**(2026-08-28 の裁定変更)。原則は
10624
+ * 「人間の承認が挟まったものは削除の選択肢を出さない」で、逆に status が無い記録・
10625
+ * frontmatter が壊れた記録は**画面から片付けたい掃除の主対象**なので消せる。
10626
+ *
10627
+ * 呼び出し側は2つ:
10628
+ * - draftNote …… 未確認の記録。「確認済みにする」と並べて警告帯の中に置く
10629
+ * - 詳細ページの操作列 …… 警告帯が出ない記録。置き場が他に無いのでここに寄せる
10630
+ */
10631
+ function buildDeleteControls(rec) {
10632
+ if (humanConfirmedStatus(rec)) return null;
10582
10633
  var path = deletableRecordPath(rec);
10583
10634
  if (!path) return null;
10584
- var wrap = el("div", { class: "record-delete" });
10585
- var btn = el("button", { class: "record-delete-btn", type: "button", text: "このレコードを削除…" });
10635
+ // 「…」は**押すと確認が出る**合図(「このレコードを削除…」から引き継いだ慣習)。
10636
+ // 隣の「確認済みにする」は押した先で名前入力があるので、二択の重さの違いを語尾で示す
10637
+ var btn = el("button", { class: "record-delete-btn", type: "button", text: "削除…" });
10586
10638
  var msg = el("div", { class: "record-delete-msg" });
10587
10639
  btn.addEventListener("click", async function () {
10588
10640
  // **確認を await する。** 落とすと Promise が truthy で確認なしに消える
10589
10641
  if (!(await confirmDanger(deleteConfirmLines(rec, path), { okLabel: "削除する", returnTo: btn }))) return;
10590
- await sendDeleteRecord(path, msg, btn, function () { markRecordDeleted(rec); });
10642
+ await sendDeleteRecord(rec, path, msg, btn, function () { markRecordDeleted(rec); });
10591
10643
  });
10592
- wrap.appendChild(btn);
10593
- wrap.appendChild(msg);
10594
- return wrap;
10644
+ return { btn: btn, msg: msg };
10595
10645
  }
10596
- /** 詳細ページの末尾(本文・提案・frontmatter の後)に、履歴と削除の口を並べる */
10646
+ /**
10647
+ * 警告帯が出ない記録の削除の口を、上の操作列(AIで編集・GitHubで編集の行)の末尾へ置く。
10648
+ *
10649
+ * **draft には出さない**——そちらは警告帯の中で「確認済みにする」と対になっている
10650
+ * (2つ出すと、同じ操作の口がページに2か所できる)。取り消せない操作を高頻度の
10651
+ * 操作の隣に置くことにはなるが、**警告帯の無い記録には他に置き場が無い**。
10652
+ * 見た目は控えめな文字のボタンのままにして、隣の実体のあるボタンと重さを分ける。
10653
+ */
10654
+ function attachDeleteToEditRow(rec, editRow) {
10655
+ if (!rec || rec.rstatus === "draft") return null;
10656
+ var del = buildDeleteControls(rec);
10657
+ if (!del) return null;
10658
+ editRow.appendChild(del.btn);
10659
+ return del.msg; // 結果を出す行は操作列の下(呼び出し側が最後に並べる)
10660
+ }
10661
+ /** 詳細ページの末尾(本文・提案・frontmatter の後)に、AIの変更履歴と frontmatter を並べる */
10597
10662
  function appendRecordFooter(rec) {
10598
10663
  var history = buildAiHistory(rec);
10599
10664
  if (history) app.appendChild(history);
10600
10665
  var fmp = renderFmPanel(rec.fm);
10601
10666
  if (fmp) app.appendChild(fmp);
10602
- var del = buildDeleteSection(rec);
10603
- if (del) app.appendChild(del);
10604
10667
  // 開き直しても削除済みのまま見せる(印は rec が持っている)
10605
10668
  if (rec.deleted) markRecordDeleted(rec);
10606
10669
  }
10607
10670
 
10671
+ /* ---------------------------------------------------------------------------
10672
+ * 削除の即時反映(トゥームストーン)
10673
+ * ---------------------------------------------------------------------------
10674
+ * 静的サイトなので、削除の commit が入っても**次のビルドまで SiteData は古いまま**。
10675
+ * 何もしないと「消したのに、一覧にもつながりにもまだ居る」ように見える。
10676
+ * そこで削除した記録をこのブラウザに覚えておき、描画の手前で間引く。
10677
+ *
10678
+ * **覚えるのは押した本人のブラウザだけ。** 他の閲覧者には次のビルドまで見えたまま
10679
+ * (2026-08-28 の裁定で了解済みの範囲)。サーバに人ごとの表示状態を持たせない。
10680
+ *
10681
+ * **いつまでも隠し続けない。** 隠しっぱなしにすると、同じ記録を作り直したのに
10682
+ * 「作ったはずのものが出てこない」状態が永久に続く(破棄の条件は applyDeleteTombstones)。
10683
+ */
10684
+ function deletedStoreKey() { return "cortex-deleted:" + REPO_SLUG; }
10685
+ /** 「削除より後に始まったビルドか」を判断するときの余裕(下の tombstoneStale) */
10686
+ var TOMBSTONE_MARGIN_MS = 30 * 60 * 1000;
10687
+
10688
+ function readTombstones() {
10689
+ try {
10690
+ var raw = localStorage.getItem(deletedStoreKey());
10691
+ var list = raw ? JSON.parse(raw) : [];
10692
+ return Object.prototype.toString.call(list) === "[object Array]" ? list : [];
10693
+ } catch (e) { return []; }
10694
+ }
10695
+ function writeTombstones(list) {
10696
+ try { localStorage.setItem(deletedStoreKey(), JSON.stringify(list)); } catch (e) {}
10697
+ }
10698
+ /**
10699
+ * 削除できたことを覚える。**同じパスは足さない**(押し直しで配列が伸びない)。
10700
+ *
10701
+ * gen0(削除操作をした画面のビルド時刻)も残す。破棄の判定をできるだけ
10702
+ * **サーバ時刻同士の比較**にするため(下の tombstoneStale)。
10703
+ */
10704
+ function rememberDeleted(rec, path) {
10705
+ var list = readTombstones();
10706
+ for (var i = 0; i < list.length; i++) {
10707
+ if (list[i] && list[i].path === path) return;
10708
+ }
10709
+ list.push({ path: path, id: (rec && rec.id) || "", at: Date.now(), gen0: SITE.generatedAt || "" });
10710
+ writeTombstones(list);
10711
+ }
10712
+ /**
10713
+ * ビルド時刻の文字列(cli.ts が入れる "YYYY-MM-DD HH:mm UTC")をミリ秒に直す。
10714
+ * 読めなければ NaN——呼び出し側は**隠し続ける側に倒す**(嘘の再表示より安全)。
10715
+ */
10716
+ function siteTimeMs(s) {
10717
+ if (typeof s !== "string" || !s) return NaN;
10718
+ return Date.parse(s.replace(" UTC", "Z").replace(" ", "T"));
10719
+ }
10720
+ /**
10721
+ * このトゥームストーンはもう用済みか(=削除より後のビルドにも記録が残っている)。
10722
+ *
10723
+ * **generatedAt はコミット時点ではない。** 削除コミットより前の状態から始まった
10724
+ * ビルドが削除の後に完成すると、generatedAt は削除時刻より新しいのに削除は
10725
+ * 入っていない。ビルドの所要時間とクライアント時計のずれを見て 30 分の余裕を取る。
10726
+ * 副作用は「同じ記録を作り直した直後だけ、本人のブラウザで最大30分見えない」。
10727
+ *
10728
+ * **主軸はサーバ時刻同士の比較**(現行の generatedAt と、削除した画面の gen0)。
10729
+ * クライアントの時計が大きく狂っていても機能が丸ごとは死なない(進んだ時計なら
10730
+ * 再表示が遅れるだけ)。
10731
+ */
10732
+ function tombstoneStale(t, nowMs) {
10733
+ if (isNaN(nowMs)) return false;
10734
+ var byAt = typeof t.at === "number" && nowMs > t.at + TOMBSTONE_MARGIN_MS;
10735
+ var gen0 = siteTimeMs(t.gen0);
10736
+ // gen0 を持たない古い形式のエントリは at だけで見る(後方互換・安全側)
10737
+ if (isNaN(gen0)) return byAt;
10738
+ return byAt && nowMs > gen0 + TOMBSTONE_MARGIN_MS;
10739
+ }
10740
+ /** トゥームストーンの相手を SITE から探す(id で引き、無ければパスで照合) */
10741
+ function findTombstoned(t) {
10742
+ if (t.id && byId[t.id]) return byId[t.id];
10743
+ if (!t.path) return null;
10744
+ var pool = (SITE.decisions || []).slice();
10745
+ (SITE.sections || []).forEach(function (sec) { pool = pool.concat(sec.records || []); });
10746
+ for (var i = 0; i < pool.length; i++) {
10747
+ if (repoPathOf(pool[i]) === t.path) return pool[i];
10748
+ }
10749
+ return null;
10750
+ }
10751
+ /**
10752
+ * ページを開いたときに1回だけ走らせる。**残すのは「まだ SiteData に居て、かつ
10753
+ * 削除より後のビルドでは消えているはず」のものだけ**:
10754
+ *
10755
+ * 1. 相手が SiteData に居ない → ビルドに反映済み。エントリを捨てる
10756
+ * 2. 居るが、削除より後のビルドにも居る → 作り直された(または revert)。
10757
+ * 捨てて**表示に戻す**(隠し続けると「作り直したのに出ない」になる)
10758
+ * 3. それ以外 → 削除済みの印を立てる
10759
+ */
10760
+ function applyDeleteTombstones() {
10761
+ var list = readTombstones();
10762
+ if (!list.length) return;
10763
+ var nowMs = siteTimeMs(SITE.generatedAt);
10764
+ var kept = [];
10765
+ for (var i = 0; i < list.length; i++) {
10766
+ var t = list[i];
10767
+ if (!t || typeof t.path !== "string" || !t.path) continue;
10768
+ var rec = findTombstoned(t);
10769
+ if (!rec) continue;
10770
+ if (tombstoneStale(t, nowMs)) continue;
10771
+ rec.deleted = true;
10772
+ kept.push(t);
10773
+ }
10774
+ if (kept.length !== list.length) writeTombstones(kept);
10775
+ }
10776
+ applyDeleteTombstones();
10777
+
10778
+ /** 一覧に出すレコード。**描画の手前で間引く**——件数表示も絞り込みの選択肢も自然に揃う */
10779
+ function visibleRecords(items) {
10780
+ return (items || []).filter(function (r) { return !r.deleted; });
10781
+ }
10782
+ /**
10783
+ * 削除済みを除いたグラフ。**消費側で1行ずつ摘まむのではなく、ここ1箇所で作る**
10784
+ * ——グラフ・エクスプローラー・件数表示が別々に間引くと、片方だけ消え残る。
10785
+ */
10786
+ function graphWithoutDeleted() {
10787
+ var g = SITE.graph || {};
10788
+ var nodes = g.nodes || [];
10789
+ var edges = g.edges || [];
10790
+ var gone = {};
10791
+ var any = false;
10792
+ nodes.forEach(function (n) {
10793
+ var rec = n.recordId ? byId[n.recordId] : null;
10794
+ if (rec && rec.deleted) { gone[n.id] = true; any = true; }
10795
+ });
10796
+ if (!any) return { nodes: nodes, edges: edges };
10797
+ return {
10798
+ nodes: nodes.filter(function (n) { return !gone[n.id]; }),
10799
+ // 削除した点に触れる辺も落とす(残すと相手のいない線が浮く)
10800
+ edges: edges.filter(function (e) { return !gone[e.from] && !gone[e.to]; }),
10801
+ };
10802
+ }
10803
+
10608
10804
  // ---------- 汎用詳細 ----------
10609
10805
  function renderGenericDetail(r) {
10610
10806
  app.innerHTML = "";
@@ -10624,9 +10820,12 @@ ${deriveCapTile.toString()}
10624
10820
  if (r.editUrl) {
10625
10821
  editRowR.appendChild(el("a", { class: "edit-btn", href: r.editUrl, target: "_blank", rel: "noopener", text: "GitHubで編集" }));
10626
10822
  }
10823
+ // 警告帯が出ない記録の削除の口はここ(編集の行の末尾)。draft には出さない
10824
+ var delMsgR = attachDeleteToEditRow(r, editRowR);
10627
10825
  head.appendChild(el("div", { class: "detail-actions" }, [
10628
10826
  el("div", { class: "detail-actions-row" }, [ traceBtn2 ]),
10629
10827
  editRowR.children.length ? editRowR : null,
10828
+ delMsgR,
10630
10829
  ]));
10631
10830
  app.appendChild(head);
10632
10831
 
@@ -10682,7 +10881,9 @@ ${deriveCapTile.toString()}
10682
10881
  var graphState = { showWeak: true, q: "" };
10683
10882
 
10684
10883
  function renderGraph() {
10685
- countEl.textContent = "点 " + SITE.graph.nodes.length + " / つながり " + SITE.graph.edges.length;
10884
+ // 削除済みを除いたグラフを1本作り、件数・凡例・描画の全部をこれで賄う
10885
+ var graph = graphWithoutDeleted();
10886
+ countEl.textContent = "点 " + graph.nodes.length + " / つながり " + graph.edges.length;
10686
10887
 
10687
10888
  var filters = el("div", { class: "filters" });
10688
10889
  var search = el("input", { type: "search", placeholder: "キーワードで探す(決定・用語・レポートは本文の中まで探します。見つかった点とそのつながりが光ります)" });
@@ -10699,7 +10900,7 @@ ${deriveCapTile.toString()}
10699
10900
  var legend = el("div", { class: "graph-legend" });
10700
10901
  var legendShown = {};
10701
10902
  Object.keys(GROUP_STYLE).forEach(function (g) {
10702
- var anyNode = SITE.graph.nodes.some(function (n) { return n.group === g; });
10903
+ var anyNode = graph.nodes.some(function (n) { return n.group === g; });
10703
10904
  if (!anyNode || legendShown[GROUP_STYLE[g].label]) return;
10704
10905
  legendShown[GROUP_STYLE[g].label] = true;
10705
10906
  var item = el("span", {}, [el("span", { class: "dot" }), document.createTextNode(GROUP_STYLE[g].label)]);
@@ -10728,19 +10929,19 @@ ${deriveCapTile.toString()}
10728
10929
  legend.appendChild(toggle);
10729
10930
  app.appendChild(legend);
10730
10931
 
10731
- startGraph(canvas, search);
10932
+ startGraph(canvas, search, graph);
10732
10933
  }
10733
10934
 
10734
- function startGraph(canvas, searchInput) {
10735
- var allEdges = SITE.graph.edges.filter(function (e) {
10935
+ function startGraph(canvas, searchInput, graph) {
10936
+ var allEdges = graph.edges.filter(function (e) {
10736
10937
  return graphState.showWeak || !WEAK_RELS[e.rel];
10737
10938
  });
10738
10939
  // 弱い辺を消したとき、レコード以外の孤立ノードは隠す
10739
10940
  var used = {};
10740
10941
  allEdges.forEach(function (e) { used[e.from] = true; used[e.to] = true; });
10741
10942
  // ビルド時に算出済みの座標(n.x/n.y)があればそれを使う=ウォームアップ不要で開いた瞬間に整列済み。
10742
- var preLaidOut = SITE.graph.nodes.some(function (n) { return typeof n.x === "number"; });
10743
- var nodes = SITE.graph.nodes
10943
+ var preLaidOut = graph.nodes.some(function (n) { return typeof n.x === "number"; });
10944
+ var nodes = graph.nodes
10744
10945
  .filter(function (n) { return n.recordId || used[n.id]; })
10745
10946
  .map(function (n, i) {
10746
10947
  return { id: n.id, label: n.label, group: n.group, recordId: n.recordId, url: n.url,
@@ -11291,9 +11492,12 @@ ${deriveCapTile.toString()}
11291
11492
  if (d.editUrl) {
11292
11493
  editRowD.appendChild(el("a", { class: "edit-btn", href: d.editUrl, target: "_blank", rel: "noopener", text: "GitHubで編集" }));
11293
11494
  }
11495
+ // 警告帯が出ない記録の削除の口はここ(編集の行の末尾)。draft には出さない
11496
+ var delMsgD = attachDeleteToEditRow(d, editRowD);
11294
11497
  head.appendChild(el("div", { class: "detail-actions" }, [
11295
11498
  el("div", { class: "detail-actions-row" }, [ traceBtn ]),
11296
11499
  editRowD.children.length ? editRowD : null,
11500
+ delMsgD,
11297
11501
  ]));
11298
11502
  app.appendChild(head);
11299
11503
 
@@ -11362,10 +11566,13 @@ ${deriveCapTile.toString()}
11362
11566
  supersedes: "置換", reference: "参照", source: "出典", link: "リンク",
11363
11567
  };
11364
11568
  var explorerState = { folder: null, path: [], q: "", sx: 0, autoRight: false };
11569
+ // 索引もグラフ本体と同じ材料(削除済みを除いたもの)から作る。別々に間引くと、
11570
+ // つながり画面から消した点がエクスプローラーの列にだけ残る
11571
+ var explorerGraph = graphWithoutDeleted();
11365
11572
  var nodeById = {};
11366
- SITE.graph.nodes.forEach(function (n) { nodeById[n.id] = n; });
11573
+ explorerGraph.nodes.forEach(function (n) { nodeById[n.id] = n; });
11367
11574
  var adjDir = {};
11368
- SITE.graph.edges.forEach(function (e) {
11575
+ explorerGraph.edges.forEach(function (e) {
11369
11576
  (adjDir[e.from] = adjDir[e.from] || []).push({ id: e.to, rel: e.rel, dir: "out" });
11370
11577
  (adjDir[e.to] = adjDir[e.to] || []).push({ id: e.from, rel: e.rel, dir: "in" });
11371
11578
  });
@@ -11382,9 +11589,10 @@ ${deriveCapTile.toString()}
11382
11589
  // Gold層のフォルダ(1列目)。検索ヒット表示と使い分ける
11383
11590
  var goldGroups = [];
11384
11591
  if (SITE.home) goldGroups.push({ name: "Home", ids: [SITE.home.id] });
11385
- goldGroups.push({ name: "Decisions", ids: SITE.decisions.map(function (d) { return d.id; }) });
11592
+ // 削除済みは列にも件数にも出さない(一覧・グラフと同じ扱いに揃える)
11593
+ goldGroups.push({ name: "Decisions", ids: visibleRecords(SITE.decisions).map(function (d) { return d.id; }) });
11386
11594
  (SITE.sections || []).forEach(function (sec) {
11387
- goldGroups.push({ name: sec.name, ids: sec.records.map(function (r) { return r.id; }) });
11595
+ goldGroups.push({ name: sec.name, ids: visibleRecords(sec.records).map(function (r) { return r.id; }) });
11388
11596
  });
11389
11597
 
11390
11598
  function renderExplorer() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.13",
3
+ "version": "0.12.14",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,