@takagaki/cortex-decisions-viewer 0.12.18 → 0.12.19

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
@@ -39,6 +39,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.sanitizeBody = sanitizeBody;
40
40
  exports.readFleetSources = readFleetSources;
41
41
  exports.readHarnessSchedules = readHarnessSchedules;
42
+ exports.readHarnessShelf = readHarnessShelf;
42
43
  exports.readGoldProposals = readGoldProposals;
43
44
  exports.buildHomeGrounding = buildHomeGrounding;
44
45
  exports.collectMeetings = collectMeetings;
@@ -563,6 +564,35 @@ async function readHarnessSchedules(repoRoot) {
563
564
  return undefined;
564
565
  return (0, render_1.sanitizeHarnessSchedules)(rows);
565
566
  }
567
+ /**
568
+ * この案件で使う機能の棚(リポジトリ直下 `ハーネス連携/shelf.json`)を読む。
569
+ *
570
+ * **読み方は readHarnessSchedules と同じ流儀**(無い/壊れ→undefined・在れば配列)。
571
+ * ただし**意味が1つ違う**: このファイルは「無い」ことに意味がある(設計 B-1)。
572
+ * - 無い … まだ棚を作っていない → 画面はオン中の行を**注記なしで**並べる
573
+ * - 在って空 … 意図した空 → 画面は空状態の文言に倒す
574
+ * ここで空配列と undefined を同一視すると、**画面がこの2つを言い分けられなくなる**
575
+ *(導入直後の既存案件が、いきなり「棚に無いがオン」の注記だらけになる)。
576
+ *
577
+ * `items` が配列でなければ undefined。**黙って [] にしない**——手で書いた棚を
578
+ * 「0件の棚」として画面に出すと、空状態の文言が嘘になる(intake 側の2分岐と同じ判断)。
579
+ */
580
+ async function readHarnessShelf(repoRoot) {
581
+ let json;
582
+ try {
583
+ const raw = await node_fs_1.promises.readFile(path.join(repoRoot, "ハーネス連携", "shelf.json"), "utf8");
584
+ json = JSON.parse(raw);
585
+ }
586
+ catch {
587
+ return undefined; // 無い・壊れている → キーごと出さない(=まだ棚を作っていない)
588
+ }
589
+ if (json == null || typeof json !== "object" || Array.isArray(json))
590
+ return undefined;
591
+ const rows = json.items;
592
+ if (!Array.isArray(rows))
593
+ return undefined;
594
+ return (0, render_1.sanitizeHarnessShelf)(rows);
595
+ }
566
596
  /**
567
597
  * AIからの更新提案(`.cortex/gold-proposals.json`)を読む。
568
598
  *
@@ -1699,6 +1729,7 @@ async function build(opts) {
1699
1729
  }
1700
1730
  let home = null;
1701
1731
  let homeGrounding;
1732
+ let homeUpdatedAt;
1702
1733
  const sections = [];
1703
1734
  if (knowledgeRoot) {
1704
1735
  try {
@@ -1714,6 +1745,16 @@ async function build(opts) {
1714
1745
  catch {
1715
1746
  // ホーム未作成の案件では着地画面なし(Decisionsタブが既定になる)
1716
1747
  }
1748
+ // タイトル横の「最終更新」。**catch の外に置く**——上の catch は「ホーム未作成」
1749
+ // という単一の意味を持つので、ここの想定外の例外まで飲ませない。
1750
+ // 走る場所は repoRoot(絶対パス)。process.cwd() 起点にしない——Decisions
1751
+ // ディレクトリで実行しても機能が生きるし、テストが本物の経路を通せる。
1752
+ // Home.md 1ファイルへの git 呼び出しが1回増えるだけで、レコード数には依存しない
1753
+ //(下の「git は2回だけ呼ぶ」の不変条件は保っている)
1754
+ if (home) {
1755
+ homeUpdatedAt =
1756
+ (0, repo_1.lastSubstantiveUpdate)(repoRoot, path.basename(knowledgeRoot) + "/Home.md") ?? undefined;
1757
+ }
1717
1758
  const rootEntries = await node_fs_1.promises.readdir(knowledgeRoot, { withFileTypes: true });
1718
1759
  for (const e of rootEntries) {
1719
1760
  if (!e.isDirectory() || e.name === "Decisions")
@@ -1751,6 +1792,8 @@ async function build(opts) {
1751
1792
  const fleet = await readFleetSources(repoRoot);
1752
1793
  // いまオンになっているハーネスの機能。無ければ undefined でタイルは「すべてオフ」
1753
1794
  const harnessSchedules = await readHarnessSchedules(repoRoot);
1795
+ // この案件で使う機能の棚。**無ければ undefined のまま**(「まだ棚を作っていない」)
1796
+ const harnessShelf = await readHarnessShelf(repoRoot);
1754
1797
  // すでにある会議(投入モーダルの「会議名」の選択肢)。intake の有無に関わらず載せる——
1755
1798
  // 出し分けは表示側の仕事で、ここで絞ると「投入の口を後から付けたら選択肢が空」になる
1756
1799
  const meetings = await collectMeetings(repoRoot);
@@ -1769,6 +1812,7 @@ async function build(opts) {
1769
1812
  decisions,
1770
1813
  home,
1771
1814
  homeGrounding: homeGrounding,
1815
+ homeUpdatedAt: homeUpdatedAt,
1772
1816
  sections,
1773
1817
  graph,
1774
1818
  categories,
@@ -1805,6 +1849,9 @@ async function build(opts) {
1805
1849
  // 「画面が読む欄を build.ts が受け渡しているか」を `名前:` の字面で走査しており、
1806
1850
  // 短縮記法にすると、その守りの外へ静かに落ちる
1807
1851
  harnessSchedules: harnessSchedules,
1852
+ // 同上(短縮記法にしない)。**undefined のまま渡すのが仕様**——「棚が無い」と
1853
+ // 「棚が空」を画面が言い分けるための材料で、ここで [] へ倒すと区別が消える
1854
+ harnessShelf: harnessShelf,
1808
1855
  // null(無い・壊れ・version違い)は undefined へ落とす——SiteData の
1809
1856
  // 「知らないことはキーが無いことで表す」に合わせる(空の器を通さない)
1810
1857
  // 空の器は通さない(0件なら undefined = キーごと出さない)
package/dist/render.js CHANGED
@@ -4,6 +4,7 @@ exports.UNSETTLED_LIMITS = exports.LIVE_CONFIRM_LIMITS = void 0;
4
4
  exports.renderSite = renderSite;
5
5
  exports.sanitizeRegisteredNow = sanitizeRegisteredNow;
6
6
  exports.sanitizeHarnessSchedules = sanitizeHarnessSchedules;
7
+ exports.sanitizeHarnessShelf = sanitizeHarnessShelf;
7
8
  exports.sanitizeStateNow = sanitizeStateNow;
8
9
  exports.overlayStateOnFleet = overlayStateOnFleet;
9
10
  exports.chatRowsToAdd = chatRowsToAdd;
@@ -671,6 +672,34 @@ function sanitizeHarnessSchedules(v) {
671
672
  }
672
673
  return out;
673
674
  }
675
+ /**
676
+ * 棚(この案件で使う機能の候補)の一覧を、画面が扱ってよい形へ畳む。
677
+ *
678
+ * 🔴 **名前の篩は `sanitizeHarnessSchedules` に相乗りする(写経しない)。** 棚とオンは
679
+ * 別の状態だが、**同じ鍵(harness + skill)で突き合わせる**ので、通る名前の集合が
680
+ * 割れると「棚には置けるのにオンにできない機能」ができる。上限(200件)も向こうが持つ。
681
+ *
682
+ * 棚は**集合**なので、ここで重複を畳む(手編集や過去の並列書き込みで増えうる)。
683
+ * 描画順は名簿/カタログ順で決めるので、配列の並びそのものには意味を持たせない。
684
+ * **時間帯・曜日は落とす**——棚に周期を持たせると、intake の解決した値と食い違う
685
+ * 二重帳簿になる(棚は「いつ動くか」を決める場所ではない)。
686
+ *
687
+ * 🔴 **クロージャを持たない純関数に保つこと。** クライアントJSへ toString() で
688
+ * 流し込んでいる(参照してよいのは同じく流し込んである sanitizeHarnessSchedules だけ)。
689
+ */
690
+ function sanitizeHarnessShelf(v) {
691
+ const rows = sanitizeHarnessSchedules(v);
692
+ const out = [];
693
+ const seen = {};
694
+ for (const r of rows) {
695
+ const key = r.harness + "\t" + r.skill;
696
+ if (seen[key])
697
+ continue;
698
+ seen[key] = true;
699
+ out.push({ harness: r.harness, skill: r.skill });
700
+ }
701
+ return out;
702
+ }
674
703
  /**
675
704
  * 接続状態ストアの語彙(サーバ側 connection-state-keys.mjs の REACHABLE_STATUSES /
676
705
  * INGESTED_STATUSES と同じ。**勝手に増やさない・減らさない**)。
@@ -2550,6 +2579,8 @@ a:hover { text-decoration: underline; }
2550
2579
  .detail-actions { flex: 0 0 auto; display: flex; flex-direction: column; align-items: flex-end; gap: 8px; }
2551
2580
  .detail-actions-row { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
2552
2581
  .detail-title { font-size: 24px; font-weight: 700; margin: 0 0 10px; line-height: 1.4; }
2582
+ /* Home タイトル横の鮮度のヒント。主張しない(タイトルの一部ではなく注記) */
2583
+ .home-updated { font-size: 12px; font-weight: 400; color: var(--muted); margin-left: 10px; white-space: nowrap; }
2553
2584
  .detail-meta { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; color: var(--muted); font-size: 13px; margin-bottom: 16px; }
