@takagaki/cortex-decisions-viewer 0.12.99 → 0.12.102

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
@@ -2365,6 +2365,8 @@ async function build(opts) {
2365
2365
  const story = await (0, story_1.readStory)(repoRoot, renderGroundingSection, (p) => repo.baseUrl ? `${repo.baseUrl}/edit/${repo.branch}/${encodePath(p)}` : null,
2366
2366
  // 動きのない日を埋める終点=ビルドの時刻の日本時間の日付(時計を注入できる)
2367
2367
  (0, story_1.todayJst)(new Date(opts.generatedAt)));
2368
+ // 週・月のあらすじ(#901)。週・月のファイルが無ければ null(キーごと出さない=今までと同じ画面)
2369
+ const storyDigest = await (0, story_1.readStoryDigest)(repoRoot, renderGroundingSection, (p) => repo.baseUrl ? `${repo.baseUrl}/edit/${repo.branch}/${encodePath(p)}` : null, (0, story_1.todayJst)(new Date(opts.generatedAt)));
2368
2370
  // 案件アイコン(#348)。Cortex構成でなければ undefined(旧レイアウトには置き場が無い)
2369
2371
  const projectIcon = await findProjectIcon(knowledgeRoot);
2370
2372
  const site = {
@@ -2425,6 +2427,7 @@ async function build(opts) {
2425
2427
  aiHistory: Object.keys(aiHistory).length ? aiHistory : undefined,
2426
2428
  // 載せる日が無い案件(帳簿なし・壊れ・shadow の日だけ・ファイルなし)はキーごと出さない
2427
2429
  story: story ?? undefined,
2430
+ storyDigest: storyDigest ?? undefined,
2428
2431
  };
2429
2432
  // サムネイル(デザイン画面等)をサイトに同梱し、ノードのパスをサイト内相対に書き換える
2430
2433
  await node_fs_1.promises.mkdir(outAbs, { recursive: true });
@@ -7,7 +7,7 @@ exports.PERMISSIONS_DATA = void 0;
7
7
  * 操作タブの[権限]がこの値を CLIENT_JS に注入して読む(公開ページと同じ中身)。
8
8
  */
9
9
  exports.PERMISSIONS_DATA = {
10
- "revisedOn": "2026年9月26日",
10
+ "revisedOn": "2026年9月29日",
11
11
  "enactedOn": "2026年9月25日",
12
12
  "tools": [
13
13
  {
@@ -62,7 +62,7 @@ exports.PERMISSIONS_DATA = {
62
62
  },
63
63
  "cortexAccount": null,
64
64
  "delegated": {
65
- "text": "利用者ご本人が、AI の道具(Claude Code)から Slack の公式の接続口に、ご自身のアカウントでつなぎます。権限は Slack の同意画面で、ご本人が確かめて許可します。",
65
+ "text": "利用者ご本人が claude.ai のコネクタで Slack の公式コネクタにつなぐとき、ご本人の Slack の権限の範囲で読みます。権限は Slack の同意画面で、ご本人が確かめて許可します(ワークスペースによっては管理者の承認が要ります)。",
66
66
  "grants": null
67
67
  },
68
68
  "note": null
@@ -145,7 +145,7 @@ exports.PERMISSIONS_DATA = {
145
145
  },
146
146
  "cortexAccount": null,
147
147
  "delegated": {
148
- "text": "利用者ご本人が、AI の道具(Claude Code)から Figma の公式の接続口に、ご自身のアカウントでつなぎます。権限は Figma の同意画面で、ご本人が確かめて許可します。",
148
+ "text": "利用者ご本人が claude.ai のコネクタで Figma の公式コネクタにつなぐとき、ご本人の Figma の権限の範囲で読みます。権限は Figma の同意画面で、ご本人が確かめて許可します。",
149
149
  "grants": null
150
150
  },
151
151
  "note": null
@@ -175,7 +175,10 @@ exports.PERMISSIONS_DATA = {
175
175
  }
176
176
  ]
177
177
  },
178
- "delegated": null,
178
+ "delegated": {
179
+ "text": "利用者ご本人が claude.ai のコネクタで Google ドライブの公式コネクタにつなぐとき、ご本人の Google ドライブの権限の範囲で資料を読みます(読むだけ)。同意画面で「Google ドライブのすべてのファイルの表示、ダウンロード」にチェックを入れて、ご本人が許可します。",
180
+ "grants": null
181
+ },
179
182
  "note": "このアカウントは Google Meet の議事録にも使います(その権限は Google Meet の節にあります)。"
180
183
  },
181
184
  {
@@ -198,7 +201,7 @@ exports.PERMISSIONS_DATA = {
198
201
  },
199
202
  "cortexAccount": null,
200
203
  "delegated": {
201
- "text": "利用者ご本人が、AI の道具(Claude Code)から Cortex の Backlog の接続口に、ご自身の Backlog アカウントでつなぎます(OAuth)。ご本人の Backlog での権限の範囲で、次の操作ができます。",
204
+ "text": "利用者ご本人が claude.ai のコネクタで Cortex の Backlog の接続口(カスタムコネクタ)につなぐとき、ご自身の Backlog アカウントでログインし(OAuth)、ご本人の Backlog での権限の範囲で次の操作ができます。",
202
205
  "grants": [
203
206
  {
204
207
  "spell": "space",
@@ -262,10 +265,7 @@ exports.PERMISSIONS_DATA = {
262
265
  "notRequested": []
263
266
  },
264
267
  "cortexAccount": null,
265
- "delegated": {
266
- "text": "利用者ご本人が、AI の道具(Claude Code)から Google Chat の公式の接続口に、ご自身のアカウントでつなぎます。権限は Google の同意画面で、ご本人が確かめて許可します。",
267
- "grants": null
268
- },
268
+ "delegated": null,
269
269
  "note": null
270
270
  },
271
271
  {
package/dist/render.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.STATE_CONFIRMING = exports.PROBE_WAIT_TIMEOUT_TEXT = exports.UNSETTLED_LIMITS = exports.INGEST_LIMITS = exports.ALREADY_REGISTERED = exports.LIVE_CONFIRM_LIMITS = exports.CLAUDE_CODE_LINK = exports.PERMISSION_WIRING = void 0;
3
+ exports.CONNECTOR_ROW_ORDER = exports.BACKLOG_SPACE_MAX_AGE_MS = exports.CONNECTOR_KINDS = exports.BACKLOG_MCP_HOST = exports.CLAUDE_CONNECTORS_URL = exports.STATE_CONFIRMING = exports.PROBE_WAIT_TIMEOUT_TEXT = exports.UNSETTLED_LIMITS = exports.INGEST_LIMITS = exports.ALREADY_REGISTERED = exports.LIVE_CONFIRM_LIMITS = exports.CLAUDE_CODE_LINK = exports.PERMISSION_WIRING = void 0;
4
4
  exports.renderSite = renderSite;
5
5
  exports.renderSiteWithAssets = renderSiteWithAssets;
6
6
  exports.viewerAssetSources = viewerAssetSources;
@@ -19,6 +19,7 @@ exports.sanitizeIngestCounts = sanitizeIngestCounts;
19
19
  exports.sanitizeIngestFailures = sanitizeIngestFailures;
20
20
  exports.sanitizeStateNow = sanitizeStateNow;
21
21
  exports.devExcludedSkip = devExcludedSkip;
22
+ exports.exemptFromRegistrySieve = exemptFromRegistrySieve;
22
23
  exports.devExcludedRefs = devExcludedRefs;
23
24
  exports.devExcludedLabel = devExcludedLabel;
24
25
  exports.devExcludedBadgeVisible = devExcludedBadgeVisible;
@@ -73,6 +74,7 @@ exports.liveRowVerdict = liveRowVerdict;
73
74
  exports.liveRegistryOf = liveRegistryOf;
74
75
  exports.liveToolKey = liveToolKey;
75
76
  exports.liveToolItems = liveToolItems;
77
+ exports.connectorReadRows = connectorReadRows;
76
78
  exports.capabilityPipeRe = capabilityPipeRe;
77
79
  exports.capabilityWarnSource = capabilityWarnSource;
78
80
  exports.renderConnectionMap = renderConnectionMap;
@@ -1088,8 +1090,13 @@ const STATE_STATUSES = {
1088
1090
  // 案件カレンダーは作れたか(tools #427)。行の描画は #428。ここに無いと、中央が報告した
1089
1091
  // 「作成済み」の行が overlay に届く前に落ちて、画面は永久に「作成中」のまま
1090
1092
  calendar: ["requested", "ok", "failed"],
1093
+ // 記憶に反映したか(cortex-engine #968 条件 4)。書き手は夜の Gold(`by: "gold"`)で、ref は決まった語
1094
+ // `memory`。**この画面の便(T1)はまだ読まない**(語彙を通すだけ。行の描画は T2)。ここに無いと、
1095
+ // 栓を on にした案件でこの行が overlay に届く前に落ちる
1096
+ reflected: ["ok", "failed"],
1091
1097
  };
1092
- const STATE_ROW_FIELDS = ["cap", "ref", "workspace", "question", "status", "at", "by", "needed", "detail", "lastOkAt", "lastFailAt", "runUrl"];
1098
+ // `lastNewAt`(新しい原文が S3 に入った最後の時刻・#968)は `ingested` の行の任意の欄。無い行は今までどおり
1099
+ const STATE_ROW_FIELDS = ["cap", "ref", "workspace", "question", "status", "at", "by", "needed", "detail", "lastOkAt", "lastFailAt", "lastNewAt", "runUrl"];
1093
1100
  /**
1094
1101
  * 行の `runUrl`(その行を書いた GitHub Actions の run・#560)に許す形。**原本は
1095
1102
  * `infra/lambda/shared/connection-state-keys.mjs` の `RUN_URL_RE`**で、これはその写し
@@ -1283,6 +1290,19 @@ function devExcludedSkip(row) {
1283
1290
  var d = typeof row.detail === "string" ? row.detail : "";
1284
1291
  return d.indexOf("材料にしません") >= 0;
1285
1292
  }
1293
+ /**
1294
+ * 接続先の登録簿の篩(`registeredRefKeys`)に掛けてはいけない行か(#968)。
1295
+ *
1296
+ * `reflected` の行の ref は接続先の ID ではなく決まった語 `memory`(サーバ側 `STATE_MEMORY_REF`)。
1297
+ * 登録簿に載らないので、篩に掛けると**毎回落ちる**——「記憶に反映」の行が出ない。カレンダーの行が
1298
+ * 篩の外に置かれている(`refKeyed` を持たない会議)のと同じ扱いを、この問いに与える。
1299
+ *
1300
+ * **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す。綴り `"memory"` は
1301
+ * サーバの定数の写しで、突き合わせは `state-vocab-parity.test.mjs`)。
1302
+ */
1303
+ function exemptFromRegistrySieve(r) {
1304
+ return !!r && r.question === "reflected" && r.ref === "memory";
1305
+ }
1286
1306
  /**
1287
1307
  * 「いま材料にしていない」開発の接続先(#640)。**鍵は ref の生値**(`overlayStateOnFleet` の
1288
1308
  * `dev` の鍵と同じ畳み方——揃えないと、行に札が出ないのに Home だけ出る食い違いになる)。
@@ -1331,7 +1351,9 @@ function devExcludedRefs(rows) {
1331
1351
  r = list[i];
1332
1352
  if (!r)
1333
1353
  continue;
1334
- if (r.by === "gold" && String(r.at) > lastGoldAt)
1354
+ // 🔴 **`reflected` の行は数えない(#968)。** 書き手は `gold` だが、job 末尾で書く別の事実で、数えると
1355
+ // 「Gold が最後に回った時刻」が毎晩それに進み、中立の札が消える
1356
+ if (r.by === "gold" && r.question !== "reflected" && String(r.at) > lastGoldAt)
1335
1357
  lastGoldAt = String(r.at);
1336
1358
  if (r.cap !== "dev")
1337
1359
  continue;
@@ -3179,7 +3201,7 @@ function deriveCapTile(cap, ctx) {
3179
3201
  // 残る(TTL が無い・既知の問題)ので、登録が1件以上ある能力では規則1が守ってくれない
3180
3202
  // ——外したフォルダの `skipped` が残っているだけで「⚠ 取り込みを飛ばしました」が
3181
3203
  // 出続ける(engine #314 の症状の候補3)。**篩い方の正本は stateRefKey 1箇所**
3182
- if (refs && meta.refKeyed && !refs[stateRefKey(meta.id, r.ref, r.workspace)])
3204
+ if (refs && meta.refKeyed && !exemptFromRegistrySieve(r) && !refs[stateRefKey(meta.id, r.ref, r.workspace)])
3183
3205
  continue;
3184
3206
  // 🔴 **中立の事実を、悪い語の表に掛ける前に抜く(#640)。** `BAD_STATE_STATUSES.ingested`
3185
3207
  // は `skipped` を悪いと数える(鍵が無くて始めていない、の意味)ので、ここを外すと
@@ -3793,6 +3815,118 @@ function liveToolItems(tools, registered, rows) {
3793
3815
  }
3794
3816
  return out;
3795
3817
  }
3818
+ /* ---------------------------------------------------------------------------
3819
+ * 「Claude から原文を読む」(操作タブの欄・cortex-engine #968)
3820
+ * ------------------------------------------------------------------------- */
3821
+ /**
3822
+ * claude.ai の設定のコネクタの画面。**URL はここ 1 か所**(欄のリンクはここから引く)。
3823
+ * 古びたらここだけ直す(番人は定数と画面のリンクが同じかを見る・設計 v2 ①14)。
3824
+ */
3825
+ const CLAUDE_CONNECTORS_URL = "https://claude.ai/customize/connectors";
3826
+ exports.CLAUDE_CONNECTORS_URL = CLAUDE_CONNECTORS_URL;
3827
+ /**
3828
+ * Cortex がホストする Backlog の口の公開ホスト名。**写し**——正本は
3829
+ * `infra/lib/backlog-mcp-names.ts` の `BACKLOG_MCP_DOMAIN_NAME`(と intake の `BACKLOG_MCP_HOST`)。
3830
+ * 一致は `infra/test/backlog-mcp-domain-parity.test.mjs` が見る(食い違うと、写させた URL の先に何も無い)。
3831
+ */
3832
+ const BACKLOG_MCP_HOST = "backlog-mcp.cortex.classmethod.info";
3833
+ exports.BACKLOG_MCP_HOST = BACKLOG_MCP_HOST;
3834
+ /**
3835
+ * 接続先ごとの claude.ai のコネクタの形。**写し**——正本は `infra/lambda/shared/adapters.mjs` の
3836
+ * `connector`(`official` / `custom` / `null`)。一致は `viewer/test/connector-section.test.mjs`。
3837
+ * 鍵は登録簿の id(Drive は `drive`)。欄の行の出し分けはこの宣言だけで決める
3838
+ */
3839
+ const CONNECTOR_KINDS = {
3840
+ drive: "official",
3841
+ slack: "official",
3842
+ figma: "official",
3843
+ backlog: "custom",
3844
+ "google-chat": null,
3845
+ };
3846
+ exports.CONNECTOR_KINDS = CONNECTOR_KINDS;
3847
+ /**
3848
+ * 接続状態の行の鮮度の上限(45 日)。**写し**——正本は Backlog の口の
3849
+ * `infra/lambda/backlog-mcp/src/spaces.ts` の `SPACE_MAX_AGE_MS`(口はこれより古い行のスペースを 404 にする)。
3850
+ * 一致は `infra/test/backlog-mcp-domain-parity.test.mjs`。古い行しか無いスペースに URL を出すと、貼っても開けない
3851
+ */
3852
+ const BACKLOG_SPACE_MAX_AGE_MS = 45 * 24 * 60 * 60 * 1000;
3853
+ exports.BACKLOG_SPACE_MAX_AGE_MS = BACKLOG_SPACE_MAX_AGE_MS;
3854
+ /** 欄に出す順(使用ツールの種別 → 登録簿の id)。ここに無い種別(GitHub・会議)は欄に出さない */
3855
+ const CONNECTOR_ROW_ORDER = [
3856
+ ["google-drive", "drive"],
3857
+ ["slack", "slack"],
3858
+ ["figma", "figma"],
3859
+ ["backlog", "backlog"],
3860
+ ["google-chat", "google-chat"],
3861
+ ];
3862
+ exports.CONNECTOR_ROW_ORDER = CONNECTOR_ROW_ORDER;
3863
+ /**
3864
+ * 「Claude から原文を読む」の行(#968)。**登録済みの能力だけ**を、宣言(`CONNECTOR_KINDS`)の形で返す。
3865
+ *
3866
+ * - `official` … `{ id, kind: "official" }`
3867
+ * - `custom`(Backlog)… `{ id, kind: "custom", spaces: [{ domain, url, measured }] }`。
3868
+ * スペースは登録済みの接続先の domain(**重複は 1 つ**・登録の順)。
3869
+ * `measured` は**接続状態の行があるか**(`reachable` か `ingested` のどちらか・`liveRowVerdict` で
3870
+ * 突き合わせる)。🔴 **行が無いスペースに URL を出さない**——口は接続状態の索引にあるスペースしか
3871
+ * 受けず、登録しただけのスペースでは 404 になる(設計 v2 ①7)。ストアがまだ届いていなければ `null`
3872
+ * - `null`(Google Chat)… `{ id, kind: null }`(コネクタ未対応の札)
3873
+ *
3874
+ * 行の `at` が `BACKLOG_SPACE_MAX_AGE_MS`(45 日)より古い・読めない行は数えない(口の索引と同じ鮮度)。
3875
+ *
3876
+ * @param items 使用ツールの行(`liveToolItems` の結果。まとめ表+登録簿)
3877
+ * @param rows 接続状態ストアの行(`LIVE_STATE`)
3878
+ * @param stateSeen ストアの答えが届いたか、取りに行って失敗したか(どちらでもなければ `measured: null`)
3879
+ * @param nowMs いまの時刻(注入する)
3880
+ *
3881
+ * **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す。
3882
+ * `CONNECTOR_KINDS` / `CONNECTOR_ROW_ORDER` / `BACKLOG_MCP_HOST` / `BACKLOG_SPACE_MAX_AGE_MS` / `regRowOfToolItem` /
3883
+ * `liveRowVerdict` はこれより前に流し込んである)。
3884
+ */
3885
+ function connectorReadRows(items, rows, stateSeen, nowMs) {
3886
+ var list = Array.isArray(items) ? items : [];
3887
+ var out = [];
3888
+ var fresh = function (v) {
3889
+ if (!v || !v.row)
3890
+ return false;
3891
+ var at = Date.parse(String(v.row.at || ""));
3892
+ return at === at && nowMs - at <= BACKLOG_SPACE_MAX_AGE_MS;
3893
+ };
3894
+ for (var i = 0; i < CONNECTOR_ROW_ORDER.length; i++) {
3895
+ var toolKind = CONNECTOR_ROW_ORDER[i][0];
3896
+ var id = CONNECTOR_ROW_ORDER[i][1];
3897
+ if (!Object.prototype.hasOwnProperty.call(CONNECTOR_KINDS, id))
3898
+ continue;
3899
+ var kind = CONNECTOR_KINDS[id];
3900
+ var mine = list.filter(function (it) { return it && String(it.kind) === toolKind; });
3901
+ if (!mine.length)
3902
+ continue;
3903
+ if (kind !== "custom") {
3904
+ out.push({ id: id, kind: kind });
3905
+ continue;
3906
+ }
3907
+ var spaces = [];
3908
+ var byDomain = {};
3909
+ for (var j = 0; j < mine.length; j++) {
3910
+ var reg = regRowOfToolItem(mine[j]);
3911
+ var domain = String(reg && reg.domain ? reg.domain : "").trim().toLowerCase();
3912
+ // URL のパスに入る値なので、スペースの形の字だけを通す(組めない行は出さない)
3913
+ if (!/^[a-z0-9-]+(\.[a-z0-9-]+)+$/.test(domain))
3914
+ continue;
3915
+ var sp = byDomain["d:" + domain];
3916
+ if (!sp) {
3917
+ sp = { domain: domain, url: "https://" + BACKLOG_MCP_HOST + "/s/" + domain + "/mcp", measured: stateSeen ? false : null };
3918
+ byDomain["d:" + domain] = sp;
3919
+ spaces.push(sp);
3920
+ }
3921
+ if (stateSeen && !sp.measured) {
3922
+ sp.measured = fresh(liveRowVerdict("issues", reg, rows, "reachable")) || fresh(liveRowVerdict("issues", reg, rows, "ingested"));
3923
+ }
3924
+ }
3925
+ if (spaces.length)
3926
+ out.push({ id: id, kind: kind, spaces: spaces });
3927
+ }
3928
+ return out;
3929
+ }
3796
3930
  /**
3797
3931
  * その能力を同期するワークフローの id を当てる正規表現(持たない能力は `null`)。
3798
3932
  *
@@ -4266,7 +4400,7 @@ function buildTutorialData(site, repoSlug) {
4266
4400
  }
4267
4401
  // デザインの指摘を読む体験(デザインを同期している案件のみ出す)。
4268
4402
  // #727 K10 で取り込むのは Figma のコメント(デザイン/指摘/)だけになり、画面の本体は
4269
- // AI が Figma の MCP で開く。移行期は inventory が残る案件もあるので、文言はどちらでも嘘にしない
4403
+ // AI が Figma のコネクタで開く(#968)。移行期は inventory が残る案件もあるので、文言はどちらでも嘘にしない
4270
4404
  const usesDesign = capOn("デザイン") ?? internals.some((s) => s.kind === "デザイン" && s.enabled !== false);
4271
4405
  if (usesDesign) {
4272
4406
  specs.push({
@@ -4327,11 +4461,11 @@ const TUT_MEMBER_PROMPT = [
4327
4461
  ].join("\n");
4328
4462
  const TUT_BACKLOG_PROMPT = [
4329
4463
  "Backlogの課題に、わたしの名義でコメントを1件投稿してください。Backlogの画面を開かずにAIから記票する練習です。",
4330
- "どの課題に何を書くかは、まずわたしに質問してください(課題は 課題管理/issues/ から選べます。課題キーは各ファイルの「基本情報 > 課題キー」)。",
4464
+ "課題を取れる道具(Backlog のコネクタ)を道具の検索で探してください。同じ働きの道具が2つ以上あれば、課題管理/issues-config.json の targets の domain と同じスペースを読むものを選びます。",
4465
+ "課題はその道具で一覧して、どの課題に何を書くかをわたしに質問してください(課題管理/<プロジェクトキー>/issues/ に写しがあれば手がかりにしてかまいません)。",
4331
4466
  "",
4332
- "投稿経路は次の優先順で判定し、Backlog上の名義とあわせて先に伝えてください。",
4333
- "1. mcp__backlog__ で始まるツール(あなた本人の名義)",
4334
- "2. 1 が使えない場合は、/mcp コマンドから backlog を選んでログインするよう案内して止まる(初回だけブラウザが開き、自分の Backlog アカウントでログインします)",
4467
+ "投稿はその道具で行います(あなた本人の名義)。Backlog上の名義を先に伝えてください。",
4468
+ "道具の検索で探しても見つからない・認証で失敗するときは、つながっていません。操作タブの「Claude から原文を読む」でつなぐよう案内して止まってください。同じ働きの道具が未認証で出ていれば、その認証(/mcp)を先に案内してください。",
4335
4469
  "",
4336
4470
  "環境変数のAPIキーで Backlog のAPIを直接呼ぶことはしないでください(キーの持ち主の名義で記録されてしまいます)。",
4337
4471
  "",
@@ -4340,7 +4474,7 @@ const TUT_BACKLOG_PROMPT = [
4340
4474
  ].join("\n");
4341
4475
  const TUT_DESIGN_PROMPT = [
4342
4476
  "デザイン/指摘/ に取り込んだ Figma のコメント(Figmaファイルごとのディレクトリに月ごとの md)から指摘を1つ選び、それがどの画面への指摘なのかを説明してください。各指摘には安定ID(figma-comment:{fileKey}:{commentId})と、その画面をFigmaで開くリンクがあります。",
4343
- "画面そのものは取り込んでいません。Figma の MCP で開いて読んでください(プロジェクトによっては デザイン/inventory/ が残っているので、あれば手がかりに使ってかまいません)。Figma の MCP が使えないときは、そう伝えてリンクを示すところで止めてください。",
4477
+ "画面そのものは取り込んでいません。画面を読める道具(Figma のコネクタ)を道具の検索で探して開いてください(プロジェクトによっては デザイン/inventory/ が残っているので、あれば手がかりに使ってかまいません)。見つからない・認証で失敗するときは、操作タブの「Claude から原文を読む」でつなぐよう案内し、リンクを示すところで止めてください。同じ働きの道具が未認証で出ていれば、その認証(/mcp)を先に案内してください。",
4344
4478
  "どの指摘・画面が見たいかは、まずわたしに質問してください(希望が無ければ未解決の指摘をあなたが選んでかまいません)。",
4345
4479
  "画面の役割・主な要素と、指摘が何を求めているかを説明し、関連する決定・用語・課題があれば安定IDをたどって補足し、最後にFigmaで実物を開くリンクを提示してください。",
4346
4480
  "読むだけの練習です。デザイン/ の下はFigmaからの同期で上書きされる生成物なので手で編集せず、Figma にコメントも書き込まないでください。",
@@ -4357,7 +4491,7 @@ const TUT_SLACK_PROMPT = [
4357
4491
  "",
4358
4492
  "3. 投稿先と本文をプレビューして承認を得てから投稿し、メッセージのリンクを報告する",
4359
4493
  "",
4360
- "Slack につながっていない場合は、つなぎ方(手元の Claude Code なら /mcp コマンドから slack、クラウドならセッションのコネクタ設定)を案内して止まってください。",
4494
+ "投稿できる道具(Slack のコネクタ)は道具の検索で探してください。見つからない・認証で失敗するときは、操作タブの「Claude から原文を読む」でつなぐよう案内して止まってください。同じ働きの道具が未認証で出ていれば、その認証(/mcp)を先に案内してください。",
4361
4495
  ].join("\n");
4362
4496
  const CSS = `
4363
4497
  :root {
@@ -4747,6 +4881,32 @@ button.site-icon[aria-busy="true"] .site-icon-plus { opacity: 0; }
4747
4881
  .story-card-edit { display: inline-block; margin-top: 8px; font-size: 12.5px; }
4748
4882
  .story-card .body h2 { font-size: 14px; margin: 14px 0 6px; }
4749
4883
  .story-card .body h3 { font-size: 13.5px; margin: 12px 0 6px; }
4884
+ /* 週・月のあらすじ(#901)。週・月のファイルがある案件だけ使う */
4885
+ .story-now { display: flex; flex-direction: column; gap: 14px; margin-bottom: 20px; }
4886
+ .story-section-heading { margin: 4px 0 0; font-size: 15px; }
4887
+ .story-empty { margin: 0; font-size: 13px; color: var(--muted); }
4888
+ .story-phase { margin: 0 0 8px; font-weight: 600; }
4889
+ .story-phase-label { display: inline-block; margin-right: 8px; font-size: 12px; color: var(--muted); font-weight: 400; }
4890
+ .story-digest-human h4 { font-size: 13.5px; margin: 12px 0 6px; }
4891
+ .story-switch { display: flex; gap: 8px; margin: 0 0 12px; }
4892
+ .story-switch-btn {
4893
+ padding: 3px 16px; border-radius: 999px; border: 1px solid var(--border); background: var(--surface);
4894
+ color: inherit; font-size: 13px; cursor: pointer;
4895
+ }
4896
+ .story-switch-btn.active { background: #1d4ed8; border-color: #1d4ed8; color: #fff; font-weight: 600; }
4897
+ .story-nav { display: flex; justify-content: space-between; margin: 0 0 10px; }
4898
+ .story-nav-btn {
4899
+ padding: 3px 12px; border-radius: var(--radius); border: 1px solid var(--border); background: var(--surface);
4900
+ color: inherit; font-size: 12.5px; cursor: pointer;
4901
+ }
4902
+ .story-nav-btn:disabled { opacity: 0.4; cursor: default; }
4903
+ .story-browse { display: flex; flex-direction: column; gap: 12px; }
4904
+ .story-days-fold { margin-top: 10px; }
4905
+ .story-days-fold > summary, .story-week-fold > summary { cursor: pointer; font-size: 13px; font-weight: 600; }
4906
+ .story-days-fold > .story-cards { margin-top: 10px; }
4907
+ .story-week-fold { border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 18px; background: var(--surface); }
4908
+ .story-week-fold > .story-digest, .story-week-fold > .story-empty { margin-top: 10px; }
4909
+ .story-month-pick { display: flex; align-items: center; gap: 8px; font-size: 13px; }
4750
4910
  /* ---- Home(案件の自己記述)---- */
4751
4911
  /* 未記入の警告。**画面でいちばん強い赤にする**——27案件中24案件が未記入のまま
4752
4912
  毎晩のAI生成が走っている状態で、控えめに出しても読まれない */
@@ -4945,6 +5105,31 @@ button.site-icon[aria-busy="true"] .site-icon-plus { opacity: 0; }
4945
5105
  /* 消したレコードでは、直す口(AIで編集・GitHubで編集)を押させない */
4946
5106
  .is-disabled { opacity: 0.45; pointer-events: none; }
4947
5107
  /* ---- 操作タブ末尾の「この仕組みについて」(旧 Home.md から移設)---- */
5108
+ /* ---- 操作タブ「Claude から原文を読む」(#968)。使用ツールの畳みと同じ型(行そのものが button) ---- */
5109
+ .cx-read { margin-top: 28px; padding-top: 12px; border-top: 1px solid var(--border); }
5110
+ .cx-read-summary {
5111
+ display: flex; align-items: center; gap: 6px 12px;
5112
+ width: 100%; box-sizing: border-box; text-align: left;
5113
+ padding: 7px 0; border: none; background: none; color: inherit; font: inherit; cursor: pointer;
5114
+ }
5115
+ .cx-read-summary:hover { background: var(--accent-weak); box-shadow: 0 0 0 6px var(--accent-weak); }
5116
+ .cx-read-summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
5117
+ .cx-read-title { flex: 1 1 auto; font-size: 15px; font-weight: 700; }
5118
+ .cx-read-body { margin: 6px 0 0 12px; padding-left: 10px; border-left: 2px solid var(--border); }
5119
+ .cx-read-lead { margin: 4px 0 8px; font-size: 13.5px; }
5120
+ .cx-read-link { color: var(--accent); }
5121
+ .cx-read-list { list-style: none; margin: 0; padding: 0; }
5122
+ .cx-read-item { padding: 8px 0; border-bottom: 1px solid var(--border); font-size: 13.5px; line-height: 1.7; }
5123
+ .cx-read-item:last-child { border-bottom: none; }
5124
+ .cx-read-name { display: inline-block; min-width: 9.5em; font-weight: 600; margin-right: 8px; }
5125
+ .cx-read-how { word-break: break-word; }
5126
+ .cx-read-tag { color: var(--muted); }
5127
+ .cx-read-copy { margin: 4px 0; }
5128
+ .cx-read-copy .rt-copy-text { word-break: break-all; }
5129
+ .cx-read-sub { margin: 4px 0 0; padding-left: 1.2em; font-size: 12.5px; color: var(--muted); }
5130
+ .cx-read-spaces { list-style: none; margin: 6px 0 0; padding: 0; }
5131
+ .cx-read-space { padding: 4px 0; }
5132
+ .cx-read-space-name { display: inline-block; font-weight: 600; margin-right: 8px; }
4948
5133
  .about-cortex { margin-top: 36px; padding-top: 24px; border-top: 1px solid var(--border); }
4949
5134
  .about-cortex .section-title { font-size: 17px; font-weight: 700; margin: 0 0 10px; }
4950
5135
  .about-lead { font-size: 13.5px; line-height: 1.9; color: var(--muted); margin: 0 0 8px; }
@@ -6083,6 +6268,7 @@ ${sanitizeStateNow.toString()}
6083
6268
  // deriveCapTile / liveToolItems / 行の併記が全部この 1 本を引く。** 落とすと 4 か所が
6084
6269
  // 未定義を参照して、ライブ取得が丸ごと落ちる(画面が静的表示に戻る)
6085
6270
  ${devExcludedSkip.toString()}
6271
+ ${exemptFromRegistrySieve.toString()}
6086
6272
  ${devExcludedRefs.toString()}
6087
6273
  ${devExcludedLabel.toString()}
6088
6274
  ${devExcludedBadgeVisible.toString()}
@@ -6149,6 +6335,13 @@ ${liveRowVerdict.toString()}
6149
6335
  ${liveRegistryOf.toString()}
6150
6336
  ${liveToolKey.toString()}
6151
6337
  ${liveToolItems.toString()}
6338
+ // 「Claude から原文を読む」(#968)。定数は写し(正本は infra・一致は番人)
6339
+ var CLAUDE_CONNECTORS_URL = ${JSON.stringify(CLAUDE_CONNECTORS_URL)};
6340
+ var BACKLOG_MCP_HOST = ${JSON.stringify(BACKLOG_MCP_HOST)};
6341
+ var CONNECTOR_KINDS = ${JSON.stringify(CONNECTOR_KINDS)};
6342
+ var CONNECTOR_ROW_ORDER = ${JSON.stringify(CONNECTOR_ROW_ORDER)};
6343
+ var BACKLOG_SPACE_MAX_AGE_MS = ${JSON.stringify(BACKLOG_SPACE_MAX_AGE_MS)};
6344
+ ${connectorReadRows.toString()}
6152
6345
  // 案件カレンダー(#428)。**判定の実体はビルド時の定義**——画面側に書き写すと、
6153
6346
  // 「作成済みか」の判定が 2 枚になって片方だけ直る(=2 本目のカレンダーを作らせる)。
6154
6347
  // 🔴 **BAD_STATE_STATUSES より後に置く。** calendarPhase が「悪い」語をそこから引く
@@ -6220,7 +6413,7 @@ ${calendarAddUrl.toString()}
6220
6413
  });
6221
6414
  // ストーリー(#901 V2-5)。区画の後・ナレッジグラフと操作の前。
6222
6415
  // **build が載せた日があるときだけ**(帳簿で on の日のファイルが無い案件はキーごと無い=タブも出ない)
6223
- if (SITE.story && SITE.story.length) {
6416
+ if ((SITE.story && SITE.story.length) || SITE.storyDigest) {
6224
6417
  TABS.push({ key: "story", label: "ストーリー" });
6225
6418
  }
6226
6419
  if (SITE.graph && SITE.graph.nodes.length > 0) {
@@ -6297,13 +6490,14 @@ ${calendarAddUrl.toString()}
6297
6490
  var FIGMA_SHOT_STEP2 = "${figma_guide_images_1.FIGMA_GUIDE_STEP2}";
6298
6491
  var FIGMA_SHOT_STEP3 = "${figma_guide_images_1.FIGMA_GUIDE_STEP3}";
6299
6492
 
6300
- function copyRow(text) {
6493
+ function copyRow(text, label) {
6301
6494
  // **input と button を使わない。** 追加フォームの中に置くので、input を足すと
6302
6495
  // 「先頭がURL欄・末尾が名前欄」、button を足すと「最初のボタンが送信」という
6303
6496
  // フォームの並びが崩れる(実際にテストが壊れて気づいた)。
6304
6497
  // 表示は code、コピーは role="button" のリンクで代える。
6305
6498
  var field = el("code", { class: "rt-copy-text", text: text });
6306
- var btn = el("a", { class: "ds-toggle-btn", role: "button", tabindex: "0", text: "コピー" });
6499
+ // ボタンの語は既定「コピー」。何を写すかを言い分けたい所(#968 の欄)だけ渡す
6500
+ var btn = el("a", { class: "ds-toggle-btn", role: "button", tabindex: "0", text: label || "コピー" });
6307
6501
  var msg = el("div", { class: "ds-toggle-msg" });
6308
6502
  btn.addEventListener("click", function () {
6309
6503
  var done = function () { msg.className = "ds-toggle-msg is-ok"; msg.textContent = "✓ コピーしました。"; };
@@ -6334,6 +6528,18 @@ ${calendarAddUrl.toString()}
6334
6528
  // href に javascript: を置けるし、html: は innerHTML への抜け道(実使用は
6335
6529
  // homeGroundingSections の1箇所=ビルド時サニタイズ済みのHTMLのみ)。
6336
6530
  // 外部由来のURLを href に載せるときは、呼び出し側で isHttp() を通すこと。
6531
+ /**
6532
+ * 見出しに出す言葉。題名が記録の ID の形(term:… / rule:… / member:… / 20260928-002)のままなら、
6533
+ * 種類の接頭辞と決定番号を落として人の言葉だけにする(#551)。ID は見出しに出さず、
6534
+ * カードの title 属性と data-record-id にだけ残す。
6535
+ */
6536
+ function humanTitle(t) {
6537
+ var s = String(t == null ? "" : t).trim();
6538
+ var m = /^(?:term|rule|member|decision|person|glossary):\s*(.+)$/i.exec(s);
6539
+ if (m) s = m[1].trim();
6540
+ if (/^\d{8}-\d{3}(?:-|$)/.test(s)) s = s.replace(/^\d{8}-\d{3}-?/, "").trim();
6541
+ return s || "(題名なし)";
6542
+ }
6337
6543
  function el(tag, props, children) {
6338
6544
  var e = document.createElement(tag);
6339
6545
  if (props) Object.keys(props).forEach(function (k) {
@@ -6767,8 +6973,8 @@ ${calendarAddUrl.toString()}
6767
6973
  function followupDecisionEl(id) {
6768
6974
  var hit = null;
6769
6975
  (SITE.decisions || []).forEach(function (dd) { if (!hit && dd.id === id) hit = dd; });
6770
- if (!hit) return el("span", { class: "mono", text: id });
6771
- var a = el("a", { href: "?id=" + encodeURIComponent(id), text: hit.title || id });
6976
+ if (!hit) return el("span", { title: id, text: "(表示できない決定)" });
6977
+ var a = el("a", { href: "?id=" + encodeURIComponent(id), text: humanTitle(hit.title) });
6772
6978
  a.addEventListener("click", function (ev) { ev.preventDefault(); go(id); });
6773
6979
  return a;
6774
6980
  }
@@ -7014,7 +7220,7 @@ ${calendarAddUrl.toString()}
7014
7220
  var rec = byId[id];
7015
7221
  var li = el("li", { class: "supersede-row" });
7016
7222
  if (rec && rec._kind === "decision") {
7017
- var a = el("a", { href: "?id=" + encodeURIComponent(id), text: rec.title });
7223
+ var a = el("a", { href: "?id=" + encodeURIComponent(id), text: humanTitle(rec.title) });
7018
7224
  a.addEventListener("click", function (ev) {
7019
7225
  if (ev.defaultPrevented || ev.button !== 0 || ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey) return;
7020
7226
  ev.preventDefault();
@@ -7022,7 +7228,7 @@ ${calendarAddUrl.toString()}
7022
7228
  });
7023
7229
  li.appendChild(a);
7024
7230
  }
7025
- li.appendChild(el("span", { class: "supersede-id mono", text: (date ? date + " · " : "") + id }));
7231
+ if (date) li.appendChild(el("span", { class: "supersede-id mono", title: id, text: date }));
7026
7232
  if (unconfirmed) {
7027
7233
  li.appendChild(el("span", { class: "supersede-unconfirmed", title: "置き換えた側の決定が、まだ人の確認を経ていません", text: "未確認" }));
7028
7234
  }
@@ -8340,11 +8546,15 @@ ${calendarAddUrl.toString()}
8340
8546
  "story": {
8341
8547
  text: "このプロジェクトの日々の動きを、1日ずつまとめた記録です。直したいことや足したいことは、その日の「人の追記」に書けます。",
8342
8548
  },
8549
+ "story-digest": {
8550
+ text: "今週の日々と、週・月のあらすじです。直したいことや足したいことは、各カードの「人の追記」に書けます。",
8551
+ },
8343
8552
  "dir:レポート": {
8344
8553
  text: "レポートの生成は終了し、履歴として凍結されています(レポートはSlack配信に一本化されました)。以下は過去に生成されたレコードです。",
8345
8554
  },
8346
8555
  };
8347
8556
  var infoKey = listState.tab === "graph" ? (knowledgeView === "map" ? "graph" : "explorer") : listState.tab;
8557
+ if (infoKey === "story" && SITE.storyDigest) infoKey = "story-digest";
8348
8558
  var info = INFO[infoKey];
8349
8559
  if (!info) return;
8350
8560
  var bar = el("div", { class: "info-bar" }, [ el("span", { text: info.text }) ]);
@@ -8372,6 +8582,7 @@ ${calendarAddUrl.toString()}
8372
8582
  // 日ごとのカード(新しい日が上=build が並べた順のまま)。本文は build の関所(marked → sanitize)を
8373
8583
  // 通した HTML で、印のコメントと機械の語の行は build が除いてある。見出しは textContent で入れる
8374
8584
  function renderStory() {
8585
+ if (SITE.storyDigest) { renderStoryDigest(); return; }
8375
8586
  var list = el("div", { class: "story-cards" });
8376
8587
  (SITE.story || []).forEach(function (d) {
8377
8588
  // 動きのない日は薄い 1 行(日付+文言。畳んだ行は「9/10〜9/18」)
@@ -8392,6 +8603,186 @@ ${calendarAddUrl.toString()}
8392
8603
  app.appendChild(list);
8393
8604
  }
8394
8605
 
8606
+ // ---------- ストーリーの週・月(#901)----------
8607
+ // 週・月のあらすじ(SITE.storyDigest)がある案件だけ。無い案件は上の renderStory のまま(今までと同じ画面)。
8608
+ // 日のカードは SITE.story(build が並べた日)の「動きのある日」を週に配り直し、動きの無い日は 1 日 1 行で埋める。
8609
+ // 本文は build の関所を通した HTML、見出し・局面は textContent で入れる
8610
+ var STORY_WD = ["日", "月", "火", "水", "木", "金", "土"];
8611
+ function storyMs(d) { return Date.parse(d + "T00:00:00Z"); }
8612
+ function storyDay(ms) { return new Date(ms).toISOString().slice(0, 10); }
8613
+ function storyMonday(d) { var ms = storyMs(d); return storyDay(ms - ((new Date(ms).getUTCDay() + 6) % 7) * 86400000); }
8614
+ function storyMd(d) { return Number(d.slice(5, 7)) + "/" + Number(d.slice(8, 10)); }
8615
+ function storyWeekLabel(mon) { return storyMd(mon) + "〜" + storyMd(storyDay(storyMs(mon) + 6 * 86400000)) + " の週"; }
8616
+ function storyMonthLabel(m) { return m.slice(0, 4) + "年" + Number(m.slice(5, 7)) + "月"; }
8617
+
8618
+ function renderStoryDigest() {
8619
+ var dg = SITE.storyDigest;
8620
+ var today = dg.today;
8621
+ var pages = {};
8622
+ var oldest = null;
8623
+ (SITE.story || []).forEach(function (d) {
8624
+ if (d.quiet) return;
8625
+ pages[d.date] = d;
8626
+ if (oldest === null || d.date < oldest) oldest = d.date;
8627
+ });
8628
+ var weekOf = {};
8629
+ (dg.weeks || []).forEach(function (w) { weekOf[w.key] = w; });
8630
+ var monthOf = {};
8631
+ (dg.months || []).forEach(function (m) { monthOf[m.key] = m; });
8632
+ var thisMon = storyMonday(today);
8633
+ var lastMon = storyDay(storyMs(thisMon) - 7 * 86400000);
8634
+
8635
+ // 日の行(新しい日が上)。from〜to のうち、動きのある日はカード・無い日は薄い 1 行。
8636
+ // 載せた一番古い日より前は分からないので出さない(keepAll の「今週」だけは月曜から全部出す)
8637
+ function dayRows(from, to, keepAll) {
8638
+ var rows = [];
8639
+ for (var ms = storyMs(to); ms >= storyMs(from); ms -= 86400000) {
8640
+ var d = storyDay(ms);
8641
+ var pg = pages[d];
8642
+ if (pg) {
8643
+ rows.push(el("section", { class: "story-card" }, [
8644
+ el("h3", { class: "story-card-heading", text: pg.heading || pg.date }),
8645
+ pg.bodyHtml ? el("div", { class: "body", html: pg.bodyHtml }) : null,
8646
+ pg.editUrl ? el("a", { class: "story-card-edit", href: pg.editUrl, target: "_blank", rel: "noopener", text: "GitHub で追記する" }) : null,
8647
+ ]));
8648
+ } else if (keepAll || (oldest !== null && d >= oldest)) {
8649
+ rows.push(el("div", { class: "story-quiet" }, [
8650
+ el("span", { class: "story-quiet-date", text: d + "(" + STORY_WD[new Date(ms).getUTCDay()] + ")" }),
8651
+ el("span", { text: " は動きはありませんでした" }),
8652
+ ]));
8653
+ }
8654
+ }
8655
+ return rows;
8656
+ }
8657
+ function pageCount(from, to) {
8658
+ var n = 0;
8659
+ for (var ms = storyMs(from); ms <= storyMs(to); ms += 86400000) if (pages[storyDay(ms)]) n++;
8660
+ return n;
8661
+ }
8662
+ // あらすじの本体(局面・流れ・人の追記・GitHub のリンク)。無ければ 1 行の案内
8663
+ function digestBody(entry, phaseLabel, emptyText) {
8664
+ if (!entry) return el("p", { class: "story-empty", text: emptyText });
8665
+ var human = entry.humanHtml ? el("div", { class: "story-digest-human" }, [
8666
+ el("h4", { text: "人の追記" }),
8667
+ el("div", { class: "body", html: entry.humanHtml }),
8668
+ ]) : null;
8669
+ return el("div", { class: "story-digest" }, [
8670
+ entry.phase ? el("p", { class: "story-phase" }, [
8671
+ el("span", { class: "story-phase-label", text: phaseLabel }),
8672
+ el("span", { text: entry.phase }),
8673
+ ]) : null,
8674
+ entry.flowHtml ? el("div", { class: "body", html: entry.flowHtml }) : null,
8675
+ human,
8676
+ entry.editUrl ? el("a", { class: "story-card-edit", href: entry.editUrl, target: "_blank", rel: "noopener", text: "GitHub で追記する" }) : null,
8677
+ ]);
8678
+ }
8679
+ var WEEK_EMPTY = "あらすじはまだありません(週が閉じて 2 日後の夜に作ります)";
8680
+ var MONTH_EMPTY = "あらすじはまだありません(月が閉じて 3 日後の夜に作ります)";
8681
+ function weekEnd(mon) {
8682
+ var sun = storyDay(storyMs(mon) + 6 * 86400000);
8683
+ return sun < today ? sun : today;
8684
+ }
8685
+ function daysFold(mon) {
8686
+ var rows = dayRows(mon, weekEnd(mon), mon === thisMon);
8687
+ if (!rows.length) return null;
8688
+ return el("details", { class: "story-days-fold" }, [
8689
+ el("summary", { text: "この週の日々(" + pageCount(mon, weekEnd(mon)) + " 日)" }),
8690
+ el("div", { class: "story-cards" }, rows),
8691
+ ]);
8692
+ }
8693
+
8694
+ // 一番上: 今週の日々と先週のあらすじ
8695
+ app.appendChild(el("section", { class: "story-now" }, [
8696
+ el("h3", { class: "story-section-heading", text: "今週" }),
8697
+ el("div", { class: "story-cards" }, dayRows(thisMon, today, true)),
8698
+ el("h3", { class: "story-section-heading", text: "先週のあらすじ" }),
8699
+ weekOf[lastMon]
8700
+ ? el("section", { class: "story-card" }, [
8701
+ el("h3", { class: "story-card-heading", text: storyWeekLabel(lastMon) }),
8702
+ digestBody(weekOf[lastMon], "この週の局面", ""),
8703
+ ])
8704
+ : el("p", { class: "story-empty", text: "先週のあらすじはまだありません" }),
8705
+ ]));
8706
+
8707
+ // 週の並び(新しい週から): あらすじのある週・動きのある日のある週・先週。今週は上に出すので入れない
8708
+ var monSet = {};
8709
+ monSet[lastMon] = true;
8710
+ Object.keys(weekOf).forEach(function (k) { if (k < thisMon) monSet[k] = true; });
8711
+ Object.keys(pages).forEach(function (d) { var k = storyMonday(d); if (k < thisMon) monSet[k] = true; });
8712
+ var mons = Object.keys(monSet).sort().reverse();
8713
+ // 月の並び(新しい月から): あらすじのある月・動きのある日のある月
8714
+ var mSet = {};
8715
+ Object.keys(monthOf).forEach(function (k) { mSet[k] = true; });
8716
+ Object.keys(pages).forEach(function (d) { mSet[d.slice(0, 7)] = true; });
8717
+ var months = Object.keys(mSet).sort().reverse();
8718
+
8719
+ var view = "week";
8720
+ var weekAt = mons.indexOf(lastMon);
8721
+ var monthAt = months.length ? months[0] : null;
8722
+ var btnWeek = el("button", { class: "story-switch-btn", type: "button", text: "週" });
8723
+ var btnMonth = el("button", { class: "story-switch-btn", type: "button", text: "月" });
8724
+ var browse = el("div", { class: "story-browse" });
8725
+ app.appendChild(el("div", { class: "story-switch" }, [btnWeek, btnMonth]));
8726
+ app.appendChild(browse);
8727
+
8728
+ function fillWeek() {
8729
+ var mon = mons[weekAt];
8730
+ var prev = el("button", { class: "story-nav-btn", type: "button", text: "← 前の週" });
8731
+ var next = el("button", { class: "story-nav-btn", type: "button", text: "次の週 →" });
8732
+ if (weekAt >= mons.length - 1) prev.disabled = true;
8733
+ if (weekAt <= 0) next.disabled = true;
8734
+ prev.addEventListener("click", function () { if (weekAt < mons.length - 1) { weekAt++; fill(); } });
8735
+ next.addEventListener("click", function () { if (weekAt > 0) { weekAt--; fill(); } });
8736
+ browse.appendChild(el("div", { class: "story-nav" }, [prev, next]));
8737
+ browse.appendChild(el("section", { class: "story-card story-week" }, [
8738
+ el("h3", { class: "story-card-heading", text: storyWeekLabel(mon) }),
8739
+ digestBody(weekOf[mon], "この週の局面", WEEK_EMPTY),
8740
+ daysFold(mon),
8741
+ ]));
8742
+ }
8743
+ function fillMonth() {
8744
+ if (monthAt === null) return;
8745
+ var sel = el("select", { class: "story-month-select", "aria-label": "月を選ぶ" });
8746
+ months.forEach(function (m) {
8747
+ var o = el("option", { value: m, text: storyMonthLabel(m) });
8748
+ if (m === monthAt) o.selected = true;
8749
+ sel.appendChild(o);
8750
+ });
8751
+ sel.addEventListener("change", function () { monthAt = sel.value; fill(); });
8752
+ browse.appendChild(el("label", { class: "story-month-pick" }, [el("span", { text: "月を選ぶ" }), sel]));
8753
+ browse.appendChild(el("section", { class: "story-card story-month" }, [
8754
+ el("h3", { class: "story-card-heading", text: storyMonthLabel(monthAt) }),
8755
+ digestBody(monthOf[monthAt], "この月の局面", MONTH_EMPTY),
8756
+ ]));
8757
+ // その月にかかる週(新しい週から。今週も入れる)
8758
+ var first = monthAt + "-01";
8759
+ var mon = storyMonday(first);
8760
+ var inMonth = [];
8761
+ for (var ms = storyMs(mon); storyDay(ms).slice(0, 7) <= monthAt && storyDay(ms) <= today; ms += 7 * 86400000) {
8762
+ var k = storyDay(ms);
8763
+ if (weekOf[k] || pageCount(k, weekEnd(k)) > 0) inMonth.push(k);
8764
+ }
8765
+ inMonth.reverse().forEach(function (k) {
8766
+ browse.appendChild(el("details", { class: "story-week-fold" }, [
8767
+ el("summary", { text: storyWeekLabel(k) }),
8768
+ digestBody(weekOf[k], "この週の局面", WEEK_EMPTY),
8769
+ daysFold(k),
8770
+ ]));
8771
+ });
8772
+ }
8773
+ function fill() {
8774
+ browse.innerHTML = "";
8775
+ btnWeek.className = "story-switch-btn" + (view === "week" ? " active" : "");
8776
+ btnMonth.className = "story-switch-btn" + (view === "month" ? " active" : "");
8777
+ btnWeek.setAttribute("aria-pressed", view === "week" ? "true" : "false");
8778
+ btnMonth.setAttribute("aria-pressed", view === "month" ? "true" : "false");
8779
+ if (view === "week") fillWeek(); else fillMonth();
8780
+ }
8781
+ btnWeek.addEventListener("click", function () { view = "week"; fill(); });
8782
+ btnMonth.addEventListener("click", function () { view = "month"; fill(); });
8783
+ fill();
8784
+ }
8785
+
8395
8786
  // 導入経緯(overview.adoption)の表示情報。Home見出し横のチップに出すラベルだけを返す。
8396
8787
  function adoptionInfo(v) {
8397
8788
  // 3種とも同じ形(ラベルのみ)。導入前のDecisionの薄さの注記は出さない
@@ -12001,9 +12392,9 @@ ${calendarAddUrl.toString()}
12001
12392
  }
12002
12393
  var dg = t.delegated;
12003
12394
  if (dg) {
12004
- box.appendChild(el("h4", { class: "perm-h4", text: "ご本人が AI の道具からつなぐとき" }));
12395
+ box.appendChild(el("h4", { class: "perm-h4", text: "ご本人がコネクタでつなぐとき" }));
12005
12396
  if (dg.text) box.appendChild(el("p", { class: "perm-p", text: dg.text }));
12006
- if (dg.grants && dg.grants.length) box.appendChild(permissionTable("ご本人が AI の道具からつなぐとき", dg.grants));
12397
+ if (dg.grants && dg.grants.length) box.appendChild(permissionTable("ご本人がコネクタでつなぐとき", dg.grants));
12007
12398
  }
12008
12399
  sec.appendChild(box);
12009
12400
  });
@@ -12728,7 +13119,8 @@ ${calendarAddUrl.toString()}
12728
13119
  // アダプタのタイルに混ぜると、Teams案件(自動取り込みが使えない=手動投入が
12729
13120
  // 唯一の口)で「未対応」のタイルの裏に隠れてしまう。
12730
13121
  manualBox["会議"] = intakeCardFor("会議");
12731
- manualBox["共有資料"] = intakeCardFor("共有資料");
13122
+ // 共有資料の手動追加は出さない(#968・入口は Google ドライブ 1 つ)。
13123
+ // 代わりの 1 行は renderManualTile が出す。会議の手動追加は今までどおり
12732
13124
 
12733
13125
  // ---- 能力ごとにタイルを並べる ----
12734
13126
  //
@@ -13242,13 +13634,20 @@ ${calendarAddUrl.toString()}
13242
13634
 
13243
13635
  // アダプタ非依存の操作(手動投入)は同じ能力の中で別タイルにする
13244
13636
  function renderManualTile(cap) {
13637
+ if (cap === "共有資料") {
13638
+ // 手動追加はやめた(#968)。症状 → 一手の 1 行だけ。フォルダの登録数は上のタイルが持つ
13639
+ var drive = internalSourceOf("共有資料");
13640
+ var registered = !!(drive && Array.isArray(drive.driveFolderIds) && drive.driveFolderIds.length);
13641
+ sec.appendChild(el("p", { class: "ds-note", id: "ds-materials-drive-hint", text: registered
13642
+ ? "資料は登録した Google ドライブのフォルダに置いてください"
13643
+ : "Google ドライブのフォルダを登録すると資料を取り込めます" }));
13644
+ return;
13645
+ }
13245
13646
  var manual = manualBox[cap];
13246
13647
  if (manual) {
13247
13648
  var mBody = el("div", { class: "ds-tile-body" }, [
13248
- el("div", { class: "ds-tile-name", text: cap === "会議" ? "文字起こしを手動で追加" : "資料を手動で追加" }),
13249
- el("div", { class: "ds-tile-desc", text: cap === "会議"
13250
- ? "自動取り込みできない会議(顧客主催・別ツール)の文字起こしから議事録を作ります"
13251
- : "手元のファイルやテキストを Markdown 化して取り込みます" }),
13649
+ el("div", { class: "ds-tile-name", text: "文字起こしを手動で追加" }),
13650
+ el("div", { class: "ds-tile-desc", text: "自動取り込みできない会議(顧客主催・別ツール)の文字起こしから議事録を作ります" }),
13252
13651
  ]);
13253
13652
  var mTile = el("button", { class: "ds-tile", type: "button", id: "ds-manual-" + cap }, [
13254
13653
  el("span", { class: "ds-tile-logo", text: "📥" }),
@@ -13256,7 +13655,7 @@ ${calendarAddUrl.toString()}
13256
13655
  el("span", { class: "ds-tile-chev", text: "›" }),
13257
13656
  ]);
13258
13657
  mTile.addEventListener("click", function () {
13259
- openCapabilityModal(cap === "会議" ? "文字起こしを手動で追加" : "資料を手動で追加", [manual]);
13658
+ openCapabilityModal("文字起こしを手動で追加", [manual]);
13260
13659
  });
13261
13660
  sec.appendChild(mTile);
13262
13661
  }
@@ -16736,6 +17135,9 @@ ${calendarAddUrl.toString()}
16736
17135
  // **モーダルが開いている間は差し替えない。** 下地を入れ替えると、開いているモーダルの
16737
17136
  // 中身が切り離されたノードになる(閉じるときに refreshDataSources を呼び直す)
16738
17137
  if (isModalOpen()) return;
17138
+ // 「Claude から原文を読む」(#968)。モーダルの外の独立した欄なので、節の入力中の見送りより先に追いつかせる
17139
+ //(モーダルが開いている間は上で見送る——閉じるときに呼び直される)
17140
+ syncReadSection();
16739
17141
  var old = app.querySelector(".datasources");
16740
17142
  // **触っている最中は差し替えない。** この節の中には追加フォームの入力欄があり、
16741
17143
  // 貼りかけのURLや「保存中…」の表示ごと作り直してしまう。保存が成功していても
@@ -17115,6 +17517,9 @@ ${calendarAddUrl.toString()}
17115
17517
  // タイル側は描画時に liveConfirming() を評価する作りなので追加対応は要らない。
17116
17518
  if (liveConfirming()) showConfirmingConnMap();
17117
17519
  renderDataSources();
17520
+ // 「Claude から原文を読む」(#968)。登録済みの接続先が無ければ出さない
17521
+ var readSec = buildReadSection();
17522
+ if (readSec) app.appendChild(readSec);
17118
17523
  loadLiveFleet();
17119
17524
  // **まとめ表がまだ答えていない行があれば取り直す。** 取得前は0件なので、
17120
17525
  // 実際に張られるのは応答が届いてから(loadLiveFleet が呼び直す)
@@ -17124,6 +17529,177 @@ ${calendarAddUrl.toString()}
17124
17529
  app.appendChild(buildAboutSection());
17125
17530
  }
17126
17531
 
17532
+ /**
17533
+ * 「Claude から原文を読む」(cortex-engine #968)。**既定で畳む。**
17534
+ *
17535
+ * 原文(Drive の資料・Slack の会話・Figma の画面・Backlog の課題)は、利用者ご本人が
17536
+ * claude.ai の設定でつないだコネクタで読む。この欄はその一手を、**登録済みの接続先だけ**
17537
+ * 行にして出す(行の形は登録簿の宣言の写し 「CONNECTOR_KINDS」・判定は 「connectorReadRows」 1 本)。
17538
+ *
17539
+ * 文言の約束(帯は症状・行は一手・繰り返さない): 見出しと claude.ai の設定へのリンク 1 つ、
17540
+ * あとは行ごとの一手だけ。Backlog は**接続状態の行があるスペースだけ** URL と写すボタンを出す
17541
+ * (行が無いスペースは口が 404 を返すので「先に『接続を確認』」の一手だけ)。
17542
+ * 道具の「ブロック」は勧めない(分類 A-3=何もしない・使い方は利用者に任せる。オーナー 2026-09-29)。
17543
+ *
17544
+ * 開閉は使用ツールの畳み(「homeToolsFold」)と同じ型: 行そのものを button にし、
17545
+ * aria-expanded と hidden="until-found" を 1 か所(「setReadSectionOpen」)で動かす。保存はしない
17546
+ */
17547
+ function readSectionRowsNow() {
17548
+ // 🔴 **ストアの答えが来ないまま失敗した回も「確認中」に留めない**(取り直しが失敗し続けると
17549
+ // 永久に動かない)。失敗したら「行が無い」側に倒して「先に『接続を確認』」を出す
17550
+ return connectorReadRows(
17551
+ liveToolItems(SITE.tools || null, registryForHome(), LIVE_STATE),
17552
+ LIVE_STATE,
17553
+ LIVE_STATE_SEEN || LIVE_FLEET_FAILED || !SITE.intake,
17554
+ Date.now(),
17555
+ );
17556
+ }
17557
+ function buildReadSection() {
17558
+ var rows = readSectionRowsNow();
17559
+ if (!rows.length) return null;
17560
+ var sec = el("section", { class: "cx-read" });
17561
+ sec.setAttribute("data-sig", JSON.stringify(rows));
17562
+ var arrow = el("span", { class: "home-tools-fold-arrow", "aria-hidden": "true" });
17563
+ var btn = el("button", { class: "cx-read-summary", type: "button" }, [
17564
+ el("span", { class: "cx-read-title", text: "Claude から原文を読む" }),
17565
+ arrow,
17566
+ ]);
17567
+ btn.setAttribute("aria-controls", "cx-read-body");
17568
+ var body = el("div", { class: "cx-read-body", id: "cx-read-body" });
17569
+ body.appendChild(el("p", { class: "cx-read-lead" }, [
17570
+ el("a", { class: "cx-read-link", href: CLAUDE_CONNECTORS_URL, target: "_blank", rel: "noopener", text: "claude.ai の設定 → コネクタ ↗" }),
17571
+ ]));
17572
+ var ul = el("ul", { class: "cx-read-list" });
17573
+ rows.forEach(function (r) {
17574
+ ul.appendChild(r.kind === "custom" ? readBacklogGroup(r.spaces) : readOfficialRow(r));
17575
+ });
17576
+ body.appendChild(ul);
17577
+ sec.appendChild(btn);
17578
+ sec.appendChild(body);
17579
+ setReadSectionOpen(sec, false);
17580
+ btn.addEventListener("click", function () { setReadSectionOpen(sec, !readSectionOpen(sec)); });
17581
+ body.addEventListener("beforematch", function () { setReadSectionOpen(sec, true); });
17582
+ return sec;
17583
+ }
17584
+ /**
17585
+ * 公式のコネクタの行と、コネクタでつなげない(Google Chat)の札。Google Chat は自前の道の作り方を
17586
+ * 案内しない(Cortex の守備範囲は取り込みと記憶まで・オーナー 2026-09-29 22:20)
17587
+ */
17588
+ function readOfficialRow(r) {
17589
+ var name = { drive: "Google ドライブ", slack: "Slack", figma: "Figma", "google-chat": "Google Chat" }[r.id] || r.id;
17590
+ var how;
17591
+ if (r.kind === null) {
17592
+ how = "Google Chat は公式のコネクタがまだ無いので、記憶は Cortex が取り込んだ記録から答えます。";
17593
+ } else if (r.id === "drive") {
17594
+ how = "公式コネクタをつなぐ。同意画面で「Google ドライブのすべてのファイルの表示、ダウンロード」にチェック。";
17595
+ } else if (r.id === "slack") {
17596
+ how = "公式コネクタをつなぐ。出てこない・承認待ちなら、Slack の管理者に依頼します。";
17597
+ } else {
17598
+ how = "公式コネクタをつなぐ。";
17599
+ }
17600
+ return el("li", { class: "cx-read-item", "data-connector": r.id }, [
17601
+ el("span", { class: "cx-read-name", text: name }),
17602
+ el("span", { class: "cx-read-how" + (r.kind === null ? " cx-read-tag" : ""), text: how }),
17603
+ ]);
17604
+ }
17605
+ /** Backlog の名前(カスタムコネクタの名前の例) */
17606
+ function readBacklogLabel(sp) { return "Backlog " + sp.domain; }
17607
+ /** 組織のオーナーに頼む文 */
17608
+ function readBacklogAsk(sp) {
17609
+ return "Claude の組織に Backlog のカスタムコネクタを追加してください。名前: " + readBacklogLabel(sp) + " / URL: " + sp.url;
17610
+ }
17611
+ /** claude.ai 以外でログインしている手元の CLI の 1 行(サーバ名は英数字と - だけにする) */
17612
+ function readBacklogCli(sp) {
17613
+ return "claude mcp add --transport http --scope user backlog-" + sp.domain.split(".").join("-") + " " + sp.url;
17614
+ }
17615
+ function readCopy(kind, text, label) {
17616
+ return el("div", { class: "cx-read-copy", "data-copy": kind }, [copyRow(text, label)]);
17617
+ }
17618
+ /**
17619
+ * Backlog のまとまり(カスタムコネクタ)。**共通の説明は 1 回だけ**、スペースの行は名前と URL だけ。
17620
+ * 道は、既定の「自分でカスタムコネクタを追加」を先に置き(当社の社員は基本個人のプラン・分類 A-4)、
17621
+ * 追加できない組織のプラン(Team・Enterprise)と claude.ai 以外のログインを後ろに置く。
17622
+ * 頼む文とコマンドは、URL を出せるスペースが 1 つなら共通の行に、2 つ以上ならスペースの行ごとに写すボタンを置く
17623
+ */
17624
+ function readBacklogGroup(spaces) {
17625
+ var ready = spaces.filter(function (sp) { return sp.measured === true; });
17626
+ var one = ready.length === 1 ? ready[0] : null;
17627
+ var li = el("li", { class: "cx-read-item", "data-connector": "backlog" }, [
17628
+ el("span", { class: "cx-read-name", text: "Backlog" }),
17629
+ el("span", { class: "cx-read-how", text: "カスタムコネクタとしてつなぐ。" }),
17630
+ ]);
17631
+ if (ready.length) {
17632
+ var paths = el("ul", { class: "cx-read-sub cx-read-paths" });
17633
+ paths.appendChild(el("li", { "data-path": "self", text: "claude.ai の設定で「カスタムコネクタを追加」→ 名前と URL を貼る。" }));
17634
+ paths.appendChild(el("li", { "data-path": "owner" }, [
17635
+ el("span", { text: "追加できない(Team・Enterprise の組織)→ 頼む文を写して組織のオーナーに頼む。追加されたら一覧から「つなぐ」。" }),
17636
+ one ? readCopy("ask", readBacklogAsk(one), "頼む文をコピー") : null,
17637
+ ]));
17638
+ paths.appendChild(el("li", { "data-path": "cli" }, [
17639
+ el("span", { text: "claude.ai 以外でログインしている手元の CLI → コマンドを写して打つ。" }),
17640
+ one ? readCopy("cli", readBacklogCli(one), "コマンドをコピー") : null,
17641
+ ]));
17642
+ li.appendChild(paths);
17643
+ }
17644
+ var list = el("ul", { class: "cx-read-spaces" });
17645
+ spaces.forEach(function (sp) {
17646
+ var row = el("li", { class: "cx-read-space", "data-space": sp.domain }, [
17647
+ el("span", { class: "cx-read-space-name", text: readBacklogLabel(sp) }),
17648
+ ]);
17649
+ if (sp.measured === null) {
17650
+ row.appendChild(el("span", { class: "cx-read-how", text: "登録状況を確認中です" }));
17651
+ } else if (!sp.measured) {
17652
+ row.appendChild(el("span", { class: "cx-read-how", text: "先に課題管理の「接続を確認」を押す(URL はそのあと出ます)。" }));
17653
+ } else {
17654
+ row.appendChild(readCopy("url", sp.url, "URL をコピー"));
17655
+ if (!one) {
17656
+ row.appendChild(readCopy("ask", readBacklogAsk(sp), "頼む文をコピー"));
17657
+ row.appendChild(readCopy("cli", readBacklogCli(sp), "コマンドをコピー"));
17658
+ }
17659
+ }
17660
+ list.appendChild(row);
17661
+ });
17662
+ li.appendChild(list);
17663
+ return li;
17664
+ }
17665
+ function setReadSectionOpen(sec, open) {
17666
+ if (!sec) return;
17667
+ var btn = sec.querySelector(".cx-read-summary");
17668
+ var body = sec.querySelector(".cx-read-body");
17669
+ if (!btn || !body) return;
17670
+ btn.setAttribute("aria-expanded", open ? "true" : "false");
17671
+ if (open) body.removeAttribute("hidden");
17672
+ else body.setAttribute("hidden", "until-found");
17673
+ var arrow = btn.querySelector(".home-tools-fold-arrow");
17674
+ if (arrow) arrow.textContent = open ? "▼" : "▶";
17675
+ }
17676
+ function readSectionOpen(sec) {
17677
+ var btn = sec && sec.querySelector(".cx-read-summary");
17678
+ return !!btn && btn.getAttribute("aria-expanded") === "true";
17679
+ }
17680
+ /**
17681
+ * 実測・登録簿が届いたときに欄を追いつかせる。**中身が同じなら触らない**(60 秒ごとの取り直しで
17682
+ * 「コピーしました」が消えないように)。人が開いた欄は開いたまま差し替える
17683
+ */
17684
+ function syncReadSection() {
17685
+ if (listState.tab !== "ops") return;
17686
+ // 画面が畳まれた後に応答が届くことがある(document が無い)。そのときは何もしない
17687
+ if (typeof document === "undefined" || !document) return;
17688
+ var old = app.querySelector(".cx-read");
17689
+ var next = buildReadSection();
17690
+ if (old && next && old.getAttribute("data-sig") === next.getAttribute("data-sig")) return;
17691
+ if (old && next) {
17692
+ if (readSectionOpen(old)) setReadSectionOpen(next, true);
17693
+ old.parentNode.replaceChild(next, old);
17694
+ } else if (old) {
17695
+ old.parentNode.removeChild(old);
17696
+ } else if (next) {
17697
+ var about = app.querySelector(".about-cortex");
17698
+ if (about) app.insertBefore(next, about);
17699
+ else app.appendChild(next);
17700
+ }
17701
+ }
17702
+
17127
17703
  /**
17128
17704
  * 「この仕組みについて」——旧 Home.md にあった使い方の説明。**ビューアが持つ。**
17129
17705
  *
@@ -17191,19 +17767,7 @@ ${calendarAddUrl.toString()}
17191
17767
  if (cap === "会議") {
17192
17768
  return opsCard("🗒️", "会議の文字起こしを追加", "顧客主催の会議など、自動取り込みできない会議の文字起こしファイル(.txt / .docx 等)から議事録を生成します。ファイルを選ぶかテキストを貼って送信すると、数分で反映されます。", "transcript", "文字起こしを追加");
17193
17769
  }
17194
- if (cap === "共有資料") {
17195
- // hint(#663): 文字の層が無い PDF(画面の印刷・スキャン・画像を貼ったもの)は
17196
- // どの変換ツールでも文字が取れない。**「PDF が読めない」とは言わない**——読めないのは
17197
- // 画面を保存したものとスキャン・写真から作ったものだけで、文字の層がある PDF は今までどおり読める。
17198
- // **「画面を印刷した PDF」と書かない**(実装レビュー🟡4)——実際の操作は macOS の
17199
- // 印刷ダイアログの「PDF として保存」で、当人は「印刷していない」と読む。「スキャンした画像」も
17200
- // 同じで、スキャナの出力は普通 PDF なので「自分のは画像じゃない」と読まれる。
17201
- // **Google のスライド・ドキュメントは書かない**——「元のファイル」がローカルに存在せず、
17202
- // 書き出す時点で必ず形式を選ばされるので、指示どおりに行動できない(設計レビュー🔴3)。
17203
- // その一手は docs/ops-ui-user-stories.md の Drive の節が持つ。
17204
- return opsCard("📎", "共有資料を追加", "提案書・仕様書・議事メモなどの資料を Markdown 化して 共有資料 に取り込みます。ファイルを選ぶかテキストを貼って送信すると、数分で反映されます。", "material", "資料を追加",
17205
- "PowerPoint・Word・Excel は、PDF にせず元のファイルのまま送ってください(そのほうが正確に読めます)。画面を「PDF として保存」したものや、スキャン・写真から作った PDF は、文字を取り出せないので中身が空になります。");
17206
- }
17770
+ // 共有資料の手動追加は #968 でやめた(入口は Google ドライブ 1 つ)。ここでは作らない
17207
17771
  return null;
17208
17772
  }
17209
17773
 
@@ -18569,17 +19133,16 @@ ${calendarAddUrl.toString()}
18569
19133
  latestBadge(d),
18570
19134
  el("span", { class: "mono", text: d.date || "----" }),
18571
19135
  catBadge(d.category),
18572
- el("span", { class: "mono", text: d.id }),
18573
19136
  ]);
18574
19137
  var eb = fmErrorBadge(d);
18575
19138
  if (eb) meta.appendChild(eb);
18576
19139
  var traceD = el("a", { class: "mc-trace", href: "?tab=graph", text: "🕸 つながり" });
18577
19140
  traceD.addEventListener("click", function (ev) { ev.preventDefault(); ev.stopPropagation(); openExplorerFor(d); });
18578
19141
  meta.appendChild(traceD);
18579
- var card = el("div", { class: "card" }, [
19142
+ var card = el("div", { class: "card", title: d.id, "data-record-id": d.id }, [
18580
19143
  meta,
18581
19144
  // 消したレコードは一覧でも取り消し線(次のビルドまで一覧には残る)
18582
- el("div", { class: "card-title" + (d.deleted ? " is-deleted" : ""), text: d.title }),
19145
+ el("div", { class: "card-title" + (d.deleted ? " is-deleted" : ""), text: humanTitle(d.title) }),
18583
19146
  d.summary ? el("p", { class: "card-summary", text: d.summary }) : null,
18584
19147
  ]);
18585
19148
  card.addEventListener("click", function () { go(d.id); });
@@ -18640,16 +19203,15 @@ ${calendarAddUrl.toString()}
18640
19203
  proposalBadge(r),
18641
19204
  latestBadge(r),
18642
19205
  r.date ? el("span", { class: "mono", text: r.date }) : null,
18643
- el("span", { class: "mono", text: r.id }),
18644
19206
  ]);
18645
19207
  var ebr = fmErrorBadge(r);
18646
19208
  if (ebr) meta.appendChild(ebr);
18647
19209
  var traceR = el("a", { class: "mc-trace", href: "?tab=graph", text: "🕸 つながり" });
18648
19210
  traceR.addEventListener("click", function (ev) { ev.preventDefault(); ev.stopPropagation(); openExplorerFor(r); });
18649
19211
  meta.appendChild(traceR);
18650
- var card = el("div", { class: "card" }, [
19212
+ var card = el("div", { class: "card", title: r.id, "data-record-id": r.id }, [
18651
19213
  meta,
18652
- el("div", { class: "card-title" + (r.deleted ? " is-deleted" : ""), text: r.title }),
19214
+ el("div", { class: "card-title" + (r.deleted ? " is-deleted" : ""), text: humanTitle(r.title) }),
18653
19215
  r.summary ? el("p", { class: "card-summary", text: r.summary }) : null,
18654
19216
  ]);
18655
19217
  card.addEventListener("click", function () { go(r.id); });
@@ -19450,7 +20012,7 @@ ${calendarAddUrl.toString()}
19450
20012
  app.appendChild(back);
19451
20013
 
19452
20014
  var head = el("div", { class: "detail-head" });
19453
- head.appendChild(el("div", {}, [ el("h1", { class: "detail-title", text: r.title }) ]));
20015
+ head.appendChild(el("div", {}, [ el("h1", { class: "detail-title", text: humanTitle(r.title) }) ]));
19454
20016
  var traceBtn2 = el("a", { class: "trace-btn", href: "?tab=graph", text: "🕸 つながり" });
19455
20017
  traceBtn2.addEventListener("click", function (ev) { ev.preventDefault(); openExplorerFor(r); });
19456
20018
  var editRowR = el("div", { class: "detail-actions-row" });
@@ -19468,14 +20030,14 @@ ${calendarAddUrl.toString()}
19468
20030
  ]));
19469
20031
  app.appendChild(head);
19470
20032
 
19471
- app.appendChild(el("div", { class: "detail-meta" }, [
20033
+ var metaR = el("div", { class: "detail-meta", title: r.id, "data-record-id": r.id }, [
19472
20034
  draftBadge(r),
19473
20035
  proposalBadge(r),
19474
20036
  latestBadge(r),
19475
20037
  r.date ? el("span", { class: "mono", text: r.date }) : null,
19476
- el("span", { class: "mono", text: r.id }),
19477
20038
  r.summary ? el("span", { text: r.summary }) : null,
19478
- ]));
20039
+ ]);
20040
+ app.appendChild(metaR);
19479
20041
 
19480
20042
  var en2 = fmErrorNote(r);
19481
20043
  if (en2) app.appendChild(en2);
@@ -20126,7 +20688,7 @@ ${calendarAddUrl.toString()}
20126
20688
  app.appendChild(back);
20127
20689
 
20128
20690
  var head = el("div", { class: "detail-head" });
20129
- var headLeft = el("div", {}, [ el("h1", { class: "detail-title", text: d.title }) ]);
20691
+ var headLeft = el("div", {}, [ el("h1", { class: "detail-title", text: humanTitle(d.title) }) ]);
20130
20692
  head.appendChild(headLeft);
20131
20693
  var traceBtn = el("a", { class: "trace-btn", href: "?tab=graph", text: "🕸 つながり" });
20132
20694
  traceBtn.addEventListener("click", function (ev) { ev.preventDefault(); openExplorerFor(d); });
@@ -20152,8 +20714,9 @@ ${calendarAddUrl.toString()}
20152
20714
  latestBadge(d),
20153
20715
  el("span", { class: "mono", text: d.date || "----" }),
20154
20716
  catBadge(d.category),
20155
- el("span", { class: "mono", text: d.id }),
20156
20717
  ]);
20718
+ meta.title = d.id;
20719
+ meta.setAttribute("data-record-id", d.id);
20157
20720
  app.appendChild(meta);
20158
20721
 
20159
20722
  var en = fmErrorNote(d);
package/dist/story.js CHANGED
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.STORY_MAX_FILE_BYTES = exports.STORY_COLLAPSE_QUIET_DAYS = exports.STORY_EMPTY_LINE = exports.STORY_MAX_DAYS = exports.STORY_DAYS_DIR = exports.STORY_LEDGER_PATH = void 0;
6
+ exports.STORY_DIGEST_EMPTY_LINE = exports.STORY_MAX_MONTHS = exports.STORY_MAX_WEEKS = exports.STORY_MONTHS_DIR = exports.STORY_WEEKS_DIR = exports.STORY_MAX_FILE_BYTES = exports.STORY_COLLAPSE_QUIET_DAYS = exports.STORY_EMPTY_LINE = exports.STORY_MAX_DAYS = exports.STORY_DAYS_DIR = exports.STORY_LEDGER_PATH = void 0;
7
7
  exports.parseStoryLedger = parseStoryLedger;
8
8
  exports.onDaysOf = onDaysOf;
9
9
  exports.storyDayMarkdown = storyDayMarkdown;
@@ -12,6 +12,10 @@ exports.todayJst = todayJst;
12
12
  exports.quietStoryRow = quietStoryRow;
13
13
  exports.withQuietDays = withQuietDays;
14
14
  exports.readStory = readStory;
15
+ exports.storyWeekHeading = storyWeekHeading;
16
+ exports.storyMonthHeading = storyMonthHeading;
17
+ exports.storyDigestParts = storyDigestParts;
18
+ exports.readStoryDigest = readStoryDigest;
15
19
  /**
16
20
  * ストーリーのタブ(#901 記憶 v2・V2-5)。engine V2-3 が書く日々の動き
17
21
  * (`Cortex/Story/Days/YYYY-MM/YYYY-MM-DD.md`)と帳簿(`.cortex/story-days.json`)を読み、画面に載せる日を組む。
@@ -44,7 +48,7 @@ const DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
44
48
  const MONTH_RE = /^\d{4}-\d{2}$/;
45
49
  const HUMAN_HEADING = "## 人の追記";
46
50
  /** engine の印の行(始まり・終わり。属性は問わない)。画面には出さない */
47
- const STORY_MARKER_RE = /^<!--\s*\/?cortex-engine:story-(?:day|late)(?:\s[^\n]*?)?\s*-->$/;
51
+ const STORY_MARKER_RE = /^<!--\s*\/?cortex-engine:story-(?:day|late|week|month)(?:\s[^\n]*?)?\s*-->$/;
48
52
  const LATE_BEGIN_RE = /^<!--\s*cortex-engine:story-late(?:\s[^\n]*?)?\s*-->$/;
49
53
  const LATE_END_RE = /^<!--\s*\/cortex-engine:story-late\s*-->$/;
50
54
  /**
@@ -235,22 +239,16 @@ function withQuietDays(pages, today) {
235
239
  return out;
236
240
  }
237
241
  /**
238
- * 案件のストーリーを読む。**載せる日が無ければ `null`**(タブも器も出さない)。
239
- *
240
- * @param renderMarkdown 本文の関所(build の marked → sanitize)。ここでは HTML を作らない
242
+ * 帳簿を読む(投げない)。無い・空 → `null`(警告しない)。壊れ・版違い → `null`(`warn` のときだけ 1 行)
241
243
  */
242
- async function readStory(repoRoot, renderMarkdown,
243
- // その日のファイルを GitHub で開く URL(リポジトリ相対パス → URL。リポジトリの URL が無い案件は null)
244
- editUrlOf = () => null,
245
- // 日本時間の今日(テストは時計を注入する)
246
- today = todayJst()) {
244
+ async function loadStoryLedger(repoRoot, warn) {
247
245
  let text;
248
246
  try {
249
247
  text = await node_fs_1.promises.readFile(node_path_1.default.join(repoRoot, exports.STORY_LEDGER_PATH), "utf8");
250
248
  }
251
249
  catch (e) {
252
250
  // 無いのが既定(ストーリーの書き手の栓を入れた案件だけが持つ)。無いことは警告しない
253
- if (e?.code !== "ENOENT") {
251
+ if (warn && e?.code !== "ENOENT") {
254
252
  console.warn(`story-days.json を読めませんでした(ストーリーは出さずに続けます): ${String(e)}`);
255
253
  }
256
254
  return null;
@@ -261,9 +259,25 @@ today = todayJst()) {
261
259
  const ledger = parseStoryLedger(text);
262
260
  if (ledger === "broken") {
263
261
  // 壊れ・版違いは 1 行残す(「夜間がまだ書いていない」と切り分けるため)
264
- console.warn("story-days.json の形式か version が想定と違います(ストーリーは出さずに続けます)");
262
+ if (warn)
263
+ console.warn("story-days.json の形式か version が想定と違います(ストーリーは出さずに続けます)");
265
264
  return null;
266
265
  }
266
+ return ledger;
267
+ }
268
+ /**
269
+ * 案件のストーリーを読む。**載せる日が無ければ `null`**(タブも器も出さない)。
270
+ *
271
+ * @param renderMarkdown 本文の関所(build の marked → sanitize)。ここでは HTML を作らない
272
+ */
273
+ async function readStory(repoRoot, renderMarkdown,
274
+ // その日のファイルを GitHub で開く URL(リポジトリ相対パス → URL。リポジトリの URL が無い案件は null)
275
+ editUrlOf = () => null,
276
+ // 日本時間の今日(テストは時計を注入する)
277
+ today = todayJst()) {
278
+ const ledger = await loadStoryLedger(repoRoot, true);
279
+ if (!ledger)
280
+ return null;
267
281
  const on = onDaysOf(ledger);
268
282
  if (!on.size)
269
283
  return null;
@@ -325,3 +339,128 @@ today = todayJst()) {
325
339
  // 一番古い「動きのある日」から今日まで、日付の抜けを動きのない日の行で埋める。動きのある日が 1 つも無ければ出さない
326
340
  return out.length ? withQuietDays(out, today) : null;
327
341
  }
342
+ // ---------------------------------------------------------------------------
343
+ // 週・月のあらすじ(#901・engine の週・月の書き手が書く `Cortex/Story/Weeks`・`Cortex/Story/Months`)
344
+ // ---------------------------------------------------------------------------
345
+ exports.STORY_WEEKS_DIR = "Cortex/Story/Weeks";
346
+ exports.STORY_MONTHS_DIR = "Cortex/Story/Months";
347
+ /** 載せる週の上限(新しい週から。日の 90 日とそろえて約 13 週・分類 B) */
348
+ exports.STORY_MAX_WEEKS = 13;
349
+ /** 載せる月の上限(新しい月から。90 日がかかりうる 4 か月・分類 B) */
350
+ exports.STORY_MAX_MONTHS = 4;
351
+ /** 局面の行(`**この週の局面**: …`・`**この月の局面**: …`) */
352
+ /** 週・月の機械の塊の終わりの印 */
353
+ const DIGEST_END_RE = /^<!--\s*\/cortex-engine:story-(?:week|month)\s*-->$/;
354
+ const PHASE_RE = /^\*\*この(?:週|月)の局面\*\*\s*[::]\s*(.*)$/;
355
+ /** 週の見出し(`9/21〜9/27 の週`)。ファイルの見出しは使わず月曜の日付から作る */
356
+ function storyWeekHeading(monday) {
357
+ return `${mdOf(monday)}〜${mdOf(dayOfMs(utcMs(monday) + 6 * 86400000))} の週`;
358
+ }
359
+ /** 月の見出し(`2026年9月`) */
360
+ function storyMonthHeading(month) {
361
+ return `${month.slice(0, 4)}年${Number(month.slice(5, 7))}月`;
362
+ }
363
+ /**
364
+ * 週・月の 1 枚の本文を、画面の材料にする(純関数)。
365
+ *
366
+ * - 先頭の `# …` 見出しは捨てる(見出しは月曜の日付・年月から作る)
367
+ * - 印の行は捨てる。機械の部分は機械の語の行を**行ごと**伏せる(Days と同じ)。人の追記は印の行だけ落とす
368
+ * - `**この週の局面**: …` の行を局面(文字)として抜き出し、残りを流れ(Markdown)にする
369
+ */
370
+ function storyDigestParts(content) {
371
+ const lines = String(content ?? "").replace(/^/, "").split("\n").map((l) => l.replace(/\r$/, ""));
372
+ const first = lines.findIndex((l) => l.trim() !== "");
373
+ const body = first >= 0 && /^#\s/.test(lines[first]) ? lines.slice(first + 1) : lines;
374
+ // 🔴 人の追記は、機械の塊の**終わりの印より後ろ**の `## 人の追記` からだけ取る(塊の中に見出しの行が紛れても、
375
+ // 機械の文を人の追記に取り違えない・実装レビュー 🟠2)。終わりの印が無い 1 枚(人が印を消した)は最初の見出しから
376
+ const end = body.findIndex((l) => DIGEST_END_RE.test(l.trim()));
377
+ const at = body.findIndex((l, i) => i > end && l.trim() === HUMAN_HEADING);
378
+ // 機械の部分の `#` の見出しの行は落とす(流れは段落だけの約束。見出しが紛れると後ろの文が別の節に見える)
379
+ const main = (at < 0 ? body : body.slice(0, at)).filter((l) => !STORY_MARKER_RE.test(l.trim()) && !MACHINE_WORD_RE.test(l) && !/^\s{0,3}#{1,6}(\s|$)/.test(l));
380
+ const human = (at < 0 ? [] : body.slice(at + 1)).filter((l) => !STORY_MARKER_RE.test(l.trim()));
381
+ let phase = null;
382
+ const flow = [];
383
+ for (const l of main) {
384
+ const m = phase === null ? l.trim().match(PHASE_RE) : null;
385
+ if (m && m[1].trim()) {
386
+ phase = m[1].trim();
387
+ continue;
388
+ }
389
+ flow.push(l);
390
+ }
391
+ const tidy = (ls) => ls.join("\n").replace(/\n{3,}/g, "\n\n").trim();
392
+ return { phase: phase, flow: tidy(flow), human: tidy(human) };
393
+ }
394
+ /** 置き場の `*.md` のうち、名前が `ok` に合う通常のファイル(リンクは辿らない・上限を超えるものは読まない)を新しい順に */
395
+ async function digestFiles(dir, ok) {
396
+ let names;
397
+ try {
398
+ names = await node_fs_1.promises.readdir(dir);
399
+ }
400
+ catch {
401
+ return [];
402
+ }
403
+ const out = [];
404
+ for (const n of names) {
405
+ const key = n.endsWith(".md") ? n.slice(0, -3) : "";
406
+ if (!ok(key))
407
+ continue;
408
+ const abs = node_path_1.default.join(dir, n);
409
+ try {
410
+ const st = await node_fs_1.promises.lstat(abs);
411
+ if (!st.isFile() || st.size > exports.STORY_MAX_FILE_BYTES)
412
+ continue;
413
+ }
414
+ catch {
415
+ continue;
416
+ }
417
+ out.push({ key: key, abs: abs, name: n });
418
+ }
419
+ return out.sort((a, b) => (a.key < b.key ? 1 : a.key > b.key ? -1 : 0));
420
+ }
421
+ const isMonday = (key) => DAY_RE.test(key) && !Number.isNaN(utcMs(key)) && dayOfMs(utcMs(key)) === key && new Date(utcMs(key)).getUTCDay() === 1;
422
+ const isMonth = (key) => MONTH_RE.test(key) && Number(key.slice(5, 7)) >= 1 && Number(key.slice(5, 7)) <= 12;
423
+ /**
424
+ * 週・月のあらすじを読む。**週・月のファイルが 1 枚も無ければ `null`**(キーごと出さない=今までと同じ画面)。
425
+ * 帳簿が読めない案件も `null`(ストーリーそのものを出さない案件。警告は `readStory` が出す)。
426
+ *
427
+ * @param today 日本時間の今日(画面の「今週」の終わり。テストは時計を注入する)
428
+ */
429
+ async function readStoryDigest(repoRoot, renderMarkdown, editUrlOf = () => null, today = todayJst()) {
430
+ const ledger = await loadStoryLedger(repoRoot, false);
431
+ if (!ledger)
432
+ return null;
433
+ const load = async (dir, ok, max, headingOf) => {
434
+ const out = [];
435
+ for (const f of await digestFiles(node_path_1.default.join(repoRoot, dir), ok)) {
436
+ if (out.length >= max)
437
+ break;
438
+ let raw;
439
+ try {
440
+ raw = await node_fs_1.promises.readFile(f.abs, "utf8");
441
+ }
442
+ catch {
443
+ continue;
444
+ }
445
+ const { phase, flow, human } = storyDigestParts(raw);
446
+ const editUrl = editUrlOf(`${dir}/${f.name}`);
447
+ out.push({
448
+ key: f.key,
449
+ heading: headingOf(f.key),
450
+ ...(phase ? { phase: phase } : {}),
451
+ // 伏せて流れが空になった 1 枚は、見出しだけにしない(Days と同じ 1 行の案内)
452
+ flowHtml: flow ? await renderMarkdown(flow) : phase ? "" : await renderMarkdown(exports.STORY_DIGEST_EMPTY_LINE),
453
+ ...(human ? { humanHtml: await renderMarkdown(human) } : {}),
454
+ ...(editUrl ? { editUrl: editUrl } : {}),
455
+ });
456
+ }
457
+ return out;
458
+ };
459
+ const weeks = await load(exports.STORY_WEEKS_DIR, isMonday, exports.STORY_MAX_WEEKS, storyWeekHeading);
460
+ const months = await load(exports.STORY_MONTHS_DIR, isMonth, exports.STORY_MAX_MONTHS, storyMonthHeading);
461
+ if (!weeks.length && !months.length)
462
+ return null;
463
+ return { today: today, weeks: weeks, months: months };
464
+ }
465
+ /** 伏せて本文が空になった週・月の 1 行 */
466
+ exports.STORY_DIGEST_EMPTY_LINE = "表示できる行がありません。";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.99",
3
+ "version": "0.12.102",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,