@takagaki/cortex-decisions-viewer 0.12.55 → 0.12.57

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.
@@ -202,9 +202,17 @@ function backlogTargetOf(input) {
202
202
  function pendingIsSlackTeamId(v) {
203
203
  return /^[TE][A-Z0-9]{6,20}$/.test(String(v == null ? "" : v));
204
204
  }
205
- /** Backlog の接続先の鍵(render.ts の issuesRefOf と同形: ドメイン小文字 / キー大文字) */
205
+ /**
206
+ * Backlog の接続先の鍵(render.ts の issuesRefOf と同形: ドメイン小文字 / キー大文字)。
207
+ *
208
+ * 🔴 **規則の正本は `infra/lambda/shared/connection-state-keys.mjs` の `issuesRefOf`**(#556)。
209
+ * ここも render.ts も**写し**で、ずれると仮置きの札が実測の行に重ならない
210
+ * (押した直後の札が消えないまま、別の行に本物が出る)。
211
+ * ずれを捕まえる網は `viewer/test/state-ref-parity.test.mjs`。
212
+ */
206
213
  function pendingIssuesRef(domain, projectKey) {
207
- return String(domain == null ? "" : domain).toLowerCase() + "/" + String(projectKey == null ? "" : projectKey).toUpperCase();
214
+ return String(domain == null ? "" : domain).trim().toLowerCase() + "/" +
215
+ String(projectKey == null ? "" : projectKey).trim().toUpperCase();
208
216
  }
209
217
  // ---- エントリを作る(op → kind / action / targetId の対応表はここ 1 つ) ------------
210
218
  /**
@@ -277,8 +285,13 @@ function pendingOpOf(payload, ctx) {
277
285
  const name = pendingLabel(payload.name);
278
286
  return name ? make("slack", "update", payload.ref, { name: name }) : null;
279
287
  }
280
- case "set-notify":
281
- return typeof payload.value === "boolean" ? make("slack", "update", payload.ref, { notify: payload.value }) : null;
288
+ case "set-notify": {
289
+ if (typeof payload.value !== "boolean")
290
+ return null;
291
+ // op は 1 つ。ref の形(spaces/…)で Google Chat と決める(サーバの notifyRefKind と同じ・#572)
292
+ const kind = pendingIsSpaceRef(payload.ref) ? "google-chat" : "slack";
293
+ return make(kind, "update", payload.ref, { notify: payload.value });
294
+ }
282
295
  case "add-google-chat-space": {
283
296
  const ref = pendingGoogleChatSpaceRef(payload.url);
284
297
  // 名前は画面に欄が無い(サーバが引く)。live が来るまでは ref で出す
@@ -660,6 +673,9 @@ function pendingExternalRow(e) {
660
673
  row.type = "google-chat";
661
674
  row.name = p.name || e.targetId;
662
675
  row.ref = e.targetId;
676
+ // add に畳まれた update(通知先 ON)を落とさない(#572・Slack と同じ)
677
+ if (p.notify === true)
678
+ row.notify = true;
663
679
  }
664
680
  else {
665
681
  row.type = p.type || "github-issues";
@@ -734,14 +750,23 @@ function applyPendingOps(view, list) {
734
750
  extChanged = true;
735
751
  }
736
752
  else if (e.action === "add") {
737
- if (idx >= 0)
753
+ if (idx >= 0) {
754
+ // 本物の行が先に来ている。**add に畳まれた update(通知先の向き)だけは当てる**
755
+ // (#572)——畳んだまま捨てると、追加 → 5 分以内に本物の行 → 通知先 ON の順で
756
+ // 押した人の札が、据え置きの live で戻る
757
+ if (typeof p.notify === "boolean" && (externals[idx].notify === true) !== p.notify) {
758
+ externals[idx] = Object.assign({}, externals[idx], { notify: p.notify });
759
+ extChanged = true;
760
+ }
738
761
  continue;
762
+ }
739
763
  externals.push(pendingExternalRow(e));
740
764
  extChanged = true;
741
765
  }
742
- else if (e.kind === "slack" && idx >= 0) {
766
+ else if ((e.kind === "slack" || e.kind === "google-chat") && idx >= 0) {
767
+ // update: Slack は name / notify、Google Chat は notify だけ(名前はサーバが引く・#572)
743
768
  const patch = {};
744
- if (typeof p.name === "string" && p.name)
769
+ if (e.kind === "slack" && typeof p.name === "string" && p.name)
745
770
  patch.name = p.name;
746
771
  if (typeof p.notify === "boolean")
747
772
  patch.notify = p.notify;
package/dist/render.js CHANGED
@@ -18,6 +18,7 @@ exports.sanitizeStateNow = sanitizeStateNow;
18
18
  exports.overlayStateOnFleet = overlayStateOnFleet;
19
19
  exports.chatRowsToAdd = chatRowsToAdd;
20
20
  exports.gchatRowsToAdd = gchatRowsToAdd;
21
+ exports.overlayGchatNotify = overlayGchatNotify;
21
22
  exports.devRowsToAdd = devRowsToAdd;
22
23
  exports.markExcludedPendingRows = markExcludedPendingRows;
23
24
  exports.designRowsToAdd = designRowsToAdd;
@@ -37,6 +38,10 @@ exports.sanitizeCapabilitiesNow = sanitizeCapabilitiesNow;
37
38
  exports.sanitizeGithubOwnersNow = sanitizeGithubOwnersNow;
38
39
  exports.splitNeededParts = splitNeededParts;
39
40
  exports.ingestedAtOf = ingestedAtOf;
41
+ exports.ingestedRowOf = ingestedRowOf;
42
+ exports.runLogUrlOf = runLogUrlOf;
43
+ exports.lastSyncRunUrlOf = lastSyncRunUrlOf;
44
+ exports.runSourceWordOf = runSourceWordOf;
40
45
  exports.fleetLastSyncOf = fleetLastSyncOf;
41
46
  exports.stateRefKey = stateRefKey;
42
47
  exports.registeredRefKeys = registeredRefKeys;
@@ -850,7 +855,8 @@ const REGISTERED_CAP_NAMES = {
850
855
  */
851
856
  const REGISTERED_FIELDS = {
852
857
  chat: ["ref", "name", "workspace", "orgUnknown", "origin", "goldState", "notify"],
853
- "google-chat": ["ref", "name"],
858
+ // notify(#572): スペースも通知先にできる。真偽値の扱いは chat notify と同じ篩
859
+ "google-chat": ["ref", "name", "notify"],
854
860
  dev: ["ref", "type"],
855
861
  // 🔴 **意図的に外した開発リポジトリ(#391)。** サーバは前から別キーで返していたが、
856
862
  // ここに無いので篩が丸ごと落としていた。**これが読めないと「読み取り停止中」の行を
@@ -977,7 +983,23 @@ const STATE_STATUSES = {
977
983
  // 「作成済み」の行が overlay に届く前に落ちて、画面は永久に「作成中」のまま
978
984
  calendar: ["requested", "ok", "failed"],
979
985
  };
980
- const STATE_ROW_FIELDS = ["cap", "ref", "workspace", "question", "status", "at", "by", "needed", "detail", "lastOkAt", "lastFailAt"];
986
+ const STATE_ROW_FIELDS = ["cap", "ref", "workspace", "question", "status", "at", "by", "needed", "detail", "lastOkAt", "lastFailAt", "runUrl"];
987
+ /**
988
+ * 行の `runUrl`(その行を書いた GitHub Actions の run・#560)に許す形。**原本は
989
+ * `infra/lambda/shared/connection-state-keys.mjs` の `RUN_URL_RE`**で、これはその写し
990
+ * (画面は `href` にそのまま置くので、intake が篩ったあとでも画面側でもう一度縛る)。
991
+ * 埋め込み JS には `var RUN_URL_RE` として注入する(export すると `exports.` 参照が客先へ
992
+ * 渡って画面が死ぬ——`state-vocab-parity.test.mjs` 冒頭の実害)。
993
+ */
994
+ const RUN_URL_RE = /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/actions\/runs\/[0-9]+$/;
995
+ /** `runUrl` を持てる書き手(Actions の run の中で動く engine の書き手)。ほかの書き手の値は出さない */
996
+ const RUN_URL_WRITERS = ["ingest", "gold", "nightly"];
997
+ /**
998
+ * `runUrl` が無い行の出どころの語(#560)。run が無い書き手だけ。**`ingest` / `gold` は
999
+ * 載せない**——`runUrl` が無いのは古い engine か固定 IP の CodeBuild で、行からは区別できない
1000
+ * (「Webhook」と断定すると嘘になる)
1001
+ */
1002
+ const RUN_SOURCE_WORDS = { central: "中央の処理", button: "その場で確認", auto: "保存時に確認" };
981
1003
  /** stateNow はライブ専用キーで build.ts の網にかからない。画面側でも許可リストを通す */
982
1004
  function sanitizeStateNow(v) {
983
1005
  if (!Array.isArray(v))
@@ -999,6 +1021,9 @@ function sanitizeStateNow(v) {
999
1021
  continue; // 知らない語は「不明」に化けさせず落とす
1000
1022
  if (!item.cap || !item.ref || !item.at)
1001
1023
  continue;
1024
+ // `runUrl` は run ページの形だけ(#560)。形が違えば**欄だけ落として行は残す**
1025
+ if (item.runUrl && !RUN_URL_RE.test(item.runUrl))
1026
+ delete item.runUrl;
1002
1027
  out.push(item);
1003
1028
  }
1004
1029
  return out.length ? out : null;
@@ -1079,6 +1104,8 @@ function overlayStateOnFleet(view, rows) {
1079
1104
  gateBy: hit.by,
1080
1105
  needed: hit.needed || "",
1081
1106
  detail: hit.detail || "",
1107
+ // その行を書いた run(#560)。無ければ空
1108
+ gateRunUrl: hit.runUrl || "",
1082
1109
  });
1083
1110
  });
