@takagaki/cortex-decisions-viewer 0.12.11 → 0.12.13

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
@@ -38,6 +38,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.readFleetSources = readFleetSources;
40
40
  exports.readGoldProposals = readGoldProposals;
41
+ exports.readWeeklyCuration = readWeeklyCuration;
41
42
  exports.buildHomeGrounding = buildHomeGrounding;
42
43
  exports.collectMeetings = collectMeetings;
43
44
  exports.findBacklogDir = findBacklogDir;
@@ -519,6 +520,276 @@ async function readGoldProposals(repoRoot) {
519
520
  }
520
521
  return byPath;
521
522
  }
523
+ // ---- 週次整理レポート(.cortex/weekly-curation-report.json)------------------
524
+ //
525
+ // 週次キュレーション run が「今週AIが何を自動でやったか」と「人に判断してほしい残り」を
526
+ // 毎回全置換で書く。**読み方は readGoldProposals と同じ規律**——無い・壊れている・
527
+ // version が違うときは表示面なしで正常継続し、フィールドは明示の許可リストで射影する。
528
+ //
529
+ // 🔴 **素通しにしないこと。** engine 側が単独で版を上げるので、画面が知らないキーが
530
+ // 必ず先に入る。素通しにすると、公開範囲フィルタを通していない生の断片(内部限定の
531
+ // 引用・敗者レコードの全文など)が知らないうちに顧客の画面へ載りうる。
532
+ /** 文字列化。**配列は `, ` で連結する**(synonyms のような配列値が `a,b` に潰れるより読める) */
533
+ function wcStr(v) {
534
+ if (Array.isArray(v))
535
+ return v.map((x) => String(x)).join(", ");
536
+ return v != null ? String(v) : "";
537
+ }
538
+ /** 空文字はキーごと落とす(「無い」と「空」を画面で区別できるようにする) */
539
+ function wcOptStr(v) {
540
+ const text = wcStr(v);
541
+ return text === "" ? undefined : text;
542
+ }
543
+ /** 数値だけ通す。文字列の "0.78" も落とす(一致度の表示は数値である前提で組んである) */
544
+ function wcNum(v) {
545
+ return typeof v === "number" && Number.isFinite(v) ? v : undefined;
546
+ }
547
+ /**
548
+ * レコードの指し先(勝者・敗者・クラスタの構成員)。
549
+ * **title が無い要素は捨てる**——表示の主語が無いと、画面に出しても何の話か分からない。
550
+ * path は無いことがある(統合で消えた敗者にはもう宛先が無い)ので必須にしない。
551
+ */
552
+ function wcRef(v) {
553
+ if (v == null || typeof v !== "object" || Array.isArray(v))
554
+ return null;
555
+ const row = v;
556
+ const title = wcStr(row.title);
557
+ if (!title)
558
+ return null;
559
+ // **短縮記法(`title,`)にしないこと**(readGoldProposals と同じ理由。
560
+ // fleet-passthrough のテストが「型で宣言したフィールドを build.ts が受け渡しているか」を
561
+ // `名前:` の字面で走査しており、短縮記法にするとその守りの外へ静かに落ちる)
562
+ return {
563
+ path: wcStr(row.path),
564
+ title: title,
565
+ body: wcOptStr(row.body),
566
+ };
567
+ }
568
+ /** 共通フィールドの固定リスト。未知の kind を「汎用行」へ倒すときだけ使う */
569
+ function wcCommonRow(kind, row) {
570
+ return {
571
+ kind: kind,
572
+ path: wcOptStr(row.path),
573
+ title: wcOptStr(row.title),
574
+ type: wcOptStr(row.type),
575
+ reason: wcOptStr(row.reason),
576
+ why: wcOptStr(row.why),
577
+ status: wcOptStr(row.status),
578
+ score: wcNum(row.score),
579
+ commit: wcOptStr(row.commit),
580
+ };
581
+ }
582
+ /** 精製で書き換えた項目。**field の無い要素は捨てる**(どこを直したのか言えない) */
583
+ function wcChanges(v) {
584
+ if (!Array.isArray(v))
585
+ return [];
586
+ const out = [];
587
+ for (const c of v) {
588
+ if (c == null || typeof c !== "object" || Array.isArray(c))
589
+ continue;
590
+ const row = c;
591
+ const field = wcStr(row.field);
592
+ if (!field)
593
+ continue;
594
+ out.push({
595
+ field: field,
596
+ before: wcStr(row.before),
597
+ after: wcStr(row.after),
598
+ });
599
+ }
600
+ return out;
601
+ }
602
+ /** 根拠。gold-proposals と同じ形(source 必須・quote は公開範囲フィルタで落ちうる) */
603
+ function wcEvidence(v) {
604
+ if (!Array.isArray(v))
605
+ return [];
606
+ const out = [];
607
+ for (const e of v) {
608
+ if (e == null || typeof e !== "object" || Array.isArray(e))
609
+ continue;
610
+ const row = e;
611
+ const source = wcStr(row.source);
612
+ if (!source)
613
+ continue;
614
+ out.push({
615
+ source: source,
616
+ quote: wcOptStr(row.quote),
617
+ });
618
+ }
619
+ return out;
620
+ }
621
+ /** 畳んだ表記。文字列だけ通す(オブジェクトが混ざったら [object Object] を画面に出さない) */
622
+ function wcFolded(v) {
623
+ if (!Array.isArray(v))
624
+ return undefined;
625
+ const out = v.filter((x) => typeof x === "string" && x !== "");
626
+ return out.length ? out : undefined;
627
+ }
628
+ const WEEKLY_MERGE_KINDS = ["merge-term", "merge-member", "merge-rule"];
629
+ const WEEKLY_REFINE_KINDS = ["refine-term", "refine-member"];
630
+ const WEEKLY_AUDIT_KINDS = ["tool-discipline-rule", "machine-readable-term", "partial-name-member"];
631
+ /** AIが今週やったこと1件。必須が欠けるものは null(呼び側が読み飛ばす) */
632
+ function wcAction(v) {
633
+ if (v == null || typeof v !== "object" || Array.isArray(v))
634
+ return null;
635
+ const row = v;
636
+ const kind = wcStr(row.kind);
637
+ if (!kind)
638
+ return null;
639
+ if (WEEKLY_MERGE_KINDS.includes(kind)) {
640
+ const winner = wcRef(row.winner);
641
+ const loser = wcRef(row.loser);
642
+ // **どちらのタイトルも要る。** 「『X』を『Y』に統合」の主語述語がそろわないと文にならない
643
+ if (!winner || !loser)
644
+ return null;
645
+ return {
646
+ kind: kind,
647
+ winner: winner,
648
+ loser: loser,
649
+ folded: wcFolded(row.folded),
650
+ reason: wcOptStr(row.reason),
651
+ commit: wcOptStr(row.commit),
652
+ };
653
+ }
654
+ if (WEEKLY_REFINE_KINDS.includes(kind)) {
655
+ const title = wcStr(row.title);
656
+ const changes = wcChanges(row.changes);
657
+ // **どこを直したのか1件も言えないなら出さない。** 「精製しました」だけの行は、
658
+ // 見た人が確かめようがなく、確かめられない報告は不信の材料にしかならない
659
+ if (!title || !changes.length)
660
+ return null;
661
+ return {
662
+ kind: kind,
663
+ path: wcOptStr(row.path),
664
+ title: title,
665
+ changes: changes,
666
+ evidence: wcEvidence(row.evidence),
667
+ commit: wcOptStr(row.commit),
668
+ };
669
+ }
670
+ // 未知の kind。**捨てずに汎用行へ倒す**(契約の指示)。engine が先に kind を増やしても
671
+ // 「何かをやったこと」自体は画面に残る——黙って消えるより、読めないまま出るほうがよい
672
+ return wcCommonRow(kind, row);
673
+ }
674
+ /** 人に判断してほしい残り1件。必須が欠けるものは null(呼び側が読み飛ばす) */
675
+ function wcAttention(v) {
676
+ if (v == null || typeof v !== "object" || Array.isArray(v))
677
+ return null;
678
+ const row = v;
679
+ const kind = wcStr(row.kind);
680
+ if (!kind)
681
+ return null;
682
+ if (kind === "duplicate-cluster" || kind === "demoted-merge") {
683
+ const members = Array.isArray(row.members)
684
+ ? row.members.map(wcRef).filter((m) => m != null)
685
+ : [];
686
+ // **2件そろわないと「組」にならない。** 1件だけの重複クラスタは判断材料にならず、
687
+ // 「AIで適用」の統合プロンプトも対象が1つでは成り立たない
688
+ if (members.length < 2)
689
+ return null;
690
+ return {
691
+ kind: kind,
692
+ type: wcOptStr(row.type),
693
+ // 一致度は**欠けても行を残す**。数値が出せないだけで、重複の疑い自体は伝える価値がある
694
+ score: wcNum(row.score),
695
+ members: members,
696
+ reason: wcOptStr(row.reason),
697
+ };
698
+ }
699
+ if (WEEKLY_AUDIT_KINDS.includes(kind)) {
700
+ const title = wcStr(row.title);
701
+ if (!title)
702
+ return null;
703
+ return {
704
+ kind: kind,
705
+ path: wcOptStr(row.path),
706
+ title: title,
707
+ why: wcOptStr(row.why),
708
+ status: wcOptStr(row.status),
709
+ };
710
+ }
711
+ return wcCommonRow(kind, row);
712
+ }
713
+ /** 集計。契約の9キーを**数値だけ**通す(それ以外のキー・非数値は落とす) */
714
+ function wcStats(v) {
715
+ if (v == null || typeof v !== "object" || Array.isArray(v))
716
+ return undefined;
717
+ const row = v;
718
+ const stats = {
719
+ mergedTerms: wcNum(row.mergedTerms),
720
+ mergedMembers: wcNum(row.mergedMembers),
721
+ mergedRules: wcNum(row.mergedRules),
722
+ refinedTerms: wcNum(row.refinedTerms),
723
+ proposedTerms: wcNum(row.proposedTerms),
724
+ refinedMembers: wcNum(row.refinedMembers),
725
+ proposedMembers: wcNum(row.proposedMembers),
726
+ attentions: wcNum(row.attentions),
727
+ deferredByBudget: wcNum(row.deferredByBudget),
728
+ };
729
+ // **有効な数が1つも無ければキーごと落とす**(secretSpaces と同じ理由。空の器を通すと、
730
+ // 画面が「確かめたうえで0件」と読む余地を作る——知らないことは、キーが無いことで表す)
731
+ for (const n of Object.values(stats))
732
+ if (n != null)
733
+ return stats;
734
+ return undefined;
735
+ }
736
+ /**
737
+ * 週次整理レポート(`.cortex/weekly-curation-report.json`)を読む。
738
+ *
739
+ * **無い・壊れているときは null(表示面なし)で正常継続する。** 週次キュレーションを
740
+ * 回していない案件のほうが多い時期なので、無いことは既定の状態であって異常ではない。
741
+ * これが読めないことでビューアのビルドまで止めるのは釣り合わない。
742
+ */
743
+ async function readWeeklyCuration(repoRoot) {
744
+ let json;
745
+ try {
746
+ const raw = await node_fs_1.promises.readFile(path.join(repoRoot, ".cortex", "weekly-curation-report.json"), "utf8");
747
+ json = JSON.parse(raw);
748
+ }
749
+ catch (e) {
750
+ // 「まだ週次キュレーションを回していない」が既定の状態なので、無いことは警告しない。
751
+ // **壊れている(JSON として読めない)ときだけ**残す——静かに0件になると、
752
+ // 節が出ない理由が「作られていない」のか「読めていない」のか切り分けられない
753
+ if (e?.code !== "ENOENT") {
754
+ console.warn(`weekly-curation-report.json を読めませんでした(週次整理は出さずに続けます): ${String(e)}`);
755
+ }
756
+ return null;
757
+ }
758
+ if (json == null || typeof json !== "object" || Array.isArray(json)) {
759
+ console.warn("weekly-curation-report.json の形式が想定と違います(週次整理は出さずに続けます)");
760
+ return null;
761
+ }
762
+ const obj = json;
763
+ // **版が違うものは読まない**(gold-proposals.json と同じ規律)。engine 側が構造を
764
+ // 変えたときに、古い読み手が部分的に解釈して中途半端な報告を出すより、出さないほうがよい
765
+ if (obj.version !== 1) {
766
+ console.warn(`weekly-curation-report.json の version が未対応です(週次整理は出さずに続けます): ${String(obj.version)}`);
767
+ return null;
768
+ }
769
+ const actions = [];
770
+ if (Array.isArray(obj.actions)) {
771
+ for (const v of obj.actions) {
772
+ const act = wcAction(v);
773
+ // 必須が欠ける1件のために報告ごと落とさない(エントリ単位の読み飛ばしは正常系なので黙る)
774
+ if (act)
775
+ actions.push(act);
776
+ }
777
+ }
778
+ const attentions = [];
779
+ if (Array.isArray(obj.attentions)) {
780
+ for (const v of obj.attentions) {
781
+ const att = wcAttention(v);
782
+ if (att)
783
+ attentions.push(att);
784
+ }
785
+ }
786
+ return {
787
+ ranAt: wcStr(obj.ranAt),
788
+ actions: actions,
789
+ attentions: attentions,
790
+ stats: wcStats(obj.stats),
791
+ };
792
+ }
522
793
  // ---- Home(案件の自己記述)--------------------------------------------------
