@takagaki/cortex-decisions-viewer 0.4.43 → 0.4.45

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
@@ -42,6 +42,9 @@ const path = __importStar(require("node:path"));
42
42
  const gray_matter_1 = __importDefault(require("gray-matter"));
43
43
  const marked_1 = require("marked");
44
44
  const repo_1 = require("./repo");
45
+ // 直近のGold昇格で追加されたレコードのパス集合。レコード生成の各所から参照するのでモジュールスコープに置く
46
+ // (引数で引き回すと呼び出し側の改修が広範囲になる)。build() の冒頭で1回だけ解決する。
47
+ let LATEST_PATHS = new Set();
45
48
  const render_1 = require("./render");
46
49
  const DEFAULT_EXCLUDE = ["README.md", "template.md"];
47
50
  /** frontmatterのrelationsを安全に正規化する */
@@ -132,6 +135,9 @@ async function readFleetSources(repoRoot) {
132
135
  ref: s.ref != null ? String(s.ref) : undefined,
133
136
  url: s.url != null ? String(s.url) : undefined,
134
137
  gold: typeof s.gold === "boolean" ? s.gold : undefined,
138
+ // goldState は gold の理由。`gold: false` だけだと「意図的に外した」「宣言し忘れ」
139
+ // 「exclude による除外」が同じ値になり、**正常な除外にまで警告を出して麻痺する**。
140
+ goldState: s.goldState != null ? String(s.goldState) : undefined,
135
141
  notify: typeof s.notify === "boolean" ? s.notify : undefined,
136
142
  gate: s.gate != null ? String(s.gate) : undefined,
137
143
  }))
@@ -175,7 +181,8 @@ async function readFleetSources(repoRoot) {
175
181
  if (!Object.keys(tools).length)
176
182
  tools = undefined;
177
183
  }
178
- return { externalSources, internalSources, pipelines, tools };
184
+ const fleetGeneratedAt = typeof obj.generatedAt === "string" ? obj.generatedAt : undefined;
185
+ return { externalSources, internalSources, pipelines, tools, fleetGeneratedAt };
179
186
  }
180
187
  /** パスがディレクトリとして存在するか */
