@takagaki/cortex-decisions-viewer 0.12.14 → 0.12.15

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,7 +38,6 @@ 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;
42
41
  exports.buildHomeGrounding = buildHomeGrounding;
43
42
  exports.collectMeetings = collectMeetings;
44
43
  exports.findBacklogDir = findBacklogDir;
@@ -520,276 +519,6 @@ async function readGoldProposals(repoRoot) {
520
519
  }
521
520
  return byPath;
522
521
  }
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
- }
793
522
  // ---- Home(案件の自己記述)--------------------------------------------------
794
523
  //
795
524
  // Home.md は「案件そのものの説明」に絞る再構成の途中にある(engine 側で進行中)。
@@ -1893,8 +1622,11 @@ async function build(opts) {
1893
1622
  // すでにある会議(投入モーダルの「会議名」の選択肢)。intake の有無に関わらず載せる——
1894
1623
  // 出し分けは表示側の仕事で、ここで絞ると「投入の口を後から付けたら選択肢が空」になる
1895
1624
  const meetings = await collectMeetings(repoRoot);
1896
- // 週次整理レポート。無ければ null で、操作タブの節を出さない
1897
- const weeklyCuration = await readWeeklyCuration(repoRoot);
1625
+ // AIによるレコードの変更履歴を git から取る。**レコード数に関わらず git は2回だけ呼ぶ**。
1626
+ // git が無い・浅い clone・該当0件なら空で、履歴の節が出ないだけ(他の表示は壊れない)。
1627
+ // 走るのは latestGeneratedPaths と同じ **リポジトリルート**(process.cwd())——
1628
+ // Decisions ディレクトリで実行すると pathspec に一致せず常に空になる
1629
+ const aiHistory = (0, repo_1.collectAiChangeHistory)(process.cwd());
1898
1630
  const site = {
1899
1631
  title: opts.title,
1900
1632
  generatedAt: opts.generatedAt,
@@ -1939,7 +1671,8 @@ async function build(opts) {
1939
1671
  backlogTarget: fleet.backlogTarget,
1940
1672
  // null(無い・壊れ・version違い)は undefined へ落とす——SiteData の
1941
1673
  // 「知らないことはキーが無いことで表す」に合わせる(空の器を通さない)
1942
- weeklyCuration: weeklyCuration ?? undefined,
1674
+ // 空の器は通さない(0件なら undefined = キーごと出さない)
1675
+ aiHistory: Object.keys(aiHistory).length ? aiHistory : undefined,
1943
1676
  };
1944
1677
  // サムネイル(デザイン画面等)をサイトに同梱し、ノードのパスをサイト内相対に書き換える
1945
1678
  await node_fs_1.promises.mkdir(outAbs, { recursive: true });
package/dist/render.js CHANGED
@@ -2114,10 +2114,11 @@ a:hover { text-decoration: underline; }
2114
2114
  padding-bottom: 6px; border-bottom: 1px solid var(--border);
2115
2115
  }
2116
2116
  .home-grounding > .home-grounding-title:first-child { margin-top: 0; }
2117
- /* ---- レコード詳細の「AIによる最近の変更」(.cortex/weekly-curation-report.json 由来)----
2118
- 操作タブの「週次整理」節(0.12.12・.weekly-curation)を撤去し、同じデータを
2119
- 関係するレコードのページへ寄せた。カードは詳細ページの提案(.proposals)と
2120
- 同系色にする——どちらも「AIがやったこと・言っていることを読む」もので種類が同じ */
2117
+ /* ---- レコード詳細の「AIによる最近の変更」(リポジトリの git 履歴 由来)----
2118
+ カードは詳細ページの提案(.proposals)と同系色にする——どちらも
2119
+ 「AIがやったこと・言っていることを読む」もので種類が同じ。
2120
+ 出どころは 0.12.15 で週次整理レポートから git 履歴に変えた(レポートは毎回全置換で、
2121
+ 翌週の実行で前の履歴が消えたため) */
2121
2122
  .ai-history { margin-top: 28px; padding-top: 16px; border-top: 1px solid var(--border); }
2122
2123
  .ah-summary { cursor: pointer; font-size: 13.5px; font-weight: 700; color: var(--muted); }
2123
2124
  .ah-item {
@@ -2128,12 +2129,14 @@ a:hover { text-decoration: underline; }
2128
2129
  .ah-line { font-size: 13px; color: var(--muted); line-height: 1.8; word-break: break-word; }
2129
2130
  .ah-meta { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-top: 10px; font-size: 13px; }
2130
2131
  .ah-commit { font-size: 12.5px; color: var(--muted); font-variant-numeric: tabular-nums; }
2131
- .ah-change { border-top: 1px solid #ddd6fe; padding-top: 10px; margin-top: 10px; }
2132
- .ah-change:first-of-type { border-top: 0; padding-top: 0; }
2133
- .ah-body { margin-top: 10px; font-size: 13px; }
2134
- .ah-body-summary { cursor: pointer; color: var(--muted); }
2135
- /* 全文は数千字になりうる。折りたたみの中でも高さを縛って、開いたときにページ全体を押し流さない */
2136
- .ah-body-text {
2132
+ /* 変わった行のかたまり(消えた行/増えた行)。**ファイル全体の前後ではない**ので、
2133
+ ラベルを必ず添える(.ah-diff-label) */
2134
+ .ah-diff { margin-top: 10px; }
2135
+ .ah-diff-label { font-size: 12.5px; font-weight: 700; color: var(--muted); margin-bottom: 4px; }
2136
+ /* 何を見せていて何を見せられないのかの断り。節の末尾に置く */
2137
+ .ah-source { font-size: 12px; color: var(--muted); line-height: 1.7; margin: 12px 0 0; }
2138
+ /* 差分は数千字になりうる。折りたたみの中でも高さを縛って、開いたときにページ全体を押し流さない */
2139
+ .ah-diff-text {
2137
2140
  margin: 8px 0 0; padding: 10px 12px; max-height: 320px; overflow: auto;
2138
2141
  background: #fff; border: 1px solid var(--border); border-radius: 8px;
2139
2142
  font-size: 12.5px; line-height: 1.7; white-space: pre-wrap; word-break: break-word;
@@ -2141,16 +2144,24 @@ a:hover { text-decoration: underline; }
2141
2144
  /* ---- レコードの削除(?op=delete-record)----
2142
2145
  置き場は2つ(2026-08-28 のオーナー裁定)。**未確認の記録は警告帯の中**で
2143
2146
  「確認済みにする」と二択に、**警告帯が出ない記録は上の操作列の末尾**に置く。
2144
- 取り消せない操作なので、どちらでも枠を持たない控えめな文字のボタンにして、
2145
- 隣の実体のあるボタンと重さを分ける。押したあとの見た目は「取り消し線+無効化」 */
2147
+
2148
+ **2026-08-29 のオーナーFBで、控えめな文字ボタンから同格のボタンへ変えた。**
2149
+ 下線だけの文字は隣の「確認済みにする」より一段下に見え、二択のうち片方だけが
2150
+ 本物のボタンに見えていた——選ばせたいのは対等な二択なので、寸法をそろえる。
2151
+ **配色は背景白・赤文字**(取り消せない操作であることは、塗りではなく赤で示す。
2152
+ 帯と同じ濃さで塗ると「押してほしいほう」に見える)。
2153
+ なお、このビューアに**ダークテーマは無い**(:root の1組だけで prefers-color-scheme を
2154
+ 使っていない)ので、暗色側の指定は置かない。押したあとの見た目は「取り消し線+無効化」 */
2146
2155
  .record-delete-btn {
2147
- background: none; border: 0; padding: 0; cursor: pointer; white-space: nowrap;
2148
- font-size: 12.5px; color: var(--muted); text-decoration: underline;
2156
+ flex: 0 0 auto; cursor: pointer; white-space: nowrap;
2157
+ padding: 8px 14px; border-radius: var(--radius);
2158
+ border: 1px solid #fca5a5; background: #fff; color: #b91c1c;
2159
+ font-size: 13.5px; font-weight: 600;
2149
2160
  }
2150
- /* 警告帯の中では帯と同じ系統の色に寄せる(帯の外では既定の控えめな灰) */
2151
- .draft-note .record-delete-btn { color: #92400e; }
2152
- .record-delete-btn:hover { color: #b91c1c; }
2153
- .record-delete-btn:disabled { cursor: default; text-decoration: none; opacity: 0.7; }
2161
+ /* 警告帯の中では「確認済みにする」(.promote-btn)と寸法・丸みをそろえ、二択を同格に見せる */
2162
+ .draft-note .record-delete-btn { padding: 6px 14px; border-radius: 999px; font-size: 13px; }
2163
+ .record-delete-btn:hover { background: #fef2f2; border-color: #b91c1c; }
2164
+ .record-delete-btn:disabled { cursor: default; opacity: 0.6; }
2154
2165
  .record-delete-msg { font-size: 13px; }
2155
2166
  .record-delete-msg:empty { display: none; }
2156
2167
  .record-delete-msg.is-error { color: #b91c1c; }
@@ -2690,6 +2701,14 @@ a.ds-name:hover { text-decoration: underline; }
2690
2701
  /* 取り込みの条件は箇条書きで出す(ANDであることを読み飛ばせないように) */
2691
2702
  .sec-how-list { margin: 4px 0; padding-left: 1.3em; }
2692
2703
  .sec-how-list li { margin: 2px 0; }
2704
+ /* 会議の主催者向けの選択肢(Meet Recordings フォルダの共有・2026-08-29 オーナー承認)。
2705
+ **折りたたまない。** 週次整理の節が「誰も見ない」で撤去された経緯があるので、
2706
+ 知ってほしい選択肢を <details> の中に隠さない。従来の bot 招待の説明の下に、
2707
+ 枠で囲った別の話として置く(差し替えではなく追加であることを見た目でも示す) */
2708
+ .mt-share { border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; margin: 8px 0 0; background: #fff; }
2709
+ .mt-share-title { font-size: 12.5px; font-weight: 700; color: var(--text); margin-bottom: 4px; }
2710
+ /* 共有すると何が起きるかの1行。**説明文より強く出す**(.ds-scope-note と同じ理由) */
2711
+ .mt-share-note { color: var(--text); margin-top: 6px; }
2693
2712
  /* 0件を黙って見せない(追加フォームだけだと「未登録」か「読めていない」か区別できない) */
2694
2713
  .ds-empty { color: var(--muted); font-size: 12.5px; margin: 4px 0 10px; }
2695
2714
  .sec-input { width: 100%; box-sizing: border-box; font-size: 13px; padding: 7px 10px; margin-bottom: 8px; border: 1px solid var(--border); border-radius: 6px; background: #fff; color: var(--text); }
@@ -4103,9 +4122,35 @@ ${deriveCapTile.toString()}
4103
4122
  ul.appendChild(el("li", { text: "会議名に上の目印が入っている" }));
4104
4123
  wrap.appendChild(ul);
4105
4124
  wrap.appendChild(el("p", { text: "どちらか一方だけでは取り込まれません。" }));
4125
+ wrap.appendChild(meetingFolderShare());
4106
4126
  return wrap;
4107
4127
  }
4108
4128
 
4129
+ /**
4130
+ * 主催者向けの選択肢: 「Meet Recordings」フォルダを1回共有すると、以後は招待が要らない。
4131
+ *
4132
+ * **上の bot 招待の説明は残したまま足す(2026-08-29 オーナー承認)。** 招待方式は
4133
+ * 参加するだけの会議・他社主催の会議で今も唯一の手段なので、置き換えではない。
4134
+ *
4135
+ * **手順の細目は書かない。** 正本は apps-script/docs/meet-recordings-sharing.md で、
4136
+ * 「マイドライブの直下しか見ない」「メモを移すと対象外になる」等はそちらが持つ。
4137
+ * 画面が担うのは**選択肢があることと、共有すると何が起きるかの1行**まで
4138
+ *(同じ説明を2箇所で維持すると、必ず片方が古くなる)。
4139
+ *
4140
+ * 🔴 **注意の1行を落とさない。** フォルダごと共有するので、目印の無い会議
4141
+ *(1on1・面談)のメモも bot から**見える状態にはなる**。取り込まれないことと
4142
+ * 見えないことは別なので、両方を1行で正直に書く。
4143
+ */
4144
+ // 注: ここは埋め込みJSのテンプレートリテラル内。コメントでもバッククォートを使わない
4145
+ function meetingFolderShare() {
4146
+ var box = el("div", { class: "mt-share" });
4147
+ box.appendChild(el("div", { class: "mt-share-title", text: "毎回の招待をやめる方法(会議の主催者向け・任意)" }));
4148
+ box.appendChild(el("p", { text: "自分の Google ドライブにある「Meet Recordings」フォルダを、下のアカウントに「閲覧者」で1回だけ共有すると、以後あなたが主催する会議は招待が要らなくなります。会議名の目印はこれまでどおり必要です。" }));
4149
+ box.appendChild(copyRow(DRIVE_INGEST_ACCOUNT));
4150
+ box.appendChild(el("p", { class: "mt-share-note", text: "共有すると、そのフォルダに入る会議メモは bot から見える状態になります(実際に取り込まれるのは、会議名に目印が入っている会議だけです)。" }));
4151
+ return box;
4152
+ }
4153
+
4109
4154
  /**
4110
4155
  * Slack の承認画面へ進むボタン(2026-08-20 に接続カードから切り出した)。
4111
4156
  *
@@ -10341,49 +10386,47 @@ ${deriveCapTile.toString()}
10341
10386
  }
10342
10387
 
10343
10388
  /* ---------------------------------------------------------------------------
10344
- * AIによる最近の変更(.cortex/weekly-curation-report.json 由来)
10389
+ * AIによる最近の変更(リポジトリの git 履歴 由来)
10345
10390
  * ---------------------------------------------------------------------------
10346
- * 週次キュレーションは、重複した記録を勝手に統合し、draft の記録を勝手に精製する。
10391
+ * 夜間のパイプラインは、記録を勝手に起票し、draft の記録を勝手に書き換える。
10347
10392
  * **黙って書き換えられた記録を作らないための面**がこれ——AIが何をしたかを、
10348
10393
  * 変えられた記録自身のページに畳んで置く。
10349
10394
  *
10350
- * **正直に「直近1回分」と言う。** レポートは毎回全置換なので、ここに出せるのは
10351
- * 最後に走った週次整理の結果だけ。見出しに実行日を書いて、全期間の履歴を装わない。
10352
- * 夜間の日次精製はレポートに残らないので、ここには出ない(「週次整理による変更」と
10353
- * 限定しているので嘘にはならない)。
10395
+ * 🔴 **出どころを週次整理レポートから git 履歴に変えた(0.12.15)。**
10396
+ * レポート(.cortex/weekly-curation-report.json)は毎回全置換で、翌週の実行で
10397
+ * 差し替わると**前の履歴が画面から消えた**——オーナーが実際に踏んだ(前日に見た変更が
10398
+ * 翌日には無くなっていた)。1週間で消える履歴は、この節の目的を果たさない。
10399
+ * git のコミットは消えないので、ビルド時にそこから作る(repo.ts の collectAiChangeHistory)。
10400
+ *
10401
+ * **取れないものは取れないと言う。** git には「なぜそう直したか」の根拠(引用)が
10402
+ * 残らないので、出せるのは**変わった行・コミットメッセージ・日時**まで。
10403
+ * 節の末尾でそれを1行断る(根拠があるように見せない)。
10354
10404
  *
10355
- * **判断待ち(attentions)は出さない。** 受け皿は既に「AIからの更新提案」
10356
- * (proposalsSection)が持っていて、同じ記録に別々の入口を作らない。
10405
+ * **人のコミットは混ぜない。** 受け口の Lambda は人の操作の代理でコミットするので
10406
+ *(「Gold確認: …」「レコード削除: …」)、bot 名義だけで拾うと人の操作がAIの仕業として
10407
+ * 出る。切り分けは repo.ts の isAiCommit(ビルド時)。
10357
10408
  *
10358
10409
  * 🔴 **値はすべて textContent 経由で入れる**(el の text オプション)。
10359
- * 敗者レコードの全文・引用・理由・タイトルは外部由来の任意文字列で、
10360
- * innerHTML に渡すと記録の本文がそのまま画面のコードになる。
10410
+ * 差分の中身・コミットメッセージは**リポジトリの生テキスト**で、記録の本文がそのまま
10411
+ * 入る。innerHTML に渡すと記録の中身が画面のコードになる。
10361
10412
  */
10362
10413
 
10363
- /** そのレコードに対して行われた変更(無ければ空配列) */
10364
- function aiHistoryActions(rec) {
10365
- var wc = SITE.weeklyCuration;
10366
- if (!wc || !wc.actions || !wc.actions.length) return [];
10414
+ /** そのレコードに対するAIの変更(新しい順・ビルド側で最大5件に絞ってある) */
10415
+ function aiHistoryEntries(rec) {
10416
+ var map = SITE.aiHistory;
10417
+ if (!map) return [];
10367
10418
  var p = repoPathOf(rec);
10368
10419
  if (!p) return [];
10369
- return wc.actions.filter(function (act) {
10370
- // 統合は**勝者のページ**に出す(敗者はファイルごと消えていて、開く先が無い)。
10371
- // 精製と未知の kind は path で照合する——読めない kind でも
10372
- // 「この記録に何かをした」ことは出す(読めないまま消えるより、読めないまま出す)
10373
- if (act.winner && act.loser) return !!act.winner.path && act.winner.path === p;
10374
- return !!act.path && act.path === p;
10375
- });
10420
+ return map[p] || [];
10376
10421
  }
10377
10422
  /**
10378
- * 見出しに出す実行日(M/D)。**読めない値は素のまま出す**——
10379
- * 「いつの整理か」を言えないより、生成物が書いたままを見せるほうがよい。
10380
- * Date に通さず ISO の字面(YYYY-MM-DD)から切り出す——閲覧者のタイムゾーンで
10381
- * 日付がずれると、レポート自身が書いた実行日と食い違う(CI の UTC でも同じずれが出る)
10423
+ * 見出しに出す日付(YYYY/M/D)。**Date に通さない**——閲覧者のタイムゾーンで日付がずれると、
10424
+ * コミット自身が持っている日付と食い違う。読めない値は素のまま出す
10382
10425
  */
10383
- function aiHistoryWhen(iso) {
10426
+ function ahDate(iso) {
10384
10427
  var m = /^(\\d{4})-(\\d{2})-(\\d{2})/.exec(String(iso));
10385
10428
  if (!m) return String(iso);
10386
- return parseInt(m[2], 10) + "/" + parseInt(m[3], 10);
10429
+ return m[1] + "/" + parseInt(m[2], 10) + "/" + parseInt(m[3], 10);
10387
10430
  }
10388
10431
  // コミットへのリンク。repoBaseUrl が無ければ短縮SHAだけ出す(どの変更かは残る)
10389
10432
  function ahCommitEl(sha) {
@@ -10395,96 +10438,53 @@ ${deriveCapTile.toString()}
10395
10438
  target: "_blank", rel: "noopener", text: "コミット " + short,
10396
10439
  });
10397
10440
  }
10398
- // 精製の差分。同じページの「AIからの更新提案」(proposal-diff)と同じ形にそろえる
10399
- // ——同じ「前後」を別の見た目で出すと、同じものだと気づけない
10400
- function ahChangeEl(chg) {
10401
- return el("div", { class: "ah-change" }, [
10402
- el("div", { class: "proposal-field", text: chg.field }),
10403
- el("div", { class: "proposal-diff" }, [
10404
- el("div", { class: "proposal-row" }, [
10405
- el("span", { class: "proposal-label", text: "変更前" }),
10406
- // 空を空欄で表さない(「まだ書かれていない」のか「読めていない」のか分からなくなる)
10407
- el("span", { class: "proposal-value", text: chg.before || "(未記入)" }),
10408
- ]),
10409
- el("div", { class: "proposal-row proposal-row-new" }, [
10410
- el("span", { class: "proposal-label", text: "変更後" }),
10411
- el("span", { class: "proposal-value", text: chg.after || "(未記入)" }),
10412
- ]),
10413
- ]),
10441
+ /**
10442
+ * 変わった行のかたまり。**「消えた行/増えた行」と書く。**
10443
+ * 出しているのは差分(-U0)であってファイル全体の前後ではないので、
10444
+ * 「変更前」だけだと**記録の全文が載っていると誤読される**
10445
+ */
10446
+ function ahDiffEl(label, text) {
10447
+ return el("div", { class: "ah-diff" }, [
10448
+ el("div", { class: "ah-diff-label", text: label }),
10449
+ el("pre", { class: "ah-diff-text", text: text }),
10414
10450
  ]);
10415
10451
  }
10416
- function ahEvidenceEl(list) {
10417
- var ul = el("ul", { class: "proposal-evidence" });
10418
- list.forEach(function (evi) {
10419
- // 引用が無くても出典は出す(公開範囲フィルタで引用だけ落ちることがある)
10420
- var li = el("li", {}, [el("span", { class: "proposal-source", text: evi.source })]);
10421
- if (evi.quote) li.appendChild(el("blockquote", { class: "proposal-quote", text: evi.quote }));
10422
- ul.appendChild(li);
10423
- });
10424
- return ul;
10425
- }
10426
- // 汎用行(未知の kind)。**捨てずに「名前: 値」で並べる**——engine が先に kind を
10427
- // 増やしても「何かがあったこと」は画面に残る
10428
- function ahGenericRow(row) {
10429
- var box = el("div", { class: "ah-item" }, [el("div", { class: "ah-item-head", text: row.kind })]);
10430
- var pairs = [["対象", row.title], ["種別", row.type], ["理由", row.reason],
10431
- ["なぜ", row.why], ["状態", row.status],
10432
- ["一致度", row.score != null ? String(row.score) : ""]];
10433
- pairs.forEach(function (kv) {
10434
- if (!kv[1]) return;
10435
- box.appendChild(el("div", { class: "ah-line", text: kv[0] + ": " + kv[1] }));
10436
- });
10437
- var commit = ahCommitEl(row.commit);
10438
- if (commit) box.appendChild(el("div", { class: "ah-meta" }, [commit]));
10439
- return box;
10440
- }
10441
10452
  /** AIがこの記録にしたこと1件 */
10442
- function ahActionEl(act) {
10443
- if (act.winner && act.loser) {
10444
- var box = el("div", { class: "ah-item" }, [
10445
- el("div", { class: "ah-item-head", text: "「" + act.loser.title + "」を統合して取り込んだ" }),
10446
- ]);
10447
- if (act.folded && act.folded.length) {
10448
- box.appendChild(el("div", { class: "ah-line", text: "畳んだ表記: " + act.folded.join("、") }));
10449
- }
10450
- if (act.reason) box.appendChild(el("div", { class: "ah-line", text: act.reason }));
10451
- var commit = ahCommitEl(act.commit);
10452
- if (commit) box.appendChild(el("div", { class: "ah-meta" }, [commit]));
10453
- if (act.loser.body) {
10454
- // **全文は畳んでおく。** 数千字になりうるので開いたまま並べるとページが読めなくなる。
10455
- // それでも捨てないのは、消えた記録を復元できる形で残すのがこの節の役目だから
10456
- box.appendChild(el("details", { class: "ah-body" }, [
10457
- el("summary", { class: "ah-body-summary", text: "取り込んで削除した記録の全文" }),
10458
- el("pre", { class: "ah-body-text", text: act.loser.body }),
10459
- ]));
10453
+ function ahEntryEl(e) {
10454
+ var what = e.kind === "add" ? "AIがこの記録を作成しました" : "AIがこの記録を書き換えました";
10455
+ var box = el("div", { class: "ah-item" }, [
10456
+ el("div", { class: "ah-item-head", text: ahDate(e.at) + " " + what }),
10457
+ ]);
10458
+ if (e.subject) box.appendChild(el("div", { class: "ah-line", text: e.subject }));
10459
+ if (e.kind !== "add") {
10460
+ if (e.before) box.appendChild(ahDiffEl("変更前(消えた行)", e.before));
10461
+ if (e.after) box.appendChild(ahDiffEl("変更後(増えた行)", e.after));
10462
+ // **空欄で済ませない。** 差分を取り出せなかった(浅い clone・巨大な差分)のか、
10463
+ // 何も変わっていないのかを、読む人が区別できないままにしない
10464
+ if (!e.before && !e.after) {
10465
+ box.appendChild(el("div", { class: "ah-line", text: "変わった行をここに出せませんでした。コミットを開くと中身を確認できます。" }));
10460
10466
  }
10461
- return box;
10462
- }
10463
- if (act.changes && act.changes.length) {
10464
- var refined = el("div", { class: "ah-item" }, [
10465
- el("div", { class: "ah-item-head", text: "AIがこの記録を精製しました" }),
10466
- ]);
10467
- act.changes.forEach(function (chg) { refined.appendChild(ahChangeEl(chg)); });
10468
- if (act.evidence && act.evidence.length) {
10469
- refined.appendChild(el("div", { class: "section-label", text: "根拠" }));
10470
- refined.appendChild(ahEvidenceEl(act.evidence));
10467
+ if (e.truncated) {
10468
+ box.appendChild(el("div", { class: "ah-line", text: "※ 長いので途中までを出しています。全部はコミットで見られます。" }));
10471
10469
  }
10472
- var refineCommit = ahCommitEl(act.commit);
10473
- if (refineCommit) refined.appendChild(el("div", { class: "ah-meta" }, [refineCommit]));
10474
- return refined;
10475
10470
  }
10476
- return ahGenericRow(act);
10471
+ var commit = ahCommitEl(e.commit);
10472
+ if (commit) box.appendChild(el("div", { class: "ah-meta" }, [commit]));
10473
+ return box;
10477
10474
  }
10478
10475
  /** 「AIによる最近の変更」の折りたたみ。該当が0件なら null(節ごと出さない) */
10479
10476
  function buildAiHistory(rec) {
10480
- var acts = aiHistoryActions(rec);
10481
- if (!acts.length) return null;
10482
- var wc = SITE.weeklyCuration;
10483
- var when = wc.ranAt ? aiHistoryWhen(wc.ranAt) + " の週次整理" : "週次整理";
10477
+ var list = aiHistoryEntries(rec);
10478
+ if (!list.length) return null;
10484
10479
  var det = el("details", { class: "ai-history" }, [
10485
- el("summary", { class: "ah-summary", text: "AIによる最近の変更(" + acts.length + "件・" + when + ")" }),
10480
+ el("summary", { class: "ah-summary", text: "AIによる最近の変更(" + list.length + "件)" }),
10486
10481
  ]);
10487
- acts.forEach(function (act) { det.appendChild(ahActionEl(act)); });
10482
+ list.forEach(function (e) { det.appendChild(ahEntryEl(e)); });
10483
+ // 🔴 何を見せていて、何を見せられないのかを最後に断る
10484
+ det.appendChild(el("p", {
10485
+ class: "ah-source",
10486
+ text: "この一覧はリポジトリの変更履歴(コミット)から作っています。AIがなぜそう直したかの根拠は履歴に残らないため、変わった行とコミットだけを出しています。件数が多いときは新しいほうから5件までです。",
10487
+ }));
10488
10488
  return det;
10489
10489
  }
10490
10490
 
@@ -10632,9 +10632,10 @@ ${deriveCapTile.toString()}
10632
10632
  if (humanConfirmedStatus(rec)) return null;
10633
10633
  var path = deletableRecordPath(rec);
10634
10634
  if (!path) return null;
10635
- // 「…」は**押すと確認が出る**合図(「このレコードを削除…」から引き継いだ慣習)。
10636
- // 隣の「確認済みにする」は押した先で名前入力があるので、二択の重さの違いを語尾で示す
10637
- var btn = el("button", { class: "record-delete-btn", type: "button", text: "削除…" });
10635
+ // **語尾の「…」は 2026-08-29 のオーナーFBで外した。** 二択の相方(「確認済みにする」)と
10636
+ // 語の形が揃わず、片方だけが下書きのように見えていた。押した先に確認モーダルが必ず出ることは
10637
+ // 語尾ではなく操作そのもの(confirmDanger await する下の行)で担保している
10638
+ var btn = el("button", { class: "record-delete-btn", type: "button", text: "削除する" });
10638
10639
  var msg = el("div", { class: "record-delete-msg" });
10639
10640
  btn.addEventListener("click", async function () {
10640
10641
  // **確認を await する。** 落とすと Promise が truthy で確認なしに消える
@@ -10857,16 +10858,17 @@ ${deriveCapTile.toString()}
10857
10858
  // ---------- グラフ(force-directed / canvas) ----------
10858
10859
  // Gold層(Cortexのレコード)だけ色付け。それ以外(課題・議事録・資料・外部リンク)は一律グレー
10859
10860
  /* 濃紺の背景に小さな点で出るので、Gold 4区画+Home は明るい色で互いに離す
10860
- * (決定事項=青・用語集=緑・メンバー=ピンク・ルール=オレンジ・Home=黄は
10861
- * 2026-08-28 のオーナー裁定と分類B。レポートは黄が Home と衝突するためシアンへ)。
10861
+ * (決定事項=青・用語集=緑・メンバー=ピンク・ルール=黄・Home=オレンジ。
10862
+ * レポートは黄が近いのでシアンへ)。**Home とルールは 2026-08-29 のオーナーFBで
10863
+ * 入れ替えた**——案件の顔である Home のほうが強い色(オレンジ)を持つ。
10862
10864
  * 凡例の表記は日本語に統一する(区画名は画面のタブ名と同じ言葉を使う) */
10863
10865
  var GROUP_STYLE = {
10864
10866
  decision: { color: "#60a5fa", label: "決定事項" },
10865
10867
  term: { color: "#4ade80", label: "用語集" },
10866
10868
  member: { color: "#f472b6", label: "メンバー" },
10867
- rule: { color: "#fb923c", label: "ルール" },
10869
+ rule: { color: "#fde047", label: "ルール" },
10868
10870
  report: { color: "#22d3ee", label: "レポート" },
10869
- overview: { color: "#fde047", label: "Home" },
10871
+ overview: { color: "#fb923c", label: "Home" },
10870
10872
  record: { color: "#0e7490", label: "その他レコード" },
10871
10873
  silver: { color: "#9ca3af", label: "コンテキスト(課題・議事録・資料・リンク等)" },
10872
10874
  bronze: { color: "#9ca3af", label: "コンテキスト(課題・議事録・資料・リンク等)" },
package/dist/repo.js CHANGED
@@ -1,8 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AI_HISTORY_MAX_ENTRIES = void 0;
3
4
  exports.resolveRepo = resolveRepo;
4
5
  exports.latestGeneratedPaths = latestGeneratedPaths;
5
6
  exports.normalizeToHttps = normalizeToHttps;
7
+ exports.isAiCommit = isAiCommit;
8
+ exports.collectAiChangeHistory = collectAiChangeHistory;
6
9
  const node_child_process_1 = require("node:child_process");
7
10
  /**
8
11
  * GitHubのowner/repoを表すベースURLを解決する。
@@ -102,3 +105,347 @@ function normalizeToHttps(url) {
102
105
  }
103
106
  return null;
104
107
  }
108
+ // ---- AIによるレコードの変更履歴(git 由来)------------------------------------
109
+ //
110
+ // レコード詳細の「AIによる最近の変更」の出どころ。**以前は週次整理レポート
111
+ // (.cortex/weekly-curation-report.json の actions)を読んでいたが、あれは毎回全置換で、
112
+ // 翌週の実行でレポートが差し替わると前の履歴が画面から消えた**(オーナーが実際に踏んだ)。
113
+ // 「黙って書き換えられた記録を作らない」という節の目的に対して、1週間で消える履歴は
114
+ // 用を成さない。git のコミットは消えないので、そちらを正本にする。
115
+ //
116
+ // **git の呼び出しはレコード数に依存させない。ビルド1回につき2回だけ呼ぶ。**
117
+ // レコードごとに git log を回すと、案件が育つほどビルドが線形に遅くなる。
118
+ //
119
+ // パス1: --name-status … コミットの素性(誰が・いつ・何を言って)と触ったパスだけ。出力は軽い
120
+ // パス2: -p -U0 --diff-filter=M … **変更(M)の差分だけ**。追加(A)の差分は取らない
121
+ // ——追加時の「変更後」はレコード本文そのもので、
122
+ // 同じページに全文が既に出ている(site の JSON が二重になる)
123
+ //
124
+ // 例外は全部握って空を返す(latestGeneratedPaths と同じ規律)。浅い clone・git 無し・
125
+ // メッセージ規約の変更 → 「履歴の節が出ない」だけで、他の表示は壊れない。
126
+ /** 行頭に置いてコミットの境目を見分ける印。差分もパスも NUL では始まらない */
127
+ const NUL = "\u0000";
128
+ /** コミット1行の中の区切り(Unit Separator)。コミットメッセージに現れない制御文字を選ぶ */
129
+ const US = "\u001f";
130
+ /** 遡るコミット数の上限。深追いしてもビルドが遅くなるだけで、画面に出るのはレコードごとに数件 */
131
+ const AI_HISTORY_MAX_COMMITS = 400;
132
+ /** レコードごとに画面へ渡す件数(分類B・2026-08-29)。折りたたみの中でも1件が差分を持つので絞る */
133
+ exports.AI_HISTORY_MAX_ENTRIES = 5;
134
+ /** 変更前・変更後それぞれの文字数の上限。超えたら丸めて truncated を立てる */
135
+ const AI_HISTORY_MAX_DIFF_CHARS = 2000;
136
+ /** レコードのパスの形。README・雛形はレコードではない */
137
+ const RECORD_PATH = /^Cortex\/[^/]+\/records\/[^/]+\.md$/;
138
+ /** 探索範囲(git の pathspec)。README や Context 配下の差分は最初から出力に入れない */
139
+ const RECORD_PATHSPEC = "Cortex/*/records/*.md";
140
+ /**
141
+ * そのコミットは「AIがやったこと」か。
142
+ *
143
+ * 🔴 **author だけでは判定できない。** 受け口の Lambda(`infra/lambda/intake`)は
144
+ * **人の操作の代理でコミットする**(`promoteCommitMessage` = 「Gold確認: …」、
145
+ * `deleteCommitMessage` = 「レコード削除: …」。logic.mjs:407,713)。その author は
146
+ * 共有トークンの持ち主で全員同じになる(index.mjs のコメント)。bot 名義だけで判定すると、
147
+ * **人が「確認済みにする」を押した操作が「AIによる変更」として画面に出る**。
148
+ * レコードのファイルを書き換える op はこの2つだけなので、件名で先に落とす。
149
+ *
150
+ * 判定は上から順に:
151
+ * 1. 件名が**人の操作の口**の規約 → 人
152
+ * 2. 件名が**夜間パイプライン**の規約 → AI(author が PAT 名義でも拾える)
153
+ * 3. author が bot 名義 → AI
154
+ * 4. それ以外 → 人(GitHub や手元から人が直接直したもの)
155
+ */
156
+ function isAiCommit(id) {
157
+ const subject = id.subject || "";
158
+ // 1. 人の操作の代理(infra/lambda/intake/logic.mjs の promoteCommitMessage / deleteCommitMessage)
159
+ if (/^(Gold確認|レコード削除)[::]/.test(subject))
160
+ return false;
161
+ // 2. 夜間パイプライン(cortex-engine)の規約。**コロンより前(接頭辞)だけを見る。**
162
+ // 素の部分一致にすると「自動生成された用語を手で直した」「週次整理の取りこぼしを補完」
163
+ // のような**人が書いた件名**まで AI に倒れ、顧客の画面に「AIがこの記録を書き換えました」と
164
+ // 出る(この関数が防ごうとしている誤帰属の、ちょうど逆向き)。
165
+ // 規約側は「Gold自動追記: …」「週次整理: …」の形なので、接頭辞で足りる。
166
+ //
167
+ // ⚠️ **見るのは件名(%s)だけ。** latestGeneratedPaths(このファイル上部)の
168
+ // `--grep` は**メッセージ本文も**対象なので、判定の範囲が揃っていない。
169
+ // 本文まで足すと複数行になって1行1コミットのパースが壊れるため、ここは件名に留める。
170
+ // 件名に規約が無く本文にだけある形で engine が回っても、**規則3(bot 名義)で拾える**
171
+ // ——夜間パイプラインは GitHub Actions の bot 名義でコミットするため。
172
+ const scope = /^([^::]{1,40})[::]/.exec(subject);
173
+ if (scope && /自動追記|自動生成|週次整理/.test(scope[1]))
174
+ return true;
175
+ // 3. bot 名義
176
+ const who = `${id.name || ""} ${id.email || ""}`;
177
+ return /\[bot\]|github-actions/i.test(who);
178
+ }
179
+ /** git log を1回。失敗(git 無し・リポジトリでない・出力が大きすぎる)は空文字にする */
180
+ function gitLog(cwd, args) {
181
+ try {
182
+ // 🔴 **出力の形は、読む人の git 設定に左右させない。** ここで組み立てているのは
183
+ // 機械が読む出力なので、人の ~/.gitconfig で形が変わる余地を全部潰す:
184
+ //
185
+ // core.quotepath=false … 既定の git は ASCII 以外を含むパスを
186
+ // `"Cortex/Glossary/records/\344\273\256…"` と引用符でくるむ。用語集・メンバーの
187
+ // レコードは**日本語のファイル名**なので、付け忘れると画面側の照合キー
188
+ // (repoPathOf の生パス)と一致せず、履歴が**丸ごと出なくなる**(実装中に踏んだ)。
189
+ // なお、これが消すのは**非ASCIIの引用だけ**。ファイル名に `"` `\` 制御文字が
190
+ // 入ると git は常に引用符でくるむ——そのレコードは RECORD_PATH に一致せず、
191
+ // 履歴が出ないだけになる(事故ではないが、出ない理由はこれ)
192
+ // diff.mnemonicPrefix / diff.noprefix … `+++ b/<path>` の接頭辞が `w/` になったり
193
+ // 消えたりする。パスの取り出しが狂うので、下で接頭辞を明示して固定する
194
+ // log.showSignature=false … 署名の検証結果が本文に混ざる
195
+ // color.ui=false / --no-color … 色の制御文字が混ざる
196
+ return (0, node_child_process_1.execFileSync)("git", [
197
+ "-c", "core.quotepath=false",
198
+ "-c", "log.showSignature=false",
199
+ "-c", "color.ui=false",
200
+ ...args,
201
+ ], {
202
+ cwd,
203
+ encoding: "utf8",
204
+ stdio: ["ignore", "pipe", "ignore"],
205
+ // 既定の 1MB では足りない。差分を丸ごと受け取るので広めに取り、
206
+ // 溢れたら例外 → 上位で空扱い(履歴が出ないだけでビルドは通る)
207
+ maxBuffer: 256 * 1024 * 1024,
208
+ });
209
+ }
210
+ catch (e) {
211
+ // **握り潰すが、異常なものは無言にしない。** maxBuffer 超過や git の内部エラーまで
212
+ // 「履歴の節が消える」に潰れると、**ある日消えても誰も気づけない**。
213
+ //
214
+ // ただし「**git が無い**(ENOENT)」「**git リポジトリでない**(git の致命エラー = 128)」は
215
+ // **既定の状態**——手元のビルドやテストで普通に起きるので、ここで警告するとログが
216
+ // 常に注意で埋まる(gold-proposals / weekly-curation の「無いことは警告しない」と同じ規律)。
217
+ // 戻り値の契約(空文字 = 履歴なしで正常継続)はどちらでも変えない
218
+ const err = e;
219
+ if (err.code !== "ENOENT" && err.status !== 128) {
220
+ console.warn(`AIの変更履歴を git から取れませんでした(履歴は出さずに続けます): ${String(e)}`);
221
+ }
222
+ return "";
223
+ }
224
+ }
225
+ /** パス1: コミットの素性と、そのコミットが触ったレコードのパス(新しい順) */
226
+ function readCommits(cwd) {
227
+ const out = gitLog(cwd, [
228
+ "log",
229
+ `-n${AI_HISTORY_MAX_COMMITS}`,
230
+ // 改名を追わない。追うと「変更(M)」の集合から外れて差分が取れなくなる。
231
+ // 改名は削除+追加として素直に見せる。**log.follow=true を設定している人がいる**ので
232
+ // --no-follow も明示する(--follow が効くと1パス目と2パス目で対象がずれる)
233
+ "--no-renames",
234
+ "--no-follow",
235
+ "--no-color",
236
+ // パス2と揃える(--name-status はテキスト差分を作らないので発火しないが、
237
+ // 「読む人の git 設定に左右させない」の徹底を非対称にしない)
238
+ "--no-ext-diff",
239
+ "--name-status",
240
+ `--format=%x00%H%x1f%aI%x1f%an%x1f%ae%x1f%s`,
241
+ "--",
242
+ RECORD_PATHSPEC,
243
+ ]);
244
+ const commits = [];
245
+ let cur = null;
246
+ for (const line of out.split("\n")) {
247
+ if (line.startsWith(NUL)) {
248
+ const parts = line.slice(1).split(US);
249
+ // 件名に区切り文字が混ざっても壊れないよう、5つ目以降は件名として繋ぎ直す
250
+ cur = {
251
+ sha: parts[0] || "",
252
+ at: parts[1] || "",
253
+ name: parts[2] || "",
254
+ email: parts[3] || "",
255
+ subject: parts.slice(4).join(US),
256
+ files: [],
257
+ };
258
+ if (cur.sha)
259
+ commits.push(cur);
260
+ continue;
261
+ }
262
+ if (!cur)
263
+ continue;
264
+ const m = /^([A-Z])\d*\t(.+)$/.exec(line);
265
+ if (m)
266
+ cur.files.push({ status: m[1], path: m[2] });
267
+ }
268
+ return commits;
269
+ }
270
+ /**
271
+ * パス2: 変更(M)の差分だけ。鍵は `<sha>` + NUL + `<path>`。
272
+ *
273
+ * 🔴 **遡る件数はパス1が決める(`mCount` = パス1の窓に入った「変更」コミットの数)。**
274
+ * `--diff-filter=M` は表示だけでなく**コミットの選択**にも効くので、ここで素の
275
+ * `-n400` を渡すと「変更のコミット 400 件」を探して**履歴の底まで歩き、その全部の
276
+ * パッチを生成する**——レコードの変更が少ないリポジトリほど無駄が大きく、
277
+ * 作った差分の大半はパス1の窓の外で一度も引かれない。
278
+ *
279
+ * パス1の窓は「レコードを触ったコミットの新しい 400 件」という連続した区間なので、
280
+ * その中の変更コミットは**全体で最も新しい mCount 件の変更コミットと一致する**。
281
+ * だから `-n mCount` で必要十分(多くも少なくもならない)。
282
+ */
283
+ function readDiffs(cwd, mCount) {
284
+ if (mCount <= 0)
285
+ return new Map();
286
+ const out = gitLog(cwd, [
287
+ "log",
288
+ `-n${mCount}`,
289
+ "--no-renames",
290
+ "--no-follow",
291
+ "--no-color",
292
+ // diff.external / GIT_EXTERNAL_DIFF が設定されていると差分が丸ごと別物になる
293
+ "--no-ext-diff",
294
+ // 接頭辞を明示して固定する(diff.mnemonicPrefix / diff.noprefix の影響を消す)
295
+ "--src-prefix=a/",
296
+ "--dst-prefix=b/",
297
+ "-p",
298
+ // 文脈行は要らない。**変わった行だけ**を出す(画面のラベルも「消えた行/増えた行」)
299
+ "-U0",
300
+ "--diff-filter=M",
301
+ `--format=%x00%H`,
302
+ "--",
303
+ RECORD_PATHSPEC,
304
+ ]);
305
+ const diffs = new Map();
306
+ let sha = "";
307
+ let cur = null;
308
+ // 🔴 **ヘッダの見分けは「位置」で決める。行の見た目で決めない。**
309
+ // 差分の中身は「符号1文字 + 元の行」なので、レコードに `++ …` という行があると
310
+ // `+++ …` に、`-- …` があると `--- …` になる——**ファイルのヘッダと区別が付かない**。
311
+ // 見た目で判定すると、その行を落とすだけでなく**以降の差分が別のパスの下に入る**。
312
+ // 区間で見る: `diff --git` から最初の `@@` までがヘッダ、そこから次の
313
+ // `diff --git` までが中身。
314
+ let inHeader = false;
315
+ for (const line of out.split("\n")) {
316
+ if (line.startsWith(NUL)) {
317
+ sha = line.slice(1).trim();
318
+ cur = null;
319
+ inHeader = false;
320
+ continue;
321
+ }
322
+ if (line.startsWith("diff --git ")) {
323
+ cur = null;
324
+ inHeader = true;
325
+ continue;
326
+ }
327
+ if (inHeader) {
328
+ // パスは `+++ b/<path>` から取る(上の行は空白を含むパスで割れない)
329
+ if (line.startsWith("+++ ")) {
330
+ const p = line.slice(4).trim();
331
+ if (p === "/dev/null" || !sha) {
332
+ cur = null;
333
+ continue;
334
+ }
335
+ const path = p.startsWith("b/") ? p.slice(2) : p;
336
+ cur = { before: [], after: [] };
337
+ diffs.set(sha + NUL + path, cur);
338
+ continue;
339
+ }
340
+ // 最初のハンク見出しでヘッダは終わり(index / old mode / `---` はここで捨てる)
341
+ if (line.startsWith("@@"))
342
+ inHeader = false;
343
+ continue;
344
+ }
345
+ if (!cur)
346
+ continue;
347
+ // 2つ目以降のハンク見出し。中身の行には必ず符号が付くので、素の `@@` は見出しだけ
348
+ if (line.startsWith("@@"))
349
+ continue;
350
+ // `` は符号が付かないので、下の2つに落ちず自然に無視される
351
+ if (line.startsWith("-"))
352
+ cur.before.push(line.slice(1));
353
+ else if (line.startsWith("+"))
354
+ cur.after.push(line.slice(1));
355
+ }
356
+ return diffs;
357
+ }
358
+ /** 長すぎる差分は丸める(site の JSON に丸ごと載るので上限が要る) */
359
+ function clampDiff(text) {
360
+ if (text.length <= AI_HISTORY_MAX_DIFF_CHARS)
361
+ return { text, truncated: false };
362
+ let end = AI_HISTORY_MAX_DIFF_CHARS;
363
+ // 上位サロゲートで切ると、絵文字が半分だけ残って末尾が置換文字になる。1つ戻す
364
+ const code = text.charCodeAt(end - 1);
365
+ if (code >= 0xd800 && code <= 0xdbff)
366
+ end -= 1;
367
+ return { text: text.slice(0, end), truncated: true };
368
+ }
369
+ /**
370
+ * レコードのリポジトリ相対パス → AIによる変更(新しい順・最大 AI_HISTORY_MAX_ENTRIES 件)。
371
+ *
372
+ * 画面は既存の `repoPathOf(rec)`(editUrl から剥がしたパス)でここを引く。
373
+ * **いまリポジトリに無いレコード(最新の状態が削除)は地図に入れない**——
374
+ * もう開けないページの履歴を配っても読む人がいない。
375
+ */
376
+ function collectAiChangeHistory(cwd) {
377
+ const commits = readCommits(cwd);
378
+ if (!commits.length)
379
+ return {};
380
+ // パス2に渡す件数を、パス1の窓に入った「変更」コミットの数から数える
381
+ let mCount = 0;
382
+ for (const c of commits) {
383
+ if (c.files.some((f) => f.status === "M" && RECORD_PATH.test(f.path)))
384
+ mCount++;
385
+ }
386
+ const diffs = readDiffs(cwd, mCount);
387
+ const history = {};
388
+ /**
389
+ * 🔴 **削除でそのパスの収集を打ち切る。** 新しい順に走っているので、削除より古い世代は
390
+ * **人が消したはずの内容**——同じパスに記録が作り直されると(決定は除外リストを持たないので
391
+ * 実際に起こりうる)、消したはずの本文が新しい記録のページに「AIが書き換えました」として
392
+ * 出てしまう。最新が削除なら1件も溜まらないので、地図にも入らない(同じ1つの規則で両方効く)。
393
+ */
394
+ const gone = new Set();
395
+ for (const c of commits) {
396
+ const ai = isAiCommit(c);
397
+ for (const f of c.files) {
398
+ const p = f.path;
399
+ if (!RECORD_PATH.test(p))
400
+ continue;
401
+ const base = p.slice(p.lastIndexOf("/") + 1).toLowerCase();
402
+ if (base === "readme.md" || base === "template.md")
403
+ continue;
404
+ if (gone.has(p))
405
+ continue;
406
+ if (f.status === "D") {
407
+ gone.add(p);
408
+ continue;
409
+ }
410
+ if (!ai)
411
+ continue;
412
+ // A(作成)と M(変更)だけ。T(ファイル↔シンボリックリンクの置き換え)等は
413
+ // 「何が変わったか」を言えないので出さない
414
+ if (f.status !== "A" && f.status !== "M")
415
+ continue;
416
+ const list = history[p] || (history[p] = []);
417
+ if (list.length >= exports.AI_HISTORY_MAX_ENTRIES)
418
+ continue;
419
+ const entry = {
420
+ commit: c.sha,
421
+ at: c.at,
422
+ subject: c.subject,
423
+ kind: f.status === "A" ? "add" : "edit",
424
+ before: "",
425
+ after: "",
426
+ };
427
+ // **「追加(A)の差分を載せない」の門は readDiffs の --diff-filter=M ただ1つ。**
428
+ // ここでも status を見て二重に守ると、どちらを外しても症状が出ず、
429
+ // **テストで守れない不変条件**になる(実際に変異検証で素通りした)。門は1箇所に置く
430
+ // モード変更だけの M・binary 扱いの M は `+++` も `@@` も出さないので、ここは空になる
431
+ //(画面は「変わった行をここに出せませんでした」に倒れる)。.gitattributes で
432
+ // `*.md` に `-diff` を付けている案件では全件がその形になる
433
+ const d = diffs.get(c.sha + NUL + p);
434
+ if (d) {
435
+ const before = clampDiff(d.before.join("\n"));
436
+ const after = clampDiff(d.after.join("\n"));
437
+ entry.before = before.text;
438
+ entry.after = after.text;
439
+ if (before.truncated || after.truncated)
440
+ entry.truncated = true;
441
+ }
442
+ list.push(entry);
443
+ }
444
+ }
445
+ // 空の器は返さない(「知らないことはキーが無いことで表す」)
446
+ for (const p of Object.keys(history)) {
447
+ if (!history[p].length)
448
+ delete history[p];
449
+ }
450
+ return history;
451
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.14",
3
+ "version": "0.12.15",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,