@takagaki/cortex-decisions-viewer 0.12.123 → 0.12.125

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
@@ -118,6 +118,7 @@ const confirmed_1 = require("./confirmed");
118
118
  const minify_1 = require("./minify");
119
119
  const render_1 = require("./render");
120
120
  const ledger_dir_1 = require("./ledger-dir");
121
+ const view_dir_1 = require("./view-dir");
121
122
  /**
122
123
  * この画面を作ったパッケージの版。**package.json を唯一の出どころにする。**
123
124
  * 別に持つと、公開した版と画面に出る版がずれる(しかも気づけない)。
@@ -732,12 +733,18 @@ function orderRecordsByLedgers(records, recordsDir, ctx, nowIso) {
732
733
  * 全部が人になるため。engine の人の手の表がビューアに届く段 5 で差し替える。
733
734
  * 取りこぼしの代償は「確認の一言が出ない」だけ(消せる範囲は変わらない)。
734
735
  *
736
+ * 記憶 v2 の案件で人の手の表の写し(`memory-hands`)が読めたときは、2 の確認の帳簿の代わりに表の `touched` を見る
737
+ * (cortex-engine #1094 鎖 3 V10b・B9a。確認の帳簿は記憶 v2 では書かれなくなる)。表が読めない晩は今の確認の帳簿に倒す。
738
+ *
735
739
  * @param p リポジトリルート相対の path(`Cortex/<区画>/records/…`)。帳簿の鍵は NFC なので NFC でも引く
736
740
  * @param status その記録の `rstatus`(`String()` 済み)
741
+ * @param hands 人の手の表(path → touched)。渡したときは確認の帳簿を見ない
737
742
  */