181
188
  async function isDirectory(dirAbs) {
@@ -245,6 +252,7 @@ async function parseGeneric(raw, fileName, repoRelDir, repoBaseUrl, branch) {
245
252
  editUrl,
246
253
  rtype: data.type != null ? String(data.type) : "",
247
254
  rstatus: data.status != null ? String(data.status) : "",
255
+ isLatest: LATEST_PATHS.has(filePath),
248
256
  relations: toRelations(data.relations),
249
257
  source: data.source != null ? String(data.source) : "",
250
258
  bodyLinks: extractBodyLinks(content),
@@ -663,7 +671,7 @@ function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl,
663
671
  const deduped = edges.filter((e) => {
664
672
  if (e.from === e.to)
665
673
  return false;
666
- const key = `${e.from}${e.to}${e.rel}`;
674
+ const key = `${e.from}\u0000${e.to}\u0000${e.rel}`;
667
675
  if (seen.has(key))
668
676
  return false;
669
677
  seen.add(key);
@@ -901,6 +909,11 @@ async function build(opts) {
901
909
  branch: opts.branch,
902
910
  cwd: dirAbs,
903
911
  });
912
+ // 直近のGold昇格で追加されたレコードを git から解決する(レコードの date は決定日なので生成時期が分からない)。
913
+ // git が返すのはリポジトリルート相対のパス(`Cortex/...`)なので、判定も**リポジトリルート**で行う
914
+ // (Decisionsディレクトリで実行すると `-- Cortex/` に一致せず常に空になる)。
915
+ // 浅いclone・git無しの環境では空集合が返り、「最新」ラベルが出ないだけで他の表示は壊れない。
916
+ LATEST_PATHS = (0, repo_1.latestGeneratedPaths)(process.cwd());
904
917
  const decisions = [];
905
918
  for (const fileName of mdFiles) {
906
919
  const raw = await node_fs_1.promises.readFile(path.join(dirAbs, fileName), "utf8");
@@ -970,6 +983,7 @@ async function build(opts) {
970
983
  }
971
984
  : undefined,
972
985
  externalSources: fleet.externalSources,
986
+ fleetGeneratedAt: fleet.fleetGeneratedAt,
973
987
  internalSources: fleet.internalSources,
974
988
  pipelines: fleet.pipelines,
975
989
  tools: fleet.tools,
@@ -1035,6 +1049,7 @@ async function parseDecision(raw, fileName, repoRelDir, repoBaseUrl, branch) {
1035
1049
  bodyRef: "",
1036
1050
  searchText: toSearchText(content),
1037
1051
  rstatus: data.status != null ? String(data.status) : "",
1052
+ isLatest: LATEST_PATHS.has(filePath),
1038
1053
  fileName,
1039
1054
  editUrl,
1040
1055
  fm: sanitizeFm(data),
package/dist/render.js CHANGED
@@ -91,6 +91,23 @@ const CM_TOOL_LABEL = {
91
91
  * 状態: 健全=色付き+流れるアニメーション / 問題あり=黄+⚠️ / 未使用=グレー。
92
92
  * fleet-status由来の情報が何も無ければ null(マップを出さない・後方互換)。
93
93
  */
94
+ /**
95
+ * このソースが「注意が必要」か。接続マップ(ビルド時生成)とデータソース一覧(クライアント側)で
96
+ * **同じ判定を使う**ためにここに置く。片方だけ直すと、同じ画面でマップが⚠️・直下が「接続OK」になる。
97
+ *
98
+ * 判定の考え方:
99
+ * - 明示的に Gold 対象から外したもの(`gold === false`)は、読めなくて当たり前なので警告しない。
100
+ * ただし通知先(`notify`)に使っているなら、届いていないこと自体が問題なので拾う。
101
+ * - `gold` が無い(旧 fleet-status.json)ときは**警告する側に倒す**。「不明」を「対象外」と扱うと、
102
+ * 本当に壊れているものが黙って隠れる(フェイルオープン)。
103
+ */
104
+ function sourceNeedsAttention(s) {
105
+ if (s.gate === "ok")
106
+ return false;
107
+ if (s.gold === false && s.notify !== true)
108
+ return false;
109
+ return true;
110
+ }
94
111
  function renderConnectionMap(site) {
95
112
  const internals = site.internalSources ?? [];
96
113
  const externals = site.externalSources ?? [];
@@ -119,7 +136,9 @@ function renderConnectionMap(site) {
119
136
  // 問題あり: ライブ参照はgate≠okが1件以上、同期ソースは対応パイプラインの直近runがfailure
120
137
  const warn = !off &&
121
138
  (isLive
122
- ? liveExts.some((s) => s.gate !== "ok")
139
+ // カード側の集計(sourceNeedsAttention)と同じ判定にする。ここだけ接続状態だけを見ていると、
140
+ // マップが⚠️なのに直下のグループが「接続OK」という矛盾が同じ画面に出る。
141
+ ? liveExts.some((s) => sourceNeedsAttention(s))
123
142
  : pipes.some((p) => c.pipe != null && c.pipe.test(p.id) && p.applicable !== false && p.lastConclusion === "failure"));
124
143
  const state = off ? "cm-off" : warn ? "cm-warn" : "cm-ok";
125
144
  const toolName = toolVal && toolVal !== "none"
@@ -549,6 +568,8 @@ a:hover { text-decoration: underline; }
549
568
  .badge.cat-ビジネス { background: #fde9ef; color: #be185d; }
550
569
  .badge.cat-デザイン { background: #fce7f3; color: #a21caf; }
551
570
  .badge.draft { background: #fef3c7; color: #92400e; border: 1px solid #fcd34d; }
571
+ /* 直近のGold昇格で作られたレコードの印。draft(黄・要対応)と区別できるよう、新着を示す緑系にする */
572
+ .badge.latest { background: #dcfce7; color: #166534; border: 1px solid #86efac; }
552
573
  .badge.error { background: #fee2e2; color: #b91c1c; border: 1px solid #fca5a5; }
553
574
  .draft-note {
554
575
  display: flex; gap: 8px; align-items: flex-start;
@@ -655,7 +676,8 @@ a:hover { text-decoration: underline; }
655
676
  /* ---- データソースと自動化 ---- */
656
677
  .datasources { margin-top: 34px; }
657
678
  .datasources-head { font-size: 18px; font-weight: 700; color: var(--text); margin: 0 0 4px; }
658
- .datasources-intro { color: var(--muted); font-size: 13.5px; line-height: 1.8; margin: 0 0 14px; }
679
+ .datasources-intro { color: var(--muted); font-size: 13.5px; line-height: 1.8; margin: 0 0 6px; }
680
+ .datasources-asof { color: var(--muted); font-size: 12.5px; line-height: 1.7; margin: 0 0 14px; }
659
681
  .ds-sub { font-size: 13px; font-weight: 700; color: var(--muted); margin: 20px 0 8px; }
660
682
  .ds-list { display: flex; flex-direction: column; gap: 10px; }
661
683
  .ds-card {
@@ -720,6 +742,7 @@ a.ds-name:hover { text-decoration: underline; }
720
742
  .ds-badges { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; flex: 0 0 auto; }
721
743
  .badge.ds-gold { background: #fef3c7; color: #92400e; border: 1px solid #fcd34d; }
722
744
  .badge.ds-excluded { background: #eceef1; color: #6b7280; }
745
+ .badge.ds-undeclared { background: #fff4e5; color: #9a6700; }
723
746
  .badge.ds-notify { background: #eff6ff; color: #1e40af; border: 1px solid #bfdbfe; }
724
747
  .ds-gate { display: inline-flex; align-items: center; gap: 5px; font-size: 12.5px; font-weight: 600; flex: 0 0 auto; }
725
748
  .ds-gate::before { content: ""; width: 8px; height: 8px; border-radius: 999px; background: currentColor; }
@@ -922,6 +945,11 @@ const CLIENT_JS = `
922
945
  function draftBadge(rec) {
923
946
  return rec && rec.rstatus === "draft" ? el("span", { class: "badge draft", text: "AI生成・未確認" }) : null;
924
947
  }
948
+ // 直近のGold昇格で作られたレコードの印。レコードの date は決定日なので、日付順に並べても
949
+ // 「今回の実行で何が増えたか」は分からない。ビルド時に git から判定した結果(isLatest)を出す
950
+ function latestBadge(rec) {
951
+ return rec && rec.isLatest ? el("span", { class: "badge latest", text: "最新の生成" }) : null;
952
+ }
925
953
  // 詳細ページの警告。バッジより強く「そのまま信じないでほしい」と伝える
926
954
  function draftNote(rec) {
927
955
  if (!rec || rec.rstatus !== "draft") return null;
@@ -1151,17 +1179,24 @@ const CLIENT_JS = `
1151
1179
  var base = SITE.repoBaseUrl;
1152
1180
  var INFO = {
1153
1181
  "decisions": {
1154
- text: "議事録・課題から確定した決定が毎晩自動で追記されます(update-decision-log-auto)。",
1182
+ text: "議事録・課題から確定した決定が毎晩自動で追記されます。",
1155
1183
  btnLabel: "実行履歴を確認 →",
1156
- btnUrl: base ? base + "/actions/workflows/update-decision-log.yml" : null,
1184
+ btnUrl: base ? base + "/actions/workflows/update-gold.yml" : null,
1157
1185
  },
1158
1186
  "dir:Glossary": {
1159
- text: "毎晩の自動走査が新しい用語を draft として直接追加します(update-glossary-auto)。AIが生成した定義は仮のものです。「AI生成・未確認」の用語は必ず内容を確認し、適切な定義に置き換えて status: active にしてください。",
1187
+ text: "毎晩の自動走査が新しい用語を draft として直接追加します。AIが生成した定義は仮のものです。「AI生成・未確認」の用語は必ず内容を確認し、適切な定義に置き換えて status: active にしてください。",
1160
1188
  btnLabel: "実行履歴を確認 →",
1161
- btnUrl: base ? base + "/actions/workflows/update-glossary.yml" : null,
1189
+ btnUrl: base ? base + "/actions/workflows/update-gold.yml" : null,
1190
+ },
1191
+ "dir:Members": {
1192
+ text: "プロジェクト関係者の名簿です。人名の表記ゆれを解決する根拠になります。毎晩の自動走査が新しい参加者を draft として追加します。",
1193
+ btnLabel: "実行履歴を確認 →",
1194
+ btnUrl: base ? base + "/actions/workflows/update-gold.yml" : null,
1162
1195
  },
1163
1196
  "dir:Rules": {
1164
1197
  text: "案件で継続的に守る制約・ルールの一覧です。AIはここを読んで、違反する提案を避けます。",
1198
+ btnLabel: "実行履歴を確認 →",
1199
+ btnUrl: base ? base + "/actions/workflows/update-gold.yml" : null,
1165
1200
  },
1166
1201
  "graph": {
1167
1202
  text: "決定・用語・レポートと、その根拠になった課題・議事録・資料のつながりを自動でつないだ地図です。点をクリックすると、そのページや実際のシステム(Backlog・GitHub等)に移動できます。",
@@ -1334,6 +1369,14 @@ const CLIENT_JS = `
1334
1369
  var sec = el("section", { class: "datasources" });
1335
1370
  sec.appendChild(el("h2", { class: "datasources-head", text: "データソースと自動化" }));
1336
1371
  sec.appendChild(el("p", { class: "datasources-intro", text: "このCortexが接続している情報源と、毎晩自動でGoldに昇格する処理の一覧です。" }));
1372
+ // **いつ時点の情報かを必ず出す。** この節は夜間バッチが作るスナップショットを読んでいるので、
1373
+ // 設定を変えても次の生成までは古い値のまま。時刻が無いと「変えたのに反映されない」ではなく
1374
+ // 「変えたはずなのに違う値が出ている」と読まれてしまう(実際に設定変更直後にそうなった)。
1375
+ var asOf = SITE.fleetGeneratedAt ? dsFmtTime(SITE.fleetGeneratedAt) : "";
1376
+ if (asOf) {
1377
+ sec.appendChild(el("p", { class: "datasources-asof",
1378
+ text: "接続状況は " + asOf + " 時点の自動チェック結果です。設定を変えた場合、ここに反映されるのは次回の実行後になります。" }));
1379
+ }
1337
1380
 
1338
1381
  if (internals.length) {
1339
1382
  sec.appendChild(el("div", { class: "ds-sub", text: "リポジトリ内の情報源(自動同期)" }));
@@ -1400,14 +1443,27 @@ const CLIENT_JS = `
1400
1443
  var cards = el("div", { class: "ds-list" });
1401
1444
  g.items.forEach(function (s) { cards.appendChild(extSourceCard(s, meta)); });
1402
1445
  if (g.items.length >= 4) {
1403
- // 折りたたみ(既定は閉)。summaryに件数と状態の要約を出す
1446
+ // 折りたたみ(既定は閉)。summaryに件数と状態の要約を出す。
1447
+ // 判定は接続マップ側の sourceNeedsAttention と揃える(片方だけ直すと同じ画面で矛盾する)。
1448
+ // 意図的に外したソースが接続できないのは当たり前なので数えない。ただし通知先に
1449
+ // 使っているなら届いていないこと自体が問題なので拾う。gold 不明は警告する側に倒す。
1404
1450
  var bad = 0;
1405
- g.items.forEach(function (s) { if (s.gate !== "ok") bad++; });
1451
+ var noToken = 0;
1452
+ g.items.forEach(function (s) {
1453
+ if (s.gate === "ok") return;
1454
+ if (s.gold === false && s.notify !== true) return;
1455
+ bad++;
1456
+ if (s.gate === "no_token") noToken++;
1457
+ });
1406
1458
  var sum = el("summary", {}, [
1407
1459
  el("span", { text: meta.icon + " " + meta.label + "(" + g.items.length + (UNIT[g.type] || "件") + ")" }),
1408
1460
  bad === 0
1409
1461
  ? el("span", { class: "badge ds-sum-ok", text: "接続OK" })
1410
- : el("span", { class: "badge ds-sum-warn", text: "⚠️ " + bad + "件に問題" }),
1462
+ // 原因が1つに集約できるときは原因を書く。「19件に問題」より「トークンが未設定」の方が、
1463
+ // 読んだ人が次に何をすればよいか分かる(実際、19件の原因がトークン1本ということがあった)。
1464
+ : el("span", { class: "badge ds-sum-warn", text: noToken === bad
1465
+ ? "⚠️ " + bad + "件が読めていません(トークン未設定)"
1466
+ : "⚠️ " + bad + "件に問題" }),
1411
1467
  ]);
1412
1468
  var det = el("details", { class: "ds-group" }, [sum, cards]);
1413
1469
  if (anchorId) det.id = anchorId;
@@ -1425,7 +1481,7 @@ const CLIENT_JS = `
1425
1481
  // 注記は内部・外部の両グループの後ろに1つ(内部の直下だけに置くと外部ソースが
1426
1482
  // Gold昇格の対象外に見えるため)。除外の方法もここで案内する。
1427
1483
  if (internals.length || sources.length || chatOff || devOff) {
1428
- sec.appendChild(el("p", { class: "ds-note", text: "上記の情報源はすべて毎晩の Gold 昇格が読み取ります(「除外」バッジのものを除く。チャンネル・リポジトリ単位で除外できます)。" }));
1484
+ sec.appendChild(el("p", { class: "ds-note", text: "「Gold昇格の読み取り対象」のものだけを毎晩の Gold 昇格が読み取ります。「除外」は意図的に外したもの、「未宣言(対象外)」はまだ判断されていないもので、どちらも読み取りません(チャンネル・リポジトリ単位で切り替えられます)。" }));
1429
1485
  }
1430
1486
 
1431
1487
  if (pipes.length) {
@@ -1471,7 +1527,10 @@ const CLIENT_JS = `
1471
1527
  // 外部ソース1件のカード(グループ化表示・展開表示の両方から使う)
1472
1528
  function extSourceCard(s, meta) {
1473
1529
  var badges = el("div", { class: "ds-badges" });
1530
+ // 「意図的に外した」と「宣言し忘れ」を分けて出す。同じ「除外」と書くと、
1531
+ // 判断されていないだけのものが「判断済み」に見えてしまう。
1474
1532
  if (s.gold === true) badges.appendChild(el("span", { class: "badge ds-gold", text: "Gold昇格の読み取り対象" }));
1533
+ else if (s.goldState === "undeclared") badges.appendChild(el("span", { class: "badge ds-undeclared", text: "未宣言(対象外)" }));
1475
1534
  else if (s.gold === false) badges.appendChild(el("span", { class: "badge ds-excluded", text: "除外" }));
1476
1535
  if (s.notify === true) badges.appendChild(el("span", { class: "badge ds-notify", text: "通知先" }));
1477
1536
  var gate = DS_GATE[s.gate] || DS_GATE["unknown"];
@@ -1747,6 +1806,7 @@ const CLIENT_JS = `
1747
1806
  pg.slice.forEach(function (d) {
1748
1807
  var meta = el("div", { class: "card-meta" }, [
1749
1808
  draftBadge(d),
1809
+ latestBadge(d),
1750
1810
  el("span", { class: "mono", text: d.date || "----" }),
1751
1811
  catBadge(d.category),
1752
1812
  el("span", { class: "mono", text: d.id }),
@@ -1814,6 +1874,7 @@ const CLIENT_JS = `
1814
1874
  pg.slice.forEach(function (r) {
1815
1875
  var meta = el("div", { class: "card-meta" }, [
1816
1876
  draftBadge(r),
1877
+ latestBadge(r),
1817
1878
  r.date ? el("span", { class: "mono", text: r.date }) : null,
1818
1879
  el("span", { class: "mono", text: r.id }),
1819
1880
  ]);
@@ -1864,6 +1925,7 @@ const CLIENT_JS = `
1864
1925
 
1865
1926
  app.appendChild(el("div", { class: "detail-meta" }, [
1866
1927
  draftBadge(r),
1928
+ latestBadge(r),
1867
1929
  r.date ? el("span", { class: "mono", text: r.date }) : null,
1868
1930
  el("span", { class: "mono", text: r.id }),
1869
1931
  r.summary ? el("span", { text: r.summary }) : null,
@@ -2521,6 +2583,7 @@ const CLIENT_JS = `
2521
2583
 
2522
2584
  var meta = el("div", { class: "detail-meta" }, [
2523
2585
  draftBadge(d),
2586
+ latestBadge(d),
2524
2587
  el("span", { class: "mono", text: d.date || "----" }),
2525
2588
  catBadge(d.category),
2526
2589
  el("span", { class: "mono", text: d.id }),
package/dist/repo.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.resolveRepo = resolveRepo;
4
+ exports.latestGeneratedPaths = latestGeneratedPaths;
4
5
  exports.normalizeToHttps = normalizeToHttps;
5
6
  const node_child_process_1 = require("node:child_process");
6
7
  /**
@@ -15,6 +16,51 @@ function resolveRepo(opts) {
15
16
  const remote = tryGitRemote(opts.cwd);
16
17
  return { baseUrl: remote ? normalizeToHttps(remote) : null, branch };
17
18
  }
19
+ /**
20
+ * 直近のGold昇格で追加されたレコードのパス集合を返す。
21
+ *
22
+ * 「最新の生成物」をビューアで区別するために使う。レコードの frontmatter の `date` は
23
+ * **決定日**(過去の議事録から起こした決定は古い日付になる)なので、生成のタイミングは
24
+ * git 側からしか分からない。
25
+ *
26
+ * 判定: 夜間パイプラインのコミット(メッセージが「自動追記」を含む)のうち最新の1回を探し、
27
+ * そのコミットで **追加(A)** されたファイルを返す。同じ夜に複数フェーズが別コミットで入るため、
28
+ * 最新コミットと同じ「日」のパイプラインコミットまでをまとめて1回の実行として扱う。
29
+ *
30
+ * clone が浅い(--depth 1 等)・git が無い環境では**空集合を返す**(ラベルが出ないだけで壊れない)。
31
+ */
32
+ function latestGeneratedPaths(cwd) {
33
+ const empty = new Set();
34
+ try {
35
+ // 夜間パイプラインのコミットを新しい順に数件。--grep はコミットメッセージの規約に依存するため、
36
+ // 変わっても「ラベルが出ない」だけで済むように例外は握る。
37
+ const log = (0, node_child_process_1.execSync)('git log -n 20 --grep="自動追記" --pretty=format:"%H %cI" -- Cortex/', { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
38
+ if (!log)
39
+ return empty;
40
+ const rows = log.split("\n").map((l) => {
41
+ const [sha, iso] = l.trim().split(/\s+/);
42
+ return { sha, day: (iso || "").slice(0, 10) };
43
+ });
44
+ const latestDay = rows[0].day;
45
+ if (!latestDay)
46
+ return empty;
47
+ const out = new Set();
48
+ for (const r of rows) {
49
+ if (r.day !== latestDay)
50
+ break; // 直近の実行日ぶんだけ
51
+ const files = (0, node_child_process_1.execSync)(`git show --diff-filter=A --name-only --pretty=format: ${r.sha}`, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
52
+ for (const f of files.split("\n")) {
53
+ const p = f.trim();
54
+ if (p)
55
+ out.add(p);
56
+ }
57
+ }
58
+ return out;
59
+ }
60
+ catch {
61
+ return empty; // 浅いclone・git無し・メッセージ規約の変更 → ラベルなしで続行
62
+ }
63
+ }
18
64
  function tryGitRemote(cwd) {
19
65
  try {
20
66
  return (0, node_child_process_1.execSync)("git config --get remote.origin.url", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.4.43",
3
+ "version": "0.4.45",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,