@takagaki/cortex-decisions-viewer 0.12.64 → 0.12.66

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/README.md CHANGED
@@ -20,7 +20,7 @@ npx -y @takagaki/cortex-decisions-viewer build --out site --title "Decisions"
20
20
  | `index.html` | 画面の骨組みと、埋め込みデータ(`<script type="application/json">`) |
21
21
  | `404.html` | `index.html` と同じもの(サブパス直叩きのフォールバック) |
22
22
  | `assets/viewer.<内容ハッシュ>.js` / `.css` | 画面のJSとCSS。内容が同じなら名前も同じ(版が同じなら全案件で同じ名前) |
23
- | `bodies/*.html` | レコードの本文(詳細を開いたときに取りに行く) |
23
+ | `bodies/decisions/<id>.html` / `bodies/home.html` / `bodies/<区画>/<id>.html` | レコードの本文(詳細を開いたときに取りに行く)。**名前は並び順ではなくレコードから決まる**(cortex-engine #382)。先頭に `<!-- cortex-body id="..." -->` の印が入り、画面は取ってきた本文がそのレコードのものか確かめる |
24
24
  | `thumbs/*` | グラフのサムネイル画像 |
25
25
 
26
26
  **配置は `site/` を丸ごと置く**(`index.html` だけを置いても画面は出ない)。
package/dist/build.js CHANGED
@@ -44,8 +44,12 @@ exports.readGoldProposals = readGoldProposals;
44
44
  exports.buildHomeGrounding = buildHomeGrounding;
45
45
  exports.collectMeetings = collectMeetings;
46
46
  exports.findBacklogDir = findBacklogDir;
47
+ exports.bodyFileSlug = bodyFileSlug;
48
+ exports.bodyStampId = bodyStampId;
49
+ exports.bodyStampComment = bodyStampComment;
47
50
  exports.build = build;
48
51
  exports.createRefResolver = createRefResolver;
52
+ const node_crypto_1 = require("node:crypto");
49
53
  const node_fs_1 = require("node:fs");
50
54
  const path = __importStar(require("node:path"));
51
55
  const gray_matter_1 = __importDefault(require("gray-matter"));
@@ -911,6 +915,7 @@ async function parseGeneric(raw, fileName, repoRelDir, repoBaseUrl, branch) {
911
915
  summary,
912
916
  bodyHtml,
913
917
  bodyRef: "",
918
+ bodyId: "",
914
919
  searchText: toSearchText(content),
915
920
  fileName,
916
921
  editUrl,
@@ -1700,25 +1705,120 @@ function computeLayout(graph) {
1700
1705
  }
1701
1706
  }
1702
1707
  /**
1703
- * 各レコードの bodyHtml を out/bodies/<i>.html に書き出し、bodyRef にパスを設定して
1708
+ * 本文ファイルの名前に使える形へ畳む。
1709
+ *
1710
+ * 🔴 **畳んだときの区別に連番を使わない。** 連番は「並び順で本文を引く」形に戻る——
1711
+ * それが cortex-engine #382 の不具合そのもの(レコードを1件消すと、それより古い全レコードの
1712
+ * 本文が1つずつずれる)。畳んだ跡があるときは**元の値から決まる印**を足して区別する。
1713
+ * 同じ入力なら常に同じ名前になり、他のレコードが増えても消えても変わらない。
1714
+ *
1715
+ * ASCII に倒すのは配布先(S3 + CloudFront)で確かめられていない形を持ち込まないため。
1716
+ * 案件の id と区画名は日本語を含みうる(レコードの id はファイル名から導出されることがある)。
1717
+ */
1718
+ function bodyFileSlug(raw) {
1719
+ const s = String(raw ?? "").normalize("NFC");
1720
+ const folded = s
1721
+ .replace(/[^A-Za-z0-9._-]+/g, "-")
1722
+ .replace(/-{2,}/g, "-")
1723
+ // 🔴 `..` を残さない。ポータルの門は URI に `..` を含むと無条件で 403 を返す
1724
+ // (classmethod-cortex-portal の viewer-gate.js)ので、本文が丸ごと読めなくなる
1725
+ .replace(/\.{2,}/g, ".")
1726
+ .replace(/^[-.]+|[-.]+$/g, "");
1727
+ if (s !== "" && folded === s)
1728
+ return s; // 畳んでいない=そのまま使える
1729
+ const h = shortDigest(s);
1730
+ return folded ? `${folded}-${h}` : h;
1731
+ }
1732
+ /** 元の値から決まる短い印(並び順に依らない区別に使う。assets のハッシュ名と同じ桁数) */
1733
+ function shortDigest(s) {
1734
+ return (0, node_crypto_1.createHash)("sha256").update(s, "utf8").digest("hex").slice(0, 8);
1735
+ }
1736
+ /**
1737
+ * 本文がどのレコードのものかを表す印。`<kind>:<id>` の形で、
1738
+ * 本文ファイルの先頭コメントと inlineデータの `bodyId` の両方に同じ文字列が入る。
1739
+ *
1740
+ * `<` `>` `&` `"` を逃がすので、id に何が入っていてもコメントを閉じたり属性を割ったりしない。
1741
+ *
1742
+ * `extra`(同じ区画に id が重複したときのファイル名)を渡すと `<kind>:<id>:<extra>` になる。
1743
+ * 🔴 **重複しているときは印にも区別が要る。** 印が `<kind>:<id>` だけだと、重複した2件の
1744
+ * 一方が消えた日に、残った方の本文が**消えた方の印と一致してしまう**——古い画面に
1745
+ * 別レコードの本文が出る(#382 の症状そのもの)。
1746
+ */
1747
+ function bodyStampId(kind, id, extra) {
1748
+ const esc = (v) => String(v ?? "")
1749
+ .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1750
+ return `${esc(kind)}:${esc(id)}` + (extra ? `:${esc(extra)}` : "");
1751
+ }
1752
+ /** 本文ファイルの先頭に置く印の行(画面側の BODY_STAMP_RE と対) */
1753
+ function bodyStampComment(stampId) {
1754
+ return `<!-- cortex-body id="${stampId}" -->\n`;
1755
+ }
1756
+ /**
1757
+ * 各レコードの bodyHtml を out/bodies/... へ書き出し、bodyRef にパスを設定して
1704
1758
  * inlineデータ上の bodyHtml を空にする(初期ロードのpayloadから本文を外す)。
1759
+ *
1760
+ * 🔴 **ファイル名は並び順ではなくレコードから決まる**(cortex-engine #382)。
1761
+ * 前は `bodies/<並び順の番号>.html` だった。詳細を開いた画面は埋め込まれた bodyRef を
1762
+ * fetch するので、**削除で番号が詰まると、開きっぱなしの画面が別レコードの本文を出す**。
1763
+ * 見出しはそのレコードのまま本文だけが入れ替わるので、見比べない限り誰も気づけない。
1764
+ *
1765
+ * 置き場:
1766
+ * 決定 bodies/decisions/<id>.html
1767
+ * Home bodies/home.html
1768
+ * 区画レコード bodies/<区画のディレクトリ名>/<id>.html
1769
+ *
1770
+ * 併せて各ファイルの先頭に印(bodyStampComment)を埋める。画面はこれを突き合わせて、
1771
+ * 食い違ったら本文を出さない(配布の途中・移行期・id が重複した案件の保険)。
1705
1772
  */
1706
1773
  async function externalizeBodies(site, outAbs) {
1707
1774
  const all = [
1708
- ...site.decisions,
1709
- ...(site.home ? [site.home] : []),
1710
- ...site.sections.flatMap((s) => s.records),
1775
+ ...site.decisions.map((rec) => ({ rec, kind: "decisions", dir: "bodies/decisions" })),
1776
+ ...site.sections.flatMap((s) => s.records.map((rec) => ({ rec, kind: s.name, dir: `bodies/${bodyFileSlug(s.name)}` }))),
1711
1777
  ];
1712
- if (all.length === 0)
1778
+ const home = site.home;
1779
+ // **先に丸ごと消す。** 出力ディレクトリは作り直されないので、消さないと前のビルドの
1780
+ // 本文(番号の名前を含む)がファイルとして残る。配布先は `aws s3 sync --delete` が
1781
+ // 消すが、手元のプレビューでは古い本文が URL 直打ちで生き続ける(アイコンと同じ手当て)。
1782
+ // 🔴 **0件の回にも消す。** 早期 return より前に置くこと——全部消した案件が
1783
+ // 「前のビルドの本文だけが残る」状態になる(消したはずのものが読める、が一番まずい)
1784
+ await node_fs_1.promises.rm(path.join(outAbs, "bodies"), { recursive: true, force: true });
1785
+ if (all.length === 0 && !home)
1713
1786
  return;
1714
1787
  await node_fs_1.promises.mkdir(path.join(outAbs, "bodies"), { recursive: true });
1715
- let i = 0;
1716
- for (const rec of all) {
1717
- const ref = `bodies/${i}.html`;
1718
- await node_fs_1.promises.writeFile(path.join(outAbs, ref), rec.bodyHtml ?? "", "utf8");
1788
+ const write = async (rec, ref, stampId) => {
1789
+ await node_fs_1.promises.mkdir(path.dirname(path.join(outAbs, ref)), { recursive: true });
1790
+ await node_fs_1.promises.writeFile(path.join(outAbs, ref), bodyStampComment(stampId) + (rec.bodyHtml ?? ""), "utf8");
1719
1791
  rec.bodyRef = ref;
1792
+ rec.bodyId = stampId;
1720
1793
  rec.bodyHtml = ""; // inlineデータからは本文を除外(payload削減)
1721
- i++;
1794
+ };
1795
+ // Home は1件だけなので固定名。区画に "home" という名前があってもディレクトリなのでぶつからない
1796
+ if (home)
1797
+ await write(home, "bodies/home.html", bodyStampId("home", home.id));
1798
+ // 同じ区画に同じ id のレコードが2件あるときの避け方。
1799
+ //
1800
+ // 🔴 **`-1` `-2` の連番で避けない(設計レビューの指摘)。** 連番は並び順から決まるので、
1801
+ // 重複した2件の一方が消えた日に、残った方が空いた番号へ繰り上がる。古い画面の bodyRef と
1802
+ // 印がそのまま一致してしまい、**別レコードの本文が出る**——#382 の症状が形を変えて戻る。
1803
+ // だから**重複している側の全員**に、そのレコードの出どころから決まる印を付ける
1804
+ // (誰が消えても残った側の名前は変わらない)。印にも同じ区別を入れる。
1805
+ //
1806
+ // 区別の材料は**リポジトリの中での置き場**(editUrl にパスが入っている)。今は records/ を
1807
+ // 掘らないのでファイル名だけでも一意だが、掘るようになった日に黙って上書きし合う形にしない。
1808
+ //
1809
+ // 大文字小文字だけ違う id も同じ組として数える。macOS のローカルビルドでは
1810
+ // 別名にならず黙って上書きし合うため(S3 は区別するので手元だけ壊れる、が一番気づけない)
1811
+ const originOf = (rec) => `${rec.editUrl ?? ""}|${rec.fileName}`;
1812
+ const dupCount = new Map();
1813
+ for (const { rec, dir } of all) {
1814
+ const key = `${dir}/${bodyFileSlug(rec.id)}`.toLowerCase();
1815
+ dupCount.set(key, (dupCount.get(key) ?? 0) + 1);
1816
+ }
1817
+ for (const { rec, kind, dir } of all) {
1818
+ const base = `${dir}/${bodyFileSlug(rec.id)}`;
1819
+ const dup = (dupCount.get(base.toLowerCase()) ?? 0) > 1;
1820
+ const ref = dup ? `${base}-${shortDigest(originOf(rec))}.html` : `${base}.html`;
1821
+ await write(rec, ref, bodyStampId(kind, rec.id, dup ? originOf(rec) : undefined));
1722
1822
  }
1723
1823
  }
1724
1824
  async function build(opts) {
@@ -1979,6 +2079,7 @@ async function parseDecision(raw, fileName, repoRelDir, repoBaseUrl, branch, ref
1979
2079
  bodyLinks: extractBodyLinks(content),
1980
2080
  bodyHtml,
1981
2081
  bodyRef: "",
2082
+ bodyId: "",
1982
2083
  searchText: toSearchText(content),
1983
2084
  rstatus: data.status != null ? String(data.status) : "",
1984
2085
  isLatest: isLatestPath(filePath),
package/dist/render.js CHANGED
@@ -100,7 +100,7 @@ function renderSite(site, assets) {
100
100
  const githubBtn = site.repoBaseUrl
101
101
  ? `<a class="gh-btn" href="${escapeHtml(site.repoBaseUrl)}" target="_blank" rel="noopener" title="コンテキストリポジトリをGitHubで開く">${GITHUB_ICON}</a>`
102
102
  : "";
103
- const askBtn = `<a class="ask-ai-btn" href="${escapeHtml(askUrl)}" data-ai-prompt="${escapeHtml(ASK_PROMPT)}" target="_blank" rel="noopener" title="Claude Code Web でこの案件のリポジトリと、登録済みの開発リポジトリを開き、プロジェクトについて質問・調査できます">💬 プロジェクトについてAIに聞く</a>`;
103
+ const askBtn = `<a class="ask-ai-btn" href="${escapeHtml(askUrl)}" data-ai-prompt="${escapeHtml(ASK_PROMPT)}" target="_blank" rel="noopener" title="Claude Code Web でこのプロジェクトのリポジトリと、登録済みの開発リポジトリを開いて、質問・調査できます">💬 プロジェクトについてAIに聞く</a>`;
104
104
  // CSPでインラインが拒否される環境向けの外部参照形。省略時は従来どおり埋め込む
105
105
  const styleTag = assets
106
106
  ? `<link rel="stylesheet" href="${escapeHtml(assets.css)}" />`
@@ -3556,7 +3556,7 @@ function buildTutorialData(site, repoSlug) {
3556
3556
  },
3557
3557
  {
3558
3558
  title: "ドメインを可視化する",
3559
- desc: "案件の概念と関係を図にする",
3559
+ desc: "プロジェクトの概念と関係を図にする",
3560
3560
  prompt: "このプロジェクトの主要なドメイン概念(用語集)と、それらの関係性を、HTML アーティファクトで見やすく可視化してください。カードやボックスと線でノードと関係を表す軽量な作りにし、描画に時間のかかる精密な SVG 座標計算は避けてください。",
3561
3561
  },
3562
3562
  {
@@ -3616,7 +3616,7 @@ function buildTutorialData(site, repoSlug) {
3616
3616
  if (usesSlack) {
3617
3617
  specs.push({
3618
3618
  title: "Slackに完了を知らせよう",
3619
- desc: "チュートリアルの完了を案件のチャンネルに投稿する",
3619
+ desc: "チュートリアルの完了をプロジェクトのチャンネルに投稿する",
3620
3620
  prompt: TUT_SLACK_PROMPT,
3621
3621
  });
3622
3622
  }
@@ -3681,13 +3681,13 @@ const TUT_DESIGN_PROMPT = [
3681
3681
  "読むだけの練習です。インベントリはFigmaからの同期で上書きされる生成物なので、手で編集しないでください。",
3682
3682
  ].join("\n");
3683
3683
  const TUT_SLACK_PROMPT = [
3684
- "Cortex のチュートリアルを完了したことを、この案件の Slack チャンネルに投稿してください。最後のステップです。",
3684
+ "Cortex のチュートリアルを完了したことを、このプロジェクトの Slack チャンネルに投稿してください。最後のステップです。",
3685
3685
  "",
3686
3686
  "1. チャット/channels.json を読んで投稿先のチャンネルを選ぶ(複数あれば description を見て判断し、迷ったらわたしに聞く)",
3687
3687
  "2. 次の雛形で本文を作る({名前} は Cortex/Members/records/ にあるわたしのレコードの表記に合わせる)",
3688
3688
  "",
3689
3689
  "🎉 {名前} さんが Cortex のチュートリアルを完了しました",
3690
- "できるようになったこと: Gold層から案件を把握する・自分の情報を更新する・Backlogに記票する・資料を投入する",
3690
+ "できるようになったこと: Gold層からプロジェクトを把握する・自分の情報を更新する・Backlogに記票する・資料を投入する",
3691
3691
  "使い方の質問はこのチャンネルへどうぞ",
3692
3692
  "",
3693
3693
  "3. 投稿先と本文をプレビューして承認を得てから投稿し、メッセージのリンクを報告する",
@@ -4183,6 +4183,21 @@ button.site-icon[aria-busy="true"] .site-icon-plus { opacity: 0; }
4183
4183
  padding: 10px 12px; font-size: 13px; line-height: 1.8; color: var(--muted);
4184
4184
  }
4185
4185
  .draft-note > .deleted-note { flex: 1 0 100%; margin-bottom: 0; }
4186
+ /* 「画面が古くなっています」の帯(#382)。本文の代わりに出るので、本文と同じ幅で
4187
+ 目に入る濃さにする。帯は症状・行は一手——文は1つ、押すものは1つだけ */
4188
+ .stale-note {
4189
+ display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin: 4px 0 16px;
4190
+ background: #fffbeb; border: 1px solid #fcd34d; border-radius: 8px;
4191
+ padding: 10px 12px; font-size: 13.5px; line-height: 1.8; color: #92400e;
4192
+ }
4193
+ .draft-note > .stale-note { flex: 1 0 100%; margin-bottom: 0; }
4194
+ .stale-reload-btn {
4195
+ flex: 0 0 auto; cursor: pointer; white-space: nowrap;
4196
+ padding: 6px 14px; border-radius: 999px;
4197
+ border: 1px solid #b45309; background: #fff; color: #92400e;
4198
+ font-size: 13px; font-weight: 600;
4199
+ }
4200
+ .stale-reload-btn:hover { background: #fef3c7; }
4186
4201
  /* 消したものは、消えたと分かる形で残す(一覧・詳細の両方) */
4187
4202
  .is-deleted { text-decoration: line-through; color: var(--muted); }
4188
4203
  /* 消したレコードでは、直す口(AIで編集・GitHubで編集)を押させない */
@@ -4243,11 +4258,6 @@ button.site-icon[aria-busy="true"] .site-icon-plus { opacity: 0; }
4243
4258
  background: #ecfdf5; border: 1px solid #a7f3d0; border-radius: 8px;
4244
4259
  padding: 10px 14px; margin: 12px 0 4px; font-size: 13.5px; color: #047857; line-height: 1.7;
4245
4260
  }
4246
- .adoption-note {
4247
- display: flex; gap: 8px; align-items: flex-start;
4248
- background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 8px;
4249
- padding: 10px 14px; margin: 0 0 14px; font-size: 13.5px; color: #1e40af; line-height: 1.7;
4250
- }
4251
4261
  .chip.adoption-existing, .chip.adoption-migration { background: #eff6ff; color: #1e40af; border-color: #bfdbfe; }
4252
4262
  .chip.adoption-new { background: #ecfdf5; color: #047857; border-color: #a7f3d0; }
4253
4263
  .chip {
@@ -5291,7 +5301,7 @@ ${calendarAddUrl.toString()}
5291
5301
  return '<a class="wikilink" data-go="' + attr + '">' + disp + "</a>";
5292
5302
  });
5293
5303
  }
5294
- // 本文は bodies/<i>.html に外出しされ詳細表示時に fetch されるため、linkify は loadBody 側で適用する
5304
+ // 本文は bodies/ 以下へ外出しされ詳細表示時に fetch されるため、linkify は loadBody 側で適用する
5295
5305
  // (ここで inline bodyHtml に適用すると検索テキストにHTMLが混ざるため適用しない)。
5296
5306
 
5297
5307
  // Gold層ディレクトリ名(英語)→ タブ・フォルダの表示名(日本語)の辞書。
@@ -5749,7 +5759,7 @@ ${calendarAddUrl.toString()}
5749
5759
  // Home(案件の入口)のAI編集。Gold層の4区画と違い規約READMEが無いので、Home自身を読ませて直させる。
5750
5760
  // 「このプロジェクトが目指すもの」はAIが判断の方向を決める材料なので、未記入なら埋めることを促す。
5751
5761
  var HOME_EDIT_PROMPT = [
5752
- "Cortex/Home.md を読んでから、この案件の入口ページを修正してください。",
5762
+ "Cortex/Home.md を読んでから、このプロジェクトの入口ページを修正してください。",
5753
5763
  "何を直すかは、まずわたしに質問してください。「この案件について」「ゴール」が未記入なら、そこを埋めるところから始めてください。",
5754
5764
  "frontmatter(識別カード・engine 設定)は触らないでください。日付・進捗・実績値は書かないでください(変わるものの正本は課題管理ツール等の外部にあります)。",
5755
5765
  "このサイトの使い方の説明は書かないでください(説明は画面の側が持っています)。",
@@ -5764,10 +5774,10 @@ ${calendarAddUrl.toString()}
5764
5774
  * 「何をする案件か・誰のためか・どうなったら成功か」を必ず聞かせる。
5765
5775
  */
5766
5776
  var HOME_WRITE_PROMPT = [
5767
- "Cortex/Home.md を読んでから、この案件の「この案件について」「ゴール」の2つの節を埋めてください。",
5768
- "まず、わたしに質問して内容を聞き出してください。少なくとも次の3つを確かめてください: 何をする案件か/誰のための案件か(顧客・利用者は誰か)/どうなったら成功と言えるか。",
5777
+ "Cortex/Home.md を読んでから、このプロジェクトの「この案件について」「ゴール」の2つの節を埋めてください。",
5778
+ "まず、わたしに質問して内容を聞き出してください。少なくとも次の3つを確かめてください: 何をするプロジェクトか/誰のためのプロジェクトか(顧客・利用者は誰か)/どうなったら成功と言えるか。",
5769
5779
  "聞き出した内容をもとに、2つの節を具体的な散文で書いてください。雛形の {{…}} は残さず、実際の内容に置き換えてください。",
5770
- "この2つの節は、毎晩のAI生成(決定事項・用語集・メンバー・ルールの抽出)が案件を理解するために読む場所です。抽象的な言葉ではなく、この案件に固有の具体で書いてください。",
5780
+ "この2つの節は、毎晩のAI生成(決定事項・用語集・メンバー・ルールの抽出)がプロジェクトを理解するために読む場所です。抽象的な言葉ではなく、このプロジェクトに固有の具体で書いてください。",
5771
5781
  "このサイトの使い方の説明は書かないでください(説明は画面の側が持っています)。",
5772
5782
  "frontmatter(識別カード・engine 設定)は触らないでください。日付・進捗・実績値は書かないでください(変わるものの正本は課題管理ツール等の外部にあります)。",
5773
5783
  "書き込む前に、書いた内容を提示して承認を得てください。",
@@ -5829,7 +5839,7 @@ ${calendarAddUrl.toString()}
5829
5839
  noun: "ルール",
5830
5840
  dir: "Cortex/Rules",
5831
5841
  add: [
5832
- "Cortex/Rules/README.md と Cortex/Rules/template.md を読んで登録の規約を把握してから、ルール(案件で継続的に守る制約)を1件登録してください。",
5842
+ "Cortex/Rules/README.md と Cortex/Rules/template.md を読んで登録の規約を把握してから、ルール(プロジェクトで継続的に守る制約)を1件登録してください。",
5833
5843
  "何を登録するかは、まずわたしに質問してください。",
5834
5844
  "書き込む前に、作成するファイルの内容を提示して承認を得てください。",
5835
5845
  ].join("\\n"),
@@ -5988,16 +5998,88 @@ ${calendarAddUrl.toString()}
5988
5998
  });
5989
5999
  return box;
5990
6000
  }
5991
- // 本文HTMLは bodies/<i>.html に外出しされている。詳細表示時にfetchして埋める(キャッシュ・失敗時フォールバック付き)
6001
+ /**
6002
+ * この画面が古くなったか(開いたあとに並びが変わったか)。
6003
+ *
6004
+ * 立つのは2つ: 本文の印が食い違った・削除が成功した。立ったあとの詳細表示は
6005
+ * 本文を取りに行かず帯を出す(cortex-engine #382)。
6006
+ */
6007
+ var pageStale = false;
6008
+ /**
6009
+ * 本文の先頭に埋めた印(build.ts の bodyStampComment と対)。
6010
+ * **両側を同じ形にしておくこと**——片方だけ変えると、印が常に食い違って
6011
+ * 全レコードの本文が出なくなる(帯だけの画面になる)。
6012
+ */
6013
+ // ⚠ 埋め込みJSはテンプレート文字列の中なので、正規表現の逆スラッシュは2本で書くこと
6014
+ // (1本だと素の s になり、**印が常に食い違って全レコードの本文が消える**)
6015
+ var BODY_STAMP_RE = /^\\s*<!--\\s*cortex-body id="([^"]*)"\\s*-->/;
6016
+ /**
6017
+ * 「画面が古くなっています」の帯。**帯は症状・行は一手・繰り返さない。**
6018
+ *
6019
+ * 🔴 **自動で読み込み直さない。** 押した人の書きかけ・開いている場所を勝手に捨てない。
6020
+ * 押す口は1つだけ置いて、押すかどうかは読み手が決める。
6021
+ */
6022
+ function staleReloadNote() {
6023
+ var box = el("div", { class: "stale-note" }, [
6024
+ el("span", { text: "画面が古くなっています。再読み込みしてください。" }),
6025
+ ]);
6026
+ var btn = el("button", { class: "stale-reload-btn", type: "button", text: "再読み込み" });
6027
+ btn.addEventListener("click", function () { location.reload(); });
6028
+ box.appendChild(btn);
6029
+ return box;
6030
+ }
6031
+ /**
6032
+ * 本文の代わりに帯を出す(本文は出さない——別レコードの本文を読ませないため)。
6033
+ *
6034
+ * 🔴 **帯を出す経路の after(done)はここ1か所に集める。** Home は本文が決まるまで
6035
+ * 枠を隠している(renderHome)ので、呼び忘れた経路は**帯ごと消えて真っ白になる**。
6036
+ * 3経路(印の食い違い・404/403・削除後)で書き分けると、1つだけ落ちても気づけない。
6037
+ */
6038
+ function showStaleBody(mount, after) {
6039
+ pageStale = true;
6040
+ mount.innerHTML = "";
6041
+ mount.appendChild(staleReloadNote());
6042
+ if (typeof after === "function") after(mount);
6043
+ }
6044
+ // 本文HTMLは bodies/<種別>/<id>.html に外出しされている。詳細表示時にfetchして埋める
6045
+ // (キャッシュ・失敗時フォールバック付き)
5992
6046
  var bodyCache = {};
5993
6047
  function loadBody(rec, mount, after) {
5994
6048
  function done() { if (typeof after === "function") after(mount); }
6049
+ // 並びが変わったと分かっている画面では、本文を取りに行かない(取れても別レコードのもの)
6050
+ if (pageStale && rec && rec.bodyRef) { showStaleBody(mount, after); return; }
5995
6051
  if (!rec || !rec.bodyRef) { mount.innerHTML = linkifyWiki((rec && rec.bodyHtml) || ""); done(); return; }
5996
6052
  if (bodyCache[rec.bodyRef] != null) { mount.innerHTML = bodyCache[rec.bodyRef]; done(); return; }
5997
6053
  mount.innerHTML = '<div class="body-loading">読み込み中…</div>';
5998
- fetch(rec.bodyRef).then(function (res) { if (!res.ok) throw new Error("http " + res.status); return res.text(); })
5999
- .then(function (html) { var lk = linkifyWiki(html); bodyCache[rec.bodyRef] = lk; mount.innerHTML = lk; done(); })
6000
- .catch(function () {
6054
+ fetch(rec.bodyRef).then(function (res) {
6055
+ // 🔴 **本文が無いのは「配信されていない」ではなく「この画面が古い」。** 配布は
6056
+ // aws s3 sync --delete なので、開いたままの画面が指す本文は建て直しで消える。
6057
+ //
6058
+ // 🔴 **403 も同じ扱い(実装レビューの指摘)。** 本番の配信は CloudFront の OAC で、
6059
+ // 一覧の権限を渡していないので**無いオブジェクトは 404 ではなく 403 で返る**
6060
+ // (403 を握りつぶすカスタムエラー応答も置いていない)。404 だけを見ていると、
6061
+ // この帯は**本番でだけ出ない**。403 は通行証が切れたときにも返るが、
6062
+ // そのときも一手は同じ(再読み込みで入り口へ戻って取り直す)
6063
+ if (res.status === 404 || res.status === 403) { var e = new Error("stale"); e.stale = true; throw e; }
6064
+ if (!res.ok) throw new Error("http " + res.status);
6065
+ return res.text();
6066
+ })
6067
+ .then(function (html) {
6068
+ // 🔴 **印が一致しない本文は出さない。** 別のレコードの本文を、見出しはそのままに
6069
+ // 出してしまうのが cortex-engine #382 の症状——読み手は「その決定はそう決まった」と
6070
+ // 受け取るし、画面は正常に見えるので**見比べない限り気づけない**。
6071
+ // 読めないのは気づけるが、違うものが読めるのは気づけない。だから出さない側に倒す。
6072
+ var m = BODY_STAMP_RE.exec(html);
6073
+ if (rec.bodyId && (!m || m[1] !== rec.bodyId)) {
6074
+ // キャッシュにも入れない(正しい本文で開き直せるように)
6075
+ showStaleBody(mount, after);
6076
+ return;
6077
+ }
6078
+ var lk = linkifyWiki(m ? html.slice(m[0].length) : html);
6079
+ bodyCache[rec.bodyRef] = lk; mount.innerHTML = lk; done();
6080
+ })
6081
+ .catch(function (err) {
6082
+ if (err && err.stale) { showStaleBody(mount, after); return; }
6001
6083
  // 🔴 文字列連結でHTMLを組み立てない(editUrl は frontmatter 由来の外部文字列)。
6002
6084
  // el() は class/text/html 以外のキーをそのまま setAttribute に渡すので、
6003
6085
  // href に javascript: を置けてしまう。**isHttp() が唯一の防御**(保険ではない)
@@ -6114,22 +6196,22 @@ ${calendarAddUrl.toString()}
6114
6196
  var base = SITE.repoBaseUrl;
6115
6197
  var INFO = {
6116
6198
  "decisions": {
6117
- text: "議事録・課題から確定した決定が毎晩自動で追記されます。",
6199
+ text: "このプロジェクトで決まったことの記録です。何を・いつ・誰が決めたかと、その根拠になった会議や課題を辿れます。",
6118
6200
  btnLabel: "実行履歴を確認 →",
6119
6201
  btnUrl: base ? base + "/actions/workflows/update-gold.yml" : null,
6120
6202
  },
6121
6203
  "dir:Glossary": {
6122
- text: "毎晩の自動走査が新しい用語を draft として直接追加します。AIが生成した定義は仮のものです。「AI生成・未確認」の用語は必ず内容を確認し、適切な定義に置き換えて status: active にしてください。",
6204
+ text: "このプロジェクトで使う言葉の意味をそろえる辞書です。人によって呼び方が分かれる言葉や、このプロジェクトだけの用語を載せます。",
6123
6205
  btnLabel: "実行履歴を確認 →",
6124
6206
  btnUrl: base ? base + "/actions/workflows/update-gold.yml" : null,
6125
6207
  },
6126
6208
  "dir:Members": {
6127
- text: "プロジェクト関係者の名簿です。人名の表記ゆれを解決する根拠になります。毎晩の自動走査が新しい参加者を draft として追加します。",
6209
+ text: "このプロジェクトに関わる人の名簿です。役割と所属を持ち、資料ごとに違う表記(漢字・ローマ字など)を同じ人として結び付けます。",
6128
6210
  btnLabel: "実行履歴を確認 →",
6129
6211
  btnUrl: base ? base + "/actions/workflows/update-gold.yml" : null,
6130
6212
  },
6131
6213
  "dir:Rules": {
6132
- text: "案件で継続的に守る制約・ルールの一覧です。AIはここを読んで、違反する提案を避けます。",
6214
+ text: "このプロジェクトで継続して守る決まりごとです。一度きりの決定ではなく、以後ずっと適用される制約や作法を集めます。",
6133
6215
  btnLabel: "実行履歴を確認 →",
6134
6216
  btnUrl: base ? base + "/actions/workflows/update-gold.yml" : null,
6135
6217
  },
@@ -6166,20 +6248,15 @@ ${calendarAddUrl.toString()}
6166
6248
  renderGenericList();
6167
6249
  }
6168
6250
 
6169
- // 導入経緯(overview.adoption)の表示情報。existing/migration は Decision の薄さを注記する。
6251
+ // 導入経緯(overview.adoption)の表示情報。Home見出し横のチップに出すラベルだけを返す。
6170
6252
  function adoptionInfo(v) {
6171
- // existing/migration は同一文言で「導入前の意思決定が揃わない」旨を注記する
6172
- var note = "この案件はAIS導入前から進行しているため、導入以前の意思決定はDecisionログに残っていない場合があります。";
6173
- if (v === "existing") return { label: "既存案件に導入", note: note };
6174
- if (v === "migration") return { label: "Cortex移行", note: note };
6175
- if (v === "new") return { label: "新規導入", note: "" };
6253
+ // 3種とも同じ形(ラベルのみ)。導入前のDecisionの薄さの注記は出さない
6254
+ if (v === "existing") return { label: "既存プロジェクトに導入" };
6255
+ if (v === "migration") return { label: "Cortex移行" };
6256
+ if (v === "new") return { label: "新規導入" };
6176
6257
  return null;
6177
6258
  }
6178
6259
 
6179
- function homeAdoption() {
6180
- return SITE.home && SITE.home.fm ? adoptionInfo(SITE.home.fm.adoption) : null;
6181
- }
6182
-
6183
6260
  // ---------- はじめに(チュートリアル・Homeタブ下部に統合) ----------
6184
6261
  // localStorageキーは案件別(repoSlug)。同一ブラウザで複数案件のビューアを見ても混ざらない
6185
6262
  function tutStoreKey(n) { return "cortex-tutorial-done:" + ((TUTORIAL && TUTORIAL.slug) || "") + ":" + n; }
@@ -6477,7 +6554,7 @@ ${calendarAddUrl.toString()}
6477
6554
  circle.setAttribute("aria-expanded", siteIconMenuOpen ? "true" : "false");
6478
6555
  // 未設定は「+」が見えているので、説明は「変更」ではなく置ける話にしない——
6479
6556
  // 読み上げと tooltip は、いまの状態でできることをそのまま言う
6480
- var label = cur ? "案件アイコンを変更/外す" : "案件アイコンを設定";
6557
+ var label = cur ? "プロジェクトアイコンを変更/外す" : "プロジェクトアイコンを設定";
6481
6558
  circle.setAttribute("title", label);
6482
6559
  circle.setAttribute("aria-label", label);
6483
6560
  } else {
@@ -7046,10 +7123,10 @@ ${calendarAddUrl.toString()}
7046
7123
  function homeUnfilledCard() {
7047
7124
  var card = el("div", { class: "home-unfilled" }, [
7048
7125
  el("div", { class: "home-unfilled-body" }, [
7049
- el("div", { class: "home-unfilled-title", text: "この案件の概要とゴールが未記入です。" }),
7126
+ el("div", { class: "home-unfilled-title", text: "このプロジェクトの概要とゴールが未記入です。" }),
7050
7127
  el("div", {
7051
7128
  class: "home-unfilled-text",
7052
- text: "書かれていないと、毎晩のAI生成(決定事項・用語・ルール・名簿)がこの案件を誤解したまま抽出を続けます。",
7129
+ text: "書かれていないと、毎晩のAI生成(決定事項・用語・ルール・名簿)がこのプロジェクトを誤解したまま抽出を続けます。",
7053
7130
  }),
7054
7131
  ]),
7055
7132
  ]);
@@ -7721,7 +7798,7 @@ ${calendarAddUrl.toString()}
7721
7798
  var wrap = el("div", { class: "sec-how" });
7722
7799
  wrap.appendChild(el("p", { text: "取り込まれる条件" }));
7723
7800
  var ul = el("ul", { class: "sec-how-list" });
7724
- ul.appendChild(el("li", { text: "予定がこの案件のカレンダーにある" }));
7801
+ ul.appendChild(el("li", { text: "予定がこのプロジェクトのカレンダーにある" }));
7725
7802
  ul.appendChild(el("li", { text: "Gemini のメモが ON" }));
7726
7803
  wrap.appendChild(ul);
7727
7804
  wrap.appendChild(el("p", { text: "この 2 つが揃った会議だけが取り込まれます。" }));
@@ -7739,7 +7816,7 @@ ${calendarAddUrl.toString()}
7739
7816
  */
7740
7817
  wrap.appendChild(el("p", {
7741
7818
  class: "mt-scope",
7742
- text: "案件カレンダーの予定に添付されたメモだけを開きます。それ以外は中身を開きません。",
7819
+ text: "プロジェクトカレンダーの予定に添付されたメモだけを開きます。それ以外は中身を開きません。",
7743
7820
  }));
7744
7821
  // ---- 2番目: 案件カレンダーの行(#428) ----
7745
7822
  //
@@ -7763,7 +7840,7 @@ ${calendarAddUrl.toString()}
7763
7840
  // **待ちの1行は続けない**(作る場所が分からない人に、作った後の話をしない)
7764
7841
  wrap.appendChild(el("p", {
7765
7842
  class: "mt-cal-off",
7766
- text: "この案件では、この画面から案件カレンダーを確かめられません。カレンダーの場所は cortex の担当者にご確認ください。",
7843
+ text: "この画面からはプロジェクトカレンダーを確かめられません。カレンダーの場所は cortex の担当者にご確認ください。",
7767
7844
  }));
7768
7845
  }
7769
7846
  return wrap;
@@ -7830,7 +7907,7 @@ ${calendarAddUrl.toString()}
7830
7907
  // まだ観測が届いていない。**帯もボタンも出さない**——ここで「まだありません」+「作る」を
7831
7908
  // 出すと、既にカレンダーがある案件で速い人が押して 2 本目を作る
7832
7909
  if (cal.phase === "unknown") {
7833
- box.appendChild(el("p", { class: "mt-cal-note", text: "案件カレンダーの状況を確認中です。" }));
7910
+ box.appendChild(el("p", { class: "mt-cal-note", text: "プロジェクトカレンダーの状況を確認中です。" }));
7834
7911
  return;
7835
7912
  }
7836
7913
 
@@ -7839,7 +7916,7 @@ ${calendarAddUrl.toString()}
7839
7916
  if (cal.phase === "unclear") {
7840
7917
  box.appendChild(el("p", {
7841
7918
  class: "mt-cal-note",
7842
- text: "案件カレンダーの状況を確認しています。しばらく待っても変わらなければ、cortex の担当者に連絡してください。",
7919
+ text: "プロジェクトカレンダーの状況を確認しています。しばらく待っても変わらなければ、cortex の担当者に連絡してください。",
7843
7920
  }));
7844
7921
  return;
7845
7922
  }
@@ -7847,7 +7924,7 @@ ${calendarAddUrl.toString()}
7847
7924
  if (cal.phase === "ready") {
7848
7925
  // 🔴 **説明の1行は、その操作の直前に置く(#452)。** 押す前に読むものなので、
7849
7926
  // 操作の後ろに置くと「押してから理由を知る」順になる
7850
- box.appendChild(el("p", { class: "mt-cal-note", text: "予定を作るときに、カレンダーの欄でこの案件を選びます" }));
7927
+ box.appendChild(el("p", { class: "mt-cal-note", text: "予定を作るときに、カレンダーの欄でこのプロジェクトを選びます" }));
7851
7928
  // 手順の画像。**上の1文だけで手順が完結する**(読み上げ・画像が出ない環境)ので、
7852
7929
  // alt にも同じ内容を1文で書く。寸法は属性で明示——書かないと、モーダルを開いた
7853
7930
  // 瞬間に画像の高さのぶん下の操作がずれる(Google Chat の手順と同じ理由)。
@@ -7855,13 +7932,13 @@ ${calendarAddUrl.toString()}
7855
7932
  // 表示幅の上限とクリックでの原寸戻しは guideShot / .mt-shot が持つ(#455)
7856
7933
  box.appendChild(guideShot({
7857
7934
  src: MEET_SHOT_CREATE_EVENT, width: "1360", height: "949",
7858
- alt: "予定の作成ダイアログ。「カレンダー」の欄で案件のカレンダーが選ばれ、Google Meet を追加するボタンが示され、左の一覧の「他のカレンダー」に「Google カレンダーに追加」した案件カレンダーが並んでいる。",
7935
+ alt: "予定の作成ダイアログ。「カレンダー」の欄でプロジェクトのカレンダーが選ばれ、Google Meet を追加するボタンが示され、左の一覧の「他のカレンダー」に「Google カレンダーに追加」したプロジェクトカレンダーが並んでいる。",
7859
7936
  }));
7860
7937
  // 🔴 **全員が必ず1回押すものなので主ボタンにする**(#452 のオーナー指摘)。
7861
7938
  // 見た目は「この案件のカレンダーを作る」= .ds-toggle-btn を**そのまま借りる**
7862
7939
  // ——色・枠・丸みを写すと値が2枚になり、片方だけ直る。.mt-cal-primary は
7863
7940
  // リンク固有の分(inline-block・下線を外す)だけを足す
7864
- box.appendChild(el("p", { class: "mt-cal-note", text: "最初に 1 回だけ。追加すると、上の画面でこの案件を選べるようになります" }));
7941
+ box.appendChild(el("p", { class: "mt-cal-note", text: "最初に 1 回だけ。追加すると、上の画面でこのプロジェクトを選べるようになります" }));
7865
7942
  box.appendChild(el("a", {
7866
7943
  class: "ds-toggle-btn mt-cal-primary", href: calendarAddUrl(cal.calendarId), target: "_blank", rel: "noopener",
7867
7944
  text: "Google カレンダーに追加 →",
@@ -7891,7 +7968,7 @@ ${calendarAddUrl.toString()}
7891
7968
  // たまにしかしないので、一番下に小さく薄い文字リンクで出す
7892
7969
  box.appendChild(el("a", {
7893
7970
  class: "mt-cal-sub", href: calendarTemplateUrl(cal.calendarId), target: "_blank", rel: "noopener",
7894
- text: "この案件のミーティングを作成 ↗",
7971
+ text: "このプロジェクトのミーティングを作成 ↗",
7895
7972
  }));
7896
7973
  return;
7897
7974
  }
@@ -7927,8 +8004,8 @@ ${calendarAddUrl.toString()}
7927
8004
  return;
7928
8005
  }
7929
8006
 
7930
- box.appendChild(el("div", { class: "mt-cal-banner", text: "案件カレンダーがまだありません" }));
7931
- box.appendChild(calendarRequestButton("この案件のカレンダーを作る", "create"));
8007
+ box.appendChild(el("div", { class: "mt-cal-banner", text: "プロジェクトカレンダーがまだありません" }));
8008
+ box.appendChild(calendarRequestButton("このプロジェクトのカレンダーを作る", "create"));
7932
8009
  }
7933
8010
 
7934
8011
  /**
@@ -8004,8 +8081,8 @@ ${calendarAddUrl.toString()}
8004
8081
  [
8005
8082
  // #622: 共有先はポータルで承認された案件のメンバー全員。**押す人に範囲を正しく伝える**
8006
8083
  //(前は「案件の社内メンバーに」と書いていたが、社外のメンバーにも閲覧で共有される)
8007
- "cortex-notetaker が「" + (SITE.title || "この案件") + "」のカレンダーを作り、"
8008
- + "案件の社内メンバーに予定作成の権限で、社外のメンバーに閲覧の権限で共有します。",
8084
+ "cortex-notetaker が「" + (SITE.title || "このプロジェクト") + "」のカレンダーを作り、"
8085
+ + "プロジェクトの社内メンバーに予定作成の権限で、社外のメンバーに閲覧の権限で共有します。",
8009
8086
  "できるまで数分〜十数分かかります。",
8010
8087
  ],
8011
8088
  { okLabel: "作る", okDanger: false, returnTo: btn },
@@ -8019,14 +8096,14 @@ ${calendarAddUrl.toString()}
8019
8096
 
8020
8097
  /** 断られた人に出す1行。**ボタンは出さない**(何度押しても同じ答えなので) */
8021
8098
  function calendarOwnerOnlyNote() {
8022
- return el("p", { class: "mt-cal-note", text: "案件のオーナーが作れます。" });
8099
+ return el("p", { class: "mt-cal-note", text: "プロジェクトのオーナーが作れます。" });
8023
8100
  }
8024
8101
 
8025
8102
  /** ポータル経由で開いていない人に出す1行。**直し方がオーナーに頼むのとは別** */
8026
8103
  function calendarPortalNote() {
8027
8104
  return el("p", {
8028
8105
  class: "mt-cal-note",
8029
- text: "案件カレンダーを作るには、ポータルからこの案件を開き直してください。",
8106
+ text: "プロジェクトカレンダーを作るには、ポータルから開き直してください。",
8030
8107
  });
8031
8108
  }
8032
8109
 
@@ -9639,7 +9716,7 @@ ${calendarAddUrl.toString()}
9639
9716
  // 変えるときは依頼キット(docs/google-chat-onboarding.md の B)とポリシー側も同時に
9640
9717
  var chatScope = adapter === "slack"
9641
9718
  ? ""
9642
- : " Cortex のアプリは、会話の読み取りと、通知先に設定したスペースへの確認・通知の投稿だけに使われます。読み取った会話は、本文そのものを保存するのではなく、決定事項・用語などの抽出結果として案件の記録に反映されます。";
9719
+ : " Cortex のアプリは、会話の読み取りと、通知先に設定したスペースへの確認・通知の投稿だけに使われます。読み取った会話は、本文そのものを保存するのではなく、決定事項・用語などの抽出結果としてプロジェクトの記録に反映されます。";
9643
9720
  m.modal.appendChild(el("p", { class: "modal-desc", text: "ここに並んでいる" + chatNoun + "は、例外なく全て毎晩の Gold 昇格が読み取り、Decision・用語集の材料になります。読み取りを止めるには「登録を解除」してください。" + chatScope }));
9644
9721
  } else if (cap === "開発") {
9645
9722
  m.modal.appendChild(el("p", { class: "modal-desc", text: "ここに並んでいるリポジトリの Issues を毎晩の Gold 昇格が読み取り、Decision・用語集の材料になります。読み取りをやめるには「登録から削除」してください。" }));
@@ -9903,7 +9980,7 @@ ${calendarAddUrl.toString()}
9903
9980
  var inactive = s.enabled === false && !capPending(s.kind);
9904
9981
  if (inactive) {
9905
9982
  // **灰色(.ds-inactive)だけにしない。** 注記が無いと、なぜ灰色なのかが読めない
9906
- col.appendChild(el("span", { class: "ds-inactive-note", text: "未使用(この案件では設定されていません)" }));
9983
+ col.appendChild(el("span", { class: "ds-inactive-note", text: "未使用(このプロジェクトでは設定されていません)" }));
9907
9984
  }
9908
9985
  // id は接続マップのノードclickの飛び先(アンカー)
9909
9986
  // **id は付けない。** #101 以前はこのカードが接続マップの飛び先だったが、
@@ -11520,7 +11597,7 @@ ${calendarAddUrl.toString()}
11520
11597
  if (route === null) return "設定を読み取れないため、いまは切り替えられません。";
11521
11598
  if (route !== "fixed-ip") {
11522
11599
  return count > 0
11523
- ? "Backlog の IP 制限で同期が失敗する案件は、これを入れてください。"
11600
+ ? "Backlog の IP 制限で同期が失敗する場合は、これを入れてください。"
11524
11601
  : "先に接続先を登録してください。";
11525
11602
  }
11526
11603
  var ipLabel = ip ? "固定の送信元 IP(" + ip + ")" : "固定の送信元 IP";
@@ -11721,9 +11798,9 @@ ${calendarAddUrl.toString()}
11721
11798
  * ----------------------------------------------------------------------- */
11722
11799
 
11723
11800
  /** リンクの title。案件リポジトリに入っていない人には開けないので、押す前に分かるようにする */
11724
- var RUN_LINK_TITLE = "GitHub Actions の実行ログを開きます(案件リポジトリへのアクセスが要ります)";
11801
+ var RUN_LINK_TITLE = "GitHub Actions の実行ログを開きます(プロジェクトリポジトリへのアクセスが要ります)";
11725
11802
  /** 一覧(開始直後・run の番号がまだ無い)の title。飛び先が違うので文言も分ける */
11726
- var RUN_LIST_TITLE = "GitHub Actions の実行の一覧を開きます(案件リポジトリへのアクセスが要ります)";
11803
+ var RUN_LIST_TITLE = "GitHub Actions の実行の一覧を開きます(プロジェクトリポジトリへのアクセスが要ります)";
11727
11804
  var RUN_LIST_TEXT = "実行の一覧 ↗";
11728
11805
 
11729
11806
  /** 「実行ログ ↗」(新しいタブ・title つき)。一覧のときは文言と title を一覧のものにする */
@@ -12573,7 +12650,7 @@ ${calendarAddUrl.toString()}
12573
12650
  // 手順一式を <details> に畳むので、**いちばん要る人に対して閉じている**
12574
12651
  hint: [
12575
12652
  "URL を貼ると同期先が増えます。ビューアからの起票先は変わりません(スペースごとに別のAPIキーが要ります)。",
12576
- "リアルタイム同期を使うなら、追加したプロジェクトにも案件専用URLの登録が要ります。",
12653
+ "リアルタイム同期を使うなら、追加した Backlog プロジェクトにも、このプロジェクト専用URLの登録が要ります(同じURLを使います)。",
12577
12654
  ],
12578
12655
  build: function (url) {
12579
12656
  return { op: "backlog-target-add", url: url, baseGeneration: targetsGenerationOf(targets) };
@@ -12647,7 +12724,7 @@ ${calendarAddUrl.toString()}
12647
12724
  // 読む人の操作対象ではない——画面から直せないものなので、次の一歩は「担当者へ連絡」。
12648
12725
  // 「手で直してください」も出さない(PM に設定ファイルを編集させる案内になっていた)。
12649
12726
  if (s.ingestState === "unset") {
12650
- box.appendChild(el("p", { class: "sec-how", text: "この案件では議事録の自動取り込みがまだ用意されていません。cortex の担当者にご連絡ください。" }));
12727
+ box.appendChild(el("p", { class: "sec-how", text: "このプロジェクトでは議事録の自動取り込みがまだ用意されていません。cortex の担当者にご連絡ください。" }));
12651
12728
  used = true;
12652
12729
  } else if (s.ingestState === "broken") {
12653
12730
  box.appendChild(el("p", { class: "sec-how", text: "議事録の取り込み設定を読み取れないため、この画面からは変更できません。cortex の担当者にご連絡ください。" }));
@@ -12656,7 +12733,7 @@ ${calendarAddUrl.toString()}
12656
12733
  // 自動取り込みを止めている案件(例: Teams を使っていて手動で取り込む運用)。
12657
12734
  // ここでトグルを出すと「押せば始まる」と読めるが、実際は設定ファイルの意思表示なので
12658
12735
  // 画面から気軽に反転させるものではない。状態だけを伝える。
12659
- box.appendChild(el("p", { class: "sec-how", text: "この案件は議事録の自動取り込みを停止しています。" }));
12736
+ box.appendChild(el("p", { class: "sec-how", text: "このプロジェクトは議事録の自動取り込みを停止しています。" }));
12660
12737
  used = true;
12661
12738
  }
12662
12739
  }
@@ -12719,7 +12796,7 @@ ${calendarAddUrl.toString()}
12719
12796
  // 状態の出どころは資料の取り込み設定(materials-config.json)だが、会議と同じく
12720
12797
  // 文面には出さない(2026-08-12 の文言原則)。画面から直せない状態の次の一歩は担当者への連絡。
12721
12798
  if (s.driveState === "unset") {
12722
- box.appendChild(el("p", { class: "sec-how", text: "この案件では共有資料の取り込みがまだ用意されていません。cortex の担当者にご連絡ください。" }));
12799
+ box.appendChild(el("p", { class: "sec-how", text: "このプロジェクトでは共有資料の取り込みがまだ用意されていません。cortex の担当者にご連絡ください。" }));
12723
12800
  used = true;
12724
12801
  } else if (s.driveState === "broken") {
12725
12802
  // 壊れていると書き込みも失敗する。フォームを出して押させない
@@ -13093,11 +13170,11 @@ ${calendarAddUrl.toString()}
13093
13170
  // **貼ったURLを本文に出す。** 「URLを確かめてください」と言うのに出ていなかった
13094
13171
  duplicateConfirmLines: function (value) {
13095
13172
  return [
13096
- "このチャンネルは、別の案件でも追加されています。",
13173
+ "このチャンネルは、別のプロジェクトでも追加されています。",
13097
13174
  "",
13098
- "このまま追加すると、同じ会話が両方の案件の Decision・用語集に出ます。",
13175
+ "このまま追加すると、同じ会話が両方のプロジェクトの Decision・用語集に出ます。",
13099
13176
  "",
13100
- "全体定例のように複数の案件に関係するチャンネルなら、これは正しい状態です。",
13177
+ "全体定例のように複数のプロジェクトに関係するチャンネルなら、これは正しい状態です。",
13101
13178
  "別のチャンネルと取り違えていないか、貼ったURLを確かめてください:",
13102
13179
  String(value || ""),
13103
13180
  "",
@@ -14582,7 +14659,7 @@ ${calendarAddUrl.toString()}
14582
14659
  var sec = el("section", { class: "about-cortex" });
14583
14660
  sec.appendChild(el("h2", { class: "section-title", text: "この仕組みについて" }));
14584
14661
  sec.appendChild(el("p", { class: "about-lead", text:
14585
- "議事録・課題・共有資料・デザイン・ソースコードといった案件の情報は、普段使っているツールに散らばっています。この仕組みはそれらを自動で集め、「あとで判断のよりどころになる確定した情報」だけを抽出した層を毎晩育てます。人もAIも、まずここを読めば案件を誤らずに把握できる、という状態を作るのが目的です。" }));
14662
+ "議事録・課題・共有資料・デザイン・ソースコードといったプロジェクトの情報は、普段使っているツールに散らばっています。この仕組みはそれらを自動で集め、「あとで判断のよりどころになる確定した情報」だけを抽出した層を毎晩育てます。人もAIも、まずここを読めばプロジェクトを誤らずに把握できる、という状態を作るのが目的です。" }));
14586
14663
 
14587
14664
  sec.appendChild(el("h3", { class: "about-sub", text: "情報の見方は2つあります" }));
14588
14665
  var ways = el("ul", { class: "about-list" });
@@ -14601,9 +14678,9 @@ ${calendarAddUrl.toString()}
14601
14678
  var what = el("ul", { class: "about-list" });
14602
14679
  [
14603
14680
  ["決定事項", "何が決まったか(1決定=1ページ)。議事録や課題から毎晩自動で追加されます"],
14604
- ["用語集", "この案件固有の言葉の意味。人とAIの語彙を揃えます"],
14681
+ ["用語集", "このプロジェクト固有の言葉の意味。人とAIの語彙を揃えます"],
14605
14682
  ["メンバー", "誰が関わっているか。人名の表記ゆれを解決する名簿でもあります"],
14606
- ["ルール", "この案件で継続的に守る約束事(例:「本番リリースは金曜禁止」)"],
14683
+ ["ルール", "このプロジェクトで継続的に守る約束事(例:「本番リリースは金曜禁止」)"],
14607
14684
  ].forEach(function (row) {
14608
14685
  what.appendChild(el("li", {}, [
14609
14686
  el("strong", { text: row[0] }),
@@ -14712,7 +14789,7 @@ ${calendarAddUrl.toString()}
14712
14789
  var steps = [
14713
14790
  el("ol", { class: "rt-steps" }, [
14714
14791
  el("li", { text: "Backlog のプロジェクト設定 → インテグレーション → Webhook を開く" }),
14715
- el("li", { text: "「WebHook URL」に下の案件専用URLを貼り付ける" }),
14792
+ el("li", { text: "「WebHook URL」に、下の(このプロジェクト専用の)URL を貼り付ける" }),
14716
14793
  // **チェック名は受信側が処理する10種を全部並べる。** 「等」で省くと、担当者は
14717
14794
  // 課題の3つだけにチェックして「設定した」と思い、Wikiとドキュメントの更新は
14718
14795
  // 夜間まで届かない(画面は稼働中と出るので、欠けていることに気づけない)。
@@ -14731,7 +14808,7 @@ ${calendarAddUrl.toString()}
14731
14808
  el("div", { class: "rt-url-row" }, [field, copyBtn]),
14732
14809
  msg,
14733
14810
  // **この画面は顧客も見る。** URLに秘密が入っていることと、その扱いを書いておく
14734
- el("p", { class: "sec-note", text: "このURLはこの案件専用です(他の案件には届きません)。秘密のトークンを含むので、貼り付け先は Backlog のこのプロジェクトの設定欄だけにしてください。" }),
14811
+ el("p", { class: "sec-note", text: "このURLはこのプロジェクト専用です(他のプロジェクトには届きません)。秘密のトークンを含むので、貼り付け先は、いま設定している Backlog プロジェクトの設定欄だけにしてください。" }),
14735
14812
  // 固定 IP 経由の案件は反映が「約1分」にならない(1分のまとめ+固定 IP の同期の起動で数分)。
14736
14813
  // **同じ行を差し替える**——2 行に分けると「約1分」と「数分」が並んで矛盾する
14737
14814
  el("p", { class: "sec-how", text: viaFixedIp
@@ -14743,7 +14820,7 @@ ${calendarAddUrl.toString()}
14743
14820
  // 稼働中。**先頭は状態、手順は畳む。**「畳んである中に何があるか」を summary に書く
14744
14821
  // ——ただの「詳細」だと、URLを張り替えたい人がここを開く手掛かりを持てない
14745
14822
  var det = el("details", { class: "rt-steps-fold" }, [
14746
- el("summary", { text: "設定手順と案件専用URLを表示(URLの再確認・張り替え用)" }),
14823
+ el("summary", { text: "設定手順とプロジェクト専用URLを表示(URLの再確認・張り替え用)" }),
14747
14824
  ]);
14748
14825
  for (var i = 0; i < steps.length; i++) det.appendChild(steps[i]);
14749
14826
  // 接続先ごとに1行。**1件のときはプロジェクトキーを出さない**——どれのことかは
@@ -14875,7 +14952,7 @@ ${calendarAddUrl.toString()}
14875
14952
  "backlog": {
14876
14953
  icon: "📋",
14877
14954
  title: "Backlog のAPIキー",
14878
- how: "Backlog → 個人設定 → API で発行(この案件のプロジェクトが見えるアカウントで)",
14955
+ how: "Backlog → 個人設定 → API で発行(同期先の Backlog プロジェクトが見えるアカウントで)",
14879
14956
  // ⚠ **desc は足さない。** 「毎晩同期するために使います」の 1 行は「カードが長すぎる」で
14880
14957
  // 外した経緯があり、secret-card-guidance.test.mjs が空の段落ごと見張っている。
14881
14958
  // 用途の説明を鍵カードに足さないのは #610 でも同じ判断(2026-09-15)
@@ -14887,7 +14964,7 @@ ${calendarAddUrl.toString()}
14887
14964
  // 次の人が入れ直せば済む**話で、誰か1人に依存し続けるより回復しやすい。
14888
14965
  selfServe: {
14889
14966
  text:
14890
- "この案件を担当しているあなたのアカウントで発行して、ここに貼ってください。" +
14967
+ "このプロジェクトを担当しているあなたのアカウントで発行して、ここに貼ってください。" +
14891
14968
  "管理者への依頼は要りません。",
14892
14969
  note:
14893
14970
  "担当が変わったら、新しい担当者が同じ手順で入れ直してください" +
@@ -15135,7 +15212,7 @@ ${calendarAddUrl.toString()}
15135
15212
  // #101 でカードだけがモーダルへ移り、説明だけが画面から消えた。
15136
15213
  // 顧客にトークンの貼り付けを求める画面で、貼った値がどう扱われるかの
15137
15214
  // 説明が落ちるのは、いちばん落としてはいけない種類の欠落
15138
- el("p", { class: "sec-note", text: "入れた値はここには表示されません(保存だけできます)。保存先は案件ごとに分かれているので、他の案件には影響しません。" }),
15215
+ el("p", { class: "sec-note", text: "入れた値はここには表示されません(保存だけできます)。保存先はプロジェクトごとに分かれているので、他のプロジェクトには影響しません。" }),
15139
15216
  ]);
15140
15217
  // **作った鍵カードは1枚残らずここに載せる。** 出し入れ(接続先が0件のときに畳む)は
15141
15218
  // DOM 検索ではなくこの控えで回す——カードは能力モーダルの中身で、タイルを押すまで
@@ -15868,14 +15945,9 @@ ${calendarAddUrl.toString()}
15868
15945
 
15869
15946
  // ---------- Decisions一覧(フィルタ付き) ----------
15870
15947
  function renderDecisionList() {
15871
- // 既存案件・移行案件は導入前/移行前の意思決定が揃わないため、その旨を最初に注記する
15872
- var ai = homeAdoption();
15873
- if (ai && ai.note) {
15874
- app.appendChild(el("div", { class: "adoption-note" }, [
15875
- el("span", { text: "ℹ️" }),
15876
- el("span", { text: ai.note }),
15877
- ]));
15878
- }
15948
+ // 導入経緯(existing/migration)の注記はここに出さない。「AIS導入前の意思決定は
15949
+ // 残っていない場合があります」の帯は、オーナー決定(#626)で撤去した——帯には
15950
+ // その区画が何かだけを書く
15879
15951
  var filters = el("div", { class: "filters" });
15880
15952
  var search = el("input", { type: "search", placeholder: "キーワード検索(タイトル・要約・本文)" });
15881
15953
  search.value = listState.q;
@@ -16260,6 +16332,30 @@ ${calendarAddUrl.toString()}
16260
16332
  var meta = app.querySelector(".detail-meta");
16261
16333
  if (meta && meta.parentNode) meta.parentNode.insertBefore(note, meta.nextSibling);
16262
16334
  }
16335
+ /**
16336
+ * **削除を押して成功したときだけ**呼ぶ。この画面の並びが変わったことが確定するので、
16337
+ * 以後の詳細表示で本文を取りに行かせない(cortex-engine #382)。
16338
+ *
16339
+ * 🔴 **markRecordDeleted の中に置いてはいけない(設計レビューの指摘)。** あちらは
16340
+ * トゥームストーンの復元(applyDeleteTombstones → appendRecordFooter)からも呼ばれる。
16341
+ * 前のセッションで消したレコードを**開き直しただけ**の画面まで古い扱いになり、
16342
+ * そのページの本文が軒並み出なくなる。
16343
+ *
16344
+ * 自動で読み込み直さない・モーダルも出さない——押した人の開いている場所を勝手に捨てない。
16345
+ */
16346
+ function markPageStale() {
16347
+ pageStale = true;
16348
+ if (app.querySelector(".stale-note")) return; // 帯は1本だけ(繰り返さない)
16349
+ var stale = staleReloadNote();
16350
+ // ⚠ 未確認の警告帯(.draft-note)の**中**には入れない。同じ黄色の帯が入れ子になって、
16351
+ // どちらが何を言っているのか読めなくなる。すぐ下に、別の帯として並べる
16352
+ var banner = app.querySelector(".draft-note");
16353
+ if (banner && banner.parentNode) { banner.parentNode.insertBefore(stale, banner.nextSibling); return; }
16354
+ var note = app.querySelector(".deleted-note");
16355
+ if (note && note.parentNode) { note.parentNode.insertBefore(stale, note.nextSibling); return; }
16356
+ var meta = app.querySelector(".detail-meta");
16357
+ if (meta && meta.parentNode) meta.parentNode.insertBefore(stale, meta.nextSibling);
16358
+ }
16263
16359
  /**
16264
16360
  * 人の確認印が付いた記録か。**3値の意味は infra の HUMAN_CONFIRMED_STATUSES と同じ**
16265
16361
  * (active = 人が確認済みにした印/superseded = active を経た記録の差し替え後/
@@ -16293,7 +16389,7 @@ ${calendarAddUrl.toString()}
16293
16389
  btn.addEventListener("click", async function () {
16294
16390
  // **確認を await する。** 落とすと Promise が truthy で確認なしに消える
16295
16391
  if (!(await confirmDanger(deleteConfirmLines(rec, path), { okLabel: "削除する", returnTo: btn }))) return;
16296
- await sendDeleteRecord(rec, path, msg, btn, function () { markRecordDeleted(rec); });
16392
+ await sendDeleteRecord(rec, path, msg, btn, function () { markRecordDeleted(rec); markPageStale(); });
16297
16393
  });
16298
16394
  return { btn: btn, msg: msg };
16299
16395
  }
@@ -17652,7 +17748,7 @@ ${calendarAddUrl.toString()}
17652
17748
  var a = document.getElementById("home-link");
17653
17749
  if (!a || !/^\\/p\\/[^/]+\\//.test(location.pathname)) return;
17654
17750
  a.setAttribute("href", "/");
17655
- a.setAttribute("title", "案件一覧へ");
17751
+ a.setAttribute("title", "プロジェクト一覧へ");
17656
17752
  a.classList.add("site-title-portal");
17657
17753
  })();
17658
17754
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.64",
3
+ "version": "0.12.66",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,