738
- function humanTouchedOf(p, status, confirmation) {
743
+ function humanTouchedOf(p, status, confirmation, hands = null) {
739
744
  if ((0, confirmed_1.humanOwned)(p, status))
740
745
  return true;
746
+ if (hands)
747
+ return hands.get(p) === true || hands.get(p.normalize("NFC")) === true;
741
748
  const row = confirmation.get(p) ?? confirmation.get(p.normalize("NFC"));
742
749
  return !!row && row.deleted !== true;
743
750
  }
@@ -746,11 +753,11 @@ function humanTouchedOf(p, status, confirmation) {
746
753
  * (今の SiteData の形を保つ)。path は `repoRoot` 起点で作る(帳簿の鍵はリポジトリルート相対)。
747
754
  * 置き換えの逆引き(`attachSupersedes`)より前に呼ぶ。
748
755
  */
749
- function attachHumanTouched(records, recordsDir, repoRoot, confirmation) {
756
+ function attachHumanTouched(records, recordsDir, repoRoot, confirmation, hands = null) {
750
757
  const recordsRel = path.relative(repoRoot, recordsDir).split(path.sep).join("/");
751
758
  for (const r of records) {
752
759
  const p = recordsRel ? `${recordsRel}/${r.fileName}` : r.fileName;
753
- if (humanTouchedOf(p, r.rstatus, confirmation))
760
+ if (humanTouchedOf(p, r.rstatus, confirmation, hands))
754
761
  r.humanTouched = true;
755
762
  }
756
763
  }
@@ -2370,6 +2377,18 @@ async function externalizeBodies(site, outAbs) {
2370
2377
  }
2371
2378
  }
2372
2379
  async function build(opts) {
2380
+ // 合わせの置き場(`--view-dir`)の一時フォルダは、ビルドが落ちても消す
2381
+ const views = {};
2382
+ try {
2383
+ const r = await buildSite(opts, views);
2384
+ return views.counts ? { ...r, viewCounts: views.counts } : r;
2385
+ }
2386
+ finally {
2387
+ if (views.cleanup)
2388
+ await views.cleanup();
2389
+ }
2390
+ }
2391
+ async function buildSite(opts, views) {
2373
2392
  const dirAbs = await resolveDecisionsDir(opts.dir);
2374
2393
  const outAbs = path.resolve(opts.out);
2375
2394
  const exclude = new Set([...DEFAULT_EXCLUDE, ...opts.exclude].map((e) => e.toLowerCase()));
@@ -2399,7 +2418,22 @@ async function build(opts) {
2399
2418
  // 「リンクにしない」に倒れて全滅する**(以前は順序が逆だった)。
2400
2419
  const repoRoot = knowledgeRoot ? path.dirname(knowledgeRoot) : path.dirname(dirAbs);
2401
2420
  // 帳簿 10 本の置き場(既定は `<repoRoot>/.cortex`)。読み口はこの 1 つから読む
2402
- const ledgerDir = (0, ledger_dir_1.ledgerDirOf)(repoRoot, opts.ledgerDir);
2421
+ let ledgerDir = (0, ledger_dir_1.ledgerDirOf)(repoRoot, opts.ledgerDir);
2422
+ // 記憶 v2 の画面用ファイル(cortex-engine #1094 鎖 3 V10b)。checkout の HEAD とそろう封筒だけを差し替えた合わせの置き場を読む
2423
+ // 合わせの置き場を作れなかった回(一時フォルダの失敗など)は今の帳簿の置き場のまま組む(取り出しの失敗で画面を止めない・数は出さない)
2424
+ if (opts.viewDir) {
2425
+ try {
2426
+ const plan = await (0, view_dir_1.planViews)(path.resolve(opts.viewDir), (0, view_dir_1.checkoutGit)(repoRoot), ledgerDir);
2427
+ const merged = await (0, view_dir_1.mergeLedgerDir)(ledgerDir, plan);
2428
+ views.cleanup = merged.cleanup;
2429
+ views.counts = plan.counts;
2430
+ ledgerDir = merged.dir;
2431
+ console.log((0, view_dir_1.viewCountsLine)(plan.counts));
2432
+ }
2433
+ catch (e) {
2434
+ console.warn(`記憶 v2 の画面用ファイルを使えなかったので、今の帳簿から組みます: ${e instanceof Error ? e.message : String(e)}`);
2435
+ }
2436
+ }
2403
2437
  const targetUrls = knowledgeRoot
2404
2438
  ? await buildTargetUrlMap(repoRoot, repo.baseUrl, repo.branch)
2405
2439
  : new Map();
@@ -2427,7 +2461,12 @@ async function build(opts) {
2427
2461
  // 目次と同じ順位(engine #551)。**読めないなら `null`** で、つながり画面の点は今までどおり一律
2428
2462
  const memoryRanking = await (0, ranking_1.readMemoryRanking)(repoRoot, ledgerDir);
2429
2463
  // 人の手の表の写し(engine #1094 q-10)。あれば記憶 v2 の案件で、確認の印とボタンを出さない。**無い案件は今と 1 バイトも同じ**(キーごと無い)
2430
- const memoryV2 = await (0, hands_1.readMemoryV2)(repoRoot, ledgerDir);
2464
+ // 「記憶 v2 の案件か」は 1 本の栓 `memory_v2_writer` の値で決める(cortex-engine #1094 鎖 3 V10b)。橋の間は写しとの OR
2465
+ const memoryHands = await (0, hands_1.readMemoryHands)(repoRoot, ledgerDir);
2466
+ const memoryV2 = (0, hands_1.isMemoryV2)(opts.memoryV2Writer, memoryHands !== null);
2467
+ // 削除の確認の一言の材料(B9a): 記憶 v2 の案件で表が読めたときは表、ほかは今の確認の帳簿
2468
+ // (表が読めた案件は必ず記憶 v2 なので、表そのものを渡せばよい)
2469
+ const touchedBy = memoryHands;
2431
2470
  // 記録の id → リポジトリルート相対のパス(順位のファイルの鍵と突き合わせる。`repoRoot` 起点・cwd 起点にしない)
2432
2471
  const recordRelPaths = new Map();
2433
2472
  const relOf = (dir, fileName) => {
@@ -2447,7 +2486,7 @@ async function build(opts) {
2447
2486
  rawDates.set(rec, rd);
2448
2487
  }
2449
2488
  // 人の手(削除の確認の文面を替える材料・cortex-engine #1094 段 4)。**置き換えより前に・repoRoot で**
2450
- attachHumanTouched(decisions, dirAbs, repoRoot, ledgers.confirmation);
2489
+ attachHumanTouched(decisions, dirAbs, repoRoot, ledgers.confirmation, touchedBy);
2451
2490
  // 置き換えの逆引き(#699 F3)。**置き換えの無い案件では 1 件も付かない**
2452
2491
  attachSupersedes(decisions, (d) => rawDates.get(d) ?? d.fm.date, (d) => relOf(dirAbs, d.fileName));
2453
2492
  let home = null;
@@ -2490,7 +2529,7 @@ async function build(opts) {
2490
2529
  const recordsDir = path.join(knowledgeRoot, e.name, "records");
2491
2530
  const records = await loadGenericRecords(recordsDir, exclude, repo.baseUrl, repo.branch);
2492
2531
  // 人の手(削除の確認の文面を替える材料・cortex-engine #1094 段 4)
2493
- attachHumanTouched(records, recordsDir, repoRoot, ledgers.confirmation);
2532
+ attachHumanTouched(records, recordsDir, repoRoot, ledgers.confirmation, touchedBy);
2494
2533
  // ルールの取り下げ(`retracted_by`・cortex-engine #1094 便 4-2d)。ルールの区画だけ・取り下げの無い案件では何も付かない
2495
2534
  if (e.name === "Rules")
2496
2535
  attachRetractedBy(records, decisions, (d) => rawDates.get(d) ?? d.fm.date);
package/dist/cli.js CHANGED
@@ -2,6 +2,7 @@
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const commander_1 = require("commander");
5
+ const promises_1 = require("node:fs/promises");
5
6
  const build_1 = require("./build");
6
7
  const program = new commander_1.Command();
7
8
  program
@@ -23,6 +24,10 @@ program
23
24
  .option("--webhook-url <url>", "Backlog Webhook 受信のLambda URL(環境変数 WEBHOOK_URL でも可)")
24
25
  .option("--webhook-token <token>", "リアルタイム同期の**案件別**トークン(環境変数 WEBHOOK_TOKEN でも可。艦隊共通トークンを渡さないこと)")
25
26
  .option("--ledger-dir <dir>", "ビューアが読む帳簿(.cortex の 10 本)の置き場(未指定なら <リポジトリルート>/.cortex)")
27
+ .option("--view-dir <dir>", "記憶 v2 の画面用ファイル(保管庫の gold/v1/c/view/ を取り出した手元のフォルダ)。checkout とそろう物だけ使い、ほかは帳簿の置き場から読む")
28
+ .option("--memory-v2-writer <state>", "栓 memory_v2_writer の値(off / shadow / on。on だけが記憶 v2 の案件)")
29
+ .option("--generated-at <text>", "生成時刻の表示(未指定ならビルドした時刻 \"YYYY-MM-DD HH:MM UTC\"。2 通りのビルドの突き合わせで揃える口)")
30
+ .option("--view-report <file>", "--view-dir の数(4 分類と目次に無い数)を JSON で書く先")
26
31
  .action(async (dir, opts) => {
27
32
  try {
28
33
  const result = await (0, build_1.build)({
@@ -32,7 +37,7 @@ program
32
37
  repoUrl: opts.repoUrl,
33
38
  branch: opts.branch,
34
39
  exclude: opts.exclude ?? [],
35
- generatedAt: new Date().toISOString().replace("T", " ").slice(0, 16) + " UTC",
40
+ generatedAt: opts.generatedAt ?? new Date().toISOString().replace("T", " ").slice(0, 16) + " UTC",
36
41
  intakeUrl: opts.intakeUrl ?? process.env.INTAKE_URL,
37
42
  intakeToken: opts.intakeToken ?? process.env.INTAKE_TOKEN,
38
43
  projectKey: opts.projectKey,
@@ -40,7 +45,14 @@ program
40
45
  webhookUrl: opts.webhookUrl ?? process.env.WEBHOOK_URL,
41
46
  webhookToken: opts.webhookToken ?? process.env.WEBHOOK_TOKEN,
42
47
  ledgerDir: opts.ledgerDir,
48
+ viewDir: opts.viewDir,
49
+ memoryV2Writer: opts.memoryV2Writer,
43
50
  });
51
+ if (opts.viewReport) {
52
+ // 数だけ(名前・本文は書かない)。--view-dir を渡さなかった回は書かない
53
+ if (result.viewCounts)
54
+ await (0, promises_1.writeFile)(opts.viewReport, `${JSON.stringify(result.viewCounts)}\n`, "utf8");
55
+ }
44
56
  console.log(`✓ ${result.count} 件のレコードを ${result.outDir} にビルドしました`);
45
57
  }
46
58
  catch (err) {
package/dist/hands.js CHANGED
@@ -6,6 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.MEMORY_HANDS_VERSION = exports.MEMORY_HANDS_FILE = void 0;
7
7
  exports.isMemoryHandsCopy = isMemoryHandsCopy;
8
8
  exports.readMemoryV2 = readMemoryV2;
9
+ exports.readMemoryHands = readMemoryHands;
10
+ exports.isMemoryV2 = isMemoryV2;
9
11
  /**
10
12
  * 人の手の表の写し(`.cortex/memory-hands.json`)を読む(cortex-engine #1094 q-10・仕様 1-6)。
11
13
  *
@@ -38,15 +40,44 @@ function isMemoryHandsCopy(text) {
38
40
  }
39
41
  /** 案件リポに写しがあり、読める形か。無い → `false`(黙る)・壊れている → `false`(build のログに 1 行) */
40
42
  async function readMemoryV2(repoRoot, ledgerDir) {
43
+ return (await readMemoryHands(repoRoot, ledgerDir)) !== null;
44
+ }
45
+ /**
46
+ * 人の手の表の写しの中身(記録の path → 人の手があるか `touched`)。無い・読めない → `null`(壊れているときだけ build のログに 1 行)。
47
+ * 鍵は engine が綴った path(リポジトリルート相対・NFC)。値の欄は `touched` だけを持つ(素通しはしない)
48
+ */
49
+ async function readMemoryHands(repoRoot, ledgerDir) {
41
50
  let text;
42
51
  try {
43
52
  text = await promises_1.default.readFile(node_path_1.default.join((0, ledger_dir_1.ledgerDirOf)(repoRoot, ledgerDir), exports.MEMORY_HANDS_FILE), "utf8");
44
53
  }
45
54
  catch {
46
- return false;
55
+ return null;
47
56
  }
48
- const ok = isMemoryHandsCopy(text);
49
- if (!ok)
57
+ if (!isMemoryHandsCopy(text)) {
50
58
  console.warn(`${exports.MEMORY_HANDS_FILE}: 読めないので、この案件は記憶 v2 でないものとして出します`);
51
- return ok;
59
+ return null;
60
+ }
61
+ const records = JSON.parse(text).records;
62
+ const out = new Map();
63
+ for (const [p, v] of Object.entries(records)) {
64
+ const row = v && typeof v === "object" && !Array.isArray(v) ? v : null;
65
+ // 🔴 厳密に true のときだけ("true" や 1 を人の手と読まない)
66
+ out.set(p, !!row && row.touched === true);
67
+ }
68
+ return out;
69
+ }
70
+ /** 1 本の栓 `memory_v2_writer` の値の語彙(engine `scripts/observe/rollout.mjs` の `ROLLOUT_STATES`) */
71
+ const WRITER_STATES = new Set(["off", "shadow", "on"]);
72
+ /**
73
+ * この案件は記憶 v2 か(cortex-engine #1094 鎖 3 V10b・確定版 3 章 4)。**1 本の栓 `memory_v2_writer` の値で決める**
74
+ * (workflow が SSM の値を `--memory-v2-writer` で渡す・`on` だけが v2。`shadow` は `off` に潰す=engine と同じ)。
75
+ * 橋の間(V11b まで)は、人の手の表の写しがあることとの OR(受け口 `infra/lambda/intake/logic.mjs` の `memoryV2GateOf` と同じ OR)。
76
+ * 渡されない・語彙外の値は「栓は on でない」と読む(写しがあれば v2・今と同じ)
77
+ */
78
+ function isMemoryV2(writer, handsCopy) {
79
+ const w = typeof writer === "string" ? writer.trim() : "";
80
+ if (w && !WRITER_STATES.has(w))
81
+ console.warn(`--memory-v2-writer の値が語彙の外です(off として読みます): ${JSON.stringify(w.slice(0, 20))}`);
82
+ return w === "on" || handsCopy;
52
83
  }
package/dist/render.js CHANGED
@@ -5487,7 +5487,6 @@ a.ds-name:hover { text-decoration: underline; }
5487
5487
  border: 1px solid #f0b429; background: #fffdf5; border-radius: 12px;
5488
5488
  padding: 12px 16px; display: flex; flex-direction: column; gap: 8px;
5489
5489
  }
5490
- .ds-orgnote.is-resolved { border-color: transparent; background: transparent; padding: 0; }
5491
5490
  .ds-orgnote-title { font-weight: 700; font-size: 13.5px; color: #8a5a00; }
5492
5491
  /* 案内が指す行。**囲うことで「どのチャンネルの話か」を示す**(開閉はしない) */
5493
5492
  .ds-orgnote-rows { display: flex; flex-direction: column; gap: 8px; margin-top: 2px; }
@@ -5755,10 +5754,6 @@ a.ds-tile:hover { text-decoration: none; }
5755
5754
  とは別物で、同じ色にすると「押して繋ぎましょう」と読まれる */
5756
5755
  .ds-gate-off { color: #9ca3af; }
5757
5756
  .ds-pipes { display: flex; flex-direction: column; gap: 8px; }
5758
- /* GitHub App のインストールへのリンク。**独立した「組織ごとの接続状況」の節は
5759
- 2026-08-20 に廃止した**(行のラベル+必要な行だけ囲う形へ)が、
5760
- このクラスだけは案内と手順の両方で使い続けている */
5761
- .ds-owner-install { font-size: 12.5px; white-space: nowrap; }
5762
5757
  /* 実行状況の行に直し方を添える。同じ run の未達を別の節にも出すと画面に2回並ぶ */
5763
5758
  .ds-pipe-wrap { display: flex; flex-direction: column; gap: 3px; }
5764
5759
  .ds-pipe-todo { font-size: 12.5px; color: var(--muted); line-height: 1.7; padding-left: 4px; }
@@ -9927,7 +9922,7 @@ ${calendarAddUrl.toString()}
9927
9922
  * 行と能力タイルが「トークン未設定」と言う**状態を作らないこと。
9928
9923
  *
9929
9924
  * **help は付けない。** 次の一手(インストール導線)は組織ごとの案内
9930
- * (githubNotConnectedNodes)が1つだけ持つ。行ごとに繰り返すと一覧が案内で埋まる。
9925
+ * (githubRowHelpParts)が行ごとに持つ(#1233・帯は撤去した)。
9931
9926
  * ----------------------------------------------------------------------- */
9932
9927
  github: {
9933
9928
  text: "App が未インストール",
@@ -9952,13 +9947,13 @@ ${calendarAddUrl.toString()}
9952
9947
  * 休止状態の行に、状態の語に添えて**押す前から**出す定型の補足(#346)と、
9953
9948
  * 「接続を確認」を押した結果に添える定型の1文(#356)。
9954
9949
  * 鍵は「アダプタ種別(helpTypeOf)× 状態」。値は { resting, probed } で、どちらも文の
9955
- * パーツの配列。文字列はテキスト、{ link: <リンクの文字列> } は GitHub App の
9956
- * インストール設定へのリンク(GITHUB_APP_INSTALL_URL)。
9950
+ * パーツの配列。文字列はテキスト、{ link, kind } は GitHub App の
9951
+ * リンク(githubAppLinkUrl(kind) が宛先を作る)。
9957
9952
  *
9958
9953
  * ## 文の長さ(#356・オーナー却下を受けた原則)
9959
9954
  * **帯は症状だけ・行は具体的な一手だけ・説明を繰り返さない。** 0.12.22 では帯と行と
9960
9955
  * 押した結果に同じ admin の4文が3回出ており、オーナーが「長すぎ・冗長」と却下した。
9961
- * - resting … **1文+リンクだけ**(誰が・どこで。「何が起きているか」は帯が言う)
9956
+ * - resting … **1文+リンクだけ**(誰が・どこで。「何が起きているか」は状態の札が言う)
9962
9957
  * - probed … **状態に応じた1文だけ**(1行目のサーバの needed が言わないことだけ)。
9963
9958
  * 押した結果に resting を繰り返さない——resting は押した瞬間に隠れ、needed が
9964
9959
  * 同じ操作(Configure から追加)を宛先つきで言う
@@ -9976,7 +9971,7 @@ ${calendarAddUrl.toString()}
9976
9971
  * 🔴 **DS_GATE[状態] が help を持つ状態にはエントリを置かない。** 置くと行の補足が2段になる
9977
9972
  * (github-state-reason.test.mjs の「行の補足は最大1段」が見張る)。
9978
9973
  *
9979
- * 🔴 **URL を文字列で書き写さない**(帯・押した結果と同じ定数を使う。写経すると張り替えで
9974
+ * 🔴 **URL を文字列で書き写さない**(押した結果と同じ定数を使う。写経すると張り替えで
9980
9975
  * 片方だけ古くなる)。
9981
9976
  *
9982
9977
  * エントリがある状態では**この文だけ**を出し、サーバの needed / detail は出さない
@@ -9990,19 +9985,19 @@ ${calendarAddUrl.toString()}
9990
9985
  */
9991
9986
  var DS_GATE_RESTING_HELP = {
9992
9987
  github: {
9988
+ // 🔴 **読めない行の中に、理由に合った一手だけを出す(#1233)。** 全体の帯は撤去した。
9989
+ // 一手は組織の App の導入状況(githubRowHelpParts)で決まるので、関数で持つ
9993
9990
  unreachable: {
9994
- // 誰が(admin)・どこで(Configure の Repository access)だけ。理由は帯、宛先はリンクが持つ。
9995
- // 着地点(Repository access)を名指しするのは、押す前の画面からこの語が消えないため
9996
- //(設計レビュー②)。「admin が…してください」はオーナーの原文の形
9997
- resting: [
9998
- "このリポジトリの admin(管理者権限を持つ人)が、Configure の Repository access に追加してください。",
9999
- { link: "インストール設定を開く" },
10000
- ],
9991
+ resting: function (row) { return githubRowHelpParts(row); },
10001
9992
  // 2026-09-02 の実測: admin でないリポジトリを Configure で選ぶと「request」タグが付き、
10002
9993
  // 組織オーナーの承認待ちになる("Okay, updated" と出るのに対象に入らない)。
10003
9994
  // 1行目(サーバの needed)はこれを言わないので、ここだけが持つ
10004
9995
  probed: ["GitHub 側で request と表示された場合は、組織オーナーの承認待ちです。"],
10005
9996
  },
9997
+ // App が組織に入っていない行(鍵が 1 本も無かった)。帯を撤去したので、この行が一手を持つ
9998
+ no_token: {
9999
+ resting: function (row) { return githubRowHelpParts(row); },
10000
+ },
10006
10001
  },
10007
10002
  };
10008
10003
  /**
@@ -10019,9 +10014,32 @@ ${calendarAddUrl.toString()}
10019
10014
  return (byType && byType[gate]) || null;
10020
10015
  }
10021
10016
  /** 休止状態の行に出すパーツ(無ければ null) */
10022
- function restingHelpPartsOf(type, gate) {
10017
+ function restingHelpPartsOf(type, gate, row) {
10023
10018
  var e = restingHelpEntryOf(type, gate);
10024
- return (e && e.resting) || null;
10019
+ var r = e && e.resting;
10020
+ return (typeof r === "function" ? r(row) : r) || null;
10021
+ }
10022
+ /**
10023
+ * GitHub の読めない行に出す一手(#1233)。**組織の App の導入状況で理由に合わせる**。
10024
+ * 窓口の断りの名前(installation_not_found 等)は行まで届いていないので、同じ分かれ目の
10025
+ * 「appInstalled」 を使う(true=App は入っている=このリポジトリが選ばれていない)。
10026
+ *
10027
+ * - no_token(鍵が 1 本も無い)/appInstalled が false・不明 → 「App を入れてください」(広い方)
10028
+ * - unreachable で appInstalled が true → 「このリポジトリを選んでください」
10029
+ *
10030
+ * 出さないもの: テナント外の断り(#841・導入の有無を明かさない)と、確かめられなかった
10031
+ * 組織(source: "unknown"・帯が導線を出さない設計)
10032
+ */
10033
+ function githubRowHelpParts(row) {
10034
+ if (!row) return null;
10035
+ if (githubOrgRefused(row)) return null;
10036
+ var owner = String(row.ref || "").split("/")[0];
10037
+ var meta = owner ? githubOwnerMeta(owner) : null;
10038
+ if (meta && meta.source === "unknown") return null;
10039
+ if (row.gate === "unreachable" && meta && meta.appInstalled === true) {
10040
+ return ["GitHub App の設定で、このリポジトリを選んでください。", { link: "インストール設定を開く →", kind: "settings" }];
10041
+ }
10042
+ return ["この組織に Cortex の GitHub App を入れてください。", { link: "GitHub App を入れる →", kind: "install" }];
10025
10043
  }
10026
10044
  /** 押した結果に添えるパーツ(無ければ null)。行と同じ表から引く(写経しない) */
10027
10045
  function probedHelpPartsOf(type, gate) {
@@ -10033,7 +10051,7 @@ ${calendarAddUrl.toString()}
10033
10051
  var p = el("p", { class: cls });
10034
10052
  parts.forEach(function (part) {
10035
10053
  if (typeof part === "string") p.appendChild(document.createTextNode(part));
10036
- else if (part && part.link) p.appendChild(el("a", { href: GITHUB_APP_INSTALL_URL, target: "_blank", rel: "noopener", text: part.link }));
10054
+ else if (part && part.link) p.appendChild(el("a", { href: githubAppLinkUrl(part.kind), target: "_blank", rel: "noopener", text: part.link }));
10037
10055
  });
10038
10056
  return p;
10039
10057
  }
@@ -10088,16 +10106,18 @@ ${calendarAddUrl.toString()}
10088
10106
  * - 状態がある(未測定の行には出すものが無い)
10089
10107
  * - ok でない
10090
10108
  * - missing_scope でない(スコープ名の札 .ds-probe-scope が既に持つ)
10091
- * - no_token でない(GitHub は組織の帯が導線を1つだけ持つ設計・Slack は DS_GATE_NO_TOKEN の help)。
10092
- * **ただし GitHub のテナント外の断り(#841)は出す**——帯は出ない(導入状態が分からない)ので、
10093
- * 「担当者へ」の 1 文(needed)を持つのはこの行だけ
10109
+ * - no_token でない(Slack は DS_GATE_NO_TOKEN の help)。
10110
+ * **GitHub の no_token は出す**(#1233・帯を撤去したので行が一手を持つ。
10111
+ * テナント外の断り(#841)は「担当者へ」の 1 文(needed))
10094
10112
  * - not_in_channel でない(help は無いが、**状態の語そのものが操作の指示**
10095
10113
  * 「Slackで招待してください」。その下に夜間の「読めませんでした: not_in_channel」を
10096
10114
  * 並べても情報が増えず、招待という具体的な指示を薄める。help の有無とは別の軸)
10097
10115
  * - dsGateOf(s).help が無い(あれば DS_GATE の固定文が次の一手を持っている)
10098
10116
  */
10099
10117
  function showsRestingReason(s) {
10100
- if (s.gate === "no_token" && String(s.type || "").indexOf("github") === 0 && githubOrgRefused(s)) return true;
10118
+ // GitHub の no_token は常に出す(#1233・帯を撤去したので、行が一手を持つ)。
10119
+ // 非テナント外は githubRowHelpParts の一手、テナント外の断り(#841)はサーバの needed の 1 文
10120
+ if (s.gate === "no_token" && String(s.type || "").indexOf("github") === 0) return true;
10101
10121
  if (!s.gate || s.gate === "ok" || s.gate === "missing_scope" || s.gate === "no_token" || s.gate === "not_in_channel") return false;
10102
10122
  return !dsGateOf(s).help;
10103
10123
  }
@@ -10985,26 +11005,13 @@ ${calendarAddUrl.toString()}
10985
11005
  var again = !!(x.ref && seenRef[x.ref]);
10986
11006
  if (x.ref) seenRef[x.ref] = 1;
10987
11007
  var rowCard = extSourceCard(x, DS_TYPE[x.type] || { icon: "🔗", label: x.type || "その他" }, { quietHelp: again });
10988
- // 同じリポの issues / discussions を、接続確認の結果で揃えるための目印(updateGithubOrgBand)
11008
+ // 同じリポの issues / discussions を、接続確認の結果で揃えるための目印(syncGithubSiblingRows)
10989
11009
  if (rowCard && rowCard.setAttribute && x.ref) rowCard.setAttribute("data-ref", x.ref);
10990
11010
  return rowCard;
10991
11011
  });
10992
- if (g.state === "warn") {
10993
- var warnBox = orgNoteBox(githubNotConnectedNodes(g), cards);
10994
- // 「接続を確認」が通ったとき帯をその場で畳めるよう、いま読めていないリポの一覧を持たせる
10995
- //(probeSection → updateGithubOrgBand)。同じリポの issues / discussions は 1 本と数える
10996
- var unreadRefs = [];
10997
- g.items.forEach(function (x) {
10998
- if (x.ref && x.gate && x.gate !== "ok" && unreadRefs.indexOf(x.ref) < 0) unreadRefs.push(x.ref);
10999
- });
11000
- warnBox.setAttribute("data-unread-refs", JSON.stringify(unreadRefs));
11001
- // 見出しの件数は夜間の値のことがある(画面の行数と違う)。初期値を持ち、増減だけ当てる
11002
- var titleEl = warnBox.querySelector(":scope > .ds-orgnote-title");
11003
- var m0 = titleEl ? /^⚠️ (\\d+)件のリポジトリを読み取れていません/.exec(titleEl.textContent || "") : null;
11004
- if (m0) warnBox.setAttribute("data-unread-count", m0[1]);
11005
- out.push(warnBox);
11006
- return;
11007
- }
11012
+ // 🔴 **読めない組織を箱で囲まない(#1233)。** 読めないリポジトリが 1 件あるだけで、
11013
+ // 並んでいる全部のタイルが黄色の枠に入っていた。エラーは読めない行の中だけに出す
11014
+ //(理由に合った一手は extSourceCard の githubRowHelpParts)。
11008
11015
  if (g.state === "unsure") {
11009
11016
  // **確かめられなかった夜は、そう言う。** gh の不在・タイムアウト・
11010
11017
  // ネットワーク断で起きる。「接続OK」と出すと同じ画面の集計と食い違い、
@@ -11019,28 +11026,17 @@ ${calendarAddUrl.toString()}
11019
11026
  }
11020
11027
 
11021
11028
  /**
11022
- * 「接続を確認」の結果を、行が属する組織の帯へ反映する。
11029
+ * 「接続を確認」の結果を、同じリポジトリの兄弟の行(issues / discussions)へそろえる。
11023
11030
  *
11024
- * 読めた行を帯の「読めていないリポ」から外し、**最後の 1 本だったら帯を畳む**。
11025
- * まだ読めない行が残るなら帯は残し、見出しの件数だけ減らす。読めなかった結果は
11026
- * 帯に戻す(読めていた行が後から読めなくなった場合も、帯が嘘をつかない)。
11027
- * 帯が無い行(組織が繋がっている)では何もしない。
11031
+ * 帯は撤去した(#1233)。残すのは「行は 2 本でも接続は 1 つ」なので押した行と同じ判定に
11032
+ * そろえる仕事だけ。**条件なしで毎回そろえる**(帯の有無・状態の変化に依らない)。
11033
+ * 兄弟は同じ親(一覧・確かめられなかった組織の箱)の直下にいる。
11028
11034
  */
11029
- function updateGithubOrgBand(card, ref, ok) {
11030
- var box = card.closest ? card.closest(".ds-orgnote") : null;
11031
- if (!box || !ref) return;
11032
- var raw = box.getAttribute("data-unread-refs");
11033
- if (raw == null) return;
11034
- var refs;
11035
- try { refs = JSON.parse(raw) || []; } catch (e) { return; }
11036
- var at = refs.indexOf(ref);
11037
- if (ok && at >= 0) refs.splice(at, 1);
11038
- else if (!ok && at < 0) refs.push(ref);
11039
- else return;
11040
- box.setAttribute("data-unread-refs", JSON.stringify(refs));
11041
- // 同じリポの兄弟の行(issues / discussions)も、押した行と同じ判定に揃える
11035
+ function syncGithubSiblingRows(card, ref, ok) {
11036
+ var parent = card.parentNode;
11037
+ if (!parent || !ref) return;
11042
11038
  var gateSrc = card.querySelector(".ds-gate");
11043
- var sibs = box.querySelectorAll(".ds-card[data-ref]");
11039
+ var sibs = parent.querySelectorAll(":scope > .ds-card[data-ref]");
11044
11040
  for (var si = 0; gateSrc && si < sibs.length; si++) {
11045
11041
  var sib = sibs[si];
11046
11042
  if (sib === card || sib.getAttribute("data-ref") !== ref) continue;
@@ -11049,31 +11045,6 @@ ${calendarAddUrl.toString()}
11049
11045
  var sh = sib.querySelectorAll(".ds-gate-help");
11050
11046
  for (var hi = 0; hi < sh.length; hi++) sh[hi].hidden = ok;
11051
11047
  }
11052
- var title = box.querySelector(":scope > .ds-orgnote-title");
11053
- var cnt = parseInt(box.getAttribute("data-unread-count"), 10);
11054
- if (!isNaN(cnt)) {
11055
- cnt = Math.max(0, cnt + (ok ? -1 : 1));
11056
- box.setAttribute("data-unread-count", String(cnt));
11057
- }
11058
- var hasBand = !!title;
11059
- if (!hasBand) return;
11060
- if (refs.length === 0) {
11061
- // 帯だけを隠す。行(.ds-orgnote-rows)は残す
11062
- for (var i = 0; i < box.children.length; i++) {
11063
- var c = box.children[i];
11064
- if (c.classList.contains("ds-orgnote-rows")) continue;
11065
- c.hidden = true;
11066
- }
11067
- box.classList.add("is-resolved");
11068
- return;
11069
- }
11070
- for (var k = 0; k < box.children.length; k++) {
11071
- var ch = box.children[k];
11072
- if (!ch.classList.contains("ds-orgnote-rows")) ch.hidden = false;
11073
- }
11074
- box.classList.remove("is-resolved");
11075
- // 見出しが件数を持つ形(「N件のリポジトリを読み取れていません」)だけ数え直す
11076
- if (!isNaN(cnt)) title.textContent = "⚠️ " + Math.max(1, cnt) + "件のリポジトリを読み取れていません";
11077
11048
  }
11078
11049
 
11079
11050
  /** 確かめられなかった組織の案内。**導線は出さない** */
@@ -11092,111 +11063,6 @@ ${calendarAddUrl.toString()}
11092
11063
  ];
11093
11064
  }
11094
11065
 
11095
- /**
11096
- * 繋がっていない組織の案内。
11097
- *
11098
- * **「読めない」の原因は1つではない。** 以前はどの組織にも
11099
- * 「この組織に Cortex の GitHub App を入れると読めるようになります」と出していたが、
11100
- * **App は入っているのにそのリポジトリが選ばれていない**組織にはこれが効かない。
11101
- * インストールは済んでいるので、押しても画面は変わらず、何度押しても直らない。
11102
- *
11103
- * 「appInstalled」 が原因を分ける(engine がインストールトークンを取れたかどうか):
11104
- *
11105
- * | 値 | 意味 | 案内 |
11106
- * |---|---|---|
11107
- * | 「false」 | App のトークンが取れなかった=未インストール | インストールへ |
11108
- * | 「true」 | 取れたのに読めない=そのリポジトリが未選択 | **インストール設定**へ |
11109
- * | 「null」 | その夜は確かめられなかった | **どちらも出さない**(押しても直るとは限らない) |
11110
- *
11111
- * **自社か顧客かで文言を分けない。** 組織名(「classmethod-*」)で分岐すると、
11112
- * 新しい自社組織が増えたときに黙って間違った案内を出す。どちらにも通じる文にする。
11113
- */
11114
- function githubNotConnectedNodes(g) {
11115
- var out = [];
11116
- var o = g.meta || {};
11117
- // **件数はグループが数えたものを優先する(工程1.5)。** まとめ表の 「repos」 /
11118
- // 「unreadable」 は夜間の値で、いま画面に出ている行の数と食い違うことがある
11119
- //(#101 で3度直した形)。「groupGithubByOwner」 は**いま並んでいる行**を数えている
11120
- var repos = o.repos != null ? o.repos : g.repos != null ? g.repos : g.items.length;
11121
- var unread = o.unreadable != null ? o.unreadable : g.unread != null ? g.unread : g.items.length;
11122
- var installed = o.appInstalled;
11123
- // **読める件数を出す。** 総数だけ出すと、同じ画面の集計と食い違う(#101 で3度直した形)
11124
- var head = repos + "リポジトリ中 " + unread + "件が読めていません。";
11125
- // 🔴 **見出しは 「installed」 の分岐の中で出す(#346)。** ここで1つに畳んでいたので、
11126
- // App 導入済みの組織にも「まだ接続されていません」と出て、**同じ帯の本文と矛盾**した
11127
- // (実機でオーナーが踏み、利用者が「組織の接続からやり直し」と誤解した)
11128
- if (installed === true) {
11129
- // 🔴 **帯は症状だけ(#356・オーナー却下を受けた原則)。** 0.12.22 ではここに9文
11130
- // (App の説明・Configure の手順・admin の4文・移動改名削除・お客様の組織)を入れ、
11131
- // 同じ説明が行と押した結果にも出て3回繰り返された。見出しは件数だけ・本文は2文だけ。
11132
- // **組織名は見出しに入れない**——各行のバッジ「{組織}(GitHub App)」が持っている。
11133
- // 組織が複数ある画面でも、組織ごとの帯のまま・見出しは同じ形。
11134
- //
11135
- // **見出しでは「読み取り対象に選ばれていません」と言い切らない。** 「appInstalled」 が
11136
- // 分けられるのは「①未インストール」と「②未選択/③消えた・改名」までで、**②と③は
11137
- // 分かれない**(App のトークンではどちらも 404)。見出しは事実だけを言う
11138
- out.push(el("div", {
11139
- class: "ds-orgnote-title",
11140
- text: "⚠️ " + unread + "件のリポジトリを読み取れていません",
11141
- }));
11142
- // App は入っている。**インストールをもう一度促さないこと**——済んでいる操作を促すのは
11143
- // 「設定する必要が無いものに設定を促す」偽の警告と同じで、しかも直らない。
11144
- //
11145
- // 「誰が・どこで」は各行の1文(DS_GATE_RESTING_HELP)が持つ。ここで繰り返さない。
11146
- // 「移動・改名・削除の可能性 → 登録から外して」も言わない——②と③を区別する材料が
11147
- // 無く、外す手段は行の「登録から削除」(#354)が常に持つ。
11148
- // 「お客様の組織では、組織のオーナーに承認を依頼」も外した——request(承認待ち)の
11149
- // 1文は押した結果が持つ(同じ表の probed)。
11150
- //
11151
- // ⚠ **件数の文 「head」(「{N}リポジトリ中 {M}件が読めていません。」)もこの枝では
11152
- // 出さない(#356)。** #101 で3度直した「読める件数を出す」を意図して覆した——見出しが
11153
- // 件数を持つので、本文で二重に言うと帯が3文になる。分母(登録数)は画面から消えるが、
11154
- // 箱の中に行が全部並んでいるので数えられる。false / null の枝は従来どおり head を出す。
11155
- //
11156
- // **「各行の案内」ではなく「アクセス不可の行の案内」と言う。** 帯の箱には読めている行も
11157
- // 一緒に入る(unread が1件でもあれば組織ごと warn)。案内を持つのは「アクセス不可」の
11158
- // 行だけなので、行に出ている状態の語そのもので指す(設計レビュー②の指摘)。
11159
- //
11160
- // **「読み取り対象に入っていません」はオーナーの文(#356)。** 2026-08-21 の
11161
- // 「②未選択と③消えた・改名を区別できないので断定しない」は、帯の本文については
11162
- // この文で覆した(見出しは引き続き断定しない)。③の逃げ道は「登録から削除」。
11163
- //
11164
- // **設定画面の宛先は App 個別ページ(#346)。** いまの宛先と、そこに着ける人の条件は
11165
- // 「appSettingsLink」 の項にある
11166
- out.push(el("p", {
11167
- class: "ds-group-note",
11168
- text: "GitHub App の読み取り対象に入っていません。アクセス不可の行の案内に従って追加してください。",
11169
- }));
11170
- out.push(appSettingsLink("インストール設定を開く →"));
11171
- return out;
11172
- }
11173
- if (installed === false) {
11174
- out.push(el("div", { class: "ds-orgnote-title", text: "⚠️ " + g.title + " はまだ接続されていません" }));
11175
- out.push(el("p", {
11176
- class: "ds-group-note",
11177
- text: head + "この組織に Cortex の GitHub App を入れると読めるようになります(Issues の読み取りだけ)。"
11178
- + "お客様の組織では、組織のオーナーに承認を依頼してください。",
11179
- }));
11180
- out.push(appInstallLink("この組織にインストール →"));
11181
- return out;
11182
- }
11183
- // 「null」(確かめられなかった)と、この値を知らない古い fleet-status.json。
11184
- // **導線を出さない。** どちらが原因か分からないので、押させると外れたほうを
11185
- // 押させることになる(済んでいるインストールをもう一度、あるいは効かない設定画面へ)。
11186
- //
11187
- // 見出しは件数を言わない(件数は本文だけが持つ・見出しと二重に言わない)。
11188
- // 「githubUnsureNodes」 の「接続を確認できませんでした」(=その夜、確かめる処理自体が
11189
- // 走らなかった)とは意味が違う——こちらは**読めなかった事実はあるが、App の導入状況が
11190
- // 分からない**ので、語を分ける
11191
- out.push(el("div", { class: "ds-orgnote-title", text: "⚠️ " + g.title + " のリポジトリが読めていません(原因は確かめられていません)" }));
11192
- out.push(el("p", {
11193
- class: "ds-group-note",
11194
- text: head + "原因(GitHub App が入っていない/入っているがこれらのリポジトリが選ばれていない)は、"
11195
- + "今回の実行では確かめられませんでした。翌朝の表示をご確認ください。",
11196
- }));
11197
- return out;
11198
- }
11199
-
11200
11066
  /**
11201
11067
  * Slack のチャンネルを**平らに**並べる(2026-08-20)。
11202
11068
  *
@@ -11398,38 +11264,14 @@ ${calendarAddUrl.toString()}
11398
11264
  var GITHUB_APP_INSTALL_URL = "https://github.com/apps/classmethod-cortex-reader/installations/new";
11399
11265
 
11400
11266
  /**
11401
- * GitHub App のインストールへ行くリンク(2026-08-20 に部品化)。
11402
- *
11403
- * **同じものを2箇所に書かない。** 組織ごとの案内と、追加フォームの手順が
11404
- * 同じ導線を持つ(#138 で「1箇所にしか無い」ことが事故になったので、
11405
- * **むしろ複数あるのが正しい**)。写経すると、URLの張り替えで片方だけ古くなる。
11406
- *
11407
- * **正本は手順の側。** 組織ごとの案内は行が無いと出ないので、
11408
- * 行0件の案件では手順側しか残らない。
11409
- *
11410
- * **⚠️ ただし手順は追加フォームの中にあり、追加フォームは
11411
- * SITE.intake が無いと丸ごと消える**(renderExternalAdders の冒頭)。
11412
- * つまり **intake 無し+行0件** の案件では App の導線が1本も無くなる。
11413
- * いまは config.ts の intakeUrl が艦隊で1つ・全案件に渡っているので
11414
- * その組み合わせは起きない(2026-08-20 に確認)。
11415
- * **案件ごとに intake を出し分けるようになったら、ここを見直すこと。**
11416
- */
11417
- function appInstallLink(label) {
11418
- return el("a", {
11419
- class: "ds-owner-install",
11420
- href: GITHUB_APP_INSTALL_URL,
11421
- target: "_blank", rel: "noopener",
11422
- text: label || "GitHub App を入れる →",
11423
- });
11424
- }
11425
-
11426
- /**
11427
- * すでに入っている App の**インストール設定**(対象リポジトリの追加)へ行くリンク。
11267
+ * GitHub App の宛先 URL の作り手(入れる/インストール設定)。
11268
+ * すでに入っている App の**インストール設定**(対象リポジトリの追加)へ行くリンクの宛先でもある。
11428
11269
  *
11429
11270
  * **宛先は App 個別ページ(「GITHUB_APP_INSTALL_URL」 と同じ定数・#346)。**
11430
11271
  * 「infra/lambda/shared/github-gate.mjs」 の 「githubConfigureUrl」 とも同じ宛先なので、
11431
- * **押す前(この帯・行の補足)と押した後(確認結果の文中)でリンク先が割れない**。
11432
- * 関数は残してある——呼び手が「入れる」ではなく「設定を開く」意図で呼ぶ場所だから。
11272
+ * **押す前(行の一手)と押した後(確認結果の文中)でリンク先が割れない**。
11273
+ * 「kind」(install / settings)は意図を渡すための引数で、いまは宛先が同じ。会社ごとの App
11274
+ *(classmethod-cortex-portal #254)で割れる日に、ここだけを直す。
11433
11275
  *
11434
11276
  * ## なぜ他の2つの宛先を使わないか
11435
11277
  *
@@ -11453,13 +11295,11 @@ ${calendarAddUrl.toString()}
11453
11295
  * **GitHub 側の画面が変わったらここが最初に嘘になる。** 宛先を変えるときは
11454
11296
  * 「githubConfigureUrl」(サーバ)と揃えること。
11455
11297
  */
11456
- function appSettingsLink(label) {
11457
- return el("a", {
11458
- class: "ds-owner-install",
11459
- href: GITHUB_APP_INSTALL_URL,
11460
- target: "_blank", rel: "noopener",
11461
- text: label || "インストール設定を開く →",
11462
- });
11298
+ function githubAppLinkUrl(kind) {
11299
+ // 「kind」: "install"(組織に App を入れる)/"settings"(入っている App の対象リポジトリを選ぶ)。
11300
+ // いまは同じ宛先(App 個別ページ)。会社ごとの App(classmethod-cortex-portal #254)が動くと
11301
+ // 宛先が変わるので、**行の案内のリンクは全部ここを通す**(URL を 1 か所から作る)
11302
+ return ({ install: GITHUB_APP_INSTALL_URL, settings: GITHUB_APP_INSTALL_URL })[kind] || GITHUB_APP_INSTALL_URL;
11463
11303
  }
11464
11304
 
11465
11305
  /**
@@ -14316,9 +14156,8 @@ ${calendarAddUrl.toString()}
14316
14156
  // 再読み込みまで)で、「接続を確認 → 閉じる → その場で接続OK」にならない。
14317
14157
  // 据え置きあり=GitHub は叩かない(ストアの行は据え置きの対象外で毎回引く)。
14318
14158
  // 取り込む能力の即答(appendDispatchProbe)は先に同じ 1 行を持っている
14319
- // 組織の帯も同じ結果で描き直す(読めた最後の 1 本なら帯が消える)。
14320
- // 帯はページ描画時に 1 度だけ組むので、ここで触らないと再読み込みまで残る
14321
- if (probeTypeOf(s.type) === "github") updateGithubOrgBand(card, s.ref, j.gate === "ok");
14159
+ // 同じリポジトリの兄弟の行(issues / discussions)も同じ判定にそろえる
14160
+ if (probeTypeOf(s.type) === "github") syncGithubSiblingRows(card, s.ref, j.gate === "ok");
14322
14161
  reloadLiveState();
14323
14162
  } catch (e) {
14324
14163
  msg.className = "ds-toggle-msg is-error";
@@ -14334,11 +14173,10 @@ ${calendarAddUrl.toString()}
14334
14173
  label: "登録を解除",
14335
14174
  confirmLines: function () {
14336
14175
  return [
14337
- "「" + (s.name || s.ref) + "」を、Gold 昇格の読み取り対象から外します。",
14176
+ "「" + (s.name || s.ref) + "」を、読み取り対象から外します。",
14338
14177
  "",
14339
- "次回の Gold 昇格(毎晩22時ごろ)から、このチャンネルは読み取られなくなります。",
14340
- "",
14341
- "※ 既に Gold 層に入った内容は消えません(Decision・用語集はそのまま残ります)。",
14178
+ // 外したときの約束の 1 文(intake の REMOVED_PROMISE と同じ・cortex-engine #1094 c-30 G7)
14179
+ "外すと、次の晩から読まなくなります。すでに記憶に入った内容は残ります。",
14342
14180
  "",
14343
14181
  "解除しますか?",
14344
14182
  ];
@@ -14433,11 +14271,10 @@ ${calendarAddUrl.toString()}
14433
14271
  label: "登録を解除",
14434
14272
  confirmLines: function () {
14435
14273
  return [
14436
- "「" + (s.name || s.ref) + "」を、Gold 昇格の読み取り対象から外します。",
14437
- "",
14438
- "次回の Gold 昇格(毎晩22時ごろ)から、このスペースは読み取られなくなります。",
14274
+ "「" + (s.name || s.ref) + "」を、読み取り対象から外します。",
14439
14275
  "",
14440
- "※ 既に Gold 層に入った内容は消えません(Decision・用語集はそのまま残ります)。",
14276
+ // 外したときの約束の 1 文(intake の REMOVED_PROMISE と同じ・cortex-engine #1094 c-30 G7)
14277
+ "外すと、次の晩から読まなくなります。すでに記憶に入った内容は残ります。",
14441
14278
  // 解除は通知先の宣言(notify.json)を触らない(#572)。通知先のまま解除すると
14442
14279
  // 行が消えて画面から止められなくなる——先に止める手順をここで言う
14443
14280
  "※ 通知先にしている場合は、先に「通知を止める」を押してください(解除しても通知先は変わりません)。",
@@ -16224,24 +16061,10 @@ ${calendarAddUrl.toString()}
16224
16061
  placeholder: "owner/repo または https://github.com/owner/repo",
16225
16062
  namePlaceholder: "メモ(任意)",
16226
16063
  requiredNote: "リポジトリを入力してください。",
16227
- // **手順のリストにする(2026-08-20・Slack からの横展開)。**
16228
- //
16229
- // **App の導線をここに常設する(#138 の再発防止・唯一の防波堤)。**
16230
- // それまでは「組織ごとの接続状況」という独立した一覧の中にしか
16231
- // 導線が無く、**まだ何も繋いでいない案件=一番 App を入れてほしい案件**の
16232
- // 画面に App へ行く道が1本も無かった(実機で踏んだ)。
16233
- //
16234
- // **行が0件でも、この手順は必ず出る**(追加フォームはグループの外にある)。
16235
- // 組織ごとの案内は行が無いと出ないので、**常設の正本はこちら**。
16236
- // 「案内側に導線があるから手順側は省く」とやると即再発する。
16237
- hint: [
16238
- { text: "読み取りたいリポジトリがある組織に、Cortex の GitHub App を入れます(Issues の読み取りだけ)。", node: appInstallLink() },
16239
- "そのリポジトリを下の欄に入れて「追加」を押します(owner/repo でも、GitHub のURLでも構いません)。",
16240
- // #660: 「Gold 昇格の材料になります」は嘘になった(性質が種類で固定・GitHub は
16241
- // 決定の抽出に使わない)。**手順の最終行が伝える重さは「記録に入る」**——
16242
- // 追加が Gold 昇格の意思決定になるチャット(上の 2 つ)とは別の文にする
16243
- "追加したリポジトリの Issues と PR の活動が、毎晩の記録に入ります(決定・用語集の材料にはしません)。やめるときは、その行の「登録から削除」を押します。",
16244
- ],
16064
+ // 🔴 **追加の欄は入力と[追加]だけ(#1233)。** 以前は「App を入れる → リポジトリを足す」の
16065
+ // 手順(1・2・3)と「GitHub App を入れる →」のリンクを置いていたが、リポジトリを足すのが先で、
16066
+ // 読めなければ足した行の中が理由に合った一手を出す(githubRowHelpParts)。
16067
+ // 「毎晩の記録に入る・決定と用語の材料にはしない」の説明は、モーダル上部の説明文が 1 回だけ持つ。
16245
16068
  build: function (repo, name) { return { op: "add-github-source", repo: repo, name: name }; },
16246
16069
  adder: "github",
16247
16070
  }));
@@ -17149,21 +16972,28 @@ ${calendarAddUrl.toString()}
17149
16972
  //
17150
16973
  // **ボタンより後ろに置く。** カードは横並びの flex なので、間に挟むと
17151
16974
  // 「登録を解除」が案内文の下の行へ落ちて、既存案件の行の形が変わる
17152
- gateHelpNodes(gate, "ds-gate-help").forEach(function (n) { card.appendChild(n); });
17153
- // **最後に測った理由と直し方を、押す前から出す(#346)。** 「アクセス不可 8時間前に確認」
17154
- // だけでは、押さない人(初見の PM 等)が止まる。表にエントリがある状態は定型の補足
17155
- // (リンク込み)だけ、無い状態は needed → detail のフォールバック。どちらも ds-gate-help
17156
- // を付ける——押した結果で置き換えるとき probeSection がまとめて隠す。
17157
- // **同じリポジトリの2本目の行(Discussions)には出さない**(opts.quietHelp)。
17158
- // 同じ長文が2行に並ぶと一覧が案内で埋まる。URL のリンク化は neededNode(innerHTML は使わない)
17159
- if (!(opts && opts.quietHelp) && showsRestingReason(s)) {
17160
- var restingParts = restingHelpPartsOf(s.type, s.gate);
17161
- if (restingParts) card.appendChild(restingHelpNode(restingParts, "ds-gate-help ds-gate-resting"));
17162
- else {
17163
- // **能力に畳んでから渡す(#548)。** ここへ来るのは Slack / GitHub / Google Chat の
17164
- // 行だけなので、ボタンが書いた detail(判定語)は従来どおり出さない
17165
- var reason = stateReasonOf({ needed: s.needed, detail: s.detail, gateBy: s.gateBy, cap: gateCapOfType(s.type) });
17166
- if (reason) card.appendChild(neededNode(reason, "ds-gate-help ds-gate-reason"));
16975
+ // **案内の段は「登録から削除」「接続を確認」より後ろに置く(#1233)。** 案内は 100% 幅の段
16976
+ // なので、間に挟むと「接続を確認」だけが案内の下へ落ち、正常な行と並びが違って見える。
16977
+ // 押した結果(probe.detail)の手前に置く——結果が出たら案内は stale 処理で隠れる
16978
+ function appendRowHelp() {
16979
+ gateHelpNodes(gate, "ds-gate-help").forEach(function (n) { card.appendChild(n); });
16980
+ // **最後に測った理由と直し方を、押す前から出す(#346)。** 「アクセス不可 8時間前に確認」
16981
+ // だけでは、押さない人(初見の PM 等)が止まる。表にエントリがある状態は定型の補足
16982
+ // (リンク込み)だけ、無い状態は needed → detail のフォールバック。どちらも ds-gate-help
16983
+ // を付ける——押した結果で置き換えるとき probeSection がまとめて隠す。
16984
+ // **同じリポジトリの2本目の行(Discussions)には出さない**(opts.quietHelp)。
16985
+ // 同じ長文が2行に並ぶと一覧が案内で埋まる。URL のリンク化は neededNode(innerHTML は使わない)
16986
+ if (!(opts && opts.quietHelp) && showsRestingReason(s)) {
16987
+ var restingParts = restingHelpPartsOf(s.type, s.gate, s);
16988
+ if (restingParts) card.appendChild(restingHelpNode(restingParts, "ds-gate-help ds-gate-resting"));
16989
+ else if (!(String(s.type).indexOf("github") === 0 && (s.gate === "unreachable" || s.gate === "no_token") && !githubOrgRefused(s))) {
16990
+ // GitHub の読めない行は表が一手を決める(出さないと決めた行=確かめられなかった組織には、
16991
+ // 理由文も重ねない)。テナント外の断りだけ、サーバの needed の 1 文がここへ来る
16992
+ // **能力に畳んでから渡す(#548)。** ここへ来るのは Slack / GitHub / Google Chat の
16993
+ // 行だけなので、ボタンが書いた detail(判定語)は従来どおり出さない
16994
+ var reason = stateReasonOf({ needed: s.needed, detail: s.detail, gateBy: s.gateBy, cap: gateCapOfType(s.type) });
16995
+ if (reason) card.appendChild(neededNode(reason, "ds-gate-help ds-gate-reason"));
16996
+ }
17167
16997
  }
17168
16998
  }
17169
16999
  // 🔴 **「材料にしていない」の補足(「ds-not-ingested-note」)は撤去した(#660)。**
@@ -17193,8 +17023,11 @@ ${calendarAddUrl.toString()}
17193
17023
  var probe = probeSection(s, card, gateEl);
17194
17024
  // **ボタンは行の中(「登録を解除」の隣)。** 独立した段を作ると1行が2段になる
17195
17025
  card.appendChild(probe.btn);
17026
+ appendRowHelp();
17196
17027
  // 押した結果の詳細だけを下に。**押すまでは空**なので行は伸びない
17197
17028
  card.appendChild(probe.detail);
17029
+ } else {
17030
+ appendRowHelp();
17198
17031
  }
17199
17032
  return card;
17200
17033
  }
@@ -0,0 +1,273 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.VIEW_INDEX_FILE = exports.VIEW_HELD = exports.VIEW_NAMES = void 0;
7
+ exports.blobShaOf = blobShaOf;
8
+ exports.isReadableEnvelope = isReadableEnvelope;
9
+ exports.classifyView = classifyView;
10
+ exports.checkoutGit = checkoutGit;
11
+ exports.planViews = planViews;
12
+ exports.mergeLedgerDir = mergeLedgerDir;
13
+ exports.viewCountsLine = viewCountsLine;
14
+ /**
15
+ * 記憶 v2 の画面用ファイルの読み口 `--view-dir`(cortex-engine #1094 鎖 3 V10b・確定版 3 章 2)。
16
+ *
17
+ * engine の記憶側の晩の終わりの段 `views`(V10a・`scripts/memory-v2/memory/post.mjs`)が、`.cortex` の帳簿 8 本と順位(段 `ranking`)を
18
+ * 封筒 `{ v:1, name, run, at, commit, blob, text }` に包んで保管庫の `gold/v1/c/view/<名前>.json` に置き、最後に目次 `view-index.json`
19
+ * (名前ごとに `{ blob, commit, at, absent }`)を置く。workflow(engine の `build-viewer.yml`)がそれを 1 行の `aws s3 sync` で作業場に
20
+ * 取り出し、このビューアに `--view-dir` で渡す。🔴 **ビューアは保管庫を知らないまま**(ほどくのは手元のファイルだけ・AWS SDK を足さない)。
21
+ *
22
+ * ## 何をするか
23
+ *
24
+ * 目次を読み、名前ごとに封筒をほどいて分類する(`classifyView`・中身で決める):
25
+ * - `same`: 封筒の本文が今の帳簿の置き場のファイルとバイトで同じ(無いの封筒は、置き場にも無い)→ **封筒の本文を使う**
26
+ * - `stale`・`unexplained`・`unverified`・`unreadable`: 中身が違うか読めない → 名前ごとに今の帳簿に倒す(数え分けは `classifyView`)
27
+ * 目次に載っていない名前(目次が無い晩を含む)は `missing` に数え、`.cortex` に倒す(橋の間・B8)。
28
+ *
29
+ * 使う物だけを差し替えた**合わせの置き場**(一時フォルダ)を作り、ビルドはそこを帳簿の置き場として読む(読み口は今の `--ledger-dir` と同じ 1 本)。
30
+ * 封筒の本文が使えるのは、HEAD の `.cortex` とバイトで同じとき(same)だけなので、橋の間は `.cortex` から組んだサイトと 1 バイトも変わらない。
31
+ *
32
+ * ## 名前(engine の宣言表 `publish: "viewer"` の行・V10a の `VIEW_HELD`)
33
+ *
34
+ * - 読む 11 本のうち、記憶側が置くのは 8 本+順位 1 本(`VIEW_NAMES`)
35
+ * - 名寄せの提案(`gold-proposals`・A1 の答えまで)と確認の帳簿(`gold-confirmation`・B9a で人の手の表に寄せる)は置かれないので、いつも `.cortex` から読む
36
+ */
37
+ const node_child_process_1 = require("node:child_process");
38
+ const node_crypto_1 = require("node:crypto");
39
+ const promises_1 = __importDefault(require("node:fs/promises"));
40
+ const node_os_1 = __importDefault(require("node:os"));
41
+ const node_path_1 = __importDefault(require("node:path"));
42
+ const ledger_dir_1 = require("./ledger-dir");
43
+ /** 記憶側が置く名前(宣言表の `publish: "viewer"` の行から、置かない 2 本を除いた 8 本と順位)。並びは名前順 */
44
+ exports.VIEW_NAMES = Object.freeze([
45
+ "gold-activity",
46
+ "gold-change-log",
47
+ "gold-member-engagement",
48
+ "gold-rederivation",
49
+ "gold-term-strength",
50
+ "memory-hands",
51
+ "memory-ranking",
52
+ "mirror-moved-meetings",
53
+ "story-days",
54
+ ]);
55
+ /** 置かれない名前(いつも `.cortex` から読む)と理由 */
56
+ exports.VIEW_HELD = Object.freeze({
57
+ "gold-proposals": "名寄せの提案のカード(A1 の答えまで .cortex)",
58
+ "gold-confirmation": "削除の確認の一言(記憶 v2 の案件では人の手の表に寄せる・B9a)",
59
+ });
60
+ /** 目次の名前(engine `VIEW_INDEX_NAME`) */
61
+ exports.VIEW_INDEX_FILE = "view-index.json";
62
+ const SHA_RE = /^[0-9a-f]{40}$/;
63
+ const isSha = (s) => typeof s === "string" && SHA_RE.test(s);
64
+ /** git の blob の sha(engine `blobShaOf` と同じ・utf8 のバイト列) */
65
+ function blobShaOf(text) {
66
+ const body = Buffer.from(String(text), "utf8");
67
+ return (0, node_crypto_1.createHash)("sha1").update(Buffer.concat([Buffer.from(`blob ${body.length}\0`, "utf8"), body])).digest("hex");
68
+ }
69
+ /** 封筒が読める形か(engine `isReadableEnvelope` の写し) */
70
+ function isReadableEnvelope(e) {
71
+ if (!e || typeof e !== "object" || Array.isArray(e))
72
+ return false;
73
+ const o = e;
74
+ if (o.v !== 1 || typeof o.name !== "string" || typeof o.run !== "string" || typeof o.at !== "string")
75
+ return false;
76
+ if (!isSha(o.commit))
77
+ return false;
78
+ if (o.absent === true)
79
+ return o.blob === null && o.text === null;
80
+ return isSha(o.blob) && typeof o.text === "string";
81
+ }
82
+ /**
83
+ * 1 本の封筒の分類(ビューアの読み手の比べ)。**中身で決める**: 封筒の本文が、今の帳簿の置き場のファイルとバイトで同じなら `same`(使う)。
84
+ *
85
+ * engine の `classifyEnvelope`(`view-publish.mjs`)は「封筒の commit が HEAD の祖先か」を先に見るが、ここはそれを使わない。
86
+ * 記憶側は同じ中身の晩に置き直さない(封筒の commit は古くなる)・ビルドの checkout は浅い(`fetch-depth: 2000`)・
87
+ * push で起きたビルドは公開より前の版を checkout していることがある、の 3 つで、祖先の判じが中身と関係なく落ちるため(実装レビュー 🔴1・🟠2)。
88
+ * git は、中身が違うときに「説明がつくか」を分けるのにだけ使う:
89
+ * - `stale`: 封筒の commit が HEAD の祖先でその版の blob と合う(後から帳簿が変わった)・HEAD が封筒の commit の祖先(checkout が公開より古い)・
90
+ * 目次と封筒が食い違う(取り出しと公開が重なった)
91
+ * - `unexplained`: 名前違い・本文の sha が blob と違う・その版の blob と違う・どちらも祖先でない(枝分かれ)
92
+ * - `unverified`: 中身が違い、封筒の commit が手元の履歴に無い(浅い clone)。門の外の数
93
+ * - `unreadable`: 封筒・目次の行が読めない・形が違う
94
+ */
95
+ function classifyView({ envelope, expectedName, indexRow, currentBlob, head, isAncestor, blobAt, }) {
96
+ if (!indexRow || !isReadableEnvelope(envelope))
97
+ return "unreadable";
98
+ const e = envelope;
99
+ if (e.name !== expectedName)
100
+ return "unexplained";
101
+ if (e.absent !== true && blobShaOf(e.text) !== e.blob)
102
+ return "unexplained";
103
+ const envBlob = e.absent === true ? null : e.blob;
104
+ if (envBlob === currentBlob)
105
+ return "same";
106
+ // 目次と封筒の食い違い(取り出しの sync と公開の Put が重なった)は、後から変わったのと同じに数える
107
+ if (indexRow.commit !== e.commit || indexRow.blob !== e.blob || indexRow.absent !== (e.absent === true))
108
+ return "stale";
109
+ if (head === null)
110
+ return "unverified";
111
+ try {
112
+ if (isAncestor(e.commit, head))
113
+ return (blobAt(e.commit, e.name) ?? null) === envBlob ? "stale" : "unexplained";
114
+ return isAncestor(head, e.commit) ? "stale" : "unexplained";
115
+ }
116
+ catch {
117
+ return "unverified";
118
+ }
119
+ }
120
+ /** checkout の git(`repoRoot` で呼ぶ)。🔴 投げる口はそのまま投げる(比べの側で unexplained に倒す) */
121
+ function checkoutGit(repoRoot) {
122
+ const git = (args) => (0, node_child_process_1.execFileSync)("git", args, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
123
+ return {
124
+ head: () => {
125
+ const h = git(["rev-parse", "--verify", "HEAD^{commit}"]);
126
+ if (!isSha(h))
127
+ throw new Error("HEAD を読めません");
128
+ return h;
129
+ },
130
+ isAncestor: (a, b) => {
131
+ if (!isSha(a) || !isSha(b))
132
+ throw new Error("commit の形が違います");
133
+ try {
134
+ (0, node_child_process_1.execFileSync)("git", ["merge-base", "--is-ancestor", a, b], { cwd: repoRoot, stdio: "ignore" });
135
+ return true;
136
+ }
137
+ catch (e) {
138
+ // 1 は「祖先でない」。それ以外(128=commit が無い・浅い clone)は判じられない
139
+ if (e.status === 1)
140
+ return false;
141
+ throw e;
142
+ }
143
+ },
144
+ blobAt: (commit, name) => {
145
+ if (!isSha(commit))
146
+ throw new Error("commit の形が違います");
147
+ try {
148
+ const sha = git(["rev-parse", "--verify", "-q", `${commit}:./${ledger_dir_1.DEFAULT_LEDGER_DIR_NAME}/${name}.json`]);
149
+ return isSha(sha) ? sha : null;
150
+ }
151
+ catch {
152
+ // その版に commit があって、ファイルだけが無い → null。commit そのものが無いのは判じられない(投げる)
153
+ (0, node_child_process_1.execFileSync)("git", ["cat-file", "-e", `${commit}^{commit}`], { cwd: repoRoot, stdio: "ignore" });
154
+ return null;
155
+ }
156
+ },
157
+ };
158
+ }
159
+ async function readText(p) {
160
+ try {
161
+ return await promises_1.default.readFile(p, "utf8");
162
+ }
163
+ catch {
164
+ return null;
165
+ }
166
+ }
167
+ /** 目次を読む。無い → `null`・壊れている → `"unreadable"`・読める → 名前ごとの行 */
168
+ async function readIndex(viewDir) {
169
+ const text = await readText(node_path_1.default.join(viewDir, exports.VIEW_INDEX_FILE));
170
+ if (text === null)
171
+ return null;
172
+ try {
173
+ const doc = JSON.parse(text);
174
+ const o = doc && typeof doc === "object" && !Array.isArray(doc) ? doc : null;
175
+ const names = o && o.v === 1 && o.names && typeof o.names === "object" && !Array.isArray(o.names) ? o.names : null;
176
+ if (!names)
177
+ return "unreadable";
178
+ const out = {};
179
+ for (const [k, v] of Object.entries(names)) {
180
+ const r = v && typeof v === "object" && !Array.isArray(v) ? v : null;
181
+ // 形の違う行は null(その名前は unreadable に数える。missing に紛れさせない・実装レビュー 🟡4)
182
+ if (!r || !isSha(r.commit) || !(r.blob === null || isSha(r.blob))) {
183
+ out[k] = null;
184
+ continue;
185
+ }
186
+ out[k] = { blob: r.blob, commit: r.commit, absent: r.absent === true };
187
+ }
188
+ return out;
189
+ }
190
+ catch {
191
+ return "unreadable";
192
+ }
193
+ }
194
+ /**
195
+ * 画面用ファイルの置き場を読んで、名前ごとに使うかを決める(ファイルは書かない)。`baseDir` は今の帳簿の置き場(`.cortex` か `--ledger-dir`)で、
196
+ * same は「封筒の本文がそのフォルダのファイルとバイトで同じ」(だから合わせの置き場は今の帳簿と 1 バイトも変わらない)。
197
+ * git(HEAD・祖先・その版の blob)は、中身が違うときの数え分けにだけ使う
198
+ */
199
+ async function planViews(viewDir, git, baseDir) {
200
+ const classes = {};
201
+ const use = {};
202
+ const index = await readIndex(viewDir);
203
+ let head = null;
204
+ try {
205
+ head = git.head();
206
+ }
207
+ catch {
208
+ head = null;
209
+ }
210
+ for (const name of exports.VIEW_NAMES) {
211
+ if (index === null || (index !== "unreadable" && !Object.prototype.hasOwnProperty.call(index, name))) {
212
+ classes[name] = "missing";
213
+ continue;
214
+ }
215
+ if (index === "unreadable") {
216
+ classes[name] = "unreadable";
217
+ continue;
218
+ }
219
+ const text = await readText(node_path_1.default.join(viewDir, `${name}.json`));
220
+ let env = null;
221
+ try {
222
+ env = text === null ? null : JSON.parse(text);
223
+ }
224
+ catch {
225
+ env = null;
226
+ }
227
+ const cur = await readText(node_path_1.default.join(baseDir, `${name}.json`));
228
+ const c = classifyView({
229
+ envelope: env,
230
+ expectedName: name,
231
+ indexRow: index[name],
232
+ currentBlob: cur === null ? null : blobShaOf(cur),
233
+ head,
234
+ isAncestor: git.isAncestor,
235
+ blobAt: git.blobAt,
236
+ });
237
+ classes[name] = c;
238
+ if (c === "same")
239
+ use[name] = env.absent === true ? null : env.text;
240
+ }
241
+ const counts = { total: exports.VIEW_NAMES.length, same: 0, stale: 0, unexplained: 0, unreadable: 0, unverified: 0, missing: 0 };
242
+ for (const c of Object.values(classes))
243
+ counts[c] += 1;
244
+ return { classes, counts, use };
245
+ }
246
+ /**
247
+ * 合わせの置き場を作る: `baseDir`(`.cortex` か `--ledger-dir`)の中身を一時フォルダに写し、same の名前だけ封筒の本文で差し替える
248
+ * (「無い」の封筒はその名前を消す)。戻りの `cleanup` で消す
249
+ */
250
+ async function mergeLedgerDir(baseDir, plan) {
251
+ const dir = await promises_1.default.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), "cortex-view-"));
252
+ try {
253
+ await promises_1.default.cp(baseDir, dir, { recursive: true });
254
+ }
255
+ catch (e) {
256
+ if (e?.code !== "ENOENT") {
257
+ await promises_1.default.rm(dir, { recursive: true, force: true });
258
+ throw e;
259
+ }
260
+ }
261
+ for (const [name, text] of Object.entries(plan.use)) {
262
+ const p = node_path_1.default.join(dir, `${name}.json`);
263
+ if (text === null)
264
+ await promises_1.default.rm(p, { force: true });
265
+ else
266
+ await promises_1.default.writeFile(p, text, "utf8");
267
+ }
268
+ return { dir, cleanup: () => promises_1.default.rm(dir, { recursive: true, force: true }) };
269
+ }
270
+ /** 数の 1 行(ビルドのログ。名前は出さない) */
271
+ function viewCountsLine(c) {
272
+ return `記憶 v2 の画面用ファイル: ${c.total} 本のうち 使った ${c.same}・後から変わった ${c.stale}・説明のつかない ${c.unexplained}・読めない ${c.unreadable}・履歴で確かめられない ${c.unverified}・目次に無い ${c.missing}(使わなかった分は今の帳簿から読みました)`;
273
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.123",
3
+ "version": "0.12.125",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,