1084
1111
  return changed ? Object.assign({}, v, { externalSources: externals }) : v;
@@ -1152,10 +1179,58 @@ function gchatRowsToAdd(externals, rows) {
1152
1179
  continue;
1153
1180
  if (have[String(r.ref)])
1154
1181
  continue;
1155
- out.push({ type: "google-chat", name: r.name || r.ref, ref: r.ref });
1182
+ const row = { type: "google-chat", name: r.name || r.ref, ref: r.ref };
1183
+ // 通知先の印(#572)。真偽値の印は true のときだけ載せる(chatRowsToAdd と同じ)
1184
+ if (r.notify === true)
1185
+ row.notify = true;
1186
+ out.push(row);
1156
1187
  }
1157
1188
  return out;
1158
1189
  }
1190
+ /**
1191
+ * まとめ表が既に持っている Google Chat の行に、live(registeredNow)の「通知先」の印を重ねる(#572)。
1192
+ *
1193
+ * 🔴 **engine のまとめ表(fleet-status)は Google Chat の行に `notify` を付けない**
1194
+ * (`resolve-external-sources.mjs` は Slack にだけ付ける)。重ねないと、押した直後の仮置きが
1195
+ * 切れたあと(最長 24 時間後)に、通知先のスペースが「通知先にする」に戻って見える
1196
+ * ——画面では通知先でないのに投稿される、という取り違えになる。
1197
+ *
1198
+ * **Slack の行には触らない**(まとめ表が正しく持っている・挙動を変えない)。live に
1199
+ * `google-chat` のキーが無ければ(読めていない)何も変えない。live に同じ ref の行があれば
1200
+ * その `notify` を正とし(true なら付け、そうでなければ外す)、live に無い行はそのまま
1201
+ * (消す判断は `subtractRemovedRegistered` の責務)。
1202
+ *
1203
+ * **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す)。
1204
+ */
1205
+ function overlayGchatNotify(externals, rows) {
1206
+ const list = externals || [];
1207
+ if (!Array.isArray(rows))
1208
+ return list;
1209
+ const live = {};
1210
+ for (const r of rows) {
1211
+ if (!r || !r.ref)
1212
+ continue;
1213
+ live[String(r.ref)] = r.notify === true;
1214
+ }
1215
+ let changed = false;
1216
+ const out = list.map(function (s) {
1217
+ if (!s || s.type !== "google-chat" || !s.ref)
1218
+ return s;
1219
+ if (!Object.prototype.hasOwnProperty.call(live, String(s.ref)))
1220
+ return s;
1221
+ const want = live[String(s.ref)];
1222
+ if ((s.notify === true) === want)
1223
+ return s;
1224
+ changed = true;
1225
+ const row = Object.assign({}, s);
1226
+ if (want)
1227
+ row.notify = true;
1228
+ else
1229
+ delete row.notify;
1230
+ return row;
1231
+ });
1232
+ return changed ? out : list;
1233
+ }
1159
1234
  /**
1160
1235
  * 開発の行で、まとめ表がまだ知らないもの。**鍵は `owner/repo` の生文字列。**
1161
1236
  *
@@ -1270,10 +1345,17 @@ function materialRowsToAdd(ids, rows) {
1270
1345
  * ⚠ 画面の `targetsGenerationOf`(サーバへ送る世代)は同じ規則を持つが、この関数を
1271
1346
  * 呼べない——配信物から関数1つを切り出して直接呼ぶテストがあり、外の名前を参照した
1272
1347
  * 瞬間にそのテストが動かなくなる。あちらを直すときはここも直す。
1348
+ *
1349
+ * 🔴 **規則の正本は `infra/lambda/shared/connection-state-keys.mjs` の `issuesRefOf`**(#556)。
1350
+ * ここはその**写し**で、`viewer/` は独立した npm パッケージ(`rootDir: src`)なので
1351
+ * 原本を import できない。**ずれを捕まえる網は契約表**——
1352
+ * `infra/test/fixtures/connection-state-contract.json` の `issuesRefOf` の節を、
1353
+ * 原本(infra 側のテスト)と写し(viewer/test/state-ref-parity.test.mjs)の**両方**に流す。
1354
+ * 片方だけ直したら、直していない側が赤くなる。
1273
1355
  */
