@takagaki/cortex-decisions-viewer 0.4.44 → 0.4.47

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
@@ -135,6 +135,9 @@ async function readFleetSources(repoRoot) {
135
135
  ref: s.ref != null ? String(s.ref) : undefined,
136
136
  url: s.url != null ? String(s.url) : undefined,
137
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,
138
141
  notify: typeof s.notify === "boolean" ? s.notify : undefined,
139
142
  gate: s.gate != null ? String(s.gate) : undefined,
140
143
  }))
@@ -178,7 +181,8 @@ async function readFleetSources(repoRoot) {
178
181
  if (!Object.keys(tools).length)
179
182
  tools = undefined;
180
183
  }
181
- return { externalSources, internalSources, pipelines, tools };
184
+ const fleetGeneratedAt = typeof obj.generatedAt === "string" ? obj.generatedAt : undefined;
185
+ return { externalSources, internalSources, pipelines, tools, fleetGeneratedAt };
182
186
  }
183
187
  /** パスがディレクトリとして存在するか */
184
188
  async function isDirectory(dirAbs) {
@@ -667,7 +671,7 @@ function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl,
667
671
  const deduped = edges.filter((e) => {
668
672
  if (e.from === e.to)
669
673
  return false;
670
- const key = `${e.from}${e.to}${e.rel}`;
674
+ const key = `${e.from}\u0000${e.to}\u0000${e.rel}`;
671
675
  if (seen.has(key))
672
676
  return false;
673
677
  seen.add(key);
@@ -971,7 +975,7 @@ async function build(opts) {
971
975
  graph,
972
976
  categories,
973
977
  months,
974
- intake: opts.intakeUrl && opts.projectKey
978
+ intake: opts.intakeUrl && opts.projectKey && opts.intakeToken
975
979
  ? {
976
980
  url: opts.intakeUrl,
977
981
  token: opts.intakeToken ?? "",
@@ -979,6 +983,7 @@ async function build(opts) {
979
983
  }
980
984
  : undefined,
981
985
  externalSources: fleet.externalSources,
986
+ fleetGeneratedAt: fleet.fleetGeneratedAt,
982
987
  internalSources: fleet.internalSources,
983
988
  pipelines: fleet.pipelines,
984
989
  tools: fleet.tools,
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"
@@ -657,7 +676,8 @@ a:hover { text-decoration: underline; }
657
676
  /* ---- データソースと自動化 ---- */
658
677
  .datasources { margin-top: 34px; }
659
678
  .datasources-head { font-size: 18px; font-weight: 700; color: var(--text); margin: 0 0 4px; }
660
- .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; }
661
681
  .ds-sub { font-size: 13px; font-weight: 700; color: var(--muted); margin: 20px 0 8px; }
662
682
  .ds-list { display: flex; flex-direction: column; gap: 10px; }
663
683
  .ds-card {
@@ -722,6 +742,14 @@ a.ds-name:hover { text-decoration: underline; }
722
742
  .ds-badges { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; flex: 0 0 auto; }
723
743
  .badge.ds-gold { background: #fef3c7; color: #92400e; border: 1px solid #fcd34d; }
724
744
  .badge.ds-excluded { background: #eceef1; color: #6b7280; }
745
+ .badge.ds-undeclared { background: #fff4e5; color: #9a6700; }
746
+ .ds-toggle { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 6px; }
747
+ .ds-toggle-btn { font-size: 12px; padding: 3px 10px; border-radius: 999px; border: 1px solid var(--border); background: #fff; color: var(--text); cursor: pointer; }
748
+ .ds-toggle-btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
749
+ .ds-toggle-btn:disabled { opacity: .6; cursor: default; }
750
+ .ds-toggle-msg { font-size: 12px; color: var(--muted); }
751
+ .ds-toggle-msg.is-ok { color: #1a7f37; }
752
+ .ds-toggle-msg.is-error { color: #b42318; }
725
753
  .badge.ds-notify { background: #eff6ff; color: #1e40af; border: 1px solid #bfdbfe; }
726
754
  .ds-gate { display: inline-flex; align-items: center; gap: 5px; font-size: 12.5px; font-weight: 600; flex: 0 0 auto; }
727
755
  .ds-gate::before { content: ""; width: 8px; height: 8px; border-radius: 999px; background: currentColor; }
@@ -1348,6 +1376,14 @@ const CLIENT_JS = `
1348
1376
  var sec = el("section", { class: "datasources" });
1349
1377
  sec.appendChild(el("h2", { class: "datasources-head", text: "データソースと自動化" }));
1350
1378
  sec.appendChild(el("p", { class: "datasources-intro", text: "このCortexが接続している情報源と、毎晩自動でGoldに昇格する処理の一覧です。" }));
1379
+ // **いつ時点の情報かを必ず出す。** この節は夜間バッチが作るスナップショットを読んでいるので、
1380
+ // 設定を変えても次の生成までは古い値のまま。時刻が無いと「変えたのに反映されない」ではなく
1381
+ // 「変えたはずなのに違う値が出ている」と読まれてしまう(実際に設定変更直後にそうなった)。
1382
+ var asOf = SITE.fleetGeneratedAt ? dsFmtTime(SITE.fleetGeneratedAt) : "";
1383
+ if (asOf) {
1384
+ sec.appendChild(el("p", { class: "datasources-asof",
1385
+ text: "接続状況は " + asOf + " 時点の自動チェック結果です。設定を変えた場合、ここに反映されるのは次回の実行後になります。" }));
1386
+ }
1351
1387
 
1352
1388
  if (internals.length) {
1353
1389
  sec.appendChild(el("div", { class: "ds-sub", text: "リポジトリ内の情報源(自動同期)" }));
@@ -1414,14 +1450,27 @@ const CLIENT_JS = `
1414
1450
  var cards = el("div", { class: "ds-list" });
1415
1451
  g.items.forEach(function (s) { cards.appendChild(extSourceCard(s, meta)); });
1416
1452
  if (g.items.length >= 4) {
1417
- // 折りたたみ(既定は閉)。summaryに件数と状態の要約を出す
1453
+ // 折りたたみ(既定は閉)。summaryに件数と状態の要約を出す。
1454
+ // 判定は接続マップ側の sourceNeedsAttention と揃える(片方だけ直すと同じ画面で矛盾する)。
1455
+ // 意図的に外したソースが接続できないのは当たり前なので数えない。ただし通知先に
1456
+ // 使っているなら届いていないこと自体が問題なので拾う。gold 不明は警告する側に倒す。
1418
1457
  var bad = 0;
1419
- g.items.forEach(function (s) { if (s.gate !== "ok") bad++; });
1458
+ var noToken = 0;
1459
+ g.items.forEach(function (s) {
1460
+ if (s.gate === "ok") return;
1461
+ if (s.gold === false && s.notify !== true) return;
1462
+ bad++;
1463
+ if (s.gate === "no_token") noToken++;
1464
+ });
1420
1465
  var sum = el("summary", {}, [
1421
1466
  el("span", { text: meta.icon + " " + meta.label + "(" + g.items.length + (UNIT[g.type] || "件") + ")" }),
1422
1467
  bad === 0
1423
1468
  ? el("span", { class: "badge ds-sum-ok", text: "接続OK" })
1424
- : el("span", { class: "badge ds-sum-warn", text: "⚠️ " + bad + "件に問題" }),
1469
+ // 原因が1つに集約できるときは原因を書く。「19件に問題」より「トークンが未設定」の方が、
1470
+ // 読んだ人が次に何をすればよいか分かる(実際、19件の原因がトークン1本ということがあった)。
1471
+ : el("span", { class: "badge ds-sum-warn", text: noToken === bad
1472
+ ? "⚠️ " + bad + "件が読めていません(トークン未設定)"
1473
+ : "⚠️ " + bad + "件に問題" }),
1425
1474
  ]);
1426
1475
  var det = el("details", { class: "ds-group" }, [sum, cards]);
1427
1476
  if (anchorId) det.id = anchorId;
@@ -1439,7 +1488,7 @@ const CLIENT_JS = `
1439
1488
  // 注記は内部・外部の両グループの後ろに1つ(内部の直下だけに置くと外部ソースが
1440
1489
  // Gold昇格の対象外に見えるため)。除外の方法もここで案内する。
1441
1490
  if (internals.length || sources.length || chatOff || devOff) {
1442
- sec.appendChild(el("p", { class: "ds-note", text: "上記の情報源はすべて毎晩の Gold 昇格が読み取ります(「除外」バッジのものを除く。チャンネル・リポジトリ単位で除外できます)。" }));
1491
+ sec.appendChild(el("p", { class: "ds-note", text: "「Gold昇格の読み取り対象」のものだけを毎晩の Gold 昇格が読み取ります。「除外」は意図的に外したもの、「未宣言(対象外)」はまだ判断されていないもので、どちらも読み取りません(チャンネル・リポジトリ単位で切り替えられます)。" }));
1443
1492
  }
1444
1493
 
1445
1494
  if (pipes.length) {
@@ -1483,13 +1532,103 @@ const CLIENT_JS = `
1483
1532
  ]);
1484
1533
  }
1485
1534
  // 外部ソース1件のカード(グループ化表示・展開表示の両方から使う)
1535
+ /**
1536
+ * Gold昇格の切り替えボタン。押すと設定ファイル(channels.json)が書き換わる。
1537
+ *
1538
+ * **広げる方向(OFF→ON)には確認を出す。** そのチャンネルの内容をAIが読んで、顧客も見る
1539
+ * Decision・用語集に蒸留するようになる。社内限定のチャンネルをONにすると、顧客が見られない
1540
+ * 会話が顧客可視の層に出てくる(実際にその事故が起きている)。狭める方向は安全なので確認しない。
1541
+ *
1542
+ * 保存しても画面はすぐには変わらない(接続状況は夜間バッチのスナップショット)。
1543
+ * それを伝えないと「保存できていない」と受け取られるので、応答のメッセージをそのまま出す。
1544
+ */
1545
+ function goldToggle(s) {
1546
+ var wrap = el("div", { class: "ds-toggle" });
1547
+ var on = s.gold === true;
1548
+ var btn = el("button", {
1549
+ class: "ds-toggle-btn" + (on ? " is-on" : ""),
1550
+ type: "button",
1551
+ text: on ? "Gold昇格を止める" : "Gold昇格を始める",
1552
+ });
1553
+ var msg = el("div", { class: "ds-toggle-msg" });
1554
+ wrap.appendChild(btn);
1555
+ wrap.appendChild(msg);
1556
+
1557
+ btn.addEventListener("click", async function () {
1558
+ var next = !on;
1559
+ if (next) {
1560
+ var ok = window.confirm(
1561
+ "「" + (s.name || s.ref) + "」の内容を、顧客も見る Decision・用語集に蒸留するようになります。" +
1562
+ String.fromCharCode(10, 10) +
1563
+ "社内限定の会話が含まれるチャンネルでは、顧客に見せられない内容が出てしまう可能性があります。" +
1564
+ String.fromCharCode(10) +
1565
+ "顧客がこのチャンネルにいる(=すでに会話を見ている)場合にだけ有効にしてください。" +
1566
+ String.fromCharCode(10, 10) +
1567
+ "※ 今夜の蒸留(22時ごろ)までに止めれば取り込まれません。一度生成された記録は自動では消えません。" +
1568
+ String.fromCharCode(10, 10) +
1569
+ "有効にしますか?",
1570
+ );
1571
+ if (!ok) return;
1572
+ }
1573
+ var editor = window.prompt("変更者のお名前を入力してください(履歴に残ります)", "");
1574
+ if (editor === null) return;
1575
+ if (!editor.trim()) { msg.className = "ds-toggle-msg is-error"; msg.textContent = "⚠ お名前を入力してください。"; return; }
1576
+
1577
+ btn.disabled = true;
1578
+ msg.className = "ds-toggle-msg";
1579
+ msg.textContent = "保存中…";
1580
+ var intake = SITE.intake;
1581
+ var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
1582
+ try {
1583
+ var res = await fetch(pfx + "op=config&t=" + encodeURIComponent(intake.token), {
1584
+ method: "POST",
1585
+ headers: { "content-type": "application/json" },
1586
+ body: JSON.stringify({
1587
+ file: "チャット/channels.json",
1588
+ op: "set-channel-gold",
1589
+ ref: s.ref,
1590
+ value: next,
1591
+ editor: editor,
1592
+ }),
1593
+ });
1594
+ var j = await res.json().catch(function () { return {}; });
1595
+ if (!res.ok) {
1596
+ msg.className = "ds-toggle-msg is-error";
1597
+ msg.textContent = "✗ " + (j.message || "保存できませんでした。");
1598
+ btn.disabled = false;
1599
+ return;
1600
+ }
1601
+ msg.className = "ds-toggle-msg is-ok";
1602
+ msg.textContent = "✓ " + (j.message || "保存しました。");
1603
+ // **押した内容をボタンに反映し、逆向きの操作を必ず残す。**
1604
+ // バッジ(Gold昇格の読み取り対象/除外)は夜間バッチのスナップショット由来なので変えない。
1605
+ // 変えると「もう効いている」と誤解させるため。変えるのはボタンだけ。
1606
+ // ここを disabled のままにすると、誤って有効にした直後に**同じ画面から止められなくなる**。
1607
+ // 表示が直るのは翌朝で、その間に22時のGold昇格が走り、生成されたレコードは自動削除されない
1608
+ // (Gold層は追記専用)。危険な方向は押せるのに戻す方向が塞がる、という状態は許容できない。
1609
+ on = next;
1610
+ btn.textContent = on ? "Gold昇格を止める" : "Gold昇格を始める";
1611
+ btn.className = "ds-toggle-btn" + (on ? " is-on" : "");
1612
+ btn.disabled = false;
1613
+ } catch (e) {
1614
+ msg.className = "ds-toggle-msg is-error";
1615
+ msg.textContent = "✗ 通信に失敗しました。";
1616
+ btn.disabled = false;
1617
+ }
1618
+ });
1619
+ return wrap;
1620
+ }
1621
+
1486
1622
  function extSourceCard(s, meta) {
1487
1623
  var badges = el("div", { class: "ds-badges" });
1624
+ // 「意図的に外した」と「宣言し忘れ」を分けて出す。同じ「除外」と書くと、
1625
+ // 判断されていないだけのものが「判断済み」に見えてしまう。
1488
1626
  if (s.gold === true) badges.appendChild(el("span", { class: "badge ds-gold", text: "Gold昇格の読み取り対象" }));
1627
+ else if (s.goldState === "undeclared") badges.appendChild(el("span", { class: "badge ds-undeclared", text: "未宣言(対象外)" }));
1489
1628
  else if (s.gold === false) badges.appendChild(el("span", { class: "badge ds-excluded", text: "除外" }));
1490
1629
  if (s.notify === true) badges.appendChild(el("span", { class: "badge ds-notify", text: "通知先" }));
1491
1630
  var gate = DS_GATE[s.gate] || DS_GATE["unknown"];
1492
- return el("div", { class: "ds-card" }, [
1631
+ var card = el("div", { class: "ds-card" }, [
1493
1632
  el("div", { class: "ds-card-main" }, [
1494
1633
  el("span", { class: "ds-icon", text: meta.icon }),
1495
1634
  el("span", { class: "ds-type", text: meta.label }),
@@ -1498,6 +1637,12 @@ const CLIENT_JS = `
1498
1637
  badges,
1499
1638
  el("span", { class: "ds-gate " + gate.cls, text: gate.text }),
1500
1639
  ]);
1640
+ // Gold昇格の切り替え。書き込み経路(intake Lambda)がある案件でだけ出す。
1641
+ // Slack以外は読み手が導出しないので出さない(押せても効かないボタンは出さない)。
1642
+ if (SITE.intake && s.type === "slack" && s.ref) {
1643
+ card.appendChild(goldToggle(s));
1644
+ }
1645
+ return card;
1501
1646
  }
1502
1647
 
1503
1648
  // ---------- 操作タブ(接続マップ・データソースと自動化・取り込み。今後この受け皿に機能を足す) ----------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.4.44",
3
+ "version": "0.4.47",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -29,6 +29,7 @@
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/node": "^22.10.0",
32
+ "linkedom": "^0.18.13",
32
33
  "typescript": "^5.7.0"
33
34
  }
34
35
  }