@takagaki/cortex-decisions-viewer 0.12.22 → 0.12.23

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.
Files changed (2) hide show
  1. package/dist/render.js +218 -137
  2. package/package.json +1 -1
package/dist/render.js CHANGED
@@ -756,7 +756,7 @@ function sanitizeHarnessShelf(v) {
756
756
  * 写し忘れても赤くならなかったので、突き合わせは viewer/test/state-vocab-parity.test.mjs。
757
757
  */
758
758
  const STATE_STATUSES = {
759
- reachable: ["ok", "not_in_channel", "missing_scope", "no_token", "unreachable", "unknown", "not_in_space", "no_credentials"],
759
+ reachable: ["ok", "not_in_channel", "missing_scope", "no_token", "unreachable", "unknown", "not_in_space", "no_credentials", "no_consent", "transcripts_api_disabled", "no_access_policy", "speaker_attribution_off", "folder_not_granted"],
760
760
  ingested: ["ok", "failed", "skipped"],
761
761
  };
762
762
  const STATE_ROW_FIELDS = ["cap", "ref", "workspace", "question", "status", "at", "by", "needed", "detail", "lastOkAt", "lastFailAt"];
@@ -943,6 +943,13 @@ function gchatRowsToAdd(externals, rows) {
943
943
  *
944
944
  * **種別(issues / discussions)は登録ファイルの宣言をそのまま使う。** 無いときだけ
945
945
  * Issues に倒す——ここで決め打ちすると、Discussions だけを登録した案件のラベルが嘘になる。
946
+ *
947
+ * 🔴 **`origin: "explicit"` を付ける(#354)。** 「登録から削除」は `origin === "explicit"` の
948
+ * 行にだけ出る(導出リポに出すと押しても翌日戻る)。ここで足す行は `dev-sources.json` の
949
+ * `sources`(人が登録簿に書いたもの)からしか来ないので、明示登録であることは事実。
950
+ * 付けないと、**登録した直後の行だけ「登録から削除」が無い**(夜間のまとめ表が追いつく
951
+ * 翌朝まで、読めていない主役の行を外す手段が画面に無い——cortex-test で実際に踏んだ)。
952
+ * Slack の同じ穴を #221 で `chatRowsToAdd` が塞いだのと同型。
946
953
  */
947
954
  function devRowsToAdd(externals, rows) {
948
955
  const have = {};
@@ -957,7 +964,7 @@ function devRowsToAdd(externals, rows) {
957
964
  continue;
958
965
  if (have[String(r.ref)])
959
966
  continue;
960
- out.push({ type: r.type || "github-issues", name: r.ref, ref: r.ref });
967
+ out.push({ type: r.type || "github-issues", name: r.ref, ref: r.ref, origin: "explicit" });
961
968
  }
962
969
  return out;
963
970
  }
@@ -1627,7 +1634,7 @@ function keyMissingText(cap) {
1627
1634
  */
1628
1635
  const BAD_STATE_STATUSES = {
1629
1636
  ingested: ["failed", "skipped"],
1630
- reachable: ["not_in_channel", "missing_scope", "no_token", "unreachable", "not_in_space", "no_credentials"],
1637
+ reachable: ["not_in_channel", "missing_scope", "no_token", "unreachable", "not_in_space", "no_credentials", "no_consent", "transcripts_api_disabled", "no_access_policy", "speaker_attribution_off", "folder_not_granted"],
1631
1638
  };
1632
1639
  /**
1633
1640
  * タイル1行(と接続グラフの1ノード)の状態を、**正本から導出する**(0.12.0)。
@@ -3326,6 +3333,56 @@ const CLIENT_JS = `
3326
3333
  var SITE = JSON.parse(document.getElementById("site-data").textContent);
3327
3334
  var ADAPTER_ICONS = ${JSON.stringify(ADAPTER_ICONS)};
3328
3335
 
3336
+ // ---- intake(操作の受け口)への共通の呼び出し(#349)----
3337
+ //
3338
+ // **全 op はここを通す。** 名乗り券(ポータルが出す Cookie の cortex-actor)を body に添えるのは
3339
+ // この1箇所だけ。fetch を直書きすると、その op だけ操作者が記録されない。
3340
+ // 券は document.cookie から**1回だけ**読んでメモリに持つ。site-data にも localStorage にも書かない
3341
+ // (ビルド時の値ではなく、開いた人ごとに違う値なので)。
3342
+ //
3343
+ // **1回だけ読むのは仕様。** ポータルからログアウトしても、開いたままのタブは券の期限まで
3344
+ // 名乗り続ける——署名付き Cookie と同じ性質で、券の寿命がそのまま名乗りの寿命になる。
3345
+ var ACTOR_COOKIE = "cortex-actor";
3346
+ var actorTicketCache;
3347
+ function actorTicket() {
3348
+ if (actorTicketCache !== undefined) return actorTicketCache;
3349
+ actorTicketCache = null;
3350
+ try {
3351
+ var parts = String(document.cookie || "").split(";");
3352
+ for (var i = 0; i < parts.length; i++) {
3353
+ var kv = parts[i].trim();
3354
+ if (kv.indexOf(ACTOR_COOKIE + "=") === 0) { actorTicketCache = kv.slice(ACTOR_COOKIE.length + 1) || null; break; }
3355
+ }
3356
+ } catch (e) { actorTicketCache = null; }
3357
+ return actorTicketCache;
3358
+ }
3359
+ /**
3360
+ * intake の op を1回呼ぶ。POST は JSON の body に名乗り券(あれば)を actor として足す。
3361
+ * GET(opts.method === "GET" のとき)はカスタムヘッダも body も付けない(preflight を増やさないため・#198)。
3362
+ * 戻り値は fetch の Promise そのもの(呼び出し側の .then / await / res.json() はそのまま使える)。
3363
+ */
3364
+ function callIntake(op, body, opts) {
3365
+ var intake = SITE.intake;
3366
+ var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
3367
+ var url = pfx + "op=" + encodeURIComponent(op) + "&t=" + encodeURIComponent(intake.token);
3368
+ var o = opts || {};
3369
+ if (o.method === "GET") return fetch(url, { signal: o.signal });
3370
+ var payload = body && typeof body === "object" ? body : {};
3371
+ var ticket = actorTicket();
3372
+ if (ticket) {
3373
+ var withActor = {};
3374
+ for (var k in payload) if (Object.prototype.hasOwnProperty.call(payload, k)) withActor[k] = payload[k];
3375
+ withActor.actor = ticket;
3376
+ payload = withActor;
3377
+ }
3378
+ return fetch(url, {
3379
+ method: "POST",
3380
+ headers: { "content-type": "application/json" },
3381
+ body: JSON.stringify(payload),
3382
+ signal: o.signal,
3383
+ });
3384
+ }
3385
+
3329
3386
  // ---- ビルド時TSと共有する判定(唯一の正本は上の関数定義) ----
3330
3387
  //
3331
3388
  // **写経しない。** 接続マップ(ビルド時にSVGを描く)と画面(ブラウザで一覧を描く)が
@@ -3688,14 +3745,9 @@ ${deriveCapTile.toString()}
3688
3745
  var endBusy = beginBusy(btn, "確認中…");
3689
3746
  setStatus("", false);
3690
3747
  var who = String(name || "").trim();
3691
- var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
3692
3748
  var res, j;
3693
3749
  try {
3694
- res = await fetch(pfx + "op=promote&t=" + encodeURIComponent(intake.token), {
3695
- method: "POST",
3696
- headers: { "content-type": "application/json" },
3697
- body: JSON.stringify({ projectKey: intake.projectKey, path: path, submitter: who }),
3698
- });
3750
+ res = await callIntake("promote", { projectKey: intake.projectKey, path: path, submitter: who });
3699
3751
  j = await res.json().catch(function () { return {}; });
3700
3752
  } catch (e) {
3701
3753
  endBusy();
@@ -4406,6 +4458,44 @@ ${deriveCapTile.toString()}
4406
4458
  cls: "ds-gate-muted",
4407
4459
  help: "管理者の設定が済むと読み取りが始まります。",
4408
4460
  },
4461
+ // --- ここから Microsoft 365(#335)。**値の正本は infra 側の語彙表**
4462
+ // (slack-gate.mjs の SLACK_GATE_VALUES と connection-state-keys.mjs の
4463
+ // REACHABLE_STATUSES)で、1対1の一致を infra のテストと
4464
+ // viewer/test/state-vocab-parity.test.mjs が見張る。
4465
+ // **判定を返すのは engine の夜間実測(1-E 以降)**——画面は語彙と文面だけを先に持つ
4466
+ // (判定器そのものは infra/lambda/shared の teams-gate.mjs / sharepoint-gate.mjs /
4467
+ // teams-chat-gate.mjs に既にある)。
4468
+ //
4469
+ // **バッジは短語・次の一手は help に分ける**(DS_GATE_NO_TOKEN と同じ作り。
4470
+ // バッジに長文を入れると行が折り返す)。**内部語を出さない**——証明書・トークン・
4471
+ // テナント・サービスプリンシパルは、読む人が自分の画面で探せない言葉
4472
+ "no_consent": {
4473
+ text: "管理者の承認が必要です",
4474
+ cls: "ds-gate-warn",
4475
+ help: "貴社の Microsoft 365 管理者が承認リンクを開いて許可すると読み取りが始まります。承認を取り消した場合もこの表示になります。",
4476
+ },
4477
+ "transcripts_api_disabled": {
4478
+ text: "文字起こしの外部取得がオフです",
4479
+ cls: "ds-gate-warn",
4480
+ help: "Teams 管理センターの会議設定で「文字起こしへの API アクセス」をオンにしてください。",
4481
+ },
4482
+ "no_access_policy": {
4483
+ text: "会議へのアクセス許可が未設定です",
4484
+ cls: "ds-gate-warn",
4485
+ help: "管理者がこの主催者にアクセス ポリシーを割り当てると読み取りが始まります(反映まで最大30分)。",
4486
+ },
4487
+ // **承認済み・API アクセスもオンなのに、話者名のトグルだけがオフ**という状態が実在する。
4488
+ // transcripts_api_disabled に寄せると、既にオンにしてある設定をもう一度案内することになる
4489
+ "speaker_attribution_off": {
4490
+ text: "話者名付きの文字起こしがオフです",
4491
+ cls: "ds-gate-warn",
4492
+ help: "Teams 管理センターの会議設定で話者名付き文字起こしをオンにしてください。オフのままだと取り込みません。",
4493
+ },
4494
+ "folder_not_granted": {
4495
+ text: "フォルダへのアクセス許可がありません",
4496
+ cls: "ds-gate-warn",
4497
+ help: "SharePoint 管理者がこのフォルダに読み取り許可を付与すると取り込みが始まります。",
4498
+ },
4409
4499
  "unknown": { text: "不明", cls: "ds-gate-muted" },
4410
4500
  };
4411
4501
  /* ---------------------------------------------------------------------------
@@ -4479,16 +4569,26 @@ ${deriveCapTile.toString()}
4479
4569
  },
4480
4570
  };
4481
4571
  /**
4482
- * 休止状態の行に、状態の語に添えて**押す前から**出す定型の補足(#346)。
4483
- * 鍵は「アダプタ種別(helpTypeOf)× 状態」。値は文のパーツの配列で、文字列はテキスト、
4484
- * { link: <リンクの文字列> } は GitHub App のインストール設定へのリンク(GITHUB_APP_INSTALL_URL)。
4572
+ * 休止状態の行に、状態の語に添えて**押す前から**出す定型の補足(#346)と、
4573
+ * 「接続を確認」を押した結果に添える定型の1文(#356)。
4574
+ * 鍵は「アダプタ種別(helpTypeOf)× 状態」。値は { resting, probed } で、どちらも文の
4575
+ * パーツの配列。文字列はテキスト、{ link: <リンクの文字列> } は GitHub App の
4576
+ * インストール設定へのリンク(GITHUB_APP_INSTALL_URL)。
4577
+ *
4578
+ * ## 文の長さ(#356・オーナー却下を受けた原則)
4579
+ * **帯は症状だけ・行は具体的な一手だけ・説明を繰り返さない。** 0.12.22 では帯と行と
4580
+ * 押した結果に同じ admin の4文が3回出ており、オーナーが「長すぎ・冗長」と却下した。
4581
+ * - resting … **1文+リンクだけ**(誰が・どこで。「何が起きているか」は帯が言う)
4582
+ * - probed … **状態に応じた1文だけ**(1行目のサーバの needed が言わないことだけ)。
4583
+ * 押した結果に resting を繰り返さない——resting は押した瞬間に隠れ、needed が
4584
+ * 同じ操作(Configure から追加)を宛先つきで言う
4585
+ * 「admin」の語は画面に1回だけ(github-state-reason.test.mjs が押す前・押した後で数える)。
4485
4586
  *
4486
4587
  * ## なぜ画面の表で持つのか
4487
4588
  * サーバの needed / detail は「何が起きているか」と「どこを開くか」までを持つが、
4488
- * **誰がその操作をできるか**(リポジトリの admin なら自分で・でなければ admin に依頼・
4489
- * request は組織オーナーの承認待ち)は、夜間(engine)とボタン(Lambda)の2つの書き手が
4490
- * 別々に文を作るので、サーバ側で足すと片方だけ古くなる。判定と URL はサーバ、定型の補足は
4491
- * ここ、という役割分担にする。
4589
+ * **誰がその操作をできるか**(リポジトリの admin)と **request の意味**(組織オーナーの
4590
+ * 承認待ち)は、夜間(engine)とボタン(Lambda)の2つの書き手が別々に文を作るので、
4591
+ * サーバ側で足すと片方だけ古くなる。判定と URL はサーバ、定型の補足はここ、という役割分担。
4492
4592
  *
4493
4593
  * 🔴 **DS_GATE には入れない**(実測が返す語彙の表で、突き合わせテストの対象)。
4494
4594
  * DS_GATE_NO_TOKEN と同じく、仕組みはアダプタ非依存・データだけがアダプタ固有。
@@ -4500,21 +4600,29 @@ ${deriveCapTile.toString()}
4500
4600
  * 片方だけ古くなる)。
4501
4601
  *
4502
4602
  * エントリがある状態では**この文だけ**を出し、サーバの needed / detail は出さない
4503
- * ——理由文・補足・確認の一文の3段が issues / discussions の2行 × N リポに並ぶと、
4504
- * 一覧が案内で埋まる(DS_GATE_NO_TOKEN.github の項と同じ判断)。needed は押した結果が持つ。
4603
+ * ——理由文と補足が issues / discussions の2行 × N リポに並ぶと、一覧が案内で埋まる
4604
+ * DS_GATE_NO_TOKEN.github の項と同じ判断)。needed は押した結果が持つ。
4605
+ *
4606
+ * ⚠ **「移動・改名・削除の可能性 → 登録から外してください」は出さない。** App のトークン
4607
+ * では「未選択」と「消えた・改名」がどちらも 404 で**区別できない**(github-gate.mjs)。
4608
+ * 状態で出し分ける材料が無いので書かない。消えたリポジトリの逃げ道は、行に常に出る
4609
+ * 「登録から削除」(#354)が担う。区別する信号ができたら probed に足す
4505
4610
  */
4506
- var GITHUB_ADMIN_HELP = "このリポジトリの admin(管理者権限を持つ人)であれば、Configure から自分で追加できます(承認は不要です)。"
4507
- + "admin でない場合は、そのリポジトリの admin の人に「Cortex の GitHub App の対象にこのリポジトリを追加してください」と依頼してください。"
4508
- + "GitHub 側で request と表示された場合は、組織オーナーの承認待ちです。"
4509
- + "追加したあと「接続を確認」を押して、読めるようになったことを確かめてください。";
4510
4611
  var DS_GATE_RESTING_HELP = {
4511
4612
  github: {
4512
- unreachable: [
4513
- "App は導入済みですが、このリポジトリが読み取り対象に選ばれていない(または移動・改名・削除された)可能性があります。",
4514
- { link: "インストール設定を開く" },
4515
- " の Configure から対象の組織を選び、Repository access に追加してください。",
4516
- GITHUB_ADMIN_HELP,
4517
- ],
4613
+ unreachable: {
4614
+ // 誰が(admin)・どこで(Configure の Repository access)だけ。理由は帯、宛先はリンクが持つ。
4615
+ // 着地点(Repository access)を名指しするのは、押す前の画面からこの語が消えないため
4616
+ //(設計レビュー②)。「admin が…してください」はオーナーの原文の形
4617
+ resting: [
4618
+ "このリポジトリの admin(管理者権限を持つ人)が、Configure の Repository access に追加してください。",
4619
+ { link: "インストール設定を開く" },
4620
+ ],
4621
+ // 2026-09-02 の実測: admin でないリポジトリを Configure で選ぶと「request」タグが付き、
4622
+ // 組織オーナーの承認待ちになる("Okay, updated" と出るのに対象に入らない)。
4623
+ // 1行目(サーバの needed)はこれを言わないので、ここだけが持つ
4624
+ probed: ["GitHub 側で request と表示された場合は、組織オーナーの承認待ちです。"],
4625
+ },
4518
4626
  },
4519
4627
  };
4520
4628
  /**
@@ -4526,11 +4634,20 @@ ${deriveCapTile.toString()}
4526
4634
  var t = String(type || "");
4527
4635
  return t.indexOf("github") === 0 ? "github" : t;
4528
4636
  }
4529
- /** 表のパーツ(無ければ null)。行と押した結果の**両方**がここを通す(写経しない) */
4530
- function restingHelpPartsOf(type, gate) {
4637
+ function restingHelpEntryOf(type, gate) {
4531
4638
  var byType = DS_GATE_RESTING_HELP[helpTypeOf(type)];
4532
4639
  return (byType && byType[gate]) || null;
4533
4640
  }
4641
+ /** 休止状態の行に出すパーツ(無ければ null) */
4642
+ function restingHelpPartsOf(type, gate) {
4643
+ var e = restingHelpEntryOf(type, gate);
4644
+ return (e && e.resting) || null;
4645
+ }
4646
+ /** 押した結果に添えるパーツ(無ければ null)。行と同じ表から引く(写経しない) */
4647
+ function probedHelpPartsOf(type, gate) {
4648
+ var e = restingHelpEntryOf(type, gate);
4649
+ return (e && e.probed) || null;
4650
+ }
4534
4651
  /** パーツを1段落にする。包む要素の class は置き場所で違う(行は ds-gate-help・押した結果は ds-probe-help) */
4535
4652
  function restingHelpNode(parts, cls) {
4536
4653
  var p = el("p", { class: cls });
@@ -4895,12 +5012,7 @@ ${deriveCapTile.toString()}
4895
5012
  msg.className = "ds-toggle-msg";
4896
5013
  msg.textContent = "承認画面を準備しています…";
4897
5014
  try {
4898
- var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
4899
- var res = await fetch(pfx + "op=slack-oauth-start&t=" + encodeURIComponent(intake.token), {
4900
- method: "POST",
4901
- headers: { "content-type": "application/json" },
4902
- body: "{}",
4903
- });
5015
+ var res = await callIntake("slack-oauth-start", {});
4904
5016
  var j = await res.json().catch(function () { return {}; });
4905
5017
  if (!res.ok || !j.url) {
4906
5018
  // サーバの文言をそのまま出す(未設定のときは「準備中です」が返る)
@@ -4959,7 +5071,6 @@ ${deriveCapTile.toString()}
4959
5071
  orgResolveDone = true;
4960
5072
  var intake = SITE.intake;
4961
5073
  if (!intake) return;
4962
- var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
4963
5074
  // **描画の途中で投げない。** ここは行を組み立てている最中に呼ばれるので、
4964
5075
  // 例外が出ると**画面そのものが描かれない**(fetch が無い環境で実際にそうなった)。
4965
5076
  // 判定し直しは「できたら直る」性質のもので、失敗しても行は
@@ -4971,11 +5082,7 @@ ${deriveCapTile.toString()}
4971
5082
  // fetch が存在しない(ReferenceError)と、呼び出しが投げる
4972
5083
  if (typeof fetch !== "function") return;
4973
5084
  try {
4974
- fetch(pfx + "op=slack-resolve&t=" + encodeURIComponent(intake.token), {
4975
- method: "POST",
4976
- headers: { "content-type": "application/json" },
4977
- body: "{}",
4978
- })
5085
+ callIntake("slack-resolve", {})
4979
5086
  .then(function (res) { return res.ok ? res.json() : null; })
4980
5087
  .then(function (j) {
4981
5088
  if (!j || !j.resolved) return;
@@ -5192,35 +5299,46 @@ ${deriveCapTile.toString()}
5192
5299
  // App 導入済みの組織にも「まだ接続されていません」と出て、**同じ帯の本文と矛盾**した
5193
5300
  // (実機でオーナーが踏み、利用者が「組織の接続からやり直し」と誤解した)
5194
5301
  if (installed === true) {
5195
- // **見出しでは「読み取り対象に選ばれていません」と言い切らない。** 下のコメントの
5196
- // とおり②未選択と③移動・改名・削除は分かれないので、見出しは事実だけを言う
5302
+ // 🔴 **帯は症状だけ(#356・オーナー却下を受けた原則)。** 0.12.22 ではここに9文
5303
+ // (App の説明・Configure の手順・admin の4文・移動改名削除・お客様の組織)を入れ、
5304
+ // 同じ説明が行と押した結果にも出て3回繰り返された。見出しは件数だけ・本文は2文だけ。
5305
+ // **組織名は見出しに入れない**——各行のバッジ「{組織}(GitHub App)」が持っている。
5306
+ // 組織が複数ある画面でも、組織ごとの帯のまま・見出しは同じ形。
5307
+ //
5308
+ // **見出しでは「読み取り対象に選ばれていません」と言い切らない。** 「appInstalled」 が
5309
+ // 分けられるのは「①未インストール」と「②未選択/③消えた・改名」までで、**②と③は
5310
+ // 分かれない**(App のトークンではどちらも 404)。見出しは事実だけを言う
5197
5311
  out.push(el("div", {
5198
5312
  class: "ds-orgnote-title",
5199
- text: "⚠️ " + g.title + " は App 導入済みですが、" + unread + "件のリポジトリを読み取れていません",
5313
+ text: "⚠️ " + unread + "件のリポジトリを読み取れていません",
5200
5314
  }));
5201
5315
  // App は入っている。**インストールをもう一度促さないこと**——済んでいる操作を促すのは
5202
- // 「設定する必要が無いものに設定を促す」偽の警告と同じで、しかも直らない
5316
+ // 「設定する必要が無いものに設定を促す」偽の警告と同じで、しかも直らない。
5317
+ //
5318
+ // 「誰が・どこで」は各行の1文(DS_GATE_RESTING_HELP)が持つ。ここで繰り返さない。
5319
+ // 「移動・改名・削除の可能性 → 登録から外して」も言わない——②と③を区別する材料が
5320
+ // 無く、外す手段は行の「登録から削除」(#354)が常に持つ。
5321
+ // 「お客様の組織では、組織のオーナーに承認を依頼」も外した——request(承認待ち)の
5322
+ // 1文は押した結果が持つ(同じ表の probed)。
5203
5323
  //
5204
- // **原因を1つに断定しない(2026-08-21)。**appInstalled」 が分けられるのは
5205
- // 「①未インストール」と「②未選択/③消えた・改名」までで、**②と③は分かれない**。
5206
- // 「対象に選ばれていません」と言い切ると、③の人は一覧に無いリポジトリを
5207
- // 探し続けることになる(押しても直らない導線と同じ形)。逃げ道を1文で示す。
5324
+ // **件数の文 head」(「{N}リポジトリ中 {M}件が読めていません。」)もこの枝では
5325
+ // 出さない(#356)。** #101 で3度直した「読める件数を出す」を意図して覆した——見出しが
5326
+ // 件数を持つので、本文で二重に言うと帯が3文になる。分母(登録数)は画面から消えるが、
5327
+ // 箱の中に行が全部並んでいるので数えられる。false / null の枝は従来どおり head を出す。
5208
5328
  //
5209
- // **設定画面の宛先は App 個別ページに統一した(#346)。** 以前の
5210
- // 「/settings/installations」 は**押した本人が入れた App しか出ない**個人の一覧で、
5211
- // App を入れたのが別のメンバーなら着いた先が空だった(そのための断り書きも要らなくなった)。
5212
- // いまの宛先と、そこに着ける人の条件は 「appSettingsLink」 の項にある。
5329
+ // **「各行の案内」ではなく「アクセス不可の行の案内」と言う。** 帯の箱には読めている行も
5330
+ // 一緒に入る(unread が1件でもあれば組織ごと warn)。案内を持つのは「アクセス不可」の
5331
+ // 行だけなので、行に出ている状態の語そのもので指す(設計レビュー②の指摘)。
5213
5332
  //
5214
- // **誰がその操作をできるかまで書く(2026-09-02 の実測)。** リポジトリの admin なら
5215
- // Configure から自分で追加でき、admin でなければ GitHub が 「request」 を出して
5216
- // 組織オーナーの承認待ちになる。片方に断定せず両方の道を並べる
5333
+ // **「読み取り対象に入っていません」はオーナーの文(#356)。** 2026-08-21
5334
+ // 「②未選択と③消えた・改名を区別できないので断定しない」は、帯の本文については
5335
+ // この文で覆した(見出しは引き続き断定しない)。③の逃げ道は「登録から削除」。
5336
+ //
5337
+ // **設定画面の宛先は App 個別ページ(#346)。** いまの宛先と、そこに着ける人の条件は
5338
+ // 「appSettingsLink」 の項にある
5217
5339
  out.push(el("p", {
5218
5340
  class: "ds-group-note",
5219
- text: head + "GitHub App はこの組織に入っていますが、これらのリポジトリが読み取りの対象に選ばれていない(または移動・改名・削除された)可能性があります。"
5220
- + "「インストール設定を開く」から " + g.title + " の Configure を開き、Repository access に対象のリポジトリを追加してください。"
5221
- + GITHUB_ADMIN_HELP
5222
- + "リポジトリ自体が一覧に見当たらない場合は、移動・改名・削除されている可能性があるので、登録から外してください。"
5223
- + "お客様の組織では、組織のオーナーに承認を依頼してください。",
5341
+ text: "GitHub App の読み取り対象に入っていません。アクセス不可の行の案内に従って追加してください。",
5224
5342
  }));
5225
5343
  out.push(appSettingsLink("インストール設定を開く →"));
5226
5344
  return out;
@@ -5414,14 +5532,8 @@ ${deriveCapTile.toString()}
5414
5532
  btn.disabled = true;
5415
5533
  msg.className = "ds-toggle-msg";
5416
5534
  msg.textContent = "実行中…";
5417
- var intake = SITE.intake;
5418
- var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
5419
5535
  try {
5420
- var res = await fetch(pfx + "op=sync&t=" + encodeURIComponent(intake.token), {
5421
- method: "POST",
5422
- headers: { "content-type": "application/json" },
5423
- body: JSON.stringify({ kind: kind }),
5424
- });
5536
+ var res = await callIntake("sync", { kind: kind });
5425
5537
  var j = await res.json().catch(function () { return {}; });
5426
5538
  // **起こせなかったときも「失敗」と言わない。** 定期実行が拾うので、
5427
5539
  // 押した人にできることは無い。何が起きるかだけを伝える
@@ -7106,11 +7218,9 @@ ${deriveCapTile.toString()}
7106
7218
  var giveUp = ctrl ? setTimeout(function () { ctrl.abort(); }, LIVE_CONFIRM_TIMEOUT_MS) : null;
7107
7219
  // **成功・失敗の両方で外す。** 残すと、window を閉じないテストで node --test が終わらない
7108
7220
  var clearGiveUp = function () { if (giveUp != null) { clearTimeout(giveUp); giveUp = null; } };
7109
- var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
7110
- // GET で取る。**カスタムヘッダは付けない**(preflight を1往復増やさないため)
7111
- fetch(pfx + "op=harness-catalog&t=" + encodeURIComponent(intake.token), {
7112
- signal: ctrl ? ctrl.signal : undefined,
7113
- })
7221
+ // GET で取る。**カスタムヘッダは付けない**(preflight 1往復増やさないため)。
7222
+ // 名乗り券は body に載せる仕組みなので、GET のこの op には付かない(#349)
7223
+ callIntake("harness-catalog", null, { method: "GET", signal: ctrl ? ctrl.signal : undefined })
7114
7224
  .then(function (res) {
7115
7225
  clearGiveUp();
7116
7226
  if (!res.ok) throw new Error("http " + res.status);
@@ -8368,18 +8478,21 @@ ${deriveCapTile.toString()}
8368
8478
  * 「App が入っていない」と「App は入っているがこのリポジトリが選ばれていない」の
8369
8479
  * 言い分けも、設定ページの URL もあちらが決める——**画面で分岐を書き足さない**
8370
8480
  * (書いた瞬間に判定が2箇所になり、夜間の実測とボタンでずれる)。
8371
- * **ただし admin の定型補足だけは画面の表(DS_GATE_RESTING_HELP)から引く**(#346)——
8372
- * 「誰がその操作をできるか」はサーバの2つの書き手が別々に文を作るので、片方だけ古くなる。
8481
+ * **ただし状態に応じた定型の1文だけは画面の表(DS_GATE_RESTING_HELP probed)から引く**
8482
+ * (#346 → #356)——「request は組織オーナーの承認待ち」のような GitHub 側の意味は、
8483
+ * サーバの2つの書き手が別々に文を作るので、片方だけ古くなる。
8484
+ *
8485
+ * 🔴 **行の常時表示(resting)を押した結果に繰り返さない(#356)。** 0.12.22 では admin の
8486
+ * 4文が行と押した結果の両方に出ていた。行の1文は押した瞬間に隠れ、needed が同じ操作
8487
+ * (Configure から追加)を宛先つきで言うので、ここは needed が言わない1文だけ
8373
8488
  */
8374
8489
  function githubProbeHelpNodes(r) {
8375
8490
  if (r.gate === "ok") return [];
8376
8491
  if (r.needed) {
8377
8492
  var nodes = [neededNode(r.needed)];
8378
- // 定型の補足(admin なら自分で・でなければ admin に依頼)は**行と同じ表から引く**。
8379
- // 言い分けと URL はサーバ(needed)、定型の補足だけ画面の表——ここに文を直書きすると
8380
- // 休止状態の行と押した結果で言うことが割れる。理由と URL は needed が既に持っているので、
8381
- // 表のパーツ全部ではなく admin の4文(GITHUB_ADMIN_HELP)だけを添える
8382
- if (restingHelpPartsOf("github", r.gate)) nodes.push(restingHelpNode([GITHUB_ADMIN_HELP], "ds-probe-help"));
8493
+ // 状態に応じた1文は**行と同じ表から引く**(ここに文を直書きすると表と割れる)
8494
+ var probed = probedHelpPartsOf("github", r.gate);
8495
+ if (probed) nodes.push(restingHelpNode(probed, "ds-probe-help"));
8383
8496
  return nodes;
8384
8497
  }
8385
8498
  // needed が無いのは gate に落とし込めなかったとき。**生のエラーは隠さない**
@@ -8525,12 +8638,7 @@ ${deriveCapTile.toString()}
8525
8638
  if (!intake) return;
8526
8639
  var payload = { op: "refresh-channel-name", ref: s.ref };
8527
8640
  if (s.workspace) payload.workspace = s.workspace;
8528
- var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
8529
- fetch(pfx + "op=config&t=" + encodeURIComponent(intake.token), {
8530
- method: "POST",
8531
- headers: { "content-type": "application/json" },
8532
- body: JSON.stringify(payload),
8533
- }).then(function (res) {
8641
+ callIntake("config", payload).then(function (res) {
8534
8642
  if (!res.ok) return;
8535
8643
  // 一覧は夜間のスナップショットなので、その場では変わらない。行だけ追いつかせる
8536
8644
  var nameEl = card.querySelector(".ds-name");
@@ -8591,13 +8699,8 @@ ${deriveCapTile.toString()}
8591
8699
  // ワークスペースは Slack にしか無い概念なので、他アダプタでは載せない
8592
8700
  var payload = { type: probeTypeOf(s.type), ref: s.ref };
8593
8701
  if (s.workspace) payload.workspace = s.workspace;
8594
- var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
8595
8702
  try {
8596
- var res = await fetch(pfx + "op=probe&t=" + encodeURIComponent(intake.token), {
8597
- method: "POST",
8598
- headers: { "content-type": "application/json" },
8599
- body: JSON.stringify(payload),
8600
- });
8703
+ var res = await callIntake("probe", payload);
8601
8704
  var j = await res.json().catch(function () { return {}; });
8602
8705
  if (!res.ok) {
8603
8706
  // サーバの文言をそのまま出す(回数制限の案内・未対応のアダプタの説明を含んでいる)
@@ -9932,17 +10035,12 @@ ${deriveCapTile.toString()}
9932
10035
  async function sendConfig(payload, msg, btn, onDone, hooks) {
9933
10036
  var intake = SITE.intake;
9934
10037
  if (!intake) return;
9935
- var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
9936
10038
  // 押している間はボタンごと「処理中…」にする(#222・beginBusy の項)
9937
10039
  var endBusy = beginBusy(btn);
9938
10040
  msg.className = "ds-toggle-msg";
9939
10041
  msg.textContent = "保存中…";
9940
10042
  try {
9941
- var res = await fetch(pfx + "op=config&t=" + encodeURIComponent(intake.token), {
9942
- method: "POST",
9943
- headers: { "content-type": "application/json" },
9944
- body: JSON.stringify(payload),
9945
- });
10043
+ var res = await callIntake("config", payload);
9946
10044
  var j = await res.json().catch(function () { return {}; });
9947
10045
  // 一覧が添えられていれば、断られていても先に画面を追いつかせる(#127)
9948
10046
  var raced = hooks && hooks.onTargets && Array.isArray(j.targets) ? hooks.onTargets(j.targets) : false;
@@ -10962,17 +11060,15 @@ ${deriveCapTile.toString()}
10962
11060
  var giveUp = ctrl ? setTimeout(function () { ctrl.abort(); }, LIVE_CONFIRM_TIMEOUT_MS) : null;
10963
11061
  /** 見切りのタイマを外す。**応答が来たら必ず呼ぶ**(残すと次の取得を巻き込んで中断する) */
10964
11062
  var clearGiveUp = function () { if (giveUp != null) { clearTimeout(giveUp); giveUp = null; } };
10965
- var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
10966
- fetch(pfx + "op=state&t=" + encodeURIComponent(intake.token), {
10967
- method: "POST",
10968
- headers: { "content-type": "application/json" },
10969
- signal: ctrl ? ctrl.signal : undefined,
10970
- // **常に全能力を要求する(ALL_STATE_CAPS の項)。** 知らない値は Lambda 側が黙って
10971
- // 捨てる(断らない)ので、画面と Lambda の版が食い違っても接続状況の表示は消えない
10972
- // **保存直後だけ据え置きを迂回させる。** これが無いと、サーバが保存前の答えを返し、
10973
- // 足りない行が0件になって取り直しも張られない=手で再読み込みするまで追いつかない
10974
- body: JSON.stringify(useFresh ? { caps: ALL_STATE_CAPS, fresh: true } : { caps: ALL_STATE_CAPS }),
10975
- })
11063
+ // **常に全能力を要求する(ALL_STATE_CAPS の項)。** 知らない値は Lambda 側が黙って
11064
+ // 捨てる(断らない)ので、画面と Lambda の版が食い違っても接続状況の表示は消えない
11065
+ // **保存直後だけ据え置きを迂回させる。** これが無いと、サーバが保存前の答えを返し、
11066
+ // 足りない行が0件になって取り直しも張られない=手で再読み込みするまで追いつかない
11067
+ callIntake(
11068
+ "state",
11069
+ useFresh ? { caps: ALL_STATE_CAPS, fresh: true } : { caps: ALL_STATE_CAPS },
11070
+ { signal: ctrl ? ctrl.signal : undefined },
11071
+ )
10976
11072
  // **非2xxも失敗として扱う。** ビューアとLambdaの版は独立に進むので、
10977
11073
  // op=state 未対応のLambdaは 400 を返す。通信エラーだけを見ていると空表示になる。
10978
11074
  .then(function (res) {
@@ -11479,18 +11575,13 @@ ${deriveCapTile.toString()}
11479
11575
  msg.className = "ds-toggle-msg";
11480
11576
  msg.textContent = "保存中…";
11481
11577
  var intake = SITE.intake;
11482
- var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
11483
11578
  try {
11484
- var res = await fetch(pfx + "op=secret&t=" + encodeURIComponent(intake.token), {
11485
- method: "POST",
11486
- headers: { "content-type": "application/json" },
11487
- // **シークレット名は送らない。** どの連携かだけを送り、書き込み先はサーバが案件から決める。
11488
- // **Backlog はスペースだけ名乗る(#138)。** 名前を組み立てるのはサーバで、
11489
- // こちらが渡すのは「どのスペースの鍵か」という材料だけ。
11490
- // **押した時点で読む。** 組み立て時に固めると、接続先を登録した直後に
11491
- // 貼った鍵が「スペースを特定できません」で断られる
11492
- body: JSON.stringify(secretPayload(intake, kind, value, cardSpace)),
11493
- });
11579
+ // **シークレット名は送らない。** どの連携かだけを送り、書き込み先はサーバが案件から決める。
11580
+ // **Backlog はスペースだけ名乗る(#138)。** 名前を組み立てるのはサーバで、
11581
+ // こちらが渡すのは「どのスペースの鍵か」という材料だけ。
11582
+ // **押した時点で読む。** 組み立て時に固めると、接続先を登録した直後に
11583
+ // 貼った鍵が「スペースを特定できません」で断られる
11584
+ var res = await callIntake("secret", secretPayload(intake, kind, value, cardSpace));
11494
11585
  var j = await res.json().catch(function () { return {}; });
11495
11586
  if (!res.ok) {
11496
11587
  msg.className = "ds-toggle-msg is-error";
@@ -12239,7 +12330,6 @@ ${deriveCapTile.toString()}
12239
12330
  if (!entries || !entries.length) { setStatus("⚠ ファイルを選択してください。", true); return; }
12240
12331
  if (isT && !meeting) { setStatus("⚠ 会議名を選んでください。", true); return; }
12241
12332
  submitBtn.disabled = true;
12242
- var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
12243
12333
  function fail(msg) { setStatus("✗ " + msg, true); submitBtn.disabled = false; }
12244
12334
  try {
12245
12335
  var uploaded = [];
@@ -12248,10 +12338,8 @@ ${deriveCapTile.toString()}
12248
12338
  setStatus("アップロード中… (" + (i + 1) + "/" + entries.length + ") " + f.name, false);
12249
12339
  var pres;
12250
12340
  try {
12251
- pres = await fetch(pfx + "op=presign&t=" + encodeURIComponent(intake.token), {
12252
- method: "POST", headers: { "content-type": "application/json" },
12253
- body: JSON.stringify({ projectKey: intake.projectKey, filename: f.name, contentType: f.type || "application/octet-stream" }),
12254
- }).then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); });
12341
+ pres = await callIntake("presign", { projectKey: intake.projectKey, filename: f.name, contentType: f.type || "application/octet-stream" })
12342
+ .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); });
12255
12343
  } catch (net) { return fail("サーバーに接続できませんでした。通信環境を確認して、もう一度お試しください。"); }
12256
12344
  if (!pres.ok || !pres.j.url) return fail((pres.j && pres.j.message) || "アップロードURLの取得に失敗しました。対応していないファイル形式かもしれません。");
12257
12345
  var put;
@@ -12269,10 +12357,8 @@ ${deriveCapTile.toString()}
12269
12357
  try {
12270
12358
  var payload = { projectKey: intake.projectKey, kind: kind, files: uploaded, submitter: submitter.trim() };
12271
12359
  if (isT) payload.meeting = meeting;
12272
- sub = await fetch(pfx + "op=submit&t=" + encodeURIComponent(intake.token), {
12273
- method: "POST", headers: { "content-type": "application/json" },
12274
- body: JSON.stringify(payload),
12275
- }).then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); });
12360
+ sub = await callIntake("submit", payload)
12361
+ .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); });
12276
12362
  } catch (net3) { return fail("サーバーに接続できませんでした。もう一度お試しください。"); }
12277
12363
  if (!sub.ok) return fail((sub.j && sub.j.message) || "取り込みに失敗しました。時間をおいて再度お試しください。");
12278
12364
  status.className = "modal-status";
@@ -12630,15 +12716,10 @@ ${deriveCapTile.toString()}
12630
12716
  var endBusy = beginBusy(btn, "削除中…");
12631
12717
  msg.className = "record-delete-msg";
12632
12718
  msg.textContent = "";
12633
- var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
12634
12719
  var res, j;
12635
12720
  try {
12636
- res = await fetch(pfx + "op=delete-record&t=" + encodeURIComponent(intake.token), {
12637
- method: "POST",
12638
- headers: { "content-type": "application/json" },
12639
- // 名乗りは確認済み昇格で覚えたものを流用する(削除のためだけに名前を聞かない)
12640
- body: JSON.stringify({ projectKey: intake.projectKey, path: path, submitter: rememberedVerifier() }),
12641
- });
12721
+ // 名乗りは確認済み昇格で覚えたものを流用する(削除のためだけに名前を聞かない)
12722
+ res = await callIntake("delete-record", { projectKey: intake.projectKey, path: path, submitter: rememberedVerifier() });
12642
12723
  j = await res.json().catch(function () { return {}; });
12643
12724
  } catch (e) {
12644
12725
  endBusy();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.22",
3
+ "version": "0.12.23",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,