2554
2585
  /* GitHubへ飛ぶボタンだけ、ヘッダーの .gh-btn と同じダーク(GitHubカラー)+リキッドグラスで揃える */
2555
2586
  .edit-btn {
@@ -2844,6 +2875,26 @@ a.ds-name:hover { text-decoration: underline; }
2844
2875
  .ds-harness-state.is-error { color: #b42318; }
2845
2876
  /* 一部の提供元から読めなかったとき。**行は出したうえで**「並んでいないものがある」と断る */
2846
2877
  .ds-harness-warn { font-size: 12.5px; color: #a15c00; margin: 4px 0 10px; }
2878
+ /* ---- 棚ビュー / ピッカーのセクション(部署 → ハーネス)----
2879
+ 見出しの階層を**大きさではなく余白で**作る。モーダルの中で h3/h2 相当の字を並べると、
2880
+ モーダルの見出し(機能名)と競り合って、どれが画面の主題か読めなくなる */
2881
+ .ds-harness-dep { margin: 0 0 18px; }
2882
+ .ds-harness-dep-title { font-size: 13px; font-weight: 700; color: var(--muted); margin: 0 0 8px; letter-spacing: .04em; }
2883
+ .ds-harness-prov-title { font-size: 13px; font-weight: 700; margin: 12px 0 6px; }
2884
+ .ds-harness-dep-body > .ds-harness-prov-title:first-child { margin-top: 0; }
2885
+ /* 提供元ごとの「いま読み込めませんでした」(一覧全体の断りとは別。どの提供元の話かが分かる) */
2886
+ .ds-harness-subnote { font-size: 12.5px; color: #a15c00; margin: 0 0 6px; }
2887
+ /* 「棚に無いのにオンになっている」行の注記。**行の中に置く**——一覧の上でまとめて
2888
+ 断ると、どの行のことなのかが読めない */
2889
+ .ds-harness-note { font-size: 12.5px; color: #a15c00; }
2890
+ /* ピッカーで「これはもう動いている」ことを示す印。棚の操作と混ぜないよう小さく添える */
2891
+ .ds-harness-onmark {
2892
+ font-size: 11.5px; color: var(--accent); border: 1px solid var(--accent);
2893
+ border-radius: 999px; padding: 0 8px; flex: 0 0 auto;
2894
+ }
2895
+ /* ビューを移る導線(機能をえらぶ/戻る)。行の操作と同じ高さに並べない(別の階層の操作) */
2896
+ .ds-harness-actions { display: flex; margin: 12px 0 4px; }
2897
+ .ds-harness-pick-state { font-size: 12.5px; color: var(--muted); margin: 4px 0 10px; }
2847
2898
  /* ---- 可変長の文字列は必ず折り返す(0.4.71) ----
2848
2899
  実機の受け入れテストで、状態の文言がタイル幅を超えて溢れた(スシローのデザインタイル)。
2849
2900
  ここに出るのは engine の detail・チェックの直し方・チャンネル名・ファイルパスなど
@@ -2859,7 +2910,8 @@ a.ds-name:hover { text-decoration: underline; }
2859
2910
  .ds-empty, .sec-how, .modal-desc, .ds-toggle-msg,
2860
2911
  .ds-drive-note, .ds-inactive-note, .ds-legacy-chip, .ds-keys-code, .ds-gate-help,
2861
2912
  /* ハーネス連携の行。機能の表示名・説明・周期の文はどれも提供側が決めるので長さが読めない */
2862
- .ds-harness-name, .ds-harness-period, .ds-harness-summary, .ds-harness-state, .ds-harness-warn {
2913
+ .ds-harness-name, .ds-harness-period, .ds-harness-summary, .ds-harness-state, .ds-harness-warn,
2914
+ .ds-harness-dep-title, .ds-harness-prov-title, .ds-harness-subnote, .ds-harness-note, .ds-harness-pick-state {
2863
2915
  min-width: 0; white-space: normal; overflow-wrap: anywhere;
2864
2916
  }
2865
2917
  .ds-toggle { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 6px; }
@@ -3148,6 +3200,11 @@ button.ops-card-btn:disabled { opacity: .6; cursor: default; }
3148
3200
  /* 貼ったURLを1行で出すので、はみ出させない(箱は420px・モバイルは実質335px) */
3149
3201
  .confirm-line { font-size: 14px; color: var(--text); line-height: 1.75; margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
3150
3202
  .confirm-line:first-child { font-weight: 700; }
3203
+ /* 確認の中に挿す本文(ハーネスを「オンにする」ときの時間帯の選択)。箱の gap は
3204
+ .confirm-line の間隔なので、入れ子にすると効かない——同じ間隔をここでも持つ。
3205
+ 選択欄そのものは行側の .ds-harness-selects を流用する */
3206
+ .confirm-body { display: flex; flex-direction: column; gap: 10px; }
3207
+ .confirm-body-label { font-size: 13px; color: var(--muted); }
3151
3208
  .modal-ok { color: #047857; font-weight: 600; }
3152
3209
  .modal-status-error { color: #dc2626; font-weight: 600; }
3153
3210
  .modal-paths { margin-top: 8px; display: flex; flex-direction: column; gap: 4px; }
@@ -3263,6 +3320,7 @@ var HARNESS_WEEKDAYS = ${JSON.stringify(HARNESS_WEEKDAYS)};
3263
3320
  var HARNESS_SLOT_LABEL = ${JSON.stringify(HARNESS_SLOT_LABEL)};
3264
3321
  var HARNESS_WEEKDAY_LABEL = ${JSON.stringify(HARNESS_WEEKDAY_LABEL)};
3265
3322
  ${sanitizeHarnessSchedules.toString()}
3323
+ ${sanitizeHarnessShelf.toString()}
3266
3324
  ${chatRowsToAdd.toString()}
3267
3325
  ${gchatRowsToAdd.toString()}
3268
3326
  ${devRowsToAdd.toString()}
@@ -4146,7 +4204,19 @@ ${deriveCapTile.toString()}
4146
4204
  var h = SITE.home;
4147
4205
  countEl.textContent = "";
4148
4206
  var head = el("div", { class: "detail-head" });
4149
- var titleWrap = el("div", {}, [ el("h1", { class: "detail-title", text: h.title }) ]);
4207
+ var titleEl = el("h1", { class: "detail-title", text: h.title });
4208
+ // 鮮度のヒント。**h1 の中に入れる**——titleWrap 直下だと h1 が block なので
4209
+ // 次の行に落ちる。値はビルド時に git から導いた「実質コミット」の日付で、
4210
+ // フッタの generatedAt(サイトの再ビルド日時)とは別物(time の title で意味を補う)
4211
+ if (SITE.homeUpdatedAt) {
4212
+ titleEl.appendChild(el("time", {
4213
+ class: "home-updated",
4214
+ datetime: SITE.homeUpdatedAt,
4215
+ title: "Cortex/Home.md への最後の実質的な変更",
4216
+ text: "最終更新: " + SITE.homeUpdatedAt,
4217
+ }));
4218
+ }
4219
+ var titleWrap = el("div", {}, [ titleEl ]);
4150
4220
  var ai = h.fm ? adoptionInfo(h.fm.adoption) : null;
4151
4221
  if (ai) titleWrap.appendChild(el("span", { class: "chip adoption-" + String(h.fm.adoption), text: ai.label }));
4152
4222
  head.appendChild(titleWrap);
@@ -5295,6 +5365,19 @@ ${deriveCapTile.toString()}
5295
5365
  * ここは「押した結果がその場で見える」ためのミラー(notifyToggle の s.notify と同型)。
5296
5366
  */
5297
5367
  var HARNESS_SCHEDULES = sanitizeHarnessSchedules(SITE.harnessSchedules);
5368
+ /**
5369
+ * この案件で使う機能の棚(ページ生存中の正本)。**HARNESS_SCHEDULES と同じ置き場所**
5370
+ *(buildDataSources の中に置くと、モーダルを閉じるたびに焼き値へ巻き戻る)。
5371
+ *
5372
+ * 🔴 **null は「まだ棚を作っていない」で、空配列とは別の状態(設計 B-1)。**
5373
+ * null のときは棚の絞り込みそのものを効かせず、オン中の行を注記なしで並べる
5374
+ *——導入直後の既存案件が、いきなり「棚に無いがオン」の注記だらけになるのを避ける。
5375
+ * 空配列は「意図して空」で、空状態の文言に倒す。**null を [] へ丸めないこと。**
5376
+ *
5377
+ * **棚に置いても何も動かない。** 実行の正本は HARNESS_SCHEDULES のままで、
5378
+ * この配列は「この案件で使う候補」でしかない。
5379
+ */
5380
+ var HARNESS_SHELF = SITE.harnessShelf == null ? null : sanitizeHarnessShelf(SITE.harnessShelf);
5298
5381
  /**
5299
5382
  * リアルタイム同期の可否(op=state の realtime。#113)。取得前・取得失敗時は null。
5300
5383
  *
@@ -5894,9 +5977,11 @@ ${deriveCapTile.toString()}
5894
5977
  // Google Chat には利用範囲の開示を添える(#153)。アプリのスコープは送信能力を含むので
5895
5978
  // 「投稿しません」とは**約束しない**——通知送信の仕組み(engine 側)が既にあるため、
5896
5979
  // 読み取りと通知先への投稿だけ、という正直な形で書く。混載でも出す(スペースが並びうる)
5980
+ // 「本文を保存しない」の一文はポータルのプライバシーポリシーと対(2026-08-31 オーナー方針)。
5981
+ // 変えるときは依頼キット(docs/google-chat-onboarding.md の B)とポリシー側も同時に
5897
5982
  var chatScope = adapter === "slack"
5898
5983
  ? ""
5899
- : " Cortex のアプリは、会話の読み取りと、通知先に設定したスペースへの確認・通知の投稿だけに使われます。";
5984
+ : " Cortex のアプリは、会話の読み取りと、通知先に設定したスペースへの確認・通知の投稿だけに使われます。読み取った会話は、本文そのものを保存するのではなく、決定事項・用語などの抽出結果として案件の記録に反映されます。";
5900
5985
  m.modal.appendChild(el("p", { class: "modal-desc", text: "ここに並んでいる" + chatNoun + "は、例外なく全て毎晩の Gold 昇格が読み取り、Decision・用語集の材料になります。読み取りを止めるには「登録を解除」してください。" + chatScope }));
5901
5986
  } else if (cap === "開発") {
5902
5987
  m.modal.appendChild(el("p", { class: "modal-desc", text: "ここに並んでいるリポジトリの Issues を毎晩の Gold 昇格が読み取り、Decision・用語集の材料になります。読み取りをやめるには「登録から削除」してください。" }));
@@ -6049,6 +6134,94 @@ ${deriveCapTile.toString()}
6049
6134
  HARNESS_SCHEDULES.push(row);
6050
6135
  }
6051
6136
 
6137
+ /** 棚に置いてあるか。**一致鍵は harness + skill**(オンの判定と同じ) */
6138
+ function harnessShelfHas(harness, skill) {
6139
+ if (!HARNESS_SHELF) return false;
6140
+ for (var i = 0; i < HARNESS_SHELF.length; i++) {
6141
+ var it = HARNESS_SHELF[i];
6142
+ if (it.harness === harness && it.skill === skill) return true;
6143
+ }
6144
+ return false;
6145
+ }
6146
+
6147
+ /**
6148
+ * 棚のミラーへ足す。**最初の1件で null から配列へ変わる**——「まだ棚を作っていない」
6149
+ * 案件でピッカーから1件置くと、そこから先は「棚が在る」案件として振る舞う
6150
+ *(サーバ側でも shelf.json がこの1件目で作られる)。
6151
+ */
6152
+ function harnessShelfMirrorAdd(harness, skill) {
6153
+ if (!HARNESS_SHELF) HARNESS_SHELF = [];
6154
+ if (harnessShelfHas(harness, skill)) return;
6155
+ HARNESS_SHELF.push({ harness: harness, skill: skill });
6156
+ }
6157
+
6158
+ function harnessShelfMirrorRemove(harness, skill) {
6159
+ if (!HARNESS_SHELF) return;
6160
+ for (var i = HARNESS_SHELF.length - 1; i >= 0; i--) {
6161
+ var it = HARNESS_SHELF[i];
6162
+ if (it.harness === harness && it.skill === skill) HARNESS_SHELF.splice(i, 1);
6163
+ }
6164
+ }
6165
+
6166
+ /**
6167
+ * 棚の保存を**1件ずつ直列に**送るための鎖。
6168
+ *
6169
+ * 🔴 **並列に投げてはいけない。** 棚の操作は1件=1コミットで、同じファイルへの
6170
+ * 同時書き込みはサーバ側の commitWithRetry の衝突になる(ピッカーは連打される場所
6171
+ * ——「あれもこれも置く」が自然な操作なので、二重送信は正常系として起きる)。
6172
+ *
6173
+ * **失敗しても鎖を切らない**(then の第2引数にも同じ実行を渡す)——1件の失敗で
6174
+ * 以後の操作が永久に待たされる状態を作らない。sendConfig 自身は例外を投げないが、
6175
+ * 鎖の前段が何であっても次が走ることを、ここで保証しておく。
6176
+ */
6177
+ var harnessShelfChain = null;
6178
+ function harnessShelfSend(payload, msg, btn, onOk) {
6179
+ // 🔴 **onDone ではなく hooks.onSaved で受ける。** 棚の追加は冪等で、
6180
+ // 「既に在る」は {ok:true, changed:false} の**正常な成功**。onDone は
6181
+ // changed !== false のときしか流れないので、そこに置くと
6182
+ // 「サーバには在るのに画面は棚が空」で取り残される(次のビルドまで直らない)
6183
+ var run = function () { return sendConfig(payload, msg, btn, null, { onSaved: onOk }); };
6184
+ var next = (harnessShelfChain || Promise.resolve()).then(run, run);
6185
+ harnessShelfChain = next;
6186
+ return next;
6187
+ }
6188
+
6189
+ /**
6190
+ * セクションの骨組み(部署 → ハーネス)。**応答のトップレベルの departments が正本で、
6191
+ * 画面は名簿の写しを持たない。** 読めなければ null=この応答は骨組みを持たない
6192
+ *(古い Lambda)ので、従来どおりのフラット表示へ落とす。
6193
+ *
6194
+ * 🔴 **提供元の識別子(owner/repo)も部署の語彙キーも画面には出さない**(#124 の原則)。
6195
+ * 表示名が解決できていない(=サーバが識別子やキーをそのまま返した)ときは、
6196
+ * **名前を出さない**方に倒す——部署は「その他」、ハーネスは見出しごと出さない。
6197
+ */
6198
+ function harnessDepartmentsOf(j) {
6199
+ if (!j || typeof j !== "object" || Array.isArray(j)) return null;
6200
+ if (!Array.isArray(j.departments)) return null;
6201
+ var out = [];
6202
+ for (var i = 0; i < j.departments.length; i++) {
6203
+ var d = j.departments[i];
6204
+ if (d == null || typeof d !== "object" || Array.isArray(d)) continue;
6205
+ if (!Array.isArray(d.harnesses)) continue;
6206
+ var key = typeof d.key === "string" ? d.key : "";
6207
+ var list = [];
6208
+ for (var k = 0; k < d.harnesses.length; k++) {
6209
+ var hn = d.harnesses[k];
6210
+ if (hn == null || typeof hn !== "object" || Array.isArray(hn)) continue;
6211
+ // 名前の篩は行の篩と同じものを通す(描ける提供元だけを骨組みに載せる)
6212
+ if (sanitizeHarnessSchedules([{ harness: hn.harness, skill: "x" }]).length !== 1) continue;
6213
+ var hlabel = typeof hn.label === "string" ? hn.label : "";
6214
+ list.push({
6215
+ harness: hn.harness,
6216
+ label: hlabel && hlabel !== hn.harness ? hlabel : "",
6217
+ });
6218
+ }
6219
+ var dlabel = typeof d.label === "string" ? d.label : "";
6220
+ out.push({ key: key, label: dlabel && dlabel !== key ? dlabel : "", harnesses: list });
6221
+ }
6222
+ return out;
6223
+ }
6224
+
6052
6225
  /**
6053
6226
  * カタログの応答を、画面が扱ってよい形へ畳む。**読めなければ null(=異常表示)。**
6054
6227
  *
@@ -6107,9 +6280,34 @@ ${deriveCapTile.toString()}
6107
6280
  providers: providers,
6108
6281
  errors: errors,
6109
6282
  partial: providers.length > 0 && errors.length > 0,
6283
+ // 骨組みを持たない応答(古い Lambda)では null。**セクション表示ごと落とす**
6284
+ // ——名簿を画面側に写経して補うと、名簿を1行足した日に画面だけが古いまま残る
6285
+ departments: harnessDepartmentsOf(j),
6110
6286
  };
6111
6287
  }
6112
6288
 
6289
+ /** カタログの中の1機能(無ければ null) */
6290
+ function harnessMetaOf(cat, harness, skill) {
6291
+ for (var i = 0; i < cat.providers.length; i++) {
6292
+ var prov = cat.providers[i];
6293
+ if (prov.harness !== harness) continue;
6294
+ for (var k = 0; k < prov.skills.length; k++) {
6295
+ if (prov.skills[k].skill === skill) return prov.skills[k];
6296
+ }
6297
+ }
6298
+ return null;
6299
+ }
6300
+
6301
+ /** 一覧を読み込めなかった提供元(harness → true) */
6302
+ function harnessErroredMap(cat) {
6303
+ var errored = {};
6304
+ for (var i = 0; i < cat.errors.length; i++) {
6305
+ var er = cat.errors[i];
6306
+ if (er && typeof er === "object" && typeof er.harness === "string") errored[er.harness] = true;
6307
+ }
6308
+ return errored;
6309
+ }
6310
+
6113
6311
  /**
6114
6312
  * 語彙から select を作る。**現在値が語彙外のときだけ**先頭に空の選択肢を足す
6115
6313
  * ——既定へ勝手に倒すと、実際には実行されない行を「朝9時ごろ」と説明することになる。
@@ -6125,15 +6323,19 @@ ${deriveCapTile.toString()}
6125
6323
  return sel;
6126
6324
  }
6127
6325
 
6128
- /** オンにする前の確認。**何が・どこに・いつ届き始めるのかを書く** */
6326
+ /**
6327
+ * オンにする前の確認。**何が・どこに届き始めるのかを書く。**
6328
+ *
6329
+ * 🔴 **「いつ」は文で言わない。** 確認の中に時間帯の select を出すようになったので
6330
+ *(オンにするのと同じ流れで決められる)、括弧書きで周期も書くと、
6331
+ * **利用者が select を変えた瞬間に文のほうが嘘になる**。いつ届くかは select が示す。
6332
+ *
6333
+ * 「よろしいですか?」もここには置かない——select より後に問うため、
6334
+ * 呼び出し側が body の末尾へ積む(問いのあとに答えを左右する操作を出さない)。
6335
+ */
6129
6336
  function harnessOnConfirmLines(meta) {
6130
- // **既定の周期が言えないときは、言い切らない**(harnessDefaultPeriod の項)。
6131
- // 実際の時間帯は保存時に intake が決めるので、決まったあとに調整できることだけ伝える
6132
- var dp = harnessDefaultPeriod(meta);
6133
6337
  var lines = [
6134
- dp
6135
- ? "オンにすると、この案件の通知先チャンネルに「" + meta.display_name + "」が届き始めます(" + dp + ")。"
6136
- : "オンにすると、この案件の通知先チャンネルに「" + meta.display_name + "」が届き始めます。時間帯は、オンにしたあとこの画面で確認・調整できます。",
6338
+ "オンにすると、この案件の通知先チャンネルに「" + meta.display_name + "」が届き始めます。",
6137
6339
  ];
6138
6340
  var targets = harnessNotifyTargets();
6139
6341
  if (targets.length) {
@@ -6145,29 +6347,96 @@ ${deriveCapTile.toString()}
6145
6347
  // 届かないことだけを、はっきり伝える
6146
6348
  lines.push("⚠ この案件にはまだ配信先(通知先チャンネル)が設定されていません。オンにしても、「チャット」の通知先を設定するまで届きません。");
6147
6349
  }
6148
- lines.push("よろしいですか?");
6149
6350
  return lines;
6150
6351
  }
6151
6352
 
6353
+ /**
6354
+ * 棚に置く / 棚から外す を送る(棚ビューとピッカーの共通処理)。
6355
+ *
6356
+ * 🔴 **オン中の機能を外すときは、止まらないことを操作の瞬間に言う(設計 B-2)。**
6357
+ * 「棚から外す」と「配信が止まる」は別のことで、取り違えられると
6358
+ * 「外したのに毎朝届く」になる。**オフの機能では確認を出さない**——即時に戻せる
6359
+ * 操作なので、確認を出すのは notifyToggle と同じ判断で過剰。
6360
+ *
6361
+ * 送信は harnessShelfSend を通す(1件ずつ直列。並列は同じファイルへの同時コミット)。
6362
+ *
6363
+ * @param label 画面に出ている名前(確認文に使う。識別子は使わない)
6364
+ */
6365
+ async function harnessShelfToggle(harness, skill, label, msg, btn, after) {
6366
+ var onShelf = harnessShelfHas(harness, skill);
6367
+ if (onShelf && harnessEntryOf(harness, skill)) {
6368
+ var lines = [
6369
+ "「" + label + "」を、この案件で使う機能から外します。",
6370
+ "この機能はいまオンなので、外しても配信は止まりません。止めるには「オフにする」を押してください。",
6371
+ "よろしいですか?",
6372
+ ];
6373
+ // **赤で出さない。** 何かが消える操作ではなく、候補の一覧から外すだけ
6374
+ if (!(await openConfirm(lines, { okLabel: "使う機能から外す", returnTo: btn, okDanger: false }))) {
6375
+ msg.textContent = "";
6376
+ return;
6377
+ }
6378
+ }
6379
+ // 🔴 **順番が回ってくる前に、押したボタンを止める。** 送信は直列なので、
6380
+ // beginBusy(sendConfig の中)が効くのは自分の番が来てから——それまでは
6381
+ // 押せる見た目のまま残り、①連打で同じ追加が2本飛ぶ ②押した感触が無いので
6382
+ // 何度も押す、の両方が起きる。**戻すのは sendConfig の endBusy**
6383
+ btn.disabled = true;
6384
+ await harnessShelfSend(
6385
+ {
6386
+ op: onShelf ? "harness-shelf-remove" : "harness-shelf-add",
6387
+ harness: harness,
6388
+ skill: skill,
6389
+ },
6390
+ msg,
6391
+ btn,
6392
+ function () {
6393
+ // 応答は棚の中身を運ばないので、送った向きでミラーを当てる
6394
+ //(add は冪等・remove は無い行でも成功なので、向きだけで確定する)
6395
+ if (onShelf) harnessShelfMirrorRemove(harness, skill);
6396
+ else harnessShelfMirrorAdd(harness, skill);
6397
+ if (after) after();
6398
+ },
6399
+ );
6400
+ }
6401
+
6152
6402
  /**
6153
6403
  * 機能1件の行。
6154
6404
  *
6155
6405
  * @param meta カタログの中身。**null はゴースト行**(提供元から消えた機能。出せる
6156
6406
  * 表示名が無いので、オフにする手段だけを残す——出さないと画面から止められなくなる)
6407
+ * @param opts 棚ビュー専用の追加(骨組みを持たない応答では渡さない=従来の見た目)
6408
+ * - shelf … 棚の導線(置く/外す)を出すか
6409
+ * - unreadable … 一覧を読めなかった提供元の行か。**「もう無い」と言わない**(B-4)
6410
+ * ——次に開けば普通に並ぶので、消えたと伝えるのは嘘になる
6157
6411
  */
6158
- function harnessRow(harness, skill, meta) {
6412
+ function harnessRow(harness, skill, meta, opts) {
6413
+ opts = opts || {};
6159
6414
  var row = el("div", { class: "ds-harness-row" });
6160
- var name = el("div", {
6161
- class: "ds-harness-name",
6162
- text: meta ? meta.display_name : "(提供元にもう無い機能)",
6163
- });
6415
+ var label = meta
6416
+ ? meta.display_name
6417
+ : (opts.unreadable ? "(名前を読み込めなかった機能)" : "(提供元にもう無い機能)");
6418
+ var name = el("div", { class: "ds-harness-name", text: label });
6164
6419
  var period = el("div", { class: "ds-harness-period" });
6165
6420
  row.appendChild(el("div", { class: "ds-harness-head" }, [name, period]));
6166
6421
  if (meta && meta.summary) row.appendChild(el("div", { class: "ds-harness-summary", text: meta.summary }));
6422
+ // 「棚に無いのにオンになっている」ことの注記。**棚が在るときだけ出す**(B-1)
6423
+ // ——棚を作っていない案件で出すと、既存案件が導入直後に注記だらけになる
6424
+ var note = el("div", { class: "ds-harness-note" });
6425
+ note.hidden = true;
6426
+ row.appendChild(note);
6167
6427
  var btn = el("button", { class: "ds-toggle-btn", type: "button" });
6168
6428
  var selects = el("span", { class: "ds-harness-selects" });
6169
6429
  var msg = el("div", { class: "ds-toggle-msg" });
6170
- row.appendChild(el("div", { class: "ds-toggle" }, [btn, selects, msg]));
6430
+ var shelfBtn = opts.shelf
6431
+ ? el("button", { class: "ds-toggle-btn ds-harness-shelf-btn", type: "button" })
6432
+ : null;
6433
+ // **トグルは必ず先頭に置く**(行の先頭のボタン=オン/オフ、が既存の検査の前提)
6434
+ row.appendChild(el("div", { class: "ds-toggle" }, [btn, selects, shelfBtn, msg]));
6435
+ if (shelfBtn) {
6436
+ shelfBtn.addEventListener("click", function () {
6437
+ harnessShelfToggle(harness, skill, label, msg, shelfBtn, paint);
6438
+ });
6439
+ }
6171
6440
 
6172
6441
  /**
6173
6442
  * 時間帯(毎週なら曜日も)の調整。**確認モーダルは出さない**——同じ画面から
@@ -6231,6 +6500,26 @@ ${deriveCapTile.toString()}
6231
6500
  // **オフにしたゴースト行はボタンごと止める。** カタログに中身が無いのでオンには
6232
6501
  // 戻せない——押せる見た目のまま黙って何もしないボタンを残さない
6233
6502
  btn.disabled = !e && !meta;
6503
+ if (opts.shelf) {
6504
+ var onShelf = harnessShelfHas(harness, skill);
6505
+ // 🔴 **画面に「棚」の語を出さない(分類B・リーダー承認)。** 内部では棚と呼ぶが、
6506
+ // 利用者に見えるのは「この案件で使う機能」という1つの言い方だけにする
6507
+ //(動詞もピッカーと揃える——同じ操作が2つの名前で呼ばれない)
6508
+ shelfBtn.textContent = onShelf ? "使う機能から外す" : "使う機能に追加";
6509
+ // 🔴 **棚にあるが提供元から消えた機能は「外す」だけの行にする(B-5)。**
6510
+ // オンにも戻せないボタンを並べると、腐った行を誰も片付けられないように見える
6511
+ btn.hidden = !meta && !e;
6512
+ // 棚にも無く提供元にも無い行(外した直後・オフにした直後)には、
6513
+ // 置き直す口を出さない——実在しない機能を棚へ戻せてしまう
6514
+ shelfBtn.hidden = !meta && !onShelf;
6515
+ // **「棚から外してもオン中は表示に残る」の実体**(B-2 の約束が守られていることが、
6516
+ // 外した直後の画面そのもので見える)。
6517
+ // 🔴 **棚を作っていない案件(HARNESS_SHELF が null)では出さない(B-1)。**
6518
+ // 出すと、この機能の導入直後に既存案件のオン中の行が全部この注記を持つ
6519
+ var stray = !!e && !onShelf && !!HARNESS_SHELF;
6520
+ note.hidden = !stray;
6521
+ if (stray) note.textContent = "この案件で使う機能には入っていませんが、オンになっています。";
6522
+ }
6234
6523
  // **オフの行はカタログの既定で周期を出す**(オンの行は定義ファイルの具体値)。
6235
6524
  // 既定が欠けている行では空=周期を言わない(harnessDefaultPeriod の項。
6236
6525
  // 「時間帯未設定」はオンの行の語——オフの行に出すと、設定の抜けがあるように読める)
@@ -6253,20 +6542,53 @@ ${deriveCapTile.toString()}
6253
6542
  return;
6254
6543
  }
6255
6544
  if (!meta) return; // ゴースト行はオンにできない(カタログに中身が無い)
6545
+ var weekly = meta.cadence === "weekly";
6546
+ // **確認の中で時間帯(毎週なら曜日も)まで決める。** オンにしてから調整させると、
6547
+ // 「何を・いつ受け取るか」を決めるのに2手かかる(オーナー実機フィードバック)。
6548
+ // **開くたびに組み直す。** 使い回すと、キャンセルした選択が次に開いたときも残り、
6549
+ // 「既定でオンにする」つもりの人が前回いじった値でオンにしてしまう
6550
+ var slotSel = harnessSelect(HARNESS_SLOTS, HARNESS_SLOT_LABEL, meta.default_slot, "時間帯未設定", "時間帯");
6551
+ var daySel = weekly
6552
+ ? harnessSelect(HARNESS_WEEKDAYS, HARNESS_WEEKDAY_LABEL, meta.default_weekday, "曜日未設定", "曜日")
6553
+ : null;
6554
+ var picks = el("span", { class: "ds-harness-selects" }, [
6555
+ el("span", { class: "confirm-body-label", text: "届く時間帯:" }),
6556
+ slotSel,
6557
+ daySel,
6558
+ ]);
6559
+ var body = el("div", { class: "confirm-body" }, [picks]);
6560
+ // 🔴 **既定が欠けている(=選択が空の)ときだけ、保存時に決まることを言う。**
6561
+ // 空のまま OK すると intake が既定を**決定として**書くので、「未設定」の見た目の
6562
+ // まま黙っていると、画面と保存結果が食い違う。具体値が見えているときは言わない
6563
+ //(下の select が示していることを、文でもう一度言うことになる)
6564
+ if (!slotSel.value || (daySel && !daySel.value)) {
6565
+ body.appendChild(el("p", { class: "confirm-line", text: "時間帯は保存時に決まり、あとでこの画面で調整できます。" }));
6566
+ }
6567
+ // **問いは選択の後ろ。** lines に混ぜると「よろしいですか?→[選択]→[ボタン]」と
6568
+ // なり、問うたあとに答えを左右する操作が出る
6569
+ body.appendChild(el("p", { class: "confirm-line", text: "よろしいですか?" }));
6256
6570
  // **OK は赤くしない。** 危険な向きではなく「届き始める」向きの操作なので、
6257
6571
  // 削除と同じ赤で出すと、止めるべき操作に見える
6258
- if (!(await openConfirm(harnessOnConfirmLines(meta), { okLabel: "オンにする", returnTo: btn, okDanger: false }))) {
6572
+ if (!(await openConfirm(harnessOnConfirmLines(meta), { okLabel: "オンにする", returnTo: btn, okDanger: false, body: body }))) {
6259
6573
  msg.textContent = "";
6260
6574
  return;
6261
6575
  }
6262
- // 🔴 **初回のオンでは時間帯・曜日を送らない。** 既定の解決は intake が正本で、
6263
- // UI が既定を写経して送ると、既定を変えたときに二重帳簿になる
6264
- await sendConfig({ op: "harness-schedule-set", harness: harness, skill: skill }, msg, btn, function (j) {
6576
+ // 🔴 **既定と同じ値は送らない。** 既定の解決は intake が正本で、UI が写経して送ると
6577
+ // 既定を変えたときに二重帳簿になる。送るのは利用者が既定から**変えた**ぶんだけ
6578
+ //(set は置き換えだが、省いたキーは intake が既定へ解決する=見えていた初期選択に
6579
+ // なるので、画面と食い違わない)。
6580
+ // **select はもう DOM から外れている**(確認が閉じた)が、.value は読める
6581
+ var payload = { op: "harness-schedule-set", harness: harness, skill: skill };
6582
+ if (slotSel.value && slotSel.value !== (meta.default_slot || "")) payload.slot = slotSel.value;
6583
+ if (daySel && daySel.value && daySel.value !== (meta.default_weekday || "")) payload.weekday = daySel.value;
6584
+ await sendConfig(payload, msg, btn, function (j) {
6265
6585
  harnessMirrorSet(harness, skill, j && j.entry, {
6266
6586
  harness: harness,
6267
6587
  skill: skill,
6268
- slot: meta.default_slot,
6269
- weekday: meta.cadence === "weekly" ? meta.default_weekday : undefined,
6588
+ // 応答が entry を持たないときの当て先。**選んだ値で当てる**(既定固定にすると、
6589
+ // 変えてオンにした直後の行が既定の周期を出す)
6590
+ slot: payload.slot || meta.default_slot,
6591
+ weekday: weekly ? (payload.weekday || meta.default_weekday) : undefined,
6270
6592
  });
6271
6593
  paint();
6272
6594
  });
@@ -6276,13 +6598,15 @@ ${deriveCapTile.toString()}
6276
6598
  return row;
6277
6599
  }
6278
6600
 
6279
- /** 応答が読めたときの中身。行が1つも無ければ「0件」の文言に倒す */
6601
+ /**
6602
+ * 骨組みを持たない応答(古い Lambda)のときの中身。**従来どおりのフラット表示。**
6603
+ * 行が1つも無ければ「0件」の文言に倒す。
6604
+ *
6605
+ * **ここに棚の絞り込みを持ち込まない。** 骨組みが無い=部署もハーネスの表示名も
6606
+ * 無い状態で棚だけ効かせると、「選んだはずの機能がどこにも見当たらない」画面になる。
6607
+ */
6280
6608
  function harnessFillList(list, setState, warn, cat) {
6281
- var errored = {};
6282
- for (var i = 0; i < cat.errors.length; i++) {
6283
- var er = cat.errors[i];
6284
- if (er && typeof er === "object" && typeof er.harness === "string") errored[er.harness] = true;
6285
- }
6609
+ var errored = harnessErroredMap(cat);
6286
6610
  var seen = {};
6287
6611
  var rows = 0;
6288
6612
  for (var p = 0; p < cat.providers.length; p++) {
@@ -6311,6 +6635,221 @@ ${deriveCapTile.toString()}
6311
6635
  warn.hidden = !cat.partial;
6312
6636
  }
6313
6637
 
6638
+ /**
6639
+ * 描く順番(部署 → ハーネス)を決める。
6640
+ *
6641
+ * 🔴 **骨組みに無い提供元を落とさない。** 名簿から外れた提供元の行が棚やオンに
6642
+ * 残っていることはある(名簿は艦隊側の都合で変わる)。骨組みだけで描くと、
6643
+ * その行は**画面から消えて誰も外せなくなる**——ゴースト行と同じ理由で「その他」へ回す。
6644
+ *
6645
+ * 同じ見出しは1つに畳む(部署の表示名が解決できない行と「その他」が2つに割れない)。
6646
+ */
6647
+ function harnessSectionPlan(cat) {
6648
+ var plan = [];
6649
+ var placed = {};
6650
+ var groupFor = function (label) {
6651
+ for (var g = 0; g < plan.length; g++) if (plan[g].label === label) return plan[g];
6652
+ var made = { label: label, harnesses: [] };
6653
+ plan.push(made);
6654
+ return made;
6655
+ };
6656
+ for (var i = 0; i < cat.departments.length; i++) {
6657
+ var dep = cat.departments[i];
6658
+ var group = groupFor(dep.label || "その他");
6659
+ for (var k = 0; k < dep.harnesses.length; k++) {
6660
+ var hn = dep.harnesses[k];
6661
+ if (placed[hn.harness]) continue;
6662
+ placed[hn.harness] = true;
6663
+ group.harnesses.push(hn);
6664
+ }
6665
+ }
6666
+ var addExtra = function (harness) {
6667
+ if (placed[harness]) return;
6668
+ placed[harness] = true;
6669
+ // 表示名が分からない提供元。**識別子は見出しに出さない**(小見出しごと出さない)
6670
+ groupFor("その他").harnesses.push({ harness: harness, label: "" });
6671
+ };
6672
+ for (var p = 0; p < cat.providers.length; p++) addExtra(cat.providers[p].harness);
6673
+ if (HARNESS_SHELF) {
6674
+ for (var s = 0; s < HARNESS_SHELF.length; s++) addExtra(HARNESS_SHELF[s].harness);
6675
+ }
6676
+ for (var e = 0; e < HARNESS_SCHEDULES.length; e++) addExtra(HARNESS_SCHEDULES[e].harness);
6677
+ return plan;
6678
+ }
6679
+
6680
+ /**
6681
+ * 1つの提供元について、棚ビューに出す機能を並べ順つきで決める。
6682
+ *
6683
+ * 出すのは **棚にあるもの ∪ オンになっているもの**。
6684
+ * 🔴 **オンの機能は棚に無くても必ず出す。** 棚で隠すと、その機能を画面からオフに
6685
+ * できなくなる(ゴースト行と同じ理由)。
6686
+ *
6687
+ * 並びは**カタログ順**(提供側が決めた順)→ カタログに無いもの。
6688
+ * **棚の配列順は使わない**——棚は集合で、置いた順に意味は無い。
6689
+ */
6690
+ function harnessShelfRowsFor(cat, harness) {
6691
+ var out = [];
6692
+ var seen = {};
6693
+ var take = function (skill) {
6694
+ if (seen[skill]) return;
6695
+ seen[skill] = true;
6696
+ out.push(skill);
6697
+ };
6698
+ for (var i = 0; i < cat.providers.length; i++) {
6699
+ var prov = cat.providers[i];
6700
+ if (prov.harness !== harness) continue;
6701
+ for (var k = 0; k < prov.skills.length; k++) {
6702
+ var sk = prov.skills[k].skill;
6703
+ if (harnessShelfHas(harness, sk) || harnessEntryOf(harness, sk)) take(sk);
6704
+ }
6705
+ }
6706
+ // カタログに無いもの(提供元から消えた・一覧を読めなかった)を後ろへ
6707
+ if (HARNESS_SHELF) {
6708
+ for (var s = 0; s < HARNESS_SHELF.length; s++) {
6709
+ if (HARNESS_SHELF[s].harness === harness) take(HARNESS_SHELF[s].skill);
6710
+ }
6711
+ }
6712
+ for (var e = 0; e < HARNESS_SCHEDULES.length; e++) {
6713
+ if (HARNESS_SCHEDULES[e].harness === harness) take(HARNESS_SCHEDULES[e].skill);
6714
+ }
6715
+ return out;
6716
+ }
6717
+
6718
+ /**
6719
+ * 棚ビュー(既定)を描く。**部署セクション → ハーネス小見出し → 機能の行。**
6720
+ *
6721
+ * **開き直しではなく描き直しで呼ばれる**(ピッカーから戻ったとき)ので、
6722
+ * 先に中身を空にする。中身が無いセクション・小見出しは立てない
6723
+ *(「中身の無い見出しを立てない」は能力タイル側と同じ約束)。
6724
+ */
6725
+ function harnessRenderShelf(list, setState, cat) {
6726
+ while (list.firstChild) list.removeChild(list.firstChild);
6727
+ var errored = harnessErroredMap(cat);
6728
+ var plan = harnessSectionPlan(cat);
6729
+ var rows = 0;
6730
+ for (var i = 0; i < plan.length; i++) {
6731
+ var group = plan[i];
6732
+ var inner = el("div", { class: "ds-harness-dep-body" });
6733
+ var groupRows = 0;
6734
+ for (var k = 0; k < group.harnesses.length; k++) {
6735
+ var hn = group.harnesses[k];
6736
+ var skills = harnessShelfRowsFor(cat, hn.harness);
6737
+ var box = el("div", { class: "ds-harness-list" });
6738
+ var boxRows = 0;
6739
+ for (var s = 0; s < skills.length; s++) {
6740
+ var meta = harnessMetaOf(cat, hn.harness, skills[s]);
6741
+ var on = !!harnessEntryOf(hn.harness, skills[s]);
6742
+ var bad = !!errored[hn.harness];
6743
+ // **読めなかった提供元の「棚にあるだけ」の行は出さない。** 消えたのか
6744
+ // 読めなかったのかを言えないので、どう書いても嘘になる(次に開けば並ぶ)
6745
+ if (!meta && bad && !on) continue;
6746
+ box.appendChild(harnessRow(hn.harness, skills[s], meta, {
6747
+ // 🔴 一覧を読めなかった提供元のオン中の行は、棚の導線を出さない(B-4)。
6748
+ // 棚に在るかどうかは分かるが、「もう無い」のか「読めなかった」のかが
6749
+ // 分からないまま外す口だけ出すと、直せる状態を誤って片付けさせる
6750
+ shelf: !meta && bad ? false : true,
6751
+ unreadable: !meta && bad,
6752
+ }));
6753
+ boxRows += 1;
6754
+ }
6755
+ if (!boxRows) continue;
6756
+ // 表示名の無い提供元では小見出しごと出さない(識別子は画面に出さない)
6757
+ if (hn.label) inner.appendChild(el("h4", { class: "ds-harness-prov-title", text: hn.label }));
6758
+ if (errored[hn.harness]) {
6759
+ inner.appendChild(el("p", {
6760
+ class: "ds-harness-subnote",
6761
+ text: "この提供元の一覧を、いま読み込めませんでした。ここに並んでいない機能があるかもしれません。",
6762
+ }));
6763
+ }
6764
+ inner.appendChild(box);
6765
+ groupRows += boxRows;
6766
+ }
6767
+ if (!groupRows) continue;
6768
+ list.appendChild(el("div", { class: "ds-harness-dep" }, [
6769
+ el("h3", { class: "ds-harness-dep-title", text: group.label }),
6770
+ inner,
6771
+ ]));
6772
+ rows += groupRows;
6773
+ }
6774
+ if (rows || cat.partial) { setState(""); return; }
6775
+ // 🔴 **「棚が無い」と「棚が空」を言い分ける(B-1)。** 前者はまだ何も選んでいない
6776
+ // 案件、後者は選んだうえで全部外した案件で、次にすることが違う
6777
+ setState(HARNESS_SHELF
6778
+ // **主語を先に置く**(何の話かを言ってから、その状態を言う)
6779
+ ? "この案件で使う機能は、いま1件もありません。「機能をえらぶ」から追加できます。"
6780
+ : "この案件で使う機能は、まだ選んでいません。「機能をえらぶ」から選べます。");
6781
+ }
6782
+
6783
+ /**
6784
+ * ピッカー(機能をえらぶ)を描く。**全カタログを部署 → ハーネスで並べ、
6785
+ * 各機能に「棚に置く / 置いてある(外す)」を出す。**
6786
+ *
6787
+ * 一覧を読めなかった提供元・カタログを持たない提供元は並べない(選べるものが無い)。
6788
+ * **オン中の機能には印を付ける**(B-2: 外しても止まらないことが読めるように)。
6789
+ */
6790
+ function harnessRenderPicker(box, setPickState, cat) {
6791
+ while (box.firstChild) box.removeChild(box.firstChild);
6792
+ var plan = harnessSectionPlan(cat);
6793
+ var rows = 0;
6794
+ for (var i = 0; i < plan.length; i++) {
6795
+ var group = plan[i];
6796
+ var inner = el("div", { class: "ds-harness-dep-body" });
6797
+ var groupRows = 0;
6798
+ for (var k = 0; k < group.harnesses.length; k++) {
6799
+ var hn = group.harnesses[k];
6800
+ var list = el("div", { class: "ds-harness-list" });
6801
+ var listRows = 0;
6802
+ for (var p = 0; p < cat.providers.length; p++) {
6803
+ var prov = cat.providers[p];
6804
+ if (prov.harness !== hn.harness) continue;
6805
+ for (var s = 0; s < prov.skills.length; s++) {
6806
+ list.appendChild(harnessPickRow(prov.skills[s]));
6807
+ listRows += 1;
6808
+ }
6809
+ }
6810
+ if (!listRows) continue;
6811
+ if (hn.label) inner.appendChild(el("h4", { class: "ds-harness-prov-title", text: hn.label }));
6812
+ inner.appendChild(list);
6813
+ groupRows += listRows;
6814
+ }
6815
+ if (!groupRows) continue;
6816
+ box.appendChild(el("div", { class: "ds-harness-dep" }, [
6817
+ el("h3", { class: "ds-harness-dep-title", text: group.label }),
6818
+ inner,
6819
+ ]));
6820
+ rows += groupRows;
6821
+ }
6822
+ setPickState(rows ? "" : "いま選べる機能はありません。機能が追加されると、ここに並びます。");
6823
+ }
6824
+
6825
+ /** ピッカーの1行。**オン/オフは触らせない**(ここで決めるのは棚に置くかどうかだけ) */
6826
+ function harnessPickRow(meta) {
6827
+ var row = el("div", { class: "ds-harness-row" });
6828
+ var head = el("div", { class: "ds-harness-head" }, [
6829
+ el("div", { class: "ds-harness-name", text: meta.display_name }),
6830
+ el("div", { class: "ds-harness-period", text: harnessDefaultPeriod(meta) }),
6831
+ ]);
6832
+ // **オン中であることは、この画面でも見えていないといけない。** 見えないまま
6833
+ // 「置いてある(外す)」を押すと、止まると受け取られる(B-2)
6834
+ if (harnessEntryOf(meta.harness, meta.skill)) {
6835
+ head.appendChild(el("span", { class: "ds-harness-onmark", text: "オン中" }));
6836
+ }
6837
+ row.appendChild(head);
6838
+ if (meta.summary) row.appendChild(el("div", { class: "ds-harness-summary", text: meta.summary }));
6839
+ var btn = el("button", { class: "ds-toggle-btn ds-harness-shelf-btn", type: "button" });
6840
+ var msg = el("div", { class: "ds-toggle-msg" });
6841
+ row.appendChild(el("div", { class: "ds-toggle" }, [btn, msg]));
6842
+ var paint = function () {
6843
+ // 動詞は棚ビューと揃える(追加/外す)。印は選択中であることを示すためだけに付ける
6844
+ btn.textContent = harnessShelfHas(meta.harness, meta.skill) ? "✓ 追加済み(外す)" : "+ 使う機能に追加";
6845
+ };
6846
+ btn.addEventListener("click", function () {
6847
+ harnessShelfToggle(meta.harness, meta.skill, meta.display_name, msg, btn, paint);
6848
+ });
6849
+ paint();
6850
+ return row;
6851
+ }
6852
+
6314
6853
  /**
6315
6854
  * ハーネス連携のモーダル。**開いたときに一覧を取りに行く**(カタログは提供側の
6316
6855
  * リポジトリが持つので、ビルド時には焼けない)。
@@ -6333,9 +6872,33 @@ ${deriveCapTile.toString()}
6333
6872
  var warn = el("p", { class: "ds-harness-warn", text: "一部の提供元から一覧を読み込めませんでした。ここに並んでいない機能があるかもしれません。時間をおいて開き直してください。" });
6334
6873
  warn.hidden = true;
6335
6874
  var list = el("div", { class: "ds-harness-list" });
6336
- m.modal.appendChild(stateEl);
6337
- m.modal.appendChild(warn);
6338
- m.modal.appendChild(list);
6875
+ // 「機能をえらぶ」(ピッカーへの入口)。**骨組みが読めたときだけ出す**
6876
+ // ——部署もハーネスの表示名も無いままピッカーを出すと、ただの長い一覧になる
6877
+ var choose = el("button", { class: "ds-toggle-btn ds-harness-choose", type: "button", text: "機能をえらぶ" });
6878
+ choose.hidden = true;
6879
+ // 棚ビューは1つの箱にまとめる(ピッカーと**丸ごと**入れ替えるため)
6880
+ var shelfView = el("div", { class: "ds-harness-shelf" }, [
6881
+ stateEl,
6882
+ warn,
6883
+ list,
6884
+ el("div", { class: "ds-harness-actions" }, [choose]),
6885
+ ]);
6886
+ m.modal.appendChild(shelfView);
6887
+ // ピッカーは**同じモーダルの中のもう1つのビュー**(重ねない)。
6888
+ // 🔴 **hidden で切り替える。** 一覧を DOM から外すと alive() が false になり、
6889
+ // 遅れて届いた応答が捨てられて「読み込み中…」のまま固まる(設計 B-3)。
6890
+ var pickState = el("p", { class: "ds-harness-pick-state" });
6891
+ pickState.hidden = true;
6892
+ var pickBody = el("div", { class: "ds-harness-pick-list" });
6893
+ var back = el("button", { class: "ds-toggle-btn ds-harness-back", type: "button", text: "← 戻る" });
6894
+ var picker = el("div", { class: "ds-harness-picker" }, [
6895
+ el("div", { class: "ds-harness-actions" }, [back]),
6896
+ el("p", { class: "modal-desc", text: "この案件で使う機能を選びます。追加しただけでは動きません(動かすには、戻ってオンにしてください)。" }),
6897
+ pickState,
6898
+ pickBody,
6899
+ ]);
6900
+ picker.hidden = true;
6901
+ m.modal.appendChild(picker);
6339
6902
  m.modal.appendChild(el("div", { class: "modal-actions" }, [
6340
6903
  el("button", { class: "ds-toggle-btn", type: "button", text: "閉じる" }),
6341
6904
  ]));
@@ -6381,7 +6944,33 @@ ${deriveCapTile.toString()}
6381
6944
  if (!alive()) return;
6382
6945
  var cat = harnessCatalogOf(j);
6383
6946
  if (!cat) { fail(); return; }
6384
- harnessFillList(list, setState, warn, cat);
6947
+ // 骨組みを持たない応答(古い Lambda)では、従来どおりのフラット表示に落とす。
6948
+ // **ピッカーの入口も出さない**——選んでも並べ替える先が無い
6949
+ if (!cat.departments) { harnessFillList(list, setState, warn, cat); return; }
6950
+ warn.hidden = !cat.partial;
6951
+ choose.hidden = false;
6952
+ var setPickState = function (text) {
6953
+ pickState.textContent = text;
6954
+ pickState.hidden = text === "";
6955
+ };
6956
+ /**
6957
+ * 棚ビューへ戻る。**そのつど描き直す**——ピッカーで置いた/外したぶんを
6958
+ * 反映するため(行ごと作り直すので、棚ビューに出ていた保存の印は消える。
6959
+ * ピッカーを一往復した後なので、消えて困る情報はもう画面に無い)
6960
+ */
6961
+ var showShelf = function () {
6962
+ harnessRenderShelf(list, setState, cat);
6963
+ picker.hidden = true;
6964
+ shelfView.hidden = false;
6965
+ };
6966
+ var showPicker = function () {
6967
+ harnessRenderPicker(pickBody, setPickState, cat);
6968
+ shelfView.hidden = true;
6969
+ picker.hidden = false;
6970
+ };
6971
+ choose.addEventListener("click", showPicker);
6972
+ back.addEventListener("click", showShelf);
6973
+ showShelf();
6385
6974
  })
6386
6975
  // **必ず末尾に置く。** 上の .then の中で投げた例外まで拾う——未処理の rejection は
6387
6976
  // node --test をテストごと落とす
@@ -9234,6 +9823,21 @@ ${deriveCapTile.toString()}
9234
9823
  // 🔴 **語を戻すのは onDone より先。** removeButton の onDone は押せない状態へ
9235
9824
  // 戻す(外した行のボタン)ので、後に置くと「処理中…」の語が居座る
9236
9825
  endBusy();
9826
+ // 🔴 **「サーバの状態が確定した」ことだけを伝える口(hooks.onSaved)。**
9827
+ //
9828
+ // 下の onDone は **changed !== false のときしか流れない**。これは「何も変わって
9829
+ // いないのに追加したと見せない」ための意図した仕様で、schedules(オン/オフ)では
9830
+ // 正しい——changed:false は「効いていない」なので行を動かしてはいけない。
9831
+ //
9832
+ // ところが**冪等な op では changed:false が正常な成功**になる(棚の追加は
9833
+ // 「既にあれば {ok:true, changed:false}」が intake の契約)。そこで onDone だけに
9834
+ // 頼ると、「サーバには既に在るのに画面は棚が空」のまま取り残され、
9835
+ // **次のビルドまで自己回復しない**。res.ok で確定した事実は、changed を問わず
9836
+ // 受け取れる必要がある。
9837
+ //
9838
+ // **既存4呼び出しの onDone 意味論には触れない。** 使い分けは呼び出し側の責任で、
9839
+ // 冪等な op(棚の2つ)だけがこちらを使う。
9840
+ if (hooks && typeof hooks.onSaved === "function") hooks.onSaved(j);
9237
9841
  // **保存が効いたら、いまの登録を読み直す(段階1)。** 一覧はまとめ表(夜間バッチ)から
9238
9842
  // 描かれるので、保存しただけでは行が出ない。ここで読み直すと、登録ファイルの中身が
9239
9843
  // 返ってきて**足りない行として一覧に出る**——さらに「まとめ表がまだ答えていない行」が
@@ -9311,8 +9915,12 @@ ${deriveCapTile.toString()}
9311
9915
  * オーバーレイ要素だけにリスナを張るのでは足りない(フォーカスが body にあると経路を通らない)。
9312
9916
  *
9313
9917
  * @param lines 表示する行(1行1段落)。先頭行が主文=読み上げの名前になる
9314
- * @param opts { okLabel, returnTo, okDanger }
9918
+ * @param opts { okLabel, returnTo, okDanger, body }
9315
9919
  * okDanger … OK ボタンを赤で出すか(既定 true)。false は「広げる向き」の確認用
9920
+ * body … lines の後・ボタンの前に挿す DOM 節(任意)。**押す前に決めることがある確認**
9921
+ * のための口で、ハーネスの「オンにする」は届く時間帯の選択をここに入れる。
9922
+ * 無指定なら従来と完全に同じ見た目・同じ挙動(節の中の操作は overlay の
9923
+ * 外側クリック判定=e.target === overlay に掛からないので、素通しでよい)
9316
9924
  */
9317
9925
  function openConfirm(lines, opts) {
9318
9926
  return new Promise(function (resolve) {
@@ -9332,6 +9940,7 @@ ${deriveCapTile.toString()}
9332
9940
  if (i === 0) { pnode.id = textId; box.setAttribute("aria-labelledby", textId); }
9333
9941
  box.appendChild(pnode);
9334
9942
  });
9943
+ if (o.body) box.appendChild(o.body);
9335
9944
  var cancel = el("button", { class: "modal-cancel", type: "button", text: "キャンセル" });
9336
9945
  // **既定は赤(ds-danger)。** ここに来る操作はほぼ全部が取り消せない向きなので、
9337
9946
  // 既定を変えない。**okDanger: false のときだけ通常の見た目**——ハーネスの
package/dist/repo.js CHANGED
@@ -7,6 +7,7 @@ exports.normalizeToHttps = normalizeToHttps;
7
7
  exports.classifyCommit = classifyCommit;
8
8
  exports.isFrontmatterKeyRename = isFrontmatterKeyRename;
9
9
  exports.collectAiChangeHistory = collectAiChangeHistory;
10
+ exports.lastSubstantiveUpdate = lastSubstantiveUpdate;
10
11
  const node_child_process_1 = require("node:child_process");
11
12
  /**
12
13
  * GitHubのowner/repoを表すベースURLを解決する。
@@ -250,8 +251,13 @@ function isFrontmatterKeyRename(before, after) {
250
251
  * 揃えないと、人が「改名しながら中身も直した」変更が**無音で消える**。
251
252
  */
252
253
  const RENAME_OPT = "--find-renames=90%";
253
- /** git log を1回。失敗(git 無し・リポジトリでない・出力が大きすぎる)は空文字にする */
254
- function gitLog(cwd, args) {
254
+ /**
255
+ * git log を1回。失敗(git 無し・リポジトリでない・出力が大きすぎる)は空文字にする。
256
+ * @param what 警告に出す「何が取れなかったか」。呼び出し元が増えたので引数化した——
257
+ * 固定文のままだと Home の最終更新日が取れない日に「AIの変更履歴を…」という
258
+ * 嘘の警告が出て、診断がその文言に引っ張られる
259
+ */
260
+ function gitLog(cwd, args, what = "AIの変更履歴") {
255
261
  try {
256
262
  // 🔴 **出力の形は、読む人の git 設定に左右させない。** ここで組み立てているのは
257
263
  // 機械が読む出力なので、人の ~/.gitconfig で形が変わる余地を全部潰す:
@@ -294,7 +300,7 @@ function gitLog(cwd, args) {
294
300
  // 戻り値の契約(空文字 = 履歴なしで正常継続)はどちらでも変えない
295
301
  const err = e;
296
302
  if (err.code !== "ENOENT" && err.status !== 128) {
297
- console.warn(`AIの変更履歴を git から取れませんでした(履歴は出さずに続けます): ${String(e)}`);
303
+ console.warn(`${what}を git から取れませんでした(表示は出さずに続けます): ${String(e)}`);
298
304
  }
299
305
  return "";
300
306
  }
@@ -687,3 +693,126 @@ function collectAiChangeHistory(cwd) {
687
693
  }
688
694
  return history;
689
695
  }
696
+ // ---- Home の「実質的な最終更新日」 ------------------------------------------
697
+ //
698
+ // Home タイトル横の「最終更新: YYYY-MM-DD」の出どころ。Home の自己記述(この案件に
699
+ // ついて/ゴール/体制)は古くなるのに、画面にいつ時点の情報か出ていない——
700
+ // 手書きの日付は更新し忘れで嘘になるので、git のコミット日から自動で導く。
701
+ //
702
+ // ## この機能で許せない劣化の方向
703
+ //
704
+ // - 「出ない」は許容(git 無し・浅い clone・窓切れ → null。既存機能と同じ倒し方)
705
+ // - 「古めに出る」は許容(--follow は衝突解決マージを丸ごと落とすため、その形の
706
+ // 編集は1つ前の実質コミットの日付になる。squash・通常の PR マージはブランチ側の
707
+ // 編集コミットが本物の author date を持つので実害は小さい——知っていて許容した)
708
+ // - 🔴 **「新しすぎる日付」だけは許さない。** 鮮度を知らせる表示が「いつも新鮮です」と
709
+ // 嘘をつくのは目的そのものへの裏切りで、見た目が自然なので誰も気づけない。
710
+ // 既知の例外が1つ: **権限(mode)だけのコミット**は --name-status では M と区別が
711
+ // 付かず、実質更新に数えてしまう。区別には差分をもう1回 git から取る必要があり
712
+ // (AI変更履歴側の modeOnly がそれ)、Home.md にモードだけの変更が入る形は実質
713
+ // 無いので、ここでは扱わないと決めた(isFrontmatterKeyRename の見送りと同じ理由)
714
+ /** 遡るコミット数の上限。Home.md 1ファイルの履歴なので、これで実質十分 */
715
+ const LAST_UPDATE_MAX_COMMITS = 50;
716
+ /**
717
+ * 改名の検出閾値。🔴 **既存の RENAME_OPT(90%)を共有しない。50%(git の既定)を明示する。**
718
+ *
719
+ * 90% はレコード群の「誤検出=人が消した本文が別の記録のページに出る」という重い事故を
720
+ * 避けるための数字。ここは対象が Home.md 1ファイルで、誤検出の被害は「日付が少し
721
+ * 古めに出る」だけ——**リスクの非対称性が逆向き**なので、値だけ持ってきても根拠は
722
+ * 付いてこない。実測でも「旧構成 → Cortex/Home.md への引っ越し(見出しを整えて
723
+ * 52% 類似)」は 90% だと追加+削除に割れて追跡が切れ、50% なら R052 で追える。
724
+ * 省略せず明示するのは、diff.renames を切っている人の設定に左右されないため。
725
+ */
726
+ const LAST_UPDATE_RENAME_OPT = "--find-renames=50%";
727
+ /**
728
+ * 指定ファイルの「実質的な最終更新日」(YYYY-MM-DD)。
729
+ *
730
+ * 機械的なコミット(classifyCommit が "migration" と判定するもの)と、中身が
731
+ * 1文字も変わっていない移動(R100)は飛ばす。**人の編集と AI の編集はどちらも
732
+ * 「実質更新」として数える**——この表示は「誰が」ではなく「情報がいつ時点か」を
733
+ * 示す鮮度のヒントだから。
734
+ *
735
+ * 日付は author date(%aI)の先頭10文字。committer date にしないのは rebase や
736
+ * マージで日付が動くため(既存 AiChangeEntry.at と同じ選択で、読む人の log.date
737
+ * 設定にも左右されない)。タイムゾーンは書いた人のオフセット基準——bot の UTC 編集が
738
+ * JST より1日古く出ることがあるが、それは「古い方向」なので許容する。
739
+ *
740
+ * git が無い・リポジトリでない・浅い clone・窓内が全部機械的 → null
741
+ * (表示が出ないだけで、他は壊れない)。
742
+ *
743
+ * frontmatter の項目名だけの付け替えを落とす構造の規則(isFrontmatterKeyRename)は
744
+ * ここでは使わない——差分の取得にもう1回 git を呼ぶ割に、Home.md ではマイグレーションの
745
+ * 件名規約で落ちる形しか観測されていない(適用しないと決めた・設計レビュー承認済み)。
746
+ */
747
+ function lastSubstantiveUpdate(cwd, repoPath) {
748
+ // 🔴 **浅い clone を最初に弾く。** 浅い clone では境界コミットが親を持たないため、
749
+ // このファイルを触っていない直近のコミットまで `A <path>` として現れる(実測)。
750
+ // そのまま読むと「最終更新: 今日」という**常に新しい嘘の日付**になる——既存機能は
751
+ // 浅い clone で「出ない」に倒れるが、この計算だけは「嘘をつく」に倒れるので、
752
+ // ガードで丸ごと null にする。「A を全部飛ばす」で代用しないこと(本物の初回
753
+ // コミットは正当な A で、それを飛ばすと作られたきりの Home に日付が出ない)
754
+ try {
755
+ const shallow = (0, node_child_process_1.execFileSync)("git", ["rev-parse", "--is-shallow-repository"], {
756
+ cwd,
757
+ encoding: "utf8",
758
+ stdio: ["ignore", "pipe", "ignore"],
759
+ }).trim();
760
+ if (shallow !== "false")
761
+ return null;
762
+ }
763
+ catch {
764
+ return null; // git が無い・リポジトリでない(既定の状態なので警告もしない)
765
+ }
766
+ // 単一ファイルなので --follow で改名を git 側に追跡させる(readCommits が --no-follow
767
+ // なのは複数レコードを1回の log で読むため。ここはその制約が当てはまらない)
768
+ const out = gitLog(cwd, [
769
+ "log",
770
+ `-n${LAST_UPDATE_MAX_COMMITS}`,
771
+ "--follow",
772
+ LAST_UPDATE_RENAME_OPT,
773
+ "--no-color",
774
+ "--no-ext-diff",
775
+ "--name-status",
776
+ `--format=%x00%H%x1f%aI%x1f%an%x1f%ae%x1f%s`,
777
+ "--",
778
+ repoPath,
779
+ ], "Homeの最終更新日");
780
+ // 🔴 走査は「**飛ばされなかったファイル行を1つ以上持つ**、最も新しいコミット」を探す。
781
+ // ファイル行の無いコミット(素の log では衝突解決マージがこの形で出る)を実質更新に
782
+ // 数えない——いまは --follow がその形を落とすので踏まないが、「たまたま踏まない」を
783
+ // 不変条件にしない(--follow を外すフォールバックを将来足した瞬間に無音で壊れる)。
784
+ //
785
+ // ⚠️ **ファイル行のパスを repoPath と突き合わせて絞ってはいけない。** --follow では
786
+ // 改名より古いコミットのパスが**その時点の古いパス**で出る。目的パスで絞ると、
787
+ // 追跡してきた過去が丸ごと消える(絞りたくなっても締めないこと)。
788
+ let at = "";
789
+ let skip = true; // 先頭(コミット行より前)には何も無い
790
+ let hasSubstantiveFile = false;
791
+ const settle = () => !skip && hasSubstantiveFile && at.length >= 10 ? at.slice(0, 10) : null;
792
+ for (const line of out.split("\n")) {
793
+ if (line.startsWith(NUL)) {
794
+ const done = settle();
795
+ if (done)
796
+ return done;
797
+ const parts = line.slice(1).split(US);
798
+ at = parts[1] || "";
799
+ skip = classifyCommit({
800
+ name: parts[2] || "",
801
+ email: parts[3] || "",
802
+ subject: parts.slice(4).join(US),
803
+ }) === "migration";
804
+ hasSubstantiveFile = false;
805
+ continue;
806
+ }
807
+ // name-status 行だけを拾う(ヘッダとの間の空行はここで自然に落ちる)
808
+ const m = /^([A-Z])(\d*)\t/.exec(line);
809
+ if (!m)
810
+ continue;
811
+ if (m[1] === "D")
812
+ continue; // 防御。HEAD に居るファイルの --follow では実質出ない
813
+ if (m[1] === "R" && m[2] === "100")
814
+ continue; // 中身が1文字も変わっていない移動
815
+ hasSubstantiveFile = true;
816
+ }
817
+ return settle();
818
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.18",
3
+ "version": "0.12.19",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,