@takagaki/cortex-decisions-viewer 0.12.48 → 0.12.50

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
@@ -430,6 +430,9 @@ async function readFleetSources(repoRoot) {
430
430
  label: String(s.label ?? ""),
431
431
  url: s.url != null ? String(s.url) : undefined,
432
432
  lastSync: s.lastSync != null ? String(s.lastSync) : undefined,
433
+ // 🔴 **時刻と一緒に「誰が測ったか」も通す(#523 決定1)。** これを落とすと、
434
+ // 画面は run の成功時刻(空振りの run を含む)を「最終同期」として出し続ける
435
+ lastSyncSource: s.lastSyncSource != null ? String(s.lastSyncSource) : undefined,
433
436
  // 取り込み済みの件数。**「同期が走った」と「実際に入った」は別物。**
434
437
  // これが無いと、初回全量同期をしていない案件が「接続OK」に見える
435
438
  itemCount: typeof s.itemCount === "number" ? s.itemCount : undefined,
package/dist/render.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.UNSETTLED_LIMITS = exports.ALREADY_REGISTERED = exports.LIVE_CONFIRM_LIMITS = exports.CLAUDE_CODE_LINK = void 0;
3
+ exports.STATE_CONFIRMING = exports.UNSETTLED_LIMITS = exports.ALREADY_REGISTERED = exports.LIVE_CONFIRM_LIMITS = exports.CLAUDE_CODE_LINK = void 0;
4
4
  exports.renderSite = renderSite;
5
5
  exports.renderSiteWithAssets = renderSiteWithAssets;
6
6
  exports.viewerAssetSources = viewerAssetSources;
@@ -36,6 +36,9 @@ exports.sanitizeCapabilitiesNow = sanitizeCapabilitiesNow;
36
36
  exports.sanitizeGithubOwnersNow = sanitizeGithubOwnersNow;
37
37
  exports.splitNeededParts = splitNeededParts;
38
38
  exports.ingestedAtOf = ingestedAtOf;
39
+ exports.fleetLastSyncOf = fleetLastSyncOf;
40
+ exports.stateRefKey = stateRefKey;
41
+ exports.registeredRefKeys = registeredRefKeys;
39
42
  exports.keyMissingText = keyMissingText;
40
43
  exports.isCalendarId = isCalendarId;
41
44
  exports.calendarStateRow = calendarStateRow;
@@ -1860,20 +1863,124 @@ function splitNeededParts(text) {
1860
1863
  *
1861
1864
  * @param capId 応答側の能力名(issues / design / materials / meetings)
1862
1865
  * @returns ISO文字列。1件も無ければ ""
1866
+ * @param refs 登録簿にある ref の鍵(`registeredRefKeys`)。**渡されたときだけ篩う**
1867
+ * ——孤児行(外した接続先の残骸)の `lastOkAt` を「最終同期」として出さないため。
1868
+ * 分からない(null / undefined)ときは篩わない(#523 S1・deriveCapTile と同じ規律)
1863
1869
  */
1864
- function ingestedAtOf(rows, capId) {
1870
+ function ingestedAtOf(rows, capId, refs) {
1865
1871
  if (!Array.isArray(rows) || !capId)
1866
1872
  return "";
1867
1873
  let best = "";
1868
1874
  for (const r of rows) {
1869
1875
  if (!r || r.cap !== capId || r.question !== "ingested")
1870
1876
  continue;
1877
+ if (refs && !refs[stateRefKey(capId, r.ref, r.workspace)])
1878
+ continue;
1871
1879
  const at = r.lastOkAt || (r.status === "ok" ? r.at : "");
1872
1880
  if (typeof at === "string" && at > best)
1873
1881
  best = at;
1874
1882
  }
1875
1883
  return best;
1876
1884
  }
1885
+ /**
1886
+ * まとめ表(fleet-status.json)の「最終同期」を、画面に出してよい形で取る(#523 決定1)。
1887
+ *
1888
+ * 🔴 **run 由来の時刻は捨てる。** `lastSync` には2つの意味が入っている——
1889
+ * 接続状態ストアの `lastOkAt`(実際に取り込めた時刻・`lastSyncSource === "ingested"`)と、
1890
+ * ワークフローが成功した時刻(`"run"`。鍵が無くて1件も取り込めていない run も
1891
+ * 「成功」で終わる)。後者を「最終同期」として出すと、**登録したのに何も取り込めて
1892
+ * いない案件が「昨日同期できている」と読める**(engine #314 の「最終同期 9/8 18:34」)。
1893
+ *
1894
+ * 🔴 **欄が無い生成物も run 扱い(捨てる)。** この欄より前の engine が作った
1895
+ * fleet-status.json には `lastSyncSource` が無い。「どちらか分からない時刻」を出すのは
1896
+ * 嘘をつきうる側なので、出さない方へ倒す(決定1の趣旨)。
1897
+ *
1898
+ * **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す)。
1899
+ */
1900
+ function fleetLastSyncOf(internal) {
1901
+ if (!internal || typeof internal !== "object")
1902
+ return "";
1903
+ if (internal.lastSyncSource !== "ingested")
1904
+ return "";
1905
+ return typeof internal.lastSync === "string" ? internal.lastSync : "";
1906
+ }
1907
+ /**
1908
+ * 接続状態ストアの行の `ref` を、**登録簿との突き合わせ鍵**に畳む(#523 S1・設計v2 §2 の表)。
1909
+ *
1910
+ * 🔴 **能力ごとに鍵が違う。** `registeredRefKeys` と**必ず同じ畳み方**にすること
1911
+ * ——片方だけ変えると、登録してある接続先の行が「登録簿に無い」と判定され、
1912
+ * **実測が届いているのに画面が黙る**(いま直そうとしている嘘の裏返し)。
1913
+ * - issues … `issuesRefOf`(ドメイン小文字 / キー大文字)。設定ファイルの値は人が
1914
+ * 書いたもので大小が揃っていない
1915
+ * - materials / design … folderId / fileKey の**生値**(どちらも大小を区別する不透明ID)
1916
+ * - chat … ワークスペース + チャンネルID(IDはワークスペースの中でしか一意でない。
1917
+ * `overlayStateOnFleet` と同じ鍵)
1918
+ * - google-chat / dev … ref の生値(ワークスペースを持たない)
1919
+ *
1920
+ * **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す。
1921
+ * `issuesRefOf` はこれより**前**に流し込んである)。
1922
+ */
1923
+ function stateRefKey(capId, ref, workspace) {
1924
+ var s = String(ref == null ? "" : ref);
1925
+ if (!s)
1926
+ return "";
1927
+ if (capId === "issues") {
1928
+ var at = s.indexOf("/");
1929
+ if (at < 0)
1930
+ return "";
1931
+ return issuesRefOf(s.slice(0, at), s.slice(at + 1));
1932
+ }
1933
+ if (capId === "chat")
1934
+ return String(workspace == null ? "" : workspace) + "\t" + s;
1935
+ return s;
1936
+ }
1937
+ /**
1938
+ * 登録簿(`registeredNow` の1能力分)から、突き合わせ鍵の集合を作る(#523 S1)。
1939
+ *
1940
+ * 🔴 **チャットは鍵を2本入れる。** 正式な鍵は `ワークスペース + ref` だが、ストアの行が
1941
+ * ワークスペースを持たない場合は厳密な鍵を組めない。そこで `"" + \t + ref` も入れて
1942
+ * **「分からないものは落とさない」**側へ倒す(篩い過ぎ=画面が黙る、が一番重い事故)。
1943
+ * 逆に、登録されていないワークスペースの同IDは落ちる(設計どおり)。
1944
+ *
1945
+ * @param regKey `registeredNow` のキー(chat / google-chat / dev / design / materials / issues)
1946
+ * @param rows その能力の登録行。配列でなければ null(=分からない。篩わない)
1947
+ *
1948
+ * **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す)。
1949
+ */
1950
+ function registeredRefKeys(regKey, rows) {
1951
+ if (!Array.isArray(rows))
1952
+ return null;
1953
+ var out = {};
1954
+ for (var i = 0; i < rows.length; i++) {
1955
+ var r = rows[i];
1956
+ if (!r || typeof r !== "object")
1957
+ continue;
1958
+ if (regKey === "issues") {
1959
+ if (r.domain && r.projectKey)
1960
+ out[issuesRefOf(r.domain, r.projectKey)] = true;
1961
+ continue;
1962
+ }
1963
+ if (regKey === "design") {
1964
+ if (r.key)
1965
+ out[String(r.key)] = true;
1966
+ continue;
1967
+ }
1968
+ if (regKey === "materials") {
1969
+ if (r.id)
1970
+ out[String(r.id)] = true;
1971
+ continue;
1972
+ }
1973
+ if (!r.ref)
1974
+ continue;
1975
+ if (regKey === "chat") {
1976
+ out[String(r.workspace == null ? "" : r.workspace) + "\t" + String(r.ref)] = true;
1977
+ out["\t" + String(r.ref)] = true;
1978
+ continue;
1979
+ }
1980
+ out[String(r.ref)] = true;
1981
+ }
1982
+ return out;
1983
+ }
1877
1984
  /**
1878
1985
  * タイルと接続グラフの導出に要る、能力ごとの材料(0.12.0・#231 / #235 / #227)。
1879
1986
  *
@@ -1884,24 +1991,51 @@ function ingestedAtOf(rows, capId) {
1884
1991
  * - `noFact` … 規則4(登録はあるが実績が無い)の文言。**能力ごとに待っているものが違う**
1885
1992
  * ので1文に丸めない。会議・共有資料は中央 Apps Script の取り込み待ち(=まだ使っていない)、
1886
1993
  * 課題管理・デザインは夜間同期待ち、チャット・開発は実測待ち
1994
+ * - `alsoBad` … **実績の問いとは別に、悪ければ⚠️にする問い**(#523 S1・決定3)。
1995
+ * 取り込む4能力は `reachable`(届くか)も見る——Drive のフォルダの共有が外れると
1996
+ * `reachable=unreachable` になるが `ingested` の行は**書かれない**(測れていないので)ので、
1997
+ * `ingested` だけを見ていると「接続OK(最終同期 ◯日前)」と言い続ける。
1998
+ * **「悪い方」で判定する**(どちらかが悪ければ⚠️)。実績(規則3)は `question` の側だけで数える
1999
+ * ——reachable が ok でも、取り込みが1件も無ければ「まだ取り込みがありません」が事実
2000
+ * - `adminOnNoToken` … `reachable=no_token` を「読めていません」と言わない能力(決定3)。
2001
+ * 鍵の呼び名があれば「◯が未登録です」(画面の鍵カードと同じ語)、無ければ
2002
+ * 「管理者の設定が必要です」——Drive の資格情報は艦隊側にあり、案件の人には打つ手が無い。
2003
+ * **チャット・開発には付けない**(あちらの `no_token` は行バッジ側が
2004
+ * `DS_GATE_NO_TOKEN` で経路ごとに言い換えており、タイルの語を変えると二重管理になる)
2005
+ * - `refKeyed` … 登録簿(`registeredNow`)との突き合わせで孤児行を落とせる能力(#523 S1・決定2)。
2006
+ * 会議は接続先の登録簿を持たない(ref は固定の `transcripts`)ので篩わない
1887
2007
  *
1888
2008
  * **6能力すべてを載せること。** ここに無い能力は導出そのものが働かず、規則5
1889
2009
  * (fleet へのフォールバック)に落ちる=正本化の対象から静かに外れる。
1890
2010
  */
1891
2011
  const TILE_CAPS = {
1892
- "課題管理": { id: "issues", question: "ingested", noFact: "登録済み(まだ取り込みがありません)", keyLabel: "APIキー" },
1893
- "会議": { id: "meetings", question: "ingested", noFact: "未使用(まだ取り込みがありません)", keyLabel: "" },
1894
- "共有資料": { id: "materials", question: "ingested", noFact: "未使用(まだ取り込みがありません)", keyLabel: "" },
1895
- "デザイン": { id: "design", question: "ingested", noFact: "登録済み(まだ取り込みがありません)", keyLabel: "トークン" },
1896
- "チャット": { id: "chat", question: "reachable", noFact: "登録済み(接続は未確認です)", keyLabel: "" },
2012
+ "課題管理": { id: "issues", question: "ingested", noFact: "登録済み(まだ取り込みがありません)", keyLabel: "APIキー", alsoBad: ["reachable"], adminOnNoToken: true, refKeyed: true },
2013
+ "会議": { id: "meetings", question: "ingested", noFact: "未使用(まだ取り込みがありません)", keyLabel: "", alsoBad: ["reachable"], adminOnNoToken: true },
2014
+ "共有資料": { id: "materials", question: "ingested", noFact: "未使用(まだ取り込みがありません)", keyLabel: "", alsoBad: ["reachable"], adminOnNoToken: true, refKeyed: true },
2015
+ "デザイン": { id: "design", question: "ingested", noFact: "登録済み(まだ取り込みがありません)", keyLabel: "トークン", alsoBad: ["reachable"], adminOnNoToken: true, refKeyed: true },
2016
+ "チャット": { id: "chat", question: "reachable", noFact: "登録済み(接続は未確認です)", keyLabel: "", refKeyed: true },
1897
2017
  // チャットの2枚目のアダプタ(#153・判断⑲)。**タイルの導出はアダプタで割る**——
1898
2018
  // 割らないと Google Chat タイルが Slack の登録件数・実測で判定される。
1899
2019
  // 鍵はタイル側(capDerivation)が「能力-アダプタ」で引く。実測ストアの
1900
2020
  // `cap: "google-chat"` の行は夜間の Gold 昇格が書く(#153 PR-E で結線済み)ので、
1901
2021
  // 夜間が一度走れば規則2/3(⚠️ / 接続OK)まで進む。走る前は規則4(実績なし→中立)
1902
- "チャット-google-chat": { id: "google-chat", question: "reachable", noFact: "登録済み(接続は未確認です)", keyLabel: "" },
1903
- "開発": { id: "dev", question: "reachable", noFact: "登録済み(接続は未確認です)", keyLabel: "" },
2022
+ "チャット-google-chat": { id: "google-chat", question: "reachable", noFact: "登録済み(接続は未確認です)", keyLabel: "", refKeyed: true },
2023
+ "開発": { id: "dev", question: "reachable", noFact: "登録済み(接続は未確認です)", keyLabel: "", refKeyed: true },
1904
2024
  };
2025
+ /**
2026
+ * 登録したばかりで、まだ実測の行が1件も届いていない接続先があるときの語(#523 決定6)。
2027
+ *
2028
+ * 🔴 **`DS_GATE_PENDING`(「登録状況を確認中です」)と混ぜない。** あちらは
2029
+ * 「登録簿が読めず、何件あるのかが分からない」という別の事実で、こちらは
2030
+ * **登録は見えている・実測の到着を待っている**。上限は `UNSETTLED_TTL_MS`(15分)で、
2031
+ * 過ぎたら規則4の中立文(「まだ取り込みがありません」)に戻す——待てば出るとは
2032
+ * 言い続けない。
2033
+ *
2034
+ * ⚠ **定数そのものを export しない**(CommonJS で `exports.X` に化け、流し込んだ先で落ちる)。
2035
+ */
2036
+ const STATE_CONFIRMING_TEXT = "確認しています…";
2037
+ /** 語を**テストから固定するための入れ物**(UNSETTLED_LIMITS と同じ作法) */
2038
+ exports.STATE_CONFIRMING = { text: STATE_CONFIRMING_TEXT };
1905
2039
  /**
1906
2040
  * 「鍵が無くて飛ばした」の一文(#225)。**能力ごとに鍵の呼び名が違う。**
1907
2041
  *
@@ -2254,10 +2388,16 @@ function calendarAddUrl(id) {
2254
2388
  * `sanitizeStateNow` は0件を `null` に畳むので、`rows` だけを見ると
2255
2389
  * 「読めて空(=まだ何も起きていない事実)」が「分からない」に化ける
2256
2390
  * @param ctx.rows `stateNow` の行
2391
+ * @param ctx.refs 登録簿にある ref の鍵(`registeredRefKeys` の答え)。**分からなければ
2392
+ * 渡さない**——null を「登録が無い」として篩うと、登録簿が読めなかった回に
2393
+ * 実測が丸ごと無視され、画面が黙る(`registered: null` を 0 と混ぜないのと同じ理由)
2394
+ * @param ctx.confirming 登録したばかりで実測の到着を待っている接続先があるか(決定6)。
2395
+ * 規則4の文言だけを差し替える(状態は中立のまま)
2257
2396
  * @returns 導出結果。`null` なら規則5(従来表示へフォールバック)
2258
2397
  *
2259
2398
  * **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す。
2260
- * `TILE_CAPS` / `BAD_STATE_STATUSES` は CLIENT_JS 側にも `var` で流し込んである)。
2399
+ * `TILE_CAPS` / `BAD_STATE_STATUSES` / `STATE_CONFIRMING_TEXT` は CLIENT_JS 側にも
2400
+ * `var` で流し込んである。`stateRefKey` もこれより前に流し込む)。
2261
2401
  */
2262
2402
  function deriveCapTile(cap, ctx) {
2263
2403
  var meta = TILE_CAPS[cap];
@@ -2271,25 +2411,43 @@ function deriveCapTile(cap, ctx) {
2271
2411
  if (!ctx || !ctx.seen)
2272
2412
  return null;
2273
2413
  var rows = Array.isArray(ctx.rows) ? ctx.rows : [];
2274
- var bad = BAD_STATE_STATUSES[meta.question] || [];
2414
+ // **見る問いは「実績の問い」+ alsoBad(#523 決定3)。** 悪い方で判定する
2415
+ var questions = [meta.question].concat(meta.alsoBad || []);
2416
+ // 登録簿にある ref の鍵。**無い(null / undefined)なら篩わない**(規則1が null を
2417
+ // 0 と混ぜないのと同じ理由——読めなかったことを「登録が無い」と断定しない)
2418
+ var refs = ctx.refs || null;
2275
2419
  var failed = 0;
2276
2420
  var skipped = 0;
2421
+ var noToken = 0;
2277
2422
  var unreachable = 0;
2278
2423
  var okSeen = false;
2279
2424
  for (var i = 0; i < rows.length; i++) {
2280
2425
  var r = rows[i];
2281
- if (!r || r.cap !== meta.id || r.question !== meta.question)
2426
+ if (!r || r.cap !== meta.id)
2427
+ continue;
2428
+ if (questions.indexOf(r.question) < 0)
2429
+ continue;
2430
+ // 🔴 **登録簿に無い ref の行は数えない(#523 決定2)。** 接続先を外してもストアの行は
2431
+ // 残る(TTL が無い・既知の問題)ので、登録が1件以上ある能力では規則1が守ってくれない
2432
+ // ——外したフォルダの `skipped` が残っているだけで「⚠ 取り込みを飛ばしました」が
2433
+ // 出続ける(engine #314 の症状の候補3)。**篩い方の正本は stateRefKey 1箇所**
2434
+ if (refs && meta.refKeyed && !refs[stateRefKey(meta.id, r.ref, r.workspace)])
2282
2435
  continue;
2283
2436
  // **一度でも成功していれば実績あり。** `lastOkAt` を見るのは、いま失敗している行でも
2284
- // 「繋がったことがある」のは事実だから(規則2が先に立つので表示は⚠️のまま)
2285
- if (r.status === "ok" || r.lastOkAt)
2437
+ // 「繋がったことがある」のは事実だから(規則2が先に立つので表示は⚠️のまま)。
2438
+ // **実績は `question`(その能力が「実際に動いた」と言える問い)の側だけで数える**
2439
+ // ——Drive に届いていても取り込みが0件なら「まだ取り込みがありません」が事実
2440
+ if (r.question === meta.question && (r.status === "ok" || r.lastOkAt))
2286
2441
  okSeen = true;
2442
+ var bad = BAD_STATE_STATUSES[r.question] || [];
2287
2443
  if (bad.indexOf(r.status) < 0)
2288
2444
  continue;
2289
2445
  if (r.status === "failed")
2290
2446
  failed++;
2291
2447
  else if (r.status === "skipped")
2292
2448
  skipped++;
2449
+ else if (r.status === "no_token" && meta.adminOnNoToken)
2450
+ noToken++;
2293
2451
  else
2294
2452
  unreachable++;
2295
2453
  }
@@ -2303,6 +2461,13 @@ function deriveCapTile(cap, ctx) {
2303
2461
  // 設定が済んでいないだけで、利用者が取るべき行動が違う(鍵を入れる/原因を調べる)
2304
2462
  if (skipped)
2305
2463
  return { rule: 2, state: "warn", text: keyMissingText(cap), keyMissing: !!meta.keyLabel };
2464
+ // 鍵が無くて「届くか」すら測れなかった(#523 決定3)。**「読めていません」と言わない**
2465
+ // ——読もうとする前に止まっているので、直すのは鍵の登録(または管理者の設定)
2466
+ if (noToken) {
2467
+ return meta.keyLabel
2468
+ ? { rule: 2, state: "warn", text: keyMissingText(cap), keyMissing: true }
2469
+ : { rule: 2, state: "warn", text: "管理者の設定が必要です", keyMissing: false };
2470
+ }
2306
2471
  if (unreachable) {
2307
2472
  return { rule: 2, state: "warn", text: unreachable + "件が読めていません", keyMissing: false };
2308
2473
  }
@@ -2310,6 +2475,13 @@ function deriveCapTile(cap, ctx) {
2310
2475
  if (okSeen)
2311
2476
  return { rule: 3, state: "ok", text: "接続OK", keyMissing: false };
2312
2477
  // ---- 規則4: 実績なし → 中立 ----
2478
+ //
2479
+ // **登録したばかりなら「確認しています…」(#523 決定6)。** 登録した瞬間から
2480
+ // 「まだ取り込みがありません」と言うのは事実だが、利用者には「登録が効いていない」とも
2481
+ // 読める。実測が届くまでの短い間(`UNSETTLED_TTL_MS`)だけ、待っていることを言う。
2482
+ // **判定の材料は呼び出し側**(仮置き=pending-ops の add・capConfirming)
2483
+ if (ctx.confirming)
2484
+ return { rule: 4, state: "idle", text: STATE_CONFIRMING_TEXT, keyMissing: false };
2313
2485
  return { rule: 4, state: "idle", text: meta.noFact, keyMissing: false };
2314
2486
  }
2315
2487
  function capabilityState(cap, ctx, adapter) {
@@ -2560,8 +2732,12 @@ function renderConnectionMap(site) {
2560
2732
  else if (isLive) {
2561
2733
  statusText = "ライブ参照";
2562
2734
  }
2563
- else if (internal?.lastSync) {
2564
- statusTs = internal.lastSync;
2735
+ else if (fleetLastSyncOf(internal)) {
2736
+ // 🔴 **run 由来の時刻は焼かない(#523 決定1)。** ここは図の3行目(「最終同期 …」)で、
2737
+ // ライブが届くまでそのまま見えている。run の成功時刻(空振りを含む)を焼くと、
2738
+ // 何も取り込めていない案件の図が「昨日同期できている」と語る——
2739
+ // **ライブで訂正されるまでの数秒だけ嘘をつく**形も作らない
2740
+ statusTs = fleetLastSyncOf(internal);
2565
2741
  }
2566
2742
  // 時刻はクライアントでローカルタイム整形するため data-ts で渡す。
2567
2743
  //
@@ -2773,11 +2949,8 @@ function tutorialChallenge4Prompt(decisions) {
2773
2949
  * 規約はプロンプトに書き写さず、規約のあるファイル(README.md・template.md)をAIに読ませ、
2774
2950
  * 中身は利用者に聞き返させる(AI導線ボタンのプロンプトと同じ方針)。
2775
2951
  * 規約がどこにも無い手順(外部サービスへのつなぎ方・投稿経路の判定)だけを骨子として残す。
2776
- * 対応するスキルがある操作は、スキルが使える環境ではそちらを優先させる旨を冒頭に添える。
2777
2952
  */
2778
2953
  const TUT_MEMBER_PROMPT = [
2779
- "(update-member スキルが使える環境なら、そのスキルの手順を優先してください。)",
2780
- "",
2781
2954
  "Cortex/Members/README.md と Cortex/Members/template.md を読んで規約を把握してから、Cortex/Members/records/ にあるわたし自身のメンバーレコードの表示名(title)を変更してください。Gold層へ書き込む練習です。名簿にわたしのレコードが無ければ、新規に1件作ってかまいません。",
2782
2955
  "わたしが誰で、何をどう変えたいかは、まずわたしに質問してください。",
2783
2956
  "あなた(人間)が内容を確認して編集したので、status が draft なら active に上げてください。",
@@ -4304,9 +4477,17 @@ ${secretRegisteredAnywhere.toString()}
4304
4477
  ${sanitizeCapabilitiesNow.toString()}
4305
4478
  ${sanitizeGithubOwnersNow.toString()}
4306
4479
  ${splitNeededParts.toString()}
4480
+ // 🔴 **ingestedAtOf / deriveCapTile より前に置く。** 両方が突き合わせ鍵の畳み方を
4481
+ // ここから引く(#523 S1)。順序を崩すと、孤児行の篩が undefined を呼んで画面が落ちる
4482
+ ${stateRefKey.toString()}
4483
+ ${registeredRefKeys.toString()}
4484
+ ${fleetLastSyncOf.toString()}
4307
4485
  ${ingestedAtOf.toString()}
4308
4486
  var TILE_CAPS = ${JSON.stringify(TILE_CAPS)};
4309
4487
  var BAD_STATE_STATUSES = ${JSON.stringify(BAD_STATE_STATUSES)};
4488
+ // 登録直後の一時表示(#523 決定6)。**この定数行を落とすと、流し込んだ deriveCapTile が
4489
+ // 未定義を参照して画面のスクリプトが丸ごと死ぬ**(定数は export できないので値はここで注入)
4490
+ var STATE_CONFIRMING_TEXT = ${JSON.stringify(STATE_CONFIRMING_TEXT)};
4310
4491
  ${keyMissingText.toString()}
4311
4492
  ${deriveCapTile.toString()}
4312
4493
  // 案件カレンダー(#428)。**判定の実体はビルド時の定義**——画面側に書き写すと、
@@ -4810,10 +4991,6 @@ ${calendarAddUrl.toString()}
4810
4991
  // レポート等スキルの無いディレクトリは対象外(ボタンを出さない)。
4811
4992
  // プロンプトには規約を書き写さない。各区画の README.md(規約)と template.md(雛形)をAIに読ませ、
4812
4993
  // 中身は利用者に聞き返させる(規約の二重定義を避け、READMEの更新がそのまま効くようにするため)。
4813
- // スキルが使える環境(手元のCLI・デスクトップ等)ではそちらを優先させる旨を冒頭に添える。
4814
- function aiHead(skill) {
4815
- return "(" + skill + " スキルが使える環境なら、そのスキルの手順を優先してください。)\\n\\n";
4816
- }
4817
4994
  // 公開範囲の規律(内部限定情報を書かない等)は各区画の README.md に書いてあるので、プロンプトには重ねない。
4818
4995
  // 顧客も押すボタンなので、内側の運用事情を文面に出さない。
4819
4996
  var AI_COMMON = "";
@@ -4845,7 +5022,6 @@ ${calendarAddUrl.toString()}
4845
5022
  ].join("\\n");
4846
5023
  var GOLD_SKILL = {
4847
5024
  "Decisions": {
4848
- skill: "update-decision",
4849
5025
  noun: "決定",
4850
5026
  dir: "Cortex/Decisions",
4851
5027
  add: [
@@ -4864,7 +5040,6 @@ ${calendarAddUrl.toString()}
4864
5040
  },
4865
5041
  },
4866
5042
  "Glossary": {
4867
- skill: "update-glossary",
4868
5043
  noun: "用語",
4869
5044
  dir: "Cortex/Glossary",
4870
5045
  add: [
@@ -4882,7 +5057,6 @@ ${calendarAddUrl.toString()}
4882
5057
  },
4883
5058
  },
4884
5059
  "Members": {
4885
- skill: "update-member",
4886
5060
  noun: "メンバー",
4887
5061
  dir: "Cortex/Members",
4888
5062
  add: [
@@ -4900,7 +5074,6 @@ ${calendarAddUrl.toString()}
4900
5074
  },
4901
5075
  },
4902
5076
  "Rules": {
4903
- skill: "update-rule",
4904
5077
  noun: "ルール",
4905
5078
  dir: "Cortex/Rules",
4906
5079
  add: [
@@ -4927,7 +5100,7 @@ ${calendarAddUrl.toString()}
4927
5100
  function aiAddBar(sectionName) {
4928
5101
  var g = goldSkill(sectionName);
4929
5102
  if (!g) return null;
4930
- var prompt = aiHead(g.skill) + g.add + AI_COMMON;
5103
+ var prompt = g.add + AI_COMMON;
4931
5104
  return el("div", { class: "ai-add-bar" }, [
4932
5105
  aiLink("ai-add-btn", prompt, "+ AIで登録"),
4933
5106
  ]);
@@ -4940,7 +5113,7 @@ ${calendarAddUrl.toString()}
4940
5113
  // target=AIに読ませるファイル(対象レコード+規約のREADME)、subject=直す対象の呼び名
4941
5114
  var target = (pathRef ? pathRef + " と " : "") + g.dir + "/README.md";
4942
5115
  var subject = "この" + g.noun + "「" + title + "」" + (pathRef ? "" : "(" + rec.id + ")");
4943
- var prompt = aiHead(g.skill) + g.edit(target, subject) + AI_COMMON;
5116
+ var prompt = g.edit(target, subject) + AI_COMMON;
4944
5117
  return aiLink("ai-edit-btn", prompt, "AIで編集");
4945
5118
  }
4946
5119
 
@@ -5000,7 +5173,7 @@ ${calendarAddUrl.toString()}
5000
5173
  : "この記録は確認済み(status: active)です。**適用後も status は active のままにしてください**(draft に戻さないでください)。",
5001
5174
  );
5002
5175
  lines.push("書き込む前に、変更内容を提示して承認を得てください。");
5003
- var prompt = aiHead(g.skill) + lines.join("\\n") + AI_COMMON;
5176
+ var prompt = lines.join("\\n") + AI_COMMON;
5004
5177
  return aiLink("ai-apply-btn", prompt, "AIで適用");
5005
5178
  }
5006
5179
 
@@ -6489,10 +6662,52 @@ ${calendarAddUrl.toString()}
6489
6662
  if (s.gate == null || s.gate === "") return DS_GATE_PENDING;
6490
6663
  return DS_GATE[s.gate] || DS_GATE["unknown"];
6491
6664
  }
6665
+ /* ---------------------------------------------------------------------------
6666
+ * 取り込む能力の接続先の行に出す「届くか」(#523 S1・決定2)
6667
+ *
6668
+ * Backlog の接続先・Drive のフォルダ・Figma のファイルにも、Slack の行と同じ
6669
+ * **札と「いつ確かめたか」**を出す。語は 「DS_GATE」 が正本で、**能力ごとに直す操作が
6670
+ * 違うものだけ**ここで言い換える:
6671
+ * - Drive の 「unreachable」 … 「アクセス不可」では何をすればよいか分からない。
6672
+ * 直すのは**フォルダ側の共有**(取り込み用アカウントの招待)
6673
+ * - 鍵が無い(「no_token」)… 画面に鍵の入力欄がある能力(Backlog / Figma)は
6674
+ * 鍵カードと同じ語(「keyMissingText」 と揃える)。Drive の資格情報は艦隊側にあり、
6675
+ * 案件の人には打つ手が無いので「管理者の設定が必要です」(決定3と同じ振り分け)
6676
+ *
6677
+ * 🔴 **「DS_GATE」 本体に足さない。** あちらは**実測が返す語彙**と1対1で、
6678
+ * 「state-vocab-parity.test.mjs」 が見張っている(足し忘れた判定が「不明」に落ちる事故の番人)。
6679
+ * 能力ごとの言い換えは語彙ではないので、同じ表に混ぜるとその番人が意味を失う
6680
+ * (「DS_GATE_NO_TOKEN」 / 「DS_GATE_RESTING_HELP」 と同じ判断)。
6681
+ *
6682
+ * **バッジは短語・次の一手は help に分ける**(バッジに長文を入れると行が折り返す)。
6683
+ * ------------------------------------------------------------------------- */
6684
+ var DS_GATE_CAP = {
6685
+ materials: {
6686
+ unreachable: {
6687
+ text: "フォルダを開けません",
6688
+ cls: "ds-gate-warn",
6689
+ help: DRIVE_INGEST_ACCOUNT + " を閲覧者として招待してください(招待が外れると読めません)。",
6690
+ },
6691
+ no_token: { text: "管理者の設定が必要です", cls: "ds-gate-muted" },
6692
+ },
6693
+ issues: { no_token: { text: "APIキーが未登録です", cls: "ds-gate-muted" } },
6694
+ design: { no_token: { text: "トークンが未登録です", cls: "ds-gate-muted" } },
6695
+ };
6696
+ /**
6697
+ * その能力の行に出す「届くか」の語。**能力ごとの言い換えが無ければ 「DS_GATE」 のまま。**
6698
+ * 知らない値は 「DS_GATE」 と同じく「不明」(engine が新しい語を足したのに画面が知らない、
6699
+ * という事実を黙って消さない)。
6700
+ */
6701
+ function dsGateForCap(capId, status) {
6702
+ var byCap = DS_GATE_CAP[capId];
6703
+ if (byCap && byCap[status]) return byCap[status];
6704
+ return DS_GATE[status] || DS_GATE["unknown"];
6705
+ }
6492
6706
  /**
6493
- * dsGateOf が持つ「次の一手」を、**必ず1個の DOM ノード**にする。呼び出しは2箇所
6494
- * (行の常時表示・「接続を確認」を押した結果)で、gate.help / gate.request が
6495
- * 正本——ここを通さずに文を書き写すと、片方だけ古くなる(DS_GATE の項と同じ理由)。
6707
+ * dsGateOf が持つ「次の一手」を、**必ず1個の DOM ノード**にする。呼び出しは3箇所
6708
+ * (行の常時表示・「接続を確認」を押した結果・取り込む能力の行の「届くか」)で、
6709
+ * gate.help / gate.request が正本——ここを通さずに文を書き写すと、片方だけ古くなる
6710
+ * (DS_GATE の項と同じ理由)。
6496
6711
  *
6497
6712
  * 🔴 **要素を2つに割らない(行の補足は最大1段・github-state-reason.test.mjs
6498
6713
  * 「行の補足は最大1段」)。** gate.request(依頼文。手順が複数行あるもの)があるときは、
@@ -8099,15 +8314,114 @@ ${calendarAddUrl.toString()}
8099
8314
  return registeredCountOf(derCap, adapter) !== null;
8100
8315
  }
8101
8316
 
8317
+ /**
8318
+ * その能力の「登録簿にある ref」の鍵(#523 決定2)。**読めなければ null。**
8319
+ *
8320
+ * 🔴 **null を空の表に畳まないこと。** 空の表を渡すと「どの行も登録簿に無い」と
8321
+ * 判定され、実測が丸ごと捨てられる——**夜間が⚠️を書いているのにタイルが静か**という、
8322
+ * いま直そうとしている嘘と同じ形になる。
8323
+ *
8324
+ * 仮置き(#388)も勘案する: add した ref は登録簿に載る前から有効・remove した ref は
8325
+ * 載っていても無効(決定2「pending の 「remove-*」 も除外」)。一覧・件数と同じ材料を
8326
+ * 使うので、「行は消えたのにタイルだけ⚠️」が起きない。
8327
+ *
8328
+ * ⚠ **チャットの add だけは、登録簿が追いつくまでの数分、篩い過ぎる余地がある。**
8329
+ * 解除・追加の payload はワークスペースを持たない(どのワークスペースかはサーバが実測して
8330
+ * 決める)ので、仮置きが入れられる鍵はワークスペース無しの1本だけ。**以前に登録していた
8331
+ * チャンネルを再登録した直後**は、ストアに残った古い行(ワークスペース付き)がこの鍵に
8332
+ * 当たらず、数分だけタイルが黙る(登録簿が追いつけば直る)。倒す向きは
8333
+ * 「黙る」側で揃えてある——逆(測っていないものを⚠️にする)は直し方を示せない嘘になる。
8334
+ */
8335
+ function refKeysOf(derCap) {
8336
+ var meta = TILE_CAPS[derCap];
8337
+ if (!meta || !meta.refKeyed || !LIVE_REGISTERED) return null;
8338
+ // チャットの混載タイル(アダプタ不明)は chat と google-chat の両方を抱えるが、
8339
+ // 鍵は能力(meta.id)ごとに引くので、ここで合算するものは無い
8340
+ var keys = registeredRefKeys(meta.id, LIVE_REGISTERED[meta.id]);
8341
+ if (!keys) return null;
8342
+ var ops = pendingOpsNow();
8343
+ for (var i = 0; i < ops.length; i++) {
8344
+ var e = ops[i];
8345
+ var km = e && PENDING_KINDS[e.kind];
8346
+ if (!km || km.reg !== meta.id) continue;
8347
+ // 仮置きの鍵(targetId)は登録簿と同じ畳み方(backlog は pendingIssuesRef=issuesRefOf)。
8348
+ // チャットだけワークスペースを持たない(どのワークスペースかはサーバが実測して決める)
8349
+ var key = meta.id === "chat" ? "\t" + e.targetId : e.targetId;
8350
+ if (e.action === "add") keys[key] = true;
8351
+ else if (e.action === "remove") {
8352
+ delete keys[key];
8353
+ // 🔴 **チャットは鍵が2本ある**(「ws + \t + ref」 と 「\t + ref」)。ワークスペース無しの
8354
+ // 1本だけ消すと、**一覧から行が消えたのにタイルだけ⚠️が残る**(解除した顧客
8355
+ // ワークスペースのチャンネルの実測が孤児として落ちない・実装レビュー🟡2)。
8356
+ // 解除の payload はワークスペースを持たないので、接尾一致で両方消す
8357
+ if (meta.id === "chat") {
8358
+ for (var k in keys) {
8359
+ if (!Object.prototype.hasOwnProperty.call(keys, k)) continue;
8360
+ if (k.slice(-key.length) === key) delete keys[k];
8361
+ }
8362
+ }
8363
+ }
8364
+ }
8365
+ return keys;
8366
+ }
8367
+
8368
+ /**
8369
+ * 登録したばかりで、まだ実測の行が1件も届いていない接続先があるか(#523 決定6)。
8370
+ *
8371
+ * **対象は取り込む4能力だけ**(「alsoBad」 を持つ能力=規則4の文言が
8372
+ * 「まだ取り込みがありません」の系)。チャット・開発の規則4は既に
8373
+ * 「接続は未確認です」と言っており、待っていることが読める。
8374
+ *
8375
+ * 上限は 「UNSETTLED_TTL_MS」(15分)。過ぎたら中立文に戻る——**待てば出ると言い続けない**。
8376
+ */
8377
+ function capConfirming(derCap) {
8378
+ var meta = TILE_CAPS[derCap];
8379
+ if (!meta || !meta.alsoBad) return false;
8380
+ var ops = pendingOpsNow();
8381
+ var now = Date.now();
8382
+ for (var i = 0; i < ops.length; i++) {
8383
+ var e = ops[i];
8384
+ var km = e && PENDING_KINDS[e.kind];
8385
+ if (!km || e.action !== "add" || km.reg !== meta.id) continue;
8386
+ if (!(now - e.committedAt < UNSETTLED_TTL_MS)) continue;
8387
+ if (stateRowSeenFor(meta.id, e.targetId)) continue;
8388
+ return true;
8389
+ }
8390
+ return false;
8391
+ }
8392
+
8393
+ /** その接続先について、ストアの行が1件でも届いているか(問いは問わない・capConfirming の材料) */
8394
+ function stateRowSeenFor(capId, targetId) {
8395
+ var rows = LIVE_STATE || [];
8396
+ for (var i = 0; i < rows.length; i++) {
8397
+ var r = rows[i];
8398
+ if (!r || r.cap !== capId) continue;
8399
+ // 仮置きの鍵はワークスペースを持たないので、ここは ref の生値で比べる
8400
+ // (issues だけは両側を issuesRefOf に畳む=pendingIssuesRef と同形)
8401
+ var k = capId === "issues" ? stateRefKey(capId, r.ref) : String(r.ref || "");
8402
+ if (k === targetId) return true;
8403
+ }
8404
+ return false;
8405
+ }
8406
+
8407
+ /**
8408
+ * 導出に使う鍵(「能力」または「能力-アダプタ」)。**アダプタが分かるタイルは割る**
8409
+ * (#153・判断⑲)——割らないと、Slack の登録件数・実測で Google Chat タイルが
8410
+ * 「接続OK」を名乗る(逆も)。「能力-アダプタ」の変種は TILE_CAPS が持つ。
8411
+ * **この解決を書き写さないこと**(写しがずれると、タイルと最終同期で別の能力を見る)
8412
+ */
8413
+ function derCapOf(cap, adapter) {
8414
+ return adapter && TILE_CAPS[cap + "-" + adapter] ? cap + "-" + adapter : cap;
8415
+ }
8416
+
8102
8417
  function capDerivation(cap, adapter) {
8103
- // **アダプタが分かるタイルは、そのアダプタの名簿・実測だけで導出する(#153・判断⑲)。**
8104
- // 割らないと、Slack の登録件数・実測で Google Chat タイルが「接続OK」を名乗る(逆も)。
8105
- // 「能力-アダプタ」の変種は TILE_CAPS が持つ(無いアダプタは能力単位のまま)
8106
- var derCap = adapter && TILE_CAPS[cap + "-" + adapter] ? cap + "-" + adapter : cap;
8418
+ var derCap = derCapOf(cap, adapter);
8107
8419
  return deriveCapTile(derCap, {
8108
8420
  registered: registeredCountOf(derCap, adapter),
8109
8421
  seen: LIVE_STATE_SEEN,
8110
8422
  rows: LIVE_STATE,
8423
+ refs: refKeysOf(derCap),
8424
+ confirming: capConfirming(derCap),
8111
8425
  });
8112
8426
  }
8113
8427
 
@@ -8491,6 +8805,26 @@ ${calendarAddUrl.toString()}
8491
8805
  */
8492
8806
  var KEY_SLOTS = { backlog: {}, backlogNamed: false, figma: null };
8493
8807
 
8808
+ /**
8809
+ * まとめ表の「最終同期」を後詰めに使ってよいか(#523 決定1・実装レビュー🔴1)。
8810
+ *
8811
+ * 🔴 **ストアを読めた回は使わない。** まとめ表の値がストア由来
8812
+ * (「lastSyncSource: "ingested"」)でも、engine 側(「materialsLastOkAtOf」)は
8813
+ * **登録簿で篩っていない**ので、
8814
+ * それは**外したフォルダの時刻でありうる**。画面がストア側で孤児行を落としたそばから、
8815
+ * 篩っていない同じ値をまとめ表から拾い直すと、#314 の「最終同期 9/8 18:34」がそのまま
8816
+ * 復活する(実装レビューで実測)。
8817
+ *
8818
+ * だから後詰めは **「ストアが読めなかった回の代用」** に限る——古い Lambda・ストアの
8819
+ * 読み取り失敗・ライブを持たない案件(焼いた HTML のまま見ている状態)。
8820
+ * そこでは比べる相手が無いので、まとめ表が唯一かつ最良の答えになる。
8821
+ * engine 側の篩は S5(「fleet-status.mjs」 の後始末)で入れる。
8822
+ */
8823
+ function fleetSyncFallback(internal) {
8824
+ if (LIVE_STATE_SEEN) return "";
8825
+ return fleetLastSyncOf(internal);
8826
+ }
8827
+
8494
8828
  function buildDataSources() {
8495
8829
  KEY_SLOTS = { backlog: {}, backlogNamed: false, figma: null };
8496
8830
  // 鍵カードも受け皿と同じく作り直す(前回の切り離されたカードを掴んだままにしない)
@@ -8582,6 +8916,22 @@ ${calendarAddUrl.toString()}
8582
8916
  capBox[cap].push({ node: node, adapter: adapter || "" });
8583
8917
  }
8584
8918
 
8919
+ /**
8920
+ * 能力ブロックの「最終同期」(#523 決定1)。**出どころはストアが先・まとめ表は後詰め。**
8921
+ *
8922
+ * タイルの2行目(tileSub)と同じ材料で、同じ順に引く(「fleetSyncFallback」 が
8923
+ * 「後詰めを使ってよいか」の判定を1か所に持つ)。
8924
+ *
8925
+ * 出せないときは要素ごと出さない(「最終同期: —」のような空の行を置かない)。
8926
+ */
8927
+ function capBlockSyncEl(s, inactive) {
8928
+ if (inactive || !s || !s.kind) return null;
8929
+ var capId = capIdOf(s.kind);
8930
+ var at = capId ? ingestedAtOf(LIVE_STATE, capId, refKeysOf(derCapOf(s.kind, ""))) : "";
8931
+ var t = dsFmtTime(at || fleetSyncFallback(s) || "");
8932
+ return t ? el("span", { class: "ds-sync", text: "最終同期: " + t }) : null;
8933
+ }
8934
+
8585
8935
  if (internals.length) {
8586
8936
  // **「いま同期する」ボタンは出さない。** 受け口(Apps Script の Webアプリ)を
8587
8937
  // 公開できないため、押しても届かない。
@@ -8649,9 +8999,11 @@ ${calendarAddUrl.toString()}
8649
8999
  "data-cap": s.kind || "",
8650
9000
  }, [
8651
9001
  col,
8652
- !inactive && s.lastSync && dsFmtTime(s.lastSync)
8653
- ? el("span", { class: "ds-sync", text: "最終同期: " + dsFmtTime(s.lastSync) })
8654
- : null,
9002
+ // 🔴 **出どころはストアが先・まとめ表は後詰め(#523 決定1)。** まとめ表の
9003
+ // 「lastSync」 には run の成功時刻(空振りを含む)が混ざるので、「fleetLastSyncOf」
9004
+ // ストア由来のものだけを通す。タイルの「最終同期」と**同じ材料**にする
9005
+ // ——別々に引くと、同じ画面の2箇所が違う時刻を「最終同期」と呼ぶ
9006
+ capBlockSyncEl(s, inactive),
8655
9007
  ]);
8656
9008
  // 会議・共有資料・デザインの操作をブロック内に置く(その情報源の文脈で押せるように)
8657
9009
  var ctl = internalControls(s, inactive);
@@ -9195,10 +9547,20 @@ ${calendarAddUrl.toString()}
9195
9547
  // 「internalSources.lastSync」 は run 全体の最終成功時刻で、**どの接続先のことか
9196
9548
  // 分からない**(鍵が無くて1件も取り込めていない run でも「成功」で終わる)。
9197
9549
  // ストアの行は「その能力の接続先が実際に取り込めた事実」なので、同じ「最終同期」の
9198
- // 位置に出すならこちらが正しい。行が無ければ従来どおり run の時刻
9550
+ // 位置に出すならこちらが正しい。
9551
+ //
9552
+ // 🔴 **まとめ表の後詰めは「ストアが読めなかった回」だけ(#523 決定1・「fleetSyncFallback」)。**
9553
+ // run 由来は常に捨て、ストア由来でも**ストアを読めた回は使わない**(engine 側は
9554
+ // まだ登録簿で篩っていないので、孤児行の時刻が混ざりうる)。出せないときは何も
9555
+ // 言わない——2行目の語が「まだ取り込みがありません」と既に答えている。
9556
+ // 孤児行(外した接続先の残骸)の時刻も数えない(決定2・refs を渡す)
9199
9557
  if (st !== "off" && st !== "idle") {
9200
- var ingestedAt = ingestedAtOf(LIVE_STATE, capIdOf(cap));
9201
- var t = dsFmtTime(ingestedAt || (internal && internal.lastSync) || "");
9558
+ // 🔴 能力名は導出と同じ鍵から引く(「capIdOf」 はアダプタを見ないので、
9559
+ // google-chat のタイルで「件数は gc・時刻は chat」という食い違いができる)
9560
+ var derCapForId = derCapOf(cap, adapter);
9561
+ var stateCapId = (TILE_CAPS[derCapForId] && TILE_CAPS[derCapForId].id) || capIdOf(cap);
9562
+ var ingestedAt = ingestedAtOf(LIVE_STATE, stateCapId, refKeysOf(derCapForId));
9563
+ var t = dsFmtTime(ingestedAt || fleetSyncFallback(internal) || "");
9202
9564
  if (t) parts.push("最終同期 " + t);
9203
9565
  }
9204
9566
  return parts.join(" ");
@@ -10412,6 +10774,87 @@ ${calendarAddUrl.toString()}
10412
10774
  });
10413
10775
  }
10414
10776
 
10777
+ /**
10778
+ * その接続先の「届くか」の行(接続状態ストア・#523 S1)。**無ければ null。**
10779
+ *
10780
+ * 🔴 **突き合わせは 「stateRefKey」 を通す(両側とも)。** タイルの孤児行の篩と同じ
10781
+ * 畳み方にしておかないと、「タイルは⚠️なのに、どの行にも札が付かない」が起きる
10782
+ * ——どの接続先の話なのかが画面から分からない状態に戻る。
10783
+ *
10784
+ * **行が無ければ何も出さない**(「ingestedEl」 と同じ原則)。測っていないことを
10785
+ * 「読めていない」と書くと、直す必要があると読ませる嘘になる。
10786
+ */
10787
+ function reachableStateOf(capId, ref, workspace) {
10788
+ var rows = LIVE_STATE;
10789
+ if (!rows || !rows.length || !capId) return null;
10790
+ var want = stateRefKey(capId, ref, workspace);
10791
+ if (!want) return null;
10792
+ for (var i = 0; i < rows.length; i++) {
10793
+ var r = rows[i];
10794
+ if (!r || r.cap !== capId || r.question !== "reachable") continue;
10795
+ if (stateRefKey(capId, r.ref, r.workspace) === want) return r;
10796
+ }
10797
+ return null;
10798
+ }
10799
+
10800
+ /**
10801
+ * 接続先1件の「届くか」の札と「いつ確かめたか」を行に足す(#523 S1・決定2)。
10802
+ * **Slack の行と同じ見た目**(「.ds-gate」 + 「.ds-gate-at」)。
10803
+ *
10804
+ * 🔴 **次の一手・理由(「.ds-gate-help」)はここで足さない。** あれは 「flex: 1 1 100%」 で
10805
+ * 行を占めるので、行の途中に挟むと**ID と「この◯◯を外す」が案内文の下の行へ落ちる**
10806
+ * (Slack の行が 「gateHelpNodes」 をボタンより後ろに置いているのと同じ理由・実装レビュー🟡3)。
10807
+ * 行の組み立ての最後に 「appendReachableReason」 を呼ぶ。
10808
+ */
10809
+ function appendReachableChip(row, capId, ref, workspace) {
10810
+ var r = reachableStateOf(capId, ref, workspace);
10811
+ if (!r) return row;
10812
+ var gate = dsGateForCap(capId, r.status);
10813
+ row.appendChild(el("span", { class: "ds-gate " + gate.cls, text: gate.text }));
10814
+ var when = rtAgo(r.at);
10815
+ if (when) row.appendChild(el("span", { class: "ds-gate-at", text: when + "に確認" }));
10816
+ return row;
10817
+ }
10818
+
10819
+ /**
10820
+ * 「届くか」の次の一手か理由を、**行の最後に1段だけ**足す(#523 S1・決定2)。
10821
+ *
10822
+ * 🔴 **理由(「detail」)は社内ビューアにしか出ない。** この値の出どころは 「op=state」
10823
+ * (社内の操作タブ)だけで、顧客射影(engine が焼く fleet-status.json)には載らない。
10824
+ * ここで「顧客にも出る」前提の文を書かないこと(決定2)。
10825
+ *
10826
+ * **語そのものが次の一手を示している状態には、理由を重ねない**(「showsRestingReason」 と
10827
+ * 同じ振り分け——「ok」 / 「no_token」 / 「missing_scope」 / 「not_in_channel」)。
10828
+ * 重ねると「管理者の設定が必要です」の下に「(管理者の設定が必要です)」が並ぶ
10829
+ * (実装レビュー🟡4。規約「帯は症状・行は一手・繰り返さない」)。
10830
+ *
10831
+ * **書き手が button の detail は出さない**(「stateReasonOf」 の項——ボタンは判定の語
10832
+ * そのものや生のエラーを detail に書く。人向けの文は needed が持つ)。
10833
+ */
10834
+ function appendReachableReason(row, capId, ref, workspace) {
10835
+ var r = reachableStateOf(capId, ref, workspace);
10836
+ if (!r) return row;
10837
+ var gate = dsGateForCap(capId, r.status);
10838
+ // 次の一手を持つ語(Drive の共有外れ等)は、その1文だけ
10839
+ if (gate.help || gate.request) {
10840
+ gateHelpNodes(gate, "ds-gate-help").forEach(function (n) { row.appendChild(n); });
10841
+ return row;
10842
+ }
10843
+ if (r.status === "ok" || r.status === "no_token" || r.status === "missing_scope" || r.status === "not_in_channel") return row;
10844
+ var reason = stateReasonOf({ needed: r.needed, detail: r.detail, gateBy: r.by });
10845
+ // needed は Slack の行と同じ部品を通す(URL がリンクになる・スコープ名がラベル化される)
10846
+ if (reason) row.appendChild(r.needed ? neededNode(reason, "ds-gate-help") : el("p", { class: "ds-gate-help", text: reason }));
10847
+ return row;
10848
+ }
10849
+
10850
+ /** 一覧に出ている接続先の「届くか」をひとつの文字列にする(描き直しの要否の判定用) */
10851
+ function reachSigOf(capId, refs) {
10852
+ return (refs || []).map(function (ref) {
10853
+ var r = reachableStateOf(capId, ref && ref.ref != null ? ref.ref : ref, ref && ref.workspace);
10854
+ return r ? r.status + "@" + r.at : "-";
10855
+ }).join(",");
10856
+ }
10857
+
10415
10858
  /**
10416
10859
  * いま一覧に出ている接続先の「取り込み」をひとつの文字列にする(差し替えの要否の判定用)。
10417
10860
  *
@@ -10428,9 +10871,13 @@ ${calendarAddUrl.toString()}
10428
10871
  return r ? r.status + "@" + r.at : "-";
10429
10872
  }).join(",");
10430
10873
  }
10431
- /** 取り直しで描き直すかの署名: 取り込みの状態+仮置きの印(#388・印だけ残さないため) */
10874
+ /**
10875
+ * 取り直しで描き直すかの署名: 取り込みの状態+仮置きの印(#388・印だけ残さないため)
10876
+ * +**「届くか」の状態**(#523 S1——混ぜないと、ストアの reachable が届いても札が付かない)
10877
+ */
10432
10878
  function backlogPaintSigOf(list) {
10433
- return ingestSigOf(list) + "|" + (list || []).map(function (t) { return t && t.pending === true ? "p" : "-"; }).join(",");
10879
+ return ingestSigOf(list) + "|" + (list || []).map(function (t) { return t && t.pending === true ? "p" : "-"; }).join(",")
10880
+ + "|" + reachSigOf("issues", (list || []).map(function (t) { return issuesRefOf(t.domain, t.projectKey); }));
10434
10881
  }
10435
10882
 
10436
10883
  /**
@@ -10471,7 +10918,9 @@ ${calendarAddUrl.toString()}
10471
10918
  return (list || []).map(function (f) {
10472
10919
  var r = designIngestedStateOf(f && f.key);
10473
10920
  return r ? r.status + "@" + r.at : "-";
10474
- }).join(",");
10921
+ }).join(",")
10922
+ // 「届くか」も混ぜる(#523 S1。混ぜないと、ストアの reachable が届いても札が付かない)
10923
+ + "|" + reachSigOf("design", (list || []).map(function (f) { return f && f.key; }));
10475
10924
  }
10476
10925
 
10477
10926
  /**
@@ -10520,7 +10969,12 @@ ${calendarAddUrl.toString()}
10520
10969
  // この接続先の取り込みが最後にどうなったか(段C)。測っていなければ null
10521
10970
  ingestedEl(ingestedStateOf(t.domain, t.projectKey), "課題管理"),
10522
10971
  ]);
10523
- if (!editable) return row;
10972
+ // この接続先に届くか(#523 S1)。**「取り込み」とは別の問い**なので併記する
10973
+ // ——鍵はあるが 403 でプロジェクトが見えない、は取り込みの失敗より前の話。
10974
+ // 理由の1段は**行の最後**(ボタンより後ろ)に置く(appendReachableReason の項)
10975
+ var issuesRef = issuesRefOf(t.domain, t.projectKey);
10976
+ appendReachableChip(row, "issues", issuesRef);
10977
+ if (!editable) return appendReachableReason(row, "issues", issuesRef);
10524
10978
  var btn = el("button", { class: "ds-toggle-btn ds-danger", type: "button", text: "この接続先を外す" });
10525
10979
  btn.addEventListener("click", async function () {
10526
10980
  // **最後の1件かどうかは「押した時点」で数える。** 描画時の件数で決めると、
@@ -10540,7 +10994,7 @@ ${calendarAddUrl.toString()}
10540
10994
  sendConfig(payload, msg, btn, null, targetsHooks());
10541
10995
  });
10542
10996
  row.appendChild(btn);
10543
- return row;
10997
+ return appendReachableReason(row, "issues", issuesRef);
10544
10998
  }
10545
10999
 
10546
11000
  /**
@@ -10956,7 +11410,9 @@ ${calendarAddUrl.toString()}
10956
11410
  var d = driveNow();
10957
11411
  // 仮置きの印(drivePending)も署名に入れる——外れたときに印だけ残さない(#388)
10958
11412
  // 「確かめられたか」も混ぜる(#409。混ぜないと注記の要否が変わっても作り直されない)
10959
- return JSON.stringify([d.driveFolderIds || [], d.driveFolderNames || {}, d.driveState || "", d.drivePending || {}, registrationUnreadable()]);
11413
+ // 「届くか」も混ぜる(#523 S1。混ぜないと、ストアの reachable が届いても札が付かない)
11414
+ return JSON.stringify([d.driveFolderIds || [], d.driveFolderNames || {}, d.driveState || "", d.drivePending || {}, registrationUnreadable(),
11415
+ reachSigOf("materials", d.driveFolderIds || [])]);
10960
11416
  }, function (listBox) {
10961
11417
  var src = driveNow();
10962
11418
  var folders = src.driveFolderIds || [];
@@ -10984,6 +11440,10 @@ ${calendarAddUrl.toString()}
10984
11440
  ? { pending: true, pendingAt: src.drivePending[id] }
10985
11441
  : null),
10986
11442
  ]);
11443
+ // このフォルダに届くか(#523 S1)。engine の同期ジョブが毎回書いているのに
11444
+ // **これまで画面に出す場所が1つも無かった**(共有が外れたことを、取り込みが
11445
+ // 失敗するまで言えなかった)
11446
+ appendReachableChip(row, "materials", id);
10987
11447
  // **名前を出したときはIDも併記する。** 同名のフォルダを見分けられなくなるのと、
10988
11448
  // 「このIDを外す」という確認文と画面の表示が食い違うのを防ぐ
10989
11449
  if (fname) row.appendChild(el("span", { class: "ds-sync", text: id }));
@@ -11006,6 +11466,8 @@ ${calendarAddUrl.toString()}
11006
11466
  // **クラスのハード代入をやめる**(片方だけ直し忘れる形を構造的に消す)
11007
11467
  onDone: function () { row.classList.add("ds-removed"); },
11008
11468
  }));
11469
+ // 理由の1段は行の最後(ID と「このフォルダを外す」の後ろ・appendReachableReason の項)
11470
+ appendReachableReason(row, "materials", id);
11009
11471
  dlist.appendChild(row);
11010
11472
  });
11011
11473
  listBox.appendChild(dlist);
@@ -11134,6 +11596,9 @@ ${calendarAddUrl.toString()}
11134
11596
  // このファイルの取り込みが最後にどうなったか(段C)。測っていなければ null
11135
11597
  ingestedEl(designIngestedStateOf(f.key), "デザイン"),
11136
11598
  ]);
11599
+ // このファイルに届くか(#523 S1)。書き手は S4(いまは行が無いので何も出ない)。
11600
+ // 理由の1段は行の最後に置く(appendReachableReason の項)
11601
+ appendReachableChip(row, "design", f.key);
11137
11602
  row.appendChild(removeButton({
11138
11603
  label: "このファイルを外す",
11139
11604
  confirmLines: function () {
@@ -11167,6 +11632,7 @@ ${calendarAddUrl.toString()}
11167
11632
  else delete figSending[f.key];
11168
11633
  },
11169
11634
  }));
11635
+ appendReachableReason(row, "design", f.key);
11170
11636
  // 描き直しのあとも「外した」ことが分かる形に戻す(figsRemoved の項に経緯)
11171
11637
  if (figsRemoved.indexOf(f.key) >= 0) {
11172
11638
  row.classList.add("ds-removed");
@@ -13343,7 +13809,8 @@ ${calendarAddUrl.toString()}
13343
13809
  sync.removeAttribute("data-ts");
13344
13810
  // 接続OK のときだけ、ストア由来の取り込み時刻を添える(run の時刻ではない)。
13345
13811
  // 出せなければ語だけ——**繋がっていないものに同期時刻を並べない**(タイルと同じ規律)
13346
- var at = d.state === "ok" ? dsFmtTime(ingestedAtOf(LIVE_STATE, TILE_CAPS[cap].id)) : "";
13812
+ // 孤児行(外した接続先の残骸)の時刻は出さない(#523 決定2・タイルと同じ篩)
13813
+ var at = d.state === "ok" ? dsFmtTime(ingestedAtOf(LIVE_STATE, TILE_CAPS[cap].id, refKeysOf(cap))) : "";
13347
13814
  sync.textContent = at ? "最終同期 " + at : d.text;
13348
13815
  }
13349
13816
  var link = wrap.querySelector('.cm-link[data-cm-cap="' + cap + '"]');
@@ -13461,6 +13928,11 @@ ${calendarAddUrl.toString()}
13461
13928
  * 取れているときは図だけが古い。同じ画面で図が⚠️・直下が「接続OK」になる状態を
13462
13929
  * 黙って出さないよう、図がいつ時点かを明示する(図の再描画は別途)。
13463
13930
  * ライブが取れていないときは図と一覧が同じ時点なので、注記は出さない。
13931
+ *
13932
+ * 🔴 **「この図は◯時点」とは言わない(#523 決定5)。** ライブが届くと図の**状態**は
13933
+ * 当て直される(「applyStateToConnMap」)ので、古いのは**設定と件数だけ**。図ぜんたいを
13934
+ * 古いと言うと、訂正済みの状態まで疑わせる(逆に「最新」と言えば設定の古さが隠れる)。
13935
+ * 何が古くて何が最新かを、そのまま書く。
13464
13936
  */
13465
13937
  function updateConnMapAsOf() {
13466
13938
  var wrap = app.querySelector(".conn-map");
@@ -13470,7 +13942,7 @@ ${calendarAddUrl.toString()}
13470
13942
  if (!LIVE_FLEET || !SITE.fleetGeneratedAt) return;
13471
13943
  var mapAsOf = dsFmtTime(SITE.fleetGeneratedAt);
13472
13944
  if (!mapAsOf) return;
13473
- wrap.appendChild(el("p", { class: "conn-map-asof", text: "この図は " + mapAsOf + " 時点です(下の一覧は最新)。" }));
13945
+ wrap.appendChild(el("p", { class: "conn-map-asof", text: "設定と件数は " + mapAsOf + " 時点・接続状態は最新です。" }));
13474
13946
  }
13475
13947
  function opsCard(icon, title, desc, kind, btnLabel) {
13476
13948
  var btn = el("button", { class: "ops-card-btn", text: "+ " + btnLabel });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.48",
3
+ "version": "0.12.50",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,