1274
1356
  function issuesRefOf(domain, projectKey) {
1275
- return String(domain == null ? "" : domain).toLowerCase() + "/" +
1276
- String(projectKey == null ? "" : projectKey).toUpperCase();
1357
+ return String(domain == null ? "" : domain).trim().toLowerCase() + "/" +
1358
+ String(projectKey == null ? "" : projectKey).trim().toUpperCase();
1277
1359
  }
1278
1360
  /**
1279
1361
  * 課題管理の行で、まとめ表がまだ知らないもの。
@@ -1553,9 +1635,11 @@ function mergeRegisteredFleet(view, reg) {
1553
1635
  };
1554
1636
  if (!reg)
1555
1637
  return out;
1638
+ // Google Chat の行の「通知先」は live が正(#572・まとめ表は持っていない)。足す前に重ねる
1639
+ out.externalSources = overlayGchatNotify(v.externalSources, reg["google-chat"]);
1556
1640
  const add = registeredRowsToAdd(v, reg);
1557
1641
  if (add.externalSources.length)
1558
- out.externalSources = (v.externalSources || []).concat(add.externalSources);
1642
+ out.externalSources = (out.externalSources || []).concat(add.externalSources);
1559
1643
  if (add.design.length || add.materials.length) {
1560
1644
  const internals = (v.internalSources || []).slice();
1561
1645
  const put = (kind, patch) => {
@@ -1922,6 +2006,75 @@ function ingestedAtOf(rows, capId, refs) {
1922
2006
  }
1923
2007
  return best;
1924
2008
  }
2009
+ /**
2010
+ * `ingestedAtOf` と**同じ選び方**で、時刻ではなく**その行**を返す(#560)。「最終同期」の横に
2011
+ * 「実行ログ」を出すために要る。選び方をここに書き写さず、同じ規則で行を選ぶ
2012
+ * (分かれると「最終同期の時刻」と「そのリンク先の run」が別の行のものになる)。
2013
+ *
2014
+ * **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す)。
2015
+ * @returns 選んだ行。1件も無ければ null
2016
+ */
2017
+ function ingestedRowOf(rows, capId, refs) {
2018
+ if (!Array.isArray(rows) || !capId)
2019
+ return null;
2020
+ let best = "";
2021
+ let bestRow = null;
2022
+ for (const r of rows) {
2023
+ if (!r || r.cap !== capId || r.question !== "ingested")
2024
+ continue;
2025
+ if (refs && !refs[stateRefKey(capId, r.ref, r.workspace)])
2026
+ continue;
2027
+ const at = r.lastOkAt || (r.status === "ok" ? r.at : "");
2028
+ if (typeof at === "string" && at > best) {
2029
+ best = at;
2030
+ bestRow = r;
2031
+ }
2032
+ }
2033
+ return bestRow;
2034
+ }
2035
+ /**
2036
+ * 行の横に出す「実行ログ」のリンク先(#560)。**無ければ空文字。**
2037
+ *
2038
+ * 出すのは Actions の run の中で動く書き手(`RUN_URL_WRITERS`)の行だけ。ボタン・中央の
2039
+ * 書き手は intake が `runUrl` を消して書くが、画面側でも二重の網にする——残っていると
2040
+ * 「いま測った結果」の横に別の run のログが出る。形は `RUN_URL_RE`(`href` にそのまま置く)。
2041
+ *
2042
+ * **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す)。
2043
+ */
2044
+ function runLogUrlOf(row) {
2045
+ if (!row || typeof row.runUrl !== "string" || !RUN_URL_RE.test(row.runUrl))
2046
+ return "";
2047
+ if (RUN_URL_WRITERS.indexOf(String(row.by || "")) < 0)
2048
+ return "";
2049
+ return row.runUrl;
2050
+ }
2051
+ /**
2052
+ * 「最終同期: …」の横に出すリンク先(#560)。**その時刻を書いた run のときだけ。**
2053
+ *
2054
+ * 「最終同期」は `lastOkAt`(最後に取り込めた時刻)で、行の `runUrl` は**いまの status を
2055
+ * 書いた run**のもの。いま `failed` の行の `lastOkAt` が最大なら、その `runUrl` は失敗した run
2056
+ * を指すので出さない(間違った run へ送るより、無いほうがよい)。`ok` の行でも `lastOkAt` と
2057
+ * `statusAt` が違う(履歴だけ先に進んだ)回は出さない。
2058
+ *
2059
+ * **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す)。
2060
+ */
2061
+ function lastSyncRunUrlOf(row) {
2062
+ if (!row || row.status !== "ok")
2063
+ return "";
2064
+ if (row.lastOkAt && row.lastOkAt !== row.at)
2065
+ return "";
2066
+ return runLogUrlOf(row);
2067
+ }
2068
+ /**
2069
+ * `runUrl` の無い行に添える出どころの語(#560)。リンクが出る行・語の無い書き手は空文字。
2070
+ *
2071
+ * **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す)。
2072
+ */
2073
+ function runSourceWordOf(row) {
2074
+ if (!row || runLogUrlOf(row))
2075
+ return "";
2076
+ return RUN_SOURCE_WORDS[String(row.by || "")] || "";
2077
+ }
1925
2078
  /**
1926
2079
  * まとめ表(fleet-status.json)の「最終同期」を、画面に出してよい形で取る(#523 決定1)。
1927
2080
  *
@@ -1961,12 +2114,16 @@ function fleetLastSyncOf(internal) {
1961
2114
  * `issuesRefOf` はこれより**前**に流し込んである)。
1962
2115
  */