523
794
  //
524
795
  // Home.md は「案件そのものの説明」に絞る再構成の途中にある(engine 側で進行中)。
@@ -730,7 +1001,9 @@ function sanitizeFm(v) {
730
1001
  }
731
1002
  return v;
732
1003
  }
733
- const GOLD_TYPES = new Set(["decision", "term", "report", "overview"]);
1004
+ // グラフの凡例で独立した分類(=色)を持つ type。Gold 4区画(decision/term/member/rule)は
1005
+ // それぞれ独立の分類にする(「その他レコード」に丸めない——2026-08-28 オーナー裁定)
1006
+ const GOLD_TYPES = new Set(["decision", "term", "member", "rule", "report", "overview"]);
734
1007
  /** ディレクトリ配下の .md を再帰的に列挙する(存在しなければ空) */
735
1008
  async function walkMd(dirAbs) {
736
1009
  const out = [];
@@ -1620,6 +1893,8 @@ async function build(opts) {
1620
1893
  // すでにある会議(投入モーダルの「会議名」の選択肢)。intake の有無に関わらず載せる——
1621
1894
  // 出し分けは表示側の仕事で、ここで絞ると「投入の口を後から付けたら選択肢が空」になる
1622
1895
  const meetings = await collectMeetings(repoRoot);
1896
+ // 週次整理レポート。無ければ null で、操作タブの節を出さない
1897
+ const weeklyCuration = await readWeeklyCuration(repoRoot);
1623
1898
  const site = {
1624
1899
  title: opts.title,
1625
1900
  generatedAt: opts.generatedAt,
@@ -1662,6 +1937,9 @@ async function build(opts) {
1662
1937
  checks: fleet.checks,
1663
1938
  githubOwners: fleet.githubOwners,
1664
1939
  backlogTarget: fleet.backlogTarget,
1940
+ // null(無い・壊れ・version違い)は undefined へ落とす——SiteData の
1941
+ // 「知らないことはキーが無いことで表す」に合わせる(空の器を通さない)
1942
+ weeklyCuration: weeklyCuration ?? undefined,
1665
1943
  };
1666
1944
  // サムネイル(デザイン画面等)をサイトに同梱し、ノードのパスをサイト内相対に書き換える
1667
1945
  await node_fs_1.promises.mkdir(outAbs, { recursive: true });
package/dist/render.js CHANGED
@@ -2108,6 +2108,52 @@ a:hover { text-decoration: underline; }
2108
2108
  padding-bottom: 6px; border-bottom: 1px solid var(--border);
2109
2109
  }
2110
2110
  .home-grounding > .home-grounding-title:first-child { margin-top: 0; }
2111
+ /* ---- レコード詳細の「AIによる最近の変更」(.cortex/weekly-curation-report.json 由来)----
2112
+ 操作タブの「週次整理」節(0.12.12・.weekly-curation)を撤去し、同じデータを
2113
+ 関係するレコードのページへ寄せた。カードは詳細ページの提案(.proposals)と
2114
+ 同系色にする——どちらも「AIがやったこと・言っていることを読む」もので種類が同じ */
2115
+ .ai-history { margin-top: 28px; padding-top: 16px; border-top: 1px solid var(--border); }
2116
+ .ah-summary { cursor: pointer; font-size: 13.5px; font-weight: 700; color: var(--muted); }
2117
+ .ah-item {
2118
+ background: #f8f9ff; border: 1px solid #c7d2fe; border-radius: 10px;
2119
+ padding: 12px 14px; margin: 10px 0 0;
2120
+ }
2121
+ .ah-item-head { font-size: 14px; font-weight: 700; color: #3730a3; margin-bottom: 6px; line-height: 1.6; }
2122
+ .ah-line { font-size: 13px; color: var(--muted); line-height: 1.8; word-break: break-word; }
2123
+ .ah-meta { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-top: 10px; font-size: 13px; }
2124
+ .ah-commit { font-size: 12.5px; color: var(--muted); font-variant-numeric: tabular-nums; }
2125
+ .ah-change { border-top: 1px solid #ddd6fe; padding-top: 10px; margin-top: 10px; }
2126
+ .ah-change:first-of-type { border-top: 0; padding-top: 0; }
2127
+ .ah-body { margin-top: 10px; font-size: 13px; }
2128
+ .ah-body-summary { cursor: pointer; color: var(--muted); }
2129
+ /* 全文は数千字になりうる。折りたたみの中でも高さを縛って、開いたときにページ全体を押し流さない */
2130
+ .ah-body-text {
2131
+ margin: 8px 0 0; padding: 10px 12px; max-height: 320px; overflow: auto;
2132
+ background: #fff; border: 1px solid var(--border); border-radius: 8px;
2133
+ font-size: 12.5px; line-height: 1.7; white-space: pre-wrap; word-break: break-word;
2134
+ }
2135
+ /* ---- レコードの削除(?op=delete-record)----
2136
+ **控えめに置く。** 取り消せない操作なので、上の操作列(AIで編集・GitHubで編集)の
2137
+ 隣には並べない。押したあとの見た目は「取り消し線+無効化」で、一覧からは消さない
2138
+ (次のビルドまで SiteData は古いまま) */
2139
+ .record-delete { margin-top: 28px; padding-top: 14px; border-top: 1px solid var(--border); }
2140
+ .record-delete-btn {
2141
+ background: none; border: 0; padding: 0; cursor: pointer;
2142
+ font-size: 12.5px; color: var(--muted); text-decoration: underline;
2143
+ }
2144
+ .record-delete-btn:hover { color: #b91c1c; }
2145
+ .record-delete-btn:disabled { cursor: default; text-decoration: none; opacity: 0.7; }
2146
+ .record-delete-msg { font-size: 13px; margin-top: 6px; }
2147
+ .record-delete-msg.is-error { color: #b91c1c; }
2148
+ .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;
2151
+ padding: 10px 12px; font-size: 13px; line-height: 1.8; color: var(--muted);
2152
+ }
2153
+ /* 消したものは、消えたと分かる形で残す(一覧・詳細の両方) */
2154
+ .is-deleted { text-decoration: line-through; color: var(--muted); }
2155
+ /* 消したレコードでは、直す口(AIで編集・GitHubで編集)を押させない */
2156
+ .is-disabled { opacity: 0.45; pointer-events: none; }
2111
2157
  /* ---- 操作タブ末尾の「この仕組みについて」(旧 Home.md から移設)---- */
2112
2158
  .about-cortex { margin-top: 36px; padding-top: 24px; border-top: 1px solid var(--border); }
2113
2159
  .about-cortex .section-title { font-size: 17px; font-weight: 700; margin: 0 0 10px; }
@@ -2924,6 +2970,10 @@ ${deriveCapTile.toString()}
2924
2970
  // **偶然**だった(テストもその偶然に乗っていた)。候補として明示する
2925
2971
  var HAS_FLEET = !!((SITE.internalSources || []).length || (SITE.externalSources || []).length ||
2926
2972
  (SITE.pipelines || []).length || SITE.capabilities || (SITE.secretKinds || []).length);
2973
+ // **週次整理は候補に数えない(2026-08-28 に節ごと撤去)。** レポートの中身は
2974
+ // 関係するレコードのページ(「AIによる最近の変更」)へ移したので、この受け皿には
2975
+ // もう週次整理の面が無い。数えたままにすると、fleet も intake も無い案件で
2976
+ // **中身の無い操作タブ**が立つ(#283 以前の挙動へ戻す)
2927
2977
  if (SITE.intake || HAS_FLEET) {
2928
2978
  TABS.push({ key: "ops", label: "操作" });
2929
2979
  }
@@ -6591,6 +6641,67 @@ ${deriveCapTile.toString()}
6591
6641
  });
6592
6642
  }
6593
6643
 
6644
+ /**
6645
+ * 「通知先」の札。**組み立てる場所を1つにする**——下のトグルで後から付け外しするので、
6646
+ * 描画時と押した後で別々に作ると、押した行だけ見た目の違う札が並ぶ。
6647
+ */
6648
+ function notifyBadge() {
6649
+ return el("span", { class: "badge ds-notify", text: "通知先" });
6650
+ }
6651
+
6652
+ /**
6653
+ * 通知先の入り切り(op=config の set-notify)。
6654
+ *
6655
+ * 書き換わるのは チャット/notify.json だけで、**読み取りの対象(Gold 昇格)は動かない。**
6656
+ * 載っている=通知先という決め方の正本は engine の notify-targets.mjs で、
6657
+ * **画面が別のフィルタを持たない**のがその規約——出し分けの条件も、あちらが落とす条件
6658
+ * (ワークスペースを宣言している=顧客ワークスペース)と同じ1つだけにする。
6659
+ *
6660
+ * **顧客ワークスペースの行にはトグルを出さない(#104)。** 読み取り専用の接続なので
6661
+ * chat:write が無く、投げれば必ず失敗する(サーバも断る)。押せる形にしておくと
6662
+ * 「押したら断られる」だけの操作が並ぶので、代わりに理由をその行に常時出す
6663
+ * ——ワークスペースの札の説明は hover しないと読めない。
6664
+ *
6665
+ * **確認ダイアログは出さない。** ON も OFF も同じ画面から即時に戻せる
6666
+ * (取り消せない「登録を解除」とは扱いを分ける)。
6667
+ *
6668
+ * **一覧は書き換えない。** 反転させるのは自分のボタンの語と「通知先」の札だけ
6669
+ * (sendConfig の約束どおり。何も変わらなかったときは onDone が呼ばれないので動かない)。
6670
+ */
6671
+ function notifyToggle(s, badges) {
6672
+ var wrap = el("div", { class: "ds-toggle" });
6673
+ if (s.workspace) {
6674
+ wrap.appendChild(el("span", {
6675
+ class: "ds-toggle-msg",
6676
+ text: "通知先にできません(読み取り専用の接続)",
6677
+ }));
6678
+ return wrap;
6679
+ }
6680
+ var on = s.notify === true;
6681
+ var labelOf = function () { return on ? "通知を止める" : "通知先にする"; };
6682
+ var btn = el("button", { class: "ds-toggle-btn", type: "button", text: labelOf() });
6683
+ var msg = el("div", { class: "ds-toggle-msg" });
6684
+ btn.addEventListener("click", async function () {
6685
+ var next = !on;
6686
+ // **ワークスペースは送らない。** この分岐に来るのは宣言の無い行だけで、
6687
+ // サーバも ON では受け取らない(送れば断られる)
6688
+ await sendConfig({ op: "set-notify", ref: s.ref, value: next }, msg, btn, function () {
6689
+ on = next;
6690
+ // 行の元データも合わせる。ライブの読み直しで同じ行が描き直されるまでの間、
6691
+ // ここだけが「いまどちらか」を持っている
6692
+ s.notify = next;
6693
+ btn.textContent = labelOf();
6694
+ var badge = badges.querySelector(".ds-notify");
6695
+ // 並びは描画時と同じ(札の先頭)。後ろに足すと、押した行だけ順番が変わる
6696
+ if (on && !badge) badges.insertBefore(notifyBadge(), badges.firstChild);
6697
+ if (!on && badge) badges.removeChild(badge);
6698
+ });
6699
+ });
6700
+ wrap.appendChild(btn);
6701
+ wrap.appendChild(msg);
6702
+ return wrap;
6703
+ }
6704
+
6594
6705
  /**
6595
6706
  * いま同期している Backlog の接続先の一覧(#127)。**1件でも配列で返す。**
6596
6707
  *
@@ -8206,7 +8317,7 @@ ${deriveCapTile.toString()}
8206
8317
  title: "このリポジトリは読み取り対象から除外されています(開発の登録ファイル dev-sources.json の exclude)。この画面からは戻せません。",
8207
8318
  }));
8208
8319
  }
8209
- if (s.notify === true) badges.appendChild(el("span", { class: "badge ds-notify", text: "通知先" }));
8320
+ if (s.notify === true) badges.appendChild(notifyBadge());
8210
8321
  // **どのワークスペースのチャンネルかを、必ず行に出す(#104・2026-08-20 に社内も)。**
8211
8322
  //
8212
8323
  // 折りたたみの見出しでまとめるのをやめた(オーナー指摘「複雑になっちゃってる」)ので、
@@ -8314,6 +8425,9 @@ ${deriveCapTile.toString()}
8314
8425
  // ——Slack は channels.json 由来が derived なので、出すのは derived と both。
8315
8426
  if (SITE.intake && s.type === "slack" && s.ref && s.origin !== "explicit") {
8316
8427
  card.appendChild(removeChannelButton(s, card));
8428
+ // 通知先の入り切り。**解除と同じ条件で出す**——書き換える先は違う(notify.json)が、
8429
+ // どちらも「チャット/channels.json に登録がある行」でしか効かない
8430
+ card.appendChild(notifyToggle(s, badges));
8317
8431
  }
8318
8432
  // GitHubリポの登録から削除。**ON/OFF のトグルは撤去した(2026-08-21)**
8319
8433
  //(stateToggle のコメント参照)。
@@ -10116,7 +10230,8 @@ ${deriveCapTile.toString()}
10116
10230
  meta.appendChild(traceD);
10117
10231
  var card = el("div", { class: "card" }, [
10118
10232
  meta,
10119
- el("div", { class: "card-title", text: d.title }),
10233
+ // 消したレコードは一覧でも取り消し線(次のビルドまで一覧には残る)
10234
+ el("div", { class: "card-title" + (d.deleted ? " is-deleted" : ""), text: d.title }),
10120
10235
  d.summary ? el("p", { class: "card-summary", text: d.summary }) : null,
10121
10236
  ]);
10122
10237
  card.addEventListener("click", function () { go(d.id); });
@@ -10185,7 +10300,7 @@ ${deriveCapTile.toString()}
10185
10300
  meta.appendChild(traceR);
10186
10301
  var card = el("div", { class: "card" }, [
10187
10302
  meta,
10188
- el("div", { class: "card-title", text: r.title }),
10303
+ el("div", { class: "card-title" + (r.deleted ? " is-deleted" : ""), text: r.title }),
10189
10304
  r.summary ? el("p", { class: "card-summary", text: r.summary }) : null,
10190
10305
  ]);
10191
10306
  card.addEventListener("click", function () { go(r.id); });
@@ -10198,6 +10313,298 @@ ${deriveCapTile.toString()}
10198
10313
  draw();
10199
10314
  }
10200
10315
 
10316
+ /* ---------------------------------------------------------------------------
10317
+ * AIによる最近の変更(.cortex/weekly-curation-report.json 由来)
10318
+ * ---------------------------------------------------------------------------
10319
+ * 週次キュレーションは、重複した記録を勝手に統合し、draft の記録を勝手に精製する。
10320
+ * **黙って書き換えられた記録を作らないための面**がこれ——AIが何をしたかを、
10321
+ * 変えられた記録自身のページに畳んで置く。
10322
+ *
10323
+ * **正直に「直近1回分」と言う。** レポートは毎回全置換なので、ここに出せるのは
10324
+ * 最後に走った週次整理の結果だけ。見出しに実行日を書いて、全期間の履歴を装わない。
10325
+ * 夜間の日次精製はレポートに残らないので、ここには出ない(「週次整理による変更」と
10326
+ * 限定しているので嘘にはならない)。
10327
+ *
10328
+ * **判断待ち(attentions)は出さない。** 受け皿は既に「AIからの更新提案」
10329
+ * (proposalsSection)が持っていて、同じ記録に別々の入口を作らない。
10330
+ *
10331
+ * 🔴 **値はすべて textContent 経由で入れる**(el の text オプション)。
10332
+ * 敗者レコードの全文・引用・理由・タイトルは外部由来の任意文字列で、
10333
+ * innerHTML に渡すと記録の本文がそのまま画面のコードになる。
10334
+ */
10335
+
10336
+ /** そのレコードに対して行われた変更(無ければ空配列) */
10337
+ function aiHistoryActions(rec) {
10338
+ var wc = SITE.weeklyCuration;
10339
+ if (!wc || !wc.actions || !wc.actions.length) return [];
10340
+ var p = repoPathOf(rec);
10341
+ if (!p) return [];
10342
+ return wc.actions.filter(function (act) {
10343
+ // 統合は**勝者のページ**に出す(敗者はファイルごと消えていて、開く先が無い)。
10344
+ // 精製と未知の kind は path で照合する——読めない kind でも
10345
+ // 「この記録に何かをした」ことは出す(読めないまま消えるより、読めないまま出す)
10346
+ if (act.winner && act.loser) return !!act.winner.path && act.winner.path === p;
10347
+ return !!act.path && act.path === p;
10348
+ });
10349
+ }
10350
+ /**
10351
+ * 見出しに出す実行日(M/D)。**読めない値は素のまま出す**——
10352
+ * 「いつの整理か」を言えないより、生成物が書いたままを見せるほうがよい。
10353
+ * Date に通さず ISO の字面(YYYY-MM-DD)から切り出す——閲覧者のタイムゾーンで
10354
+ * 日付がずれると、レポート自身が書いた実行日と食い違う(CI の UTC でも同じずれが出る)
10355
+ */
10356
+ function aiHistoryWhen(iso) {
10357
+ var m = /^(\\d{4})-(\\d{2})-(\\d{2})/.exec(String(iso));
10358
+ if (!m) return String(iso);
10359
+ return parseInt(m[2], 10) + "/" + parseInt(m[3], 10);
10360
+ }
10361
+ // コミットへのリンク。repoBaseUrl が無ければ短縮SHAだけ出す(どの変更かは残る)
10362
+ function ahCommitEl(sha) {
10363
+ if (!sha) return null;
10364
+ var short = sha.length > 8 ? sha.slice(0, 8) : sha;
10365
+ if (!SITE.repoBaseUrl) return el("span", { class: "ah-commit", text: "コミット " + short });
10366
+ return el("a", {
10367
+ class: "ah-commit", href: SITE.repoBaseUrl + "/commit/" + encodeURIComponent(sha),
10368
+ target: "_blank", rel: "noopener", text: "コミット " + short,
10369
+ });
10370
+ }
10371
+ // 精製の差分。同じページの「AIからの更新提案」(proposal-diff)と同じ形にそろえる
10372
+ // ——同じ「前後」を別の見た目で出すと、同じものだと気づけない
10373
+ function ahChangeEl(chg) {
10374
+ return el("div", { class: "ah-change" }, [
10375
+ el("div", { class: "proposal-field", text: chg.field }),
10376
+ el("div", { class: "proposal-diff" }, [
10377
+ el("div", { class: "proposal-row" }, [
10378
+ el("span", { class: "proposal-label", text: "変更前" }),
10379
+ // 空を空欄で表さない(「まだ書かれていない」のか「読めていない」のか分からなくなる)
10380
+ el("span", { class: "proposal-value", text: chg.before || "(未記入)" }),
10381
+ ]),
10382
+ el("div", { class: "proposal-row proposal-row-new" }, [
10383
+ el("span", { class: "proposal-label", text: "変更後" }),
10384
+ el("span", { class: "proposal-value", text: chg.after || "(未記入)" }),
10385
+ ]),
10386
+ ]),
10387
+ ]);
10388
+ }
10389
+ function ahEvidenceEl(list) {
10390
+ var ul = el("ul", { class: "proposal-evidence" });
10391
+ list.forEach(function (evi) {
10392
+ // 引用が無くても出典は出す(公開範囲フィルタで引用だけ落ちることがある)
10393
+ var li = el("li", {}, [el("span", { class: "proposal-source", text: evi.source })]);
10394
+ if (evi.quote) li.appendChild(el("blockquote", { class: "proposal-quote", text: evi.quote }));
10395
+ ul.appendChild(li);
10396
+ });
10397
+ return ul;
10398
+ }
10399
+ // 汎用行(未知の kind)。**捨てずに「名前: 値」で並べる**——engine が先に kind を
10400
+ // 増やしても「何かがあったこと」は画面に残る
10401
+ function ahGenericRow(row) {
10402
+ var box = el("div", { class: "ah-item" }, [el("div", { class: "ah-item-head", text: row.kind })]);
10403
+ var pairs = [["対象", row.title], ["種別", row.type], ["理由", row.reason],
10404
+ ["なぜ", row.why], ["状態", row.status],
10405
+ ["一致度", row.score != null ? String(row.score) : ""]];
10406
+ pairs.forEach(function (kv) {
10407
+ if (!kv[1]) return;
10408
+ box.appendChild(el("div", { class: "ah-line", text: kv[0] + ": " + kv[1] }));
10409
+ });
10410
+ var commit = ahCommitEl(row.commit);
10411
+ if (commit) box.appendChild(el("div", { class: "ah-meta" }, [commit]));
10412
+ return box;
10413
+ }
10414
+ /** AIがこの記録にしたこと1件 */
10415
+ function ahActionEl(act) {
10416
+ if (act.winner && act.loser) {
10417
+ var box = el("div", { class: "ah-item" }, [
10418
+ el("div", { class: "ah-item-head", text: "「" + act.loser.title + "」を統合して取り込んだ" }),
10419
+ ]);
10420
+ if (act.folded && act.folded.length) {
10421
+ box.appendChild(el("div", { class: "ah-line", text: "畳んだ表記: " + act.folded.join("、") }));
10422
+ }
10423
+ if (act.reason) box.appendChild(el("div", { class: "ah-line", text: act.reason }));
10424
+ var commit = ahCommitEl(act.commit);
10425
+ if (commit) box.appendChild(el("div", { class: "ah-meta" }, [commit]));
10426
+ if (act.loser.body) {
10427
+ // **全文は畳んでおく。** 数千字になりうるので開いたまま並べるとページが読めなくなる。
10428
+ // それでも捨てないのは、消えた記録を復元できる形で残すのがこの節の役目だから
10429
+ box.appendChild(el("details", { class: "ah-body" }, [
10430
+ el("summary", { class: "ah-body-summary", text: "取り込んで削除した記録の全文" }),
10431
+ el("pre", { class: "ah-body-text", text: act.loser.body }),
10432
+ ]));
10433
+ }
10434
+ return box;
10435
+ }
10436
+ if (act.changes && act.changes.length) {
10437
+ var refined = el("div", { class: "ah-item" }, [
10438
+ el("div", { class: "ah-item-head", text: "AIがこの記録を精製しました" }),
10439
+ ]);
10440
+ act.changes.forEach(function (chg) { refined.appendChild(ahChangeEl(chg)); });
10441
+ if (act.evidence && act.evidence.length) {
10442
+ refined.appendChild(el("div", { class: "section-label", text: "根拠" }));
10443
+ refined.appendChild(ahEvidenceEl(act.evidence));
10444
+ }
10445
+ var refineCommit = ahCommitEl(act.commit);
10446
+ if (refineCommit) refined.appendChild(el("div", { class: "ah-meta" }, [refineCommit]));
10447
+ return refined;
10448
+ }
10449
+ return ahGenericRow(act);
10450
+ }
10451
+ /** 「AIによる最近の変更」の折りたたみ。該当が0件なら null(節ごと出さない) */
10452
+ function buildAiHistory(rec) {
10453
+ var acts = aiHistoryActions(rec);
10454
+ if (!acts.length) return null;
10455
+ var wc = SITE.weeklyCuration;
10456
+ var when = wc.ranAt ? aiHistoryWhen(wc.ranAt) + " の週次整理" : "週次整理";
10457
+ var det = el("details", { class: "ai-history" }, [
10458
+ el("summary", { class: "ah-summary", text: "AIによる最近の変更(" + acts.length + "件・" + when + ")" }),
10459
+ ]);
10460
+ acts.forEach(function (act) { det.appendChild(ahActionEl(act)); });
10461
+ return det;
10462
+ }
10463
+
10464
+ // ---------- レコードの削除(?op=delete-record) ----------
10465
+ //
10466
+ // AIが起票した記録を「要らない」と判断したとき、**消すだけでは翌晩また現れる**
10467
+ // (夜間抽出は新しいソースから同じ語・同じ人を拾い直す)。削除と同じコミットで
10468
+ // 区画の除外リストへ載せることで、再登録まで止める。
10469
+ //
10470
+ // 出し分けはサーバの DELETABLE_SECTIONS と同じ4区画。ここは「押せないボタンを
10471
+ // 出さない」ためのもので、**守りはサーバ側**(promote と同じ関係)。
10472
+ var DELETABLE_PATH = /^Cortex\\/(Decisions|Glossary|Members|Rules)\\/records\\/[^/]+\\.md$/;
10473
+ // 削除で除外リストにも載る区画。**Decisions は削除だけ**——決定は一度きりの事象で
10474
+ // 同じものが再抽出されることは原則なく、engine 側にも決定の除外の仕組みが無い
10475
+ var DELETE_EXCLUDES = { Glossary: true, Members: true, Rules: true };
10476
+ function deletableRecordPath(rec) {
10477
+ if (!SITE.intake) return "";
10478
+ var p = repoPathOf(rec);
10479
+ if (!p || !DELETABLE_PATH.test(p)) return "";
10480
+ // 区画の規約・雛形はレコードではない(一覧にも出ないが、念のため出し分けでも弾く)
10481
+ var base = p.slice(p.lastIndexOf("/") + 1).toLowerCase();
10482
+ return base === "readme.md" || base === "template.md" ? "" : p;
10483
+ }
10484
+ /** 削除で除外リストに載る区画なら、その区画の表示名。載らない区画(Decisions)は空文字 */
10485
+ function deleteExclusionLabel(p) {
10486
+ var m = /^Cortex\\/([^/]+)\\//.exec(p);
10487
+ var dir = m ? m[1] : "";
10488
+ return Object.prototype.hasOwnProperty.call(DELETE_EXCLUDES, dir) ? dirLabel(dir) : "";
10489
+ }
10490
+ /**
10491
+ * 確認の文面。**何が起きるかを具体に書く**(消えるだけなのか、除外リストにも載るのか)。
10492
+ *
10493
+ * 🔴 **status による重み付けはここ1箇所に閉じる。** 2026-08-28 のオーナー裁定は
10494
+ * 「確認済み(active)の記録も消せる。ただし確認を一段重くする」。裁定が
10495
+ * 「active は消せない」へ振れたときに直すのは、この関数と出し分けだけで済ませる。
10496
+ */
10497
+ function deleteConfirmLines(rec, path) {
10498
+ var lines = ["このレコードを削除しますか?", rec.title];
10499
+ var label = deleteExclusionLabel(path);
10500
+ if (label) {
10501
+ lines.push("削除すると、夜間のAIが同じ内容を再登録しないよう、" + label + "の除外リストにも追記されます。");
10502
+ }
10503
+ if (rec.rstatus === "active") lines.push("これは人が確認済みにした記録です。");
10504
+ return lines;
10505
+ }
10506
+ async function sendDeleteRecord(path, msg, btn, onOk) {
10507
+ var intake = SITE.intake;
10508
+ if (!intake) return;
10509
+ function setError(text) {
10510
+ msg.className = "record-delete-msg is-error";
10511
+ msg.textContent = text;
10512
+ }
10513
+ // 押している間はボタンごと語を差し替える(#222・beginBusy の項)
10514
+ var endBusy = beginBusy(btn, "削除中…");
10515
+ msg.className = "record-delete-msg";
10516
+ msg.textContent = "";
10517
+ var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
10518
+ var res, j;
10519
+ try {
10520
+ res = await fetch(pfx + "op=delete-record&t=" + encodeURIComponent(intake.token), {
10521
+ method: "POST",
10522
+ headers: { "content-type": "application/json" },
10523
+ // 名乗りは確認済み昇格で覚えたものを流用する(削除のためだけに名前を聞かない)
10524
+ body: JSON.stringify({ projectKey: intake.projectKey, path: path, submitter: rememberedVerifier() }),
10525
+ });
10526
+ j = await res.json().catch(function () { return {}; });
10527
+ } catch (e) {
10528
+ endBusy();
10529
+ setError("✗ サーバーに接続できませんでした。通信環境を確認して、もう一度お試しください。");
10530
+ return;
10531
+ }
10532
+ // 🔴 **「既に削除されています」は成功として扱う。** この画面のデータは静的ビルド
10533
+ // 時点のもので、誰かが先に消していれば古いまま残って見える。押した人が望んだ状態には
10534
+ // **なっている**ので、失敗として見せると「押しても消えない」堂々巡りになる。
10535
+ // **符号で見る**(promote の ALREADY_ACTIVE と同じ理由)
10536
+ if (res.ok || (res.status === 409 && j.code === "ALREADY_DELETED")) {
10537
+ endBusy();
10538
+ onOk();
10539
+ return;
10540
+ }
10541
+ endBusy();
10542
+ setError("✗ " + (j.message || "削除できませんでした。時間をおいて再度お試しください。"));
10543
+ }
10544
+ /** 押せなくする(リンクは href ごと外す——見た目だけ薄くしても押せてしまう) */
10545
+ function disableDetailLink(a) {
10546
+ if (!a) return;
10547
+ a.removeAttribute("href");
10548
+ a.setAttribute("aria-disabled", "true");
10549
+ a.classList.add("is-disabled");
10550
+ }
10551
+ /**
10552
+ * 押した結果を画面に映す。
10553
+ *
10554
+ * **次のビルドまで SiteData は古いまま。** 削除の commit 自体が Amplify の再ビルドを
10555
+ * 誘発するので数分で追いつくが、その間も押した人には済んだものとして見せる。
10556
+ * **元データに印を持たせる**ので、一覧へ戻って開き直しても消えたままに見える。
10557
+ */
10558
+ function markRecordDeleted(rec) {
10559
+ rec.deleted = true;
10560
+ var title = app.querySelector(".detail-title");
10561
+ if (title) title.classList.add("is-deleted");
10562
+ var btn = app.querySelector(".record-delete-btn");
10563
+ if (btn) { btn.disabled = true; btn.textContent = "削除しました"; }
10564
+ // もう無い記録を直す口は押させない(AIで編集・GitHubで編集・確認済みにする)
10565
+ var links = app.querySelectorAll(".detail-actions a.ai-edit-btn, .detail-actions a.edit-btn");
10566
+ for (var i = 0; i < links.length; i++) disableDetailLink(links[i]);
10567
+ var promote = app.querySelector(".draft-note .promote-btn");
10568
+ 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
+ }
10576
+ }
10577
+ /**
10578
+ * 詳細ページ最下部の削除の口。**上の操作列には並べない**(分類B・2026-08-28)——
10579
+ * 高頻度の操作(AIで編集・GitHubで編集)の隣に、取り消せない操作を置かない。
10580
+ */
10581
+ function buildDeleteSection(rec) {
10582
+ var path = deletableRecordPath(rec);
10583
+ if (!path) return null;
10584
+ var wrap = el("div", { class: "record-delete" });
10585
+ var btn = el("button", { class: "record-delete-btn", type: "button", text: "このレコードを削除…" });
10586
+ var msg = el("div", { class: "record-delete-msg" });
10587
+ btn.addEventListener("click", async function () {
10588
+ // **確認を await する。** 落とすと Promise が truthy で確認なしに消える
10589
+ if (!(await confirmDanger(deleteConfirmLines(rec, path), { okLabel: "削除する", returnTo: btn }))) return;
10590
+ await sendDeleteRecord(path, msg, btn, function () { markRecordDeleted(rec); });
10591
+ });
10592
+ wrap.appendChild(btn);
10593
+ wrap.appendChild(msg);
10594
+ return wrap;
10595
+ }
10596
+ /** 詳細ページの末尾(本文・提案・frontmatter の後)に、履歴と削除の口を並べる */
10597
+ function appendRecordFooter(rec) {
10598
+ var history = buildAiHistory(rec);
10599
+ if (history) app.appendChild(history);
10600
+ var fmp = renderFmPanel(rec.fm);
10601
+ if (fmp) app.appendChild(fmp);
10602
+ var del = buildDeleteSection(rec);
10603
+ if (del) app.appendChild(del);
10604
+ // 開き直しても削除済みのまま見せる(印は rec が持っている)
10605
+ if (rec.deleted) markRecordDeleted(rec);
10606
+ }
10607
+
10201
10608
  // ---------- 汎用詳細 ----------
10202
10609
  function renderGenericDetail(r) {
10203
10610
  app.innerHTML = "";
@@ -10245,17 +10652,22 @@ ${deriveCapTile.toString()}
10245
10652
  app.appendChild(genBody);
10246
10653
  loadBody(r, genBody);
10247
10654
 
10248
- var fmp = renderFmPanel(r.fm);
10249
- if (fmp) app.appendChild(fmp);
10655
+ appendRecordFooter(r);
10250
10656
  }
10251
10657
 
10252
10658
  // ---------- グラフ(force-directed / canvas) ----------
10253
10659
  // Gold層(Cortexのレコード)だけ色付け。それ以外(課題・議事録・資料・外部リンク)は一律グレー
10660
+ /* 濃紺の背景に小さな点で出るので、Gold 4区画+Home は明るい色で互いに離す
10661
+ * (決定事項=青・用語集=緑・メンバー=ピンク・ルール=オレンジ・Home=黄は
10662
+ * 2026-08-28 のオーナー裁定と分類B。レポートは黄が Home と衝突するためシアンへ)。
10663
+ * 凡例の表記は日本語に統一する(区画名は画面のタブ名と同じ言葉を使う) */
10254
10664
  var GROUP_STYLE = {
10255
- decision: { color: "#60a5fa", label: "Decision" },
10256
- term: { color: "#4ade80", label: "用語" },
10257
- report: { color: "#facc15", label: "レポート" },
10258
- overview: { color: "#e879f9", label: "Home" },
10665
+ decision: { color: "#60a5fa", label: "決定事項" },
10666
+ term: { color: "#4ade80", label: "用語集" },
10667
+ member: { color: "#f472b6", label: "メンバー" },
10668
+ rule: { color: "#fb923c", label: "ルール" },
10669
+ report: { color: "#22d3ee", label: "レポート" },
10670
+ overview: { color: "#fde047", label: "Home" },
10259
10671
  record: { color: "#0e7490", label: "その他レコード" },
10260
10672
  silver: { color: "#9ca3af", label: "コンテキスト(課題・議事録・資料・リンク等)" },
10261
10673
  bronze: { color: "#9ca3af", label: "コンテキスト(課題・議事録・資料・リンク等)" },
@@ -10933,8 +11345,7 @@ ${deriveCapTile.toString()}
10933
11345
  app.appendChild(ul);
10934
11346
  }
10935
11347
 
10936
- var fmp = renderFmPanel(d.fm);
10937
- if (fmp) app.appendChild(fmp);
11348
+ appendRecordFooter(d);
10938
11349
  }
10939
11350
 
10940
11351
  function renderNotFound(id) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.11",
3
+ "version": "0.12.13",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,