1963
2116
  function stateRefKey(capId, ref, workspace) {
1964
- var s = String(ref == null ? "" : ref);
2117
+ // 🔴 **`normalizeStateRef`(connection-state-keys.mjs)の写し**(#556)。ワークスペースを
2118
+ // 足すところだけが画面固有で、ref の畳み方そのものは向こうが正本
2119
+ // (契約表 `normalizeStateRef` の節を両側に流して突き合わせる)
2120
+ var s = String(ref == null ? "" : ref).trim();
1965
2121
  if (!s)
1966
2122
  return "";
1967
2123
  if (capId === "issues") {
1968
2124
  var at = s.indexOf("/");
1969
- if (at < 0)
2125
+ // ドメインもキーも欠けた値から `issues` の鍵は組めない(原本と同じ判断)
2126
+ if (at <= 0 || at === s.length - 1)
1970
2127
  return "";
1971
2128
  return issuesRefOf(s.slice(0, at), s.slice(at + 1));
1972
2129
  }
@@ -2000,24 +2157,27 @@ function registeredRefKeys(regKey, rows) {
2000
2157
  out[issuesRefOf(r.domain, r.projectKey)] = true;
2001
2158
  continue;
2002
2159
  }
2160
+ // 🔴 **相方(`stateRefKey`)と同じだけ畳む(#556)。** あちらが前後の空白を落とすので、
2161
+ // ここが落とさないと**登録してあるのに「登録簿に無い」と判定される**行ができる
2162
+ // (いまは登録簿の値がサーバ側で trim 済みなので起きないが、片側だけ畳む形を残さない)
2003
2163
  if (regKey === "design") {
2004
2164
  if (r.key)
2005
- out[String(r.key)] = true;
2165
+ out[String(r.key).trim()] = true;
2006
2166
  continue;
2007
2167
  }
2008
2168
  if (regKey === "materials") {
2009
2169
  if (r.id)
2010
- out[String(r.id)] = true;
2170
+ out[String(r.id).trim()] = true;
2011
2171
  continue;
2012
2172
  }
2013
2173
  if (!r.ref)
2014
2174
  continue;
2015
2175
  if (regKey === "chat") {
2016
- out[String(r.workspace == null ? "" : r.workspace) + "\t" + String(r.ref)] = true;
2017
- out["\t" + String(r.ref)] = true;
2176
+ out[String(r.workspace == null ? "" : r.workspace) + "\t" + String(r.ref).trim()] = true;
2177
+ out["\t" + String(r.ref).trim()] = true;
2018
2178
  continue;
2019
2179
  }
2020
- out[String(r.ref)] = true;
2180
+ out[String(r.ref).trim()] = true;
2021
2181
  }
2022
2182
  return out;
2023
2183
  }
@@ -3926,6 +4086,12 @@ a.ds-tile:hover { text-decoration: none; }
3926
4086
  .ds-all-connected { color: var(--muted); }
3927
4087
  .ds-gate { display: inline-flex; align-items: center; gap: 5px; font-size: 12.5px; font-weight: 600; flex: 0 0 auto; }
3928
4088
  .ds-gate-at { font-size: 11px; color: var(--muted); flex: 0 0 auto; }
4089
+ /* 実行ログへの導線(#560)。時刻の横の添え物なので .ds-gate-at と同じ弱さ。押せることだけ
4090
+ 点線の下線で示す(目立たせない——案件リポジトリに入っていない人には開けない) */
4091
+ .ds-run-link { font-size: 11px; color: var(--muted); flex: 0 0 auto; text-decoration: underline dotted; text-underline-offset: 2px; }
4092
+ .ds-run-link:hover { color: var(--text); }
4093
+ /* run の無い書き手の行の出どころ(「中央の処理」「その場で確認」)。同じ弱さの文字 */
4094
+ .ds-run-src { font-size: 11px; color: var(--muted); flex: 0 0 auto; }
3929
4095
  /* 接続先1件ごとの「取り込み」(段C・2026-08-24)。**「最終同期」(.ds-sync・run単位)とは
3930
4096
  別の行に並ぶ**ので、同じ強さにしない——run の時刻より弱い添え物として読ませる。
3931
4097
  失敗だけは .ds-gate-warn と同じ色にする(同じ画面で警告の色が2種類あると、
@@ -4445,6 +4611,7 @@ var PROBE_WAIT_TIMEOUT_TEXT = ${JSON.stringify(exports.PROBE_WAIT_TIMEOUT_TEXT)}
4445
4611
  ${sanitizeRegisteredNow.toString()}
4446
4612
  ${chatRowsToAdd.toString()}
4447
4613
  ${gchatRowsToAdd.toString()}
4614
+ ${overlayGchatNotify.toString()}
4448
4615
  ${devRowsToAdd.toString()}
4449
4616
  ${markExcludedPendingRows.toString()}
4450
4617
  ${designRowsToAdd.toString()}
@@ -4490,6 +4657,12 @@ ${pending_ops_1.pendingCountDelta.toString()}
4490
4657
  ${pending_ops_1.pendingIssuesRef.toString()}
4491
4658
  var STATE_STATUSES = ${JSON.stringify(STATE_STATUSES)};
4492
4659
  var STATE_ROW_FIELDS = ${JSON.stringify(STATE_ROW_FIELDS)};
4660
+ // 行の runUrl の形と、出どころの語(#560)。**正規表現は JSON にできない**ので
4661
+ // source から組み立てる。この定数行を落とすと sanitizeStateNow が未定義を参照して
4662
+ // ライブ取得が丸ごと死ぬ
4663
+ var RUN_URL_RE = new RegExp(${JSON.stringify(RUN_URL_RE.source)});
4664
+ var RUN_URL_WRITERS = ${JSON.stringify(RUN_URL_WRITERS)};
4665
+ var RUN_SOURCE_WORDS = ${JSON.stringify(RUN_SOURCE_WORDS)};
4493
4666
  ${sanitizeStateNow.toString()}
4494
4667
  ${overlayStateOnFleet.toString()}
4495
4668
  // Home の「使用ツール」(#275)。**判定と表示語の実体はビルド時の定義**——
@@ -4522,6 +4695,10 @@ ${stateRefKey.toString()}
4522
4695
  ${registeredRefKeys.toString()}
4523
4696
  ${fleetLastSyncOf.toString()}
4524
4697
  ${ingestedAtOf.toString()}
4698
+ ${ingestedRowOf.toString()}
4699
+ ${runLogUrlOf.toString()}
4700
+ ${lastSyncRunUrlOf.toString()}
4701
+ ${runSourceWordOf.toString()}
4525
4702
  var TILE_CAPS = ${JSON.stringify(TILE_CAPS)};
4526
4703
  var BAD_STATE_STATUSES = ${JSON.stringify(BAD_STATE_STATUSES)};
4527
4704
  // 登録直後の一時表示(#523 決定6)。**この定数行を落とすと、流し込んだ deriveCapTile が
@@ -8998,9 +9175,18 @@ ${calendarAddUrl.toString()}
8998
9175
  function capBlockSyncEl(s, inactive) {
8999
9176
  if (inactive || !s || !s.kind) return null;
9000
9177
  var capId = capIdOf(s.kind);
9001
- var at = capId ? ingestedAtOf(LIVE_STATE, capId, refKeysOf(derCapOf(s.kind, ""))) : "";
9178
+ var refs = capId ? refKeysOf(derCapOf(s.kind, "")) : null;
9179
+ var at = capId ? ingestedAtOf(LIVE_STATE, capId, refs) : "";
9002
9180
  var t = dsFmtTime(at || fleetSyncFallback(s) || "");
9003
- return t ? el("span", { class: "ds-sync", text: "最終同期: " + t }) : null;
9181
+ if (!t) return null;
9182
+ var sync = el("span", { class: "ds-sync", text: "最終同期: " + t });
9183
+ // その時刻を書いた run へ(#560)。**ストア由来の時刻のときだけ**——まとめ表の後詰めに
9184
+ // run は無い。行は「ingestedAtOf」 と同じ選び方(「ingestedRowOf」)で取る。
9185
+ // run の無い書き手(中央 Apps Script の会議・資料の取り込み)は出どころの語
9186
+ var row = at ? ingestedRowOf(LIVE_STATE, capId, refs) : null;
9187
+ var url = lastSyncRunUrlOf(row);
9188
+ var trail = url ? [runLogLink(url)] : row ? runTrailNodes(row, "") : [];
9189
+ return trail.length ? frag([sync].concat(trail)) : sync;
9004
9190
  }
9005
9191
 
9006
9192
  if (internals.length) {
@@ -10365,6 +10551,8 @@ ${calendarAddUrl.toString()}
10365
10551
  // 古い「X分前に確認」を残さない(下に出るいまの結果と、時刻が2つ並ぶ)
10366
10552
  var oldAt = card.querySelector(".ds-gate-at");
10367
10553
  if (oldAt) oldAt.remove();
10554
+ // 古い run の導線も残さない(#560)。「いま測った結果」の横に夜間の run のログが並ぶ形になる
10555
+ removeRunTrail(card);
10368
10556
  // 🔴 **古いスコープ名の札も必ず消してから出し直す。** 行には夜間の判定に添えた
10369
10557
  // 札が出ていることがあり(段B)、置き換えた「接続OK」の横に古いスコープ名が
10370
10558
  // 残ると、同じ行が2つのことを言う——この節が何度も直してきた形そのもの。
@@ -10503,6 +10691,9 @@ ${calendarAddUrl.toString()}
10503
10691
  "次回の Gold 昇格(毎晩22時ごろ)から、このスペースは読み取られなくなります。",
10504
10692
  "",
10505
10693
  "※ 既に Gold 層に入った内容は消えません(Decision・用語集はそのまま残ります)。",
10694
+ // 解除は通知先の宣言(notify.json)を触らない(#572)。通知先のまま解除すると
10695
+ // 行が消えて画面から止められなくなる——先に止める手順をここで言う
10696
+ "※ 通知先にしている場合は、先に「通知を止める」を押してください(解除しても通知先は変わりません)。",
10506
10697
  "",
10507
10698
  "解除しますか?",
10508
10699
  ];
@@ -10561,6 +10752,12 @@ ${calendarAddUrl.toString()}
10561
10752
  *
10562
10753
  * **クロージャを持たない純関数に保つこと**(テストが配信物から切り出して直接呼ぶ)。
10563
10754
  * だから issuesRefOf を呼べない(規則の正本はあちら側。直すときは両方直す)。
10755
+ *
10756
+ * ⚠ **ここは前後の空白を落とさない。行の鍵(issuesRefOf)とは別物**(#556)。
10757
+ * 揃えたくなるが、**揃えなくても食い違わない**——サーバは受け取った世代を
10758
+ * normalizeGeneration に通してから比べ、そちらが空白を落とすため。逆に、ここで
10759
+ * 落とす形に変えるなら**サーバの両方**(targetsGeneration と normalizeGeneration)を
10760
+ * 同時に見ること。善意で片側だけ揃えると、正しい操作が毎回 409 になる。
10564
10761
  */
10565
10762
  function targetsGenerationOf(list) {
10566
10763
  return (list || [])
@@ -10810,13 +11007,66 @@ ${calendarAddUrl.toString()}
10810
11007
  // cap と question の両方で絞る。片方だけだと、同じ ref を持つ別の問い
10811
11008
  //(将来 issues に別の question が増えたとき)の行を取り込みとして読む
10812
11009
  if (!r || r.cap !== "issues" || r.question !== "ingested") continue;
10813
- var at = String(r.ref || "").indexOf("/");
10814
- if (at < 0) continue;
10815
- if (issuesRefOf(String(r.ref).slice(0, at), String(r.ref).slice(at + 1)) === want) return r;
11010
+ // 行側も畳み方の正本(stateRefKey normalizeStateRef の写し)を通す(#556)。
11011
+ // ここで indexOf を自前で切っていた間は、**切り方が5本目**になっていた
11012
+ if (stateRefKey("issues", r.ref) === want) return r;
10816
11013
  }
10817
11014
  return null;
10818
11015
  }
10819
11016
 
11017
+ /* -------------------------------------------------------------------------
11018
+ * 実行ログへの導線(#560)
11019
+ * -------------------------------------------------------------------------
11020
+ * 行の 「runUrl」(その行を書いた Actions の run)があれば、時刻の横に小さな「実行ログ ↗」を
11021
+ * 出す。run の無い書き手の行は、出どころを語で示す(「中央の処理」「その場で確認」)。
11022
+ * どちらを出すかの判断は純関数(runLogUrlOf / runSourceWordOf)が持ち、ここは描くだけ。
11023
+ * ----------------------------------------------------------------------- */
11024
+
11025
+ /** リンクの title。案件リポジトリに入っていない人には開けないので、押す前に分かるようにする */
11026
+ var RUN_LINK_TITLE = "GitHub Actions の実行ログを開きます(案件リポジトリへのアクセスが要ります)";
11027
+ /** 一覧(開始直後・run の番号がまだ無い)の title。飛び先が違うので文言も分ける */
11028
+ var RUN_LIST_TITLE = "GitHub Actions の実行の一覧を開きます(案件リポジトリへのアクセスが要ります)";
11029
+ var RUN_LIST_TEXT = "実行の一覧 ↗";
11030
+
11031
+ /** 「実行ログ ↗」(新しいタブ・title つき)。一覧のときは文言と title を一覧のものにする */
11032
+ function runLogLink(url, text) {
11033
+ var list = text === RUN_LIST_TEXT;
11034
+ return el("a", { class: "ds-run-link", href: url, target: "_blank", rel: "noopener", title: list ? RUN_LIST_TITLE : RUN_LINK_TITLE, text: text || "実行ログ ↗" });
11035
+ }
11036
+
11037
+ /**
11038
+ * 行の時刻の横に添えるもの(リンクか出どころの語)。**無ければ空配列。**
11039
+ * 「.ds-gate-at」「.ds-ingest」 の**兄弟**として置く(子にすると、文言の検査と
11040
+ * 「いま測った結果」への差し替えが崩れる)。
11041
+ *
11042
+ * @param kind どの時刻に添えたか(「gate」=「◯分前に確認」)。「いま測った結果」への差し替えは
11043
+ * **この印のものだけ**消す——「取り込み: 〜に成功」の横のリンク(別の行の run)は残す
11044
+ */
11045
+ function runTrailNodes(row, url, kind) {
11046
+ var u = url === undefined ? runLogUrlOf(row) : url;
11047
+ var word = u ? "" : runSourceWordOf(row);
11048
+ var node = u ? runLogLink(u) : word ? el("span", { class: "ds-run-src", text: word }) : null;
11049
+ if (!node) return [];
11050
+ if (kind) node.setAttribute("data-trail", kind);
11051
+ return [node];
11052
+ }
11053
+
11054
+ /** 複数のノードを 1 つの子として返す(「el」 の children に並べるため) */
11055
+ function frag(nodes) {
11056
+ var f = document.createDocumentFragment();
11057
+ (nodes || []).forEach(function (n) { if (n) f.appendChild(n); });
11058
+ return f;
11059
+ }
11060
+
11061
+ /**
11062
+ * 「いま測った結果」に差し替えるとき、**「◯分前に確認」に添えた**古い run の導線を外す
11063
+ * (同じ行が 2 つのことを言わない)。「取り込み: 〜に成功」の横のリンクは別の行の run なので残す
11064
+ */
11065
+ function removeRunTrail(scope) {
11066
+ var old = scope.querySelectorAll('.ds-run-link[data-trail="gate"], .ds-run-src[data-trail="gate"]');
11067
+ for (var i = 0; i < old.length; i++) old[i].remove();
11068
+ }
11069
+
10820
11070
  /**
10821
11071
  * 接続先1件の「取り込み」の表示。**測っていなければ何も出さない(null)。**
10822
11072
  *
@@ -10839,10 +11089,12 @@ ${calendarAddUrl.toString()}
10839
11089
  // 語は鍵カードの名前と揃える(keyMissingText の項)
10840
11090
  else if (r.status === "skipped") text = "取り込み: 未実行(" + keyMissingText(cap) + ")";
10841
11091
  if (!text) return null;
10842
- return el("span", {
11092
+ var span = el("span", {
10843
11093
  class: "ds-ingest" + (r.status === "failed" ? " ds-ingest-failed" : ""),
10844
11094
  text: text,
10845
11095
  });
11096
+ // その行を書いた run へ(#560)。成功でも失敗でも、この時刻を書いた run のログ
11097
+ return frag([span].concat(runTrailNodes(r)));
10846
11098
  }
10847
11099
 
10848
11100
  /**
@@ -10884,6 +11136,8 @@ ${calendarAddUrl.toString()}
10884
11136
  row.appendChild(el("span", { class: "ds-gate " + gate.cls, text: gate.text }));
10885
11137
  var when = rtAgo(r.at);
10886
11138
  if (when) row.appendChild(el("span", { class: "ds-gate-at", text: when + "に確認" }));
11139
+ // その行を書いた run へ(#560)。run の無い書き手は出どころの語
11140
+ runTrailNodes(r, undefined, "gate").forEach(function (n) { row.appendChild(n); });
10887
11141
  return row;
10888
11142
  }
10889
11143
 
@@ -11032,12 +11286,30 @@ ${calendarAddUrl.toString()}
11032
11286
  at: (before && before.at) || "",
11033
11287
  startedAt: Date.now(),
11034
11288
  message: (j && j.message) || "",
11289
+ // 起こした run(特定できた回)と一覧(#560)。描き直しでもリンクが消えないよう控える
11290
+ runUrl: (j && RUN_URL_RE.test(String(j.runUrl || ""))) ? String(j.runUrl) : "",
11291
+ runsUrl: (j && /^https:\\/\\/github\\.com\\/[^\\s"'<>]+$/.test(String(j.runsUrl || ""))) ? String(j.runsUrl) : "",
11035
11292
  };
11036
- note.textContent = (j && j.message) || "";
11293
+ probeWaitNote(note, PROBE_WAITS[key]);
11037
11294
  // 待っているあいだは取り直しを回す(届いた行で一覧が描き直される)
11038
11295
  refreshStateTimer();
11039
11296
  }
11040
11297
 
11298
+ /**
11299
+ * 待ちの 1 文と、その横の導線(#560)。**その run を特定できていればそれ、無ければ一覧。**
11300
+ * 開始直後は run の番号が無いことが多いので一覧へ。行が更新されて待ちが解けたら、
11301
+ * 行側の「実行ログ ↗」(その run)に置き換わる。run の無い経路(固定 IP・中央)は何も出ない
11302
+ */
11303
+ function probeWaitNote(note, wait) {
11304
+ note.textContent = (wait && wait.message) || "";
11305
+ if (!wait) return;
11306
+ var link = wait.runUrl ? runLogLink(wait.runUrl) : wait.runsUrl ? runLogLink(wait.runsUrl, RUN_LIST_TEXT) : null;
11307
+ if (link) {
11308
+ note.appendChild(document.createTextNode(" "));
11309
+ note.appendChild(link);
11310
+ }
11311
+ }
11312
+
11041
11313
  /**
11042
11314
  * 即答(#548 の direct)を行に映す。**「probeSection」 と同じ見た目にする**——
11043
11315
  * 夜間のスナップショットの判定といま測った判定を**同じ行に並べない**ため
@@ -11074,6 +11346,8 @@ ${calendarAddUrl.toString()}
11074
11346
  row.insertBefore(at, anchor);
11075
11347
  }
11076
11348
  at.textContent = "いま測った結果(" + (dsFmtTime(j.checkedAt) || "たった今") + ")";
11349
+ // 古い run の導線も外す(#560)。残すと「いま測った結果」の横に夜間の run のログが並ぶ
11350
+ removeRunTrail(row);
11077
11351
  // **夜間の判定に添えてあった案内は隠す。** 置き換えた判定と食い違う案内が下に残ると、
11078
11352
  // 同じ行が2つのことを言う(probeSection の stale 処理と同じ)
11079
11353
  var stale = row.querySelectorAll(".ds-gate-help");
@@ -11111,8 +11385,13 @@ ${calendarAddUrl.toString()}
11111
11385
  var note = el("p", { class: "ds-probe-wait" });
11112
11386
  var phase = probeWaitPhaseOf(capId, ref);
11113
11387
  var wait = probeWaitOf(capId, ref);
11114
- if (phase === "waiting") note.textContent = (wait && wait.message) || "";
11115
- else if (phase === "timeout") note.textContent = PROBE_WAIT_TIMEOUT_TEXT;
11388
+ if (phase === "waiting") probeWaitNote(note, wait);
11389
+ else if (phase === "timeout") {
11390
+ // 15 分待って届かないなら見に行く番。**行き先を添える**(#560)——一覧でも run でも
11391
+ note.textContent = PROBE_WAIT_TIMEOUT_TEXT;
11392
+ var to = wait && (wait.runUrl || wait.runsUrl);
11393
+ if (to) { note.appendChild(document.createTextNode(" ")); note.appendChild(runLogLink(to, wait.runUrl ? "実行ログ ↗" : RUN_LIST_TEXT)); }
11394
+ }
11116
11395
  else note.hidden = true;
11117
11396
 
11118
11397
  checkBtn.addEventListener("click", async function () {
@@ -12427,7 +12706,7 @@ ${calendarAddUrl.toString()}
12427
12706
  // 起動できなかった案件(権限・スタブ未配布)ではリンクを出さない——押しても何も無い
12428
12707
  if (j.runsUrl) {
12429
12708
  msg.appendChild(document.createTextNode(" "));
12430
- msg.appendChild(el("a", { href: j.runsUrl, target: "_blank", rel: "noopener", text: "実行状況を見る →" }));
12709
+ msg.appendChild(el("a", { href: j.runUrl || j.runsUrl, target: "_blank", rel: "noopener", title: RUN_LINK_TITLE, text: "実行状況を見る →" })); // その run を特定できていればそれ(#560)、無ければ一覧
12431
12710
  }
12432
12711
  // **「反映の状況 →」のリンクは無くなった(#221・2026-08-25)。** 指していたのは
12433
12712
  // 「画面を追いつかせるための fleet-status の実行」で、その起動ごと撤去した
@@ -12977,6 +13256,8 @@ ${calendarAddUrl.toString()}
12977
13256
  var gateAtEl = s.gateAt
12978
13257
  ? el("span", { class: "ds-gate-at", text: rtAgo(s.gateAt) + "に確認" })
12979
13258
  : null;
13259
+ // その行を書いた run へ(#560)。overlay が写した 「gateRunUrl」 / 「gateBy」 から決める
13260
+ var gateTrail = s.gateAt ? runTrailNodes({ runUrl: s.gateRunUrl || "", by: s.gateBy || "" }, undefined, "gate") : [];
12980
13261
  var card = el("div", { class: "ds-card" }, [
12981
13262
  el("div", { class: "ds-card-main" }, [
12982
13263
  el("span", { class: "ds-icon", text: meta.icon }),
@@ -12999,6 +13280,7 @@ ${calendarAddUrl.toString()}
12999
13280
  card.appendChild(el("code", { class: "ds-probe-scope", text: String(s.needed) }));
13000
13281
  }
13001
13282
  if (gateAtEl) card.appendChild(gateAtEl);
13283
+ gateTrail.forEach(function (n) { card.appendChild(n); });
13002
13284
  // チャンネルの登録解除。書き込み経路(intake Lambda)がある案件でだけ出す。
13003
13285
  // **Slack だけ。** github は登録ファイルが別(dev-sources.json)で、下のトグルと
13004
13286
  // 「登録から削除」が担う。
@@ -13021,6 +13303,11 @@ ${calendarAddUrl.toString()}
13021
13303
  // だけなので、Slack のような origin の出し分けは不要
13022
13304
  if (SITE.intake && s.type === "google-chat" && s.ref) {
13023
13305
  card.appendChild(removeSpaceButton(s, card));
13306
+ // 通知先の入り切り(#572)。Slack と同じ notifyToggle・同じ文言・同じ op(サーバは
13307
+ // ref の形(spaces/…)で Google Chat と判定する)。スペースの行に workspace は無いので
13308
+ // 「読み取り専用の接続」の分岐には入らず、常にボタンが出る。仮置きの行(本物の行が
13309
+ // 来るまで)には Slack と同じく出さない
13310
+ if (s.pending !== true) card.appendChild(notifyToggle(s, badges));
13024
13311
  }
13025
13312
  // GitHubリポの登録から削除。**ON/OFF のトグルは撤去した(2026-08-21)**
13026
13313
  //(stateToggle のコメント参照)。
@@ -14027,7 +14314,7 @@ ${calendarAddUrl.toString()}
14027
14314
  // 起動できたときだけ、実行状況への導線を出す(鍵を入れた直後に効いたか確かめられる)
14028
14315
  if (j.runsUrl) {
14029
14316
  msg.appendChild(document.createTextNode(" "));
14030
- msg.appendChild(el("a", { href: j.runsUrl, target: "_blank", rel: "noopener", text: "実行状況を見る →" }));
14317
+ msg.appendChild(el("a", { href: j.runUrl || j.runsUrl, target: "_blank", rel: "noopener", title: RUN_LINK_TITLE, text: "実行状況を見る →" })); // その run を特定できていればそれ(#560)、無ければ一覧
14031
14318
  }
14032
14319
  // 入れた値は画面に残さない(肩越しに見えるのを防ぐ)
14033
14320
  input.value = "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.55",
3
+ "version": "0.12.57",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,