@takagaki/cortex-decisions-viewer 0.12.97 → 0.12.98

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
@@ -62,6 +62,7 @@ const marked_1 = require("marked");
62
62
  const sanitize_html_1 = __importDefault(require("sanitize-html"));
63
63
  const repo_1 = require("./repo");
64
64
  const engagement_1 = require("./engagement");
65
+ const story_1 = require("./story");
65
66
  const rederivation_1 = require("./rederivation");
66
67
  // 直近のGold昇格で追加されたレコードのパス集合。レコード生成の各所から参照するのでモジュールスコープに置く
67
68
  // (引数で引き回すと呼び出し側の改修が広範囲になる)。build() の冒頭で1回だけ解決する。
@@ -2360,6 +2361,8 @@ async function build(opts) {
2360
2361
  // 走るのは latestGeneratedPaths と同じ **リポジトリルート**(process.cwd())——
2361
2362
  // Decisions ディレクトリで実行すると pathspec に一致せず常に空になる
2362
2363
  const aiHistory = (0, repo_1.collectAiChangeHistory)(process.cwd());
2364
+ // ストーリーの日々の動き(#901 V2-5)。帳簿で on の日のファイルが無ければ null(キーごと出さない)
2365
+ const story = await (0, story_1.readStory)(repoRoot, renderGroundingSection, (p) => repo.baseUrl ? `${repo.baseUrl}/edit/${repo.branch}/${encodePath(p)}` : null);
2363
2366
  // 案件アイコン(#348)。Cortex構成でなければ undefined(旧レイアウトには置き場が無い)
2364
2367
  const projectIcon = await findProjectIcon(knowledgeRoot);
2365
2368
  const site = {
@@ -2418,6 +2421,8 @@ async function build(opts) {
2418
2421
  // 「知らないことはキーが無いことで表す」に合わせる(空の器を通さない)
2419
2422
  // 空の器は通さない(0件なら undefined = キーごと出さない)
2420
2423
  aiHistory: Object.keys(aiHistory).length ? aiHistory : undefined,
2424
+ // 載せる日が無い案件(帳簿なし・壊れ・shadow の日だけ・ファイルなし)はキーごと出さない
2425
+ story: story ?? undefined,
2421
2426
  };
2422
2427
  // サムネイル(デザイン画面等)をサイトに同梱し、ノードのパスをサイト内相対に書き換える
2423
2428
  await node_fs_1.promises.mkdir(outAbs, { recursive: true });
@@ -353,23 +353,28 @@ function normalizeForCompare(text) {
353
353
  const MEMBER_DECISIONS_START = "<!-- cortex-engine:member-decisions -->";
354
354
  const MEMBER_DECISIONS_END = "<!-- /cortex-engine:member-decisions -->";
355
355
  /**
356
- * 本文から engine の機械ブロック(「この人が関わった決定」)を取り除く(#702)。
357
- *
358
- * 🔴 **engine の写し(`scripts/rederive/content-sha.mjs`)と同じ規則。** 変えたら両方を変える。
359
- * 開始の印**だけ**の行(行末の `\r` は無視)から、終了の印**だけ**の行までを(両端を含めて)消す。終了の印が
360
- * 無ければ本文の終わりまで消す。塊の前後の空行も除き、両側に本文が残れば空行 1 行でつなぐ。そのあと末尾の空行を落とす。ブロックの無い本文は触らない。
356
+ * engine が決定の記録の末尾に書く「その後」の塊の印(#901 V2-2)。始まりの印は属性つき
357
+ * (`<!-- cortex-engine:followup sha=… in=… -->`)なので完全一致ではなく形で当てる。
358
+ * 🔴 **engine の写し(`scripts/rederive/sections.mjs` の `isFollowupBegin`・`FOLLOWUP_END`)と 1 バイト一致。**
359
+ * 行末の `\r` を落としてから当てる(CRLF の記録でも印の行として当てる)
360
+ */
361
+ const FOLLOWUP_BEGIN_RE = /^<!-- cortex-engine:followup(?: [^\n]*?)? -->$/;
362
+ const FOLLOWUP_END = "<!-- /cortex-engine:followup -->";
363
+ const isFollowupBegin = (line) => FOLLOWUP_BEGIN_RE.test(String(line ?? "").replace(/\r$/, ""));
364
+ /**
365
+ * 本文から engine の機械の塊を 1 つ取り除く規則の本体(2 つの塊で同じ 1 本)。
361
366
  *
362
- * 毎晩書き換わる機械の一覧を「人が確認した版から中身が変わっています」に数えないため。
363
- * ブロックの外を人が 1 文字でも直せば、従来どおり指紋は変わる。
367
+ * 🔴 **engine の写し(`scripts/rederive/content-sha.mjs` の `withoutBlockText`)と同じ規則。** 変えたら両方を変える。
368
+ * 開始の印の行(最初の 1 つ・行末の `\r` は無視)から、終了の印**だけ**の行までを(両端を含めて)消す。終了の印が
369
+ * 無ければ本文の終わりまで消す。塊の前後の空行も除き、両側に本文が残れば空行 1 行でつなぐ。そのあと末尾の空行を落とす。塊の無い本文は触らない。
364
370
  */
365
- function stripMemberDecisions(body) {
371
+ function withoutBlock(body, isBegin, endMarker) {
366
372
  const lines = body.split("\n");
367
- // 行末の `\r` は見ない(CRLF の記録でも印の行として当てる。当てないと毎晩印が立つ)
368
- const is = (marker) => (line) => line.replace(/\r$/, "") === marker;
369
- const start = lines.findIndex(is(MEMBER_DECISIONS_START));
373
+ const start = lines.findIndex((line) => isBegin(line));
370
374
  if (start < 0)
371
375
  return body;
372
- const endRel = lines.slice(start + 1).findIndex(is(MEMBER_DECISIONS_END));
376
+ // 行末の `\r` は見ない(CRLF の記録でも印の行として当てる。当てないと毎晩印が立つ)
377
+ const endRel = lines.slice(start + 1).findIndex((line) => line.replace(/\r$/, "") === endMarker);
373
378
  const end = endRel < 0 ? lines.length : start + 1 + endRel + 1;
374
379
  const isBlank = (line) => line.trim() === "";
375
380
  // 塊の前後の空行も一緒に除き、両側に本文が残れば空行 1 行でつなぐ(engine の書き手と同じ規則・#702 実装レビュー R1)
@@ -384,6 +389,30 @@ function stripMemberDecisions(body) {
384
389
  kept.pop();
385
390
  return kept.join("\n");
386
391
  }
392
+ /**
393
+ * 本文から engine の機械ブロック(「この人が関わった決定」)を取り除く(#702)。
394
+ *
395
+ * 🔴 **engine の写し(`scripts/rederive/content-sha.mjs` の `withoutMemberDecisionsText`)と同じ規則。**
396
+ * 開始の印**だけ**の行(行末の `\r` は無視)から終了の印**だけ**の行まで。
397
+ *
398
+ * 毎晩書き換わる機械の一覧を「人が確認した版から中身が変わっています」に数えないため。
399
+ * ブロックの外を人が 1 文字でも直せば、従来どおり指紋は変わる。
400
+ */
401
+ function stripMemberDecisions(body) {
402
+ return withoutBlock(body, (line) => line.replace(/\r$/, "") === MEMBER_DECISIONS_START, MEMBER_DECISIONS_END);
403
+ }
404
+ /**
405
+ * 本文から決定の「その後」の塊(#901 V2-2)を取り除く。規則は `stripMemberDecisions` と同じ 1 本
406
+ * (始まりの印だけ `isFollowupBegin` の形で当てる)。
407
+ *
408
+ * 🔴 **engine の写し(V2-2・`scripts/rederive/content-sha.mjs` の `withoutFollowupText`)と 1 バイト一致・
409
+ * 番人は `test/followup-block-sha.test.mjs`**(engine の `contentShaOf` を実際に呼んで焼いた表
410
+ * `test/fixtures/followup-sha-contract.json`)。塊は機械が材料の変わった夜に `active` の決定にも書き直すので、
411
+ * 除かないと「人が確認した版から変わりました」に数える。塊の無い記録の指紋は 1 バイトも変えない。
412
+ */
413
+ function withoutFollowup(body) {
414
+ return withoutBlock(body, isFollowupBegin, FOLLOWUP_END);
415
+ }
387
416
  function comparableContent(raw) {
388
417
  let data = {};
389
418
  let content = String(raw ?? "");
@@ -398,7 +427,8 @@ function comparableContent(raw) {
398
427
  }
399
428
  return {
400
429
  description: normalizeForCompare(String(data.description ?? "")),
401
- body: normalizeForCompare(stripMemberDecisions(content)),
430
+ // engine と同じ順(目録の塊 → その後の塊)
431
+ body: normalizeForCompare(withoutFollowup(stripMemberDecisions(content))),
402
432
  };
403
433
  }
404
434
  /** 2 つの版で「説明文と本文」が同じか(frontmatter の他の欄の差は見ない) */
package/dist/render.js CHANGED
@@ -4736,6 +4736,15 @@ button.site-icon[aria-busy="true"] .site-icon-plus { opacity: 0; }
4736
4736
  /* 戻す(ディープリンク)は「押すと何かが起きる」側なので塗る */
4737
4737
  .rederived-btn-back { background: #1d4ed8; color: #fff; }
4738
4738
  .rederived-btn-back:hover { background: #1e40af; color: #fff; }
4739
+ /* ---- ストーリー(#901 V2-5・日ごとのカード。新しい日が上)---- */
4740
+ .story-cards { display: flex; flex-direction: column; gap: 14px; }
4741
+ .story-card {
4742
+ border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 18px; background: var(--surface);
4743
+ }
4744
+ .story-card-heading { margin: 0 0 8px; font-size: 15px; }
4745
+ .story-card-edit { display: inline-block; margin-top: 8px; font-size: 12.5px; }
4746
+ .story-card .body h2 { font-size: 14px; margin: 14px 0 6px; }
4747
+ .story-card .body h3 { font-size: 13.5px; margin: 12px 0 6px; }
4739
4748
  /* ---- Home(案件の自己記述)---- */
4740
4749
  /* 未記入の警告。**画面でいちばん強い赤にする**——27案件中24案件が未記入のまま
4741
4750
  毎晩のAI生成が走っている状態で、控えめに出しても読まれない */
@@ -6207,6 +6216,11 @@ ${calendarAddUrl.toString()}
6207
6216
  (SITE.sections || []).forEach(function (sec) {
6208
6217
  TABS.push({ key: "dir:" + sec.name, label: dirLabel(sec.name), items: sec.records });
6209
6218
  });
6219
+ // ストーリー(#901 V2-5)。区画の後・ナレッジグラフと操作の前。
6220
+ // **build が載せた日があるときだけ**(帳簿で on の日のファイルが無い案件はキーごと無い=タブも出ない)
6221
+ if (SITE.story && SITE.story.length) {
6222
+ TABS.push({ key: "story", label: "ストーリー" });
6223
+ }
6210
6224
  if (SITE.graph && SITE.graph.nodes.length > 0) {
6211
6225
  TABS.push({ key: "graph", label: "ナレッジグラフ" });
6212
6226
  }
@@ -8321,6 +8335,9 @@ ${calendarAddUrl.toString()}
8321
8335
  "explorer": {
8322
8336
  text: "決定・用語・レポートを起点に、つながり(根拠・出典・リンク)を1列ずつ辿る表示です。行をクリックすると右に次の列が開きます。",
8323
8337
  },
8338
+ "story": {
8339
+ text: "このプロジェクトの日々の動きを、1日ずつまとめた記録です。直したいことや足したいことは、その日の「人の追記」に書けます。",
8340
+ },
8324
8341
  "dir:レポート": {
8325
8342
  text: "レポートの生成は終了し、履歴として凍結されています(レポートはSlack配信に一本化されました)。以下は過去に生成されたレコードです。",
8326
8343
  },
@@ -8345,9 +8362,26 @@ ${calendarAddUrl.toString()}
8345
8362
  if (listState.tab === "decisions") { renderDecisionList(); return; }
8346
8363
  if (listState.tab === "graph") { renderKnowledge(); return; }
8347
8364
  if (listState.tab === "ops") { renderOps(); return; }
8365
+ if (listState.tab === "story") { renderStory(); return; }
8348
8366
  renderGenericList();
8349
8367
  }
8350
8368
 
8369
+ // ---------- ストーリー(#901 V2-5)----------
8370
+ // 日ごとのカード(新しい日が上=build が並べた順のまま)。本文は build の関所(marked → sanitize)を
8371
+ // 通した HTML で、印のコメントと機械の語の行は build が除いてある。見出しは textContent で入れる
8372
+ function renderStory() {
8373
+ var list = el("div", { class: "story-cards" });
8374
+ (SITE.story || []).forEach(function (d) {
8375
+ list.appendChild(el("section", { class: "story-card" }, [
8376
+ el("h3", { class: "story-card-heading", text: d.heading || d.date }),
8377
+ d.bodyHtml ? el("div", { class: "body", html: d.bodyHtml }) : null,
8378
+ // その日のファイルを GitHub で開く(表示の文字にファイル名を出さない)。URL が無い案件は出さない
8379
+ d.editUrl ? el("a", { class: "story-card-edit", href: d.editUrl, target: "_blank", rel: "noopener", text: "GitHub で追記する" }) : null,
8380
+ ]));
8381
+ });
8382
+ app.appendChild(list);
8383
+ }
8384
+
8351
8385
  // 導入経緯(overview.adoption)の表示情報。Home見出し横のチップに出すラベルだけを返す。
8352
8386
  function adoptionInfo(v) {
8353
8387
  // 3種とも同じ形(ラベルのみ)。導入前のDecisionの薄さの注記は出さない
package/dist/story.js ADDED
@@ -0,0 +1,245 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.STORY_MAX_FILE_BYTES = exports.STORY_EMPTY_LINE = exports.STORY_MAX_DAYS = exports.STORY_DAYS_DIR = exports.STORY_LEDGER_PATH = void 0;
7
+ exports.parseStoryLedger = parseStoryLedger;
8
+ exports.onDaysOf = onDaysOf;
9
+ exports.storyDayMarkdown = storyDayMarkdown;
10
+ exports.readStory = readStory;
11
+ /**
12
+ * ストーリーのタブ(#901 記憶 v2・V2-5)。engine V2-3 が書く日々の動き
13
+ * (`Cortex/Story/Days/YYYY-MM/YYYY-MM-DD.md`)と帳簿(`.cortex/story-days.json`)を読み、画面に載せる日を組む。
14
+ *
15
+ * 形の正本は engine の `scripts/story/ledger.mjs`(帳簿・版 1)と `scripts/story/days.mjs`(`renderDayFile`・`lateBlockOf`)。
16
+ *
17
+ * ## 🔴 タブを出す条件(分類 B・`docs/product-decisions.md` 2026-09-28)
18
+ *
19
+ * 画面はストーリーの書き手の栓(engine の語彙の契約の名前)を読まない。🔴 栓の名前をこのファイルに書かない——
20
+ * tools の追跡ファイルに栓の名前が出ると、語彙の番人(`infra/test/observations-stack.test.ts`)が「tools が使う栓」と数える。
21
+ * だから **帳簿が読めて(版 1)、`days` に `mode === "on"` の日があり、
22
+ * その日のファイルが 1 枚以上ある** ときだけ載せる。shadow の夜は `Cortex/**` を 1 バイトも変えず帳簿だけ
23
+ * 書く(`mode: "shadow"`)ので、shadow の日しか無い案件ではタブも器も出さない(今までと 1 バイトも同じ画面)。
24
+ * 帳簿が無い・壊れている・ファイルが無い → `null`(壊れているときだけ build のログに 1 行)。
25
+ *
26
+ * ## 🔴 機械の語を画面に出さない
27
+ *
28
+ * 印の HTML コメント(`<!-- cortex-engine:story-day … -->`・`story-late`・その終わり)は捨てる。本文の行に
29
+ * `urn:`・ファイル名・`cortex-engine`・`sha=` が紛れていたら**行ごと伏せる**(本文は AI が書いた行なので、
30
+ * 語だけ消すと文が壊れる)。課題番号・コミットの ID は engine の門が落とすので重ねない(`MACHINE_WORD_RE`)。
31
+ * 人の追記には伏せ字を掛けない(落とすのは印の行だけ)。本文の Markdown は他の本文と同じ関所(marked → sanitize)を呼び手が通す。
32
+ */
33
+ const node_fs_1 = require("node:fs");
34
+ const node_path_1 = __importDefault(require("node:path"));
35
+ exports.STORY_LEDGER_PATH = ".cortex/story-days.json";
36
+ exports.STORY_DAYS_DIR = "Cortex/Story/Days";
37
+ /** 載せる日の上限(新しい日から。分類 B・約 3 か月) */
38
+ exports.STORY_MAX_DAYS = 90;
39
+ const DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
40
+ const MONTH_RE = /^\d{4}-\d{2}$/;
41
+ const HUMAN_HEADING = "## 人の追記";
42
+ /** engine の印の行(始まり・終わり。属性は問わない)。画面には出さない */
43
+ const STORY_MARKER_RE = /^<!--\s*\/?cortex-engine:story-(?:day|late)(?:\s[^\n]*?)?\s*-->$/;
44
+ const LATE_BEGIN_RE = /^<!--\s*cortex-engine:story-late(?:\s[^\n]*?)?\s*-->$/;
45
+ const LATE_END_RE = /^<!--\s*\/cortex-engine:story-late\s*-->$/;
46
+ /**
47
+ * 画面に出さない機械の語。当たった行は**行ごと伏せる**(機械の行だけ。人の追記には掛けない)。
48
+ *
49
+ * 🔴 **明らかな機械の語だけ**: 印の残り(`<!--`・`-->`)・`urn:`・`cortex-engine`・`sha=`・ファイル名と置き場
50
+ * (`.md`・`Days/`・`story-days.json`・`.cortex/`)。課題番号(`#123`)・コミットの ID(16 進)・拡張子・パス・設定キーは
51
+ * **engine の門が行ごと落としてから書く**(`scripts/story/gates.mjs` の `MECHANISM_PATTERNS`・G-S3)ので、画面で重ねない。
52
+ * 重ねると、門を通った正当な行(`COVID-19`・`ISO-27001`・`A-1 案`)まで画面が黙って消す(実装レビュー 🟠1)
53
+ */
54
+ const MACHINE_WORD_RE = /urn:|cortex-engine|sha=|<!--|-->|\.md\b|Days\/|story-days\.json|\.cortex\//;
55
+ /** 見出しの行(`##`〜`######`) */
56
+ const SUBHEADING_RE = /^#{2,6}\s/;
57
+ /** 伏せて本文が空になった日の 1 行 */
58
+ exports.STORY_EMPTY_LINE = "この日は表示できる行がありません。";
59
+ /** 1 枚の上限(これを超えるファイルは読まない・分類 B。engine の 1 枚は 4 KiB + 人の追記) */
60
+ exports.STORY_MAX_FILE_BYTES = 64 * 1024;
61
+ /** 帳簿を読む(投げない)。無い → `{ missing }`、壊れ・版違い → `{ broken }` */
62
+ function parseStoryLedger(text) {
63
+ try {
64
+ const parsed = JSON.parse(text);
65
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
66
+ return "broken";
67
+ const o = parsed;
68
+ if (o.version !== 1)
69
+ return "broken";
70
+ if (!o.days || typeof o.days !== "object" || Array.isArray(o.days))
71
+ return "broken";
72
+ return { days: o.days };
73
+ }
74
+ catch {
75
+ return "broken";
76
+ }
77
+ }
78
+ /** 帳簿で `mode: "on"` の日 */
79
+ function onDaysOf(ledger) {
80
+ const out = new Set();
81
+ for (const [day, row] of Object.entries(ledger.days)) {
82
+ if (!DAY_RE.test(day) || !row || typeof row !== "object")
83
+ continue;
84
+ if (row.mode === "on")
85
+ out.add(day);
86
+ }
87
+ return out;
88
+ }
89
+ /** 曜日つきの見出し(`# 2026-09-24(木)`)の文字。無ければ日付だけ */
90
+ function headingOf(lines, day) {
91
+ for (const l of lines) {
92
+ const m = l.match(/^#\s+(.+?)\s*$/);
93
+ if (m && m[1].startsWith(day) && !MACHINE_WORD_RE.test(m[1]))
94
+ return m[1];
95
+ if (l.trim() !== "")
96
+ break;
97
+ }
98
+ return day;
99
+ }
100
+ /** 中身の無い見出し(次の見出しか終わりまで空行しか無い)を落とす(伏せた結果「後から届いた記録」の見出しだけが残る、を防ぐ) */
101
+ function withoutEmptySections(lines) {
102
+ return lines.filter((l, i) => {
103
+ if (!SUBHEADING_RE.test(l))
104
+ return true;
105
+ for (let j = i + 1; j < lines.length; j++) {
106
+ if (SUBHEADING_RE.test(lines[j]))
107
+ return false;
108
+ if (lines[j].trim() !== "")
109
+ return true;
110
+ }
111
+ return false;
112
+ });
113
+ }
114
+ /**
115
+ * Days 1 枚の本文を、画面に出す Markdown にする(純関数)。
116
+ *
117
+ * - 先頭の `# 日付(曜)` は見出しとして別に使うので本文から除く
118
+ * - 印の行は捨てる。`## 人の追記` の下(後から届いた記録の塊を除く)が空なら見出しごと出さない
119
+ * - 人の追記の後ろに足された「後から届いた記録」(人が機械の塊を直した日)は、人の追記より前に寄せる
120
+ * - 機械の語が紛れた行は行ごと伏せる(機械の行だけ)。中身の無い見出しは落とし、空になった日は 1 行の案内にする
121
+ */
122
+ function storyDayMarkdown(content, day) {
123
+ const lines = String(content ?? "").replace(/^\ufeff/, "").split("\n").map((l) => l.replace(/\r$/, ""));
124
+ const heading = headingOf(lines, day);
125
+ // 先頭の `# ` 見出し(最初の空でない行だけ)を除く
126
+ const first = lines.findIndex((l) => l.trim() !== "");
127
+ const body = first >= 0 && /^#\s/.test(lines[first]) ? lines.slice(first + 1) : lines;
128
+ const at = body.findIndex((l) => l.trim() === HUMAN_HEADING);
129
+ const main = at < 0 ? body : body.slice(0, at);
130
+ const tail = at < 0 ? [] : body.slice(at + 1);
131
+ // 人の追記の後ろの塊(後から届いた記録)と人の行を分ける
132
+ const human = [];
133
+ const lateAfter = [];
134
+ let inLate = false;
135
+ for (const l of tail) {
136
+ const t = l.trim();
137
+ if (LATE_BEGIN_RE.test(t)) {
138
+ inLate = true;
139
+ lateAfter.push("");
140
+ continue;
141
+ }
142
+ if (inLate) {
143
+ if (LATE_END_RE.test(t)) {
144
+ inLate = false;
145
+ continue;
146
+ }
147
+ lateAfter.push(l);
148
+ continue;
149
+ }
150
+ human.push(l);
151
+ }
152
+ // 機械の行: 印と機械の語の行を伏せる
153
+ const clean = (ls) => ls.filter((l) => !STORY_MARKER_RE.test(l.trim()) && !MACHINE_WORD_RE.test(l));
154
+ // 🔴 人の追記: 落とすのは**印の行だけ**(人が書いた行を画面が黙って消さない・実装レビュー 🟠2)
155
+ const humanKept = human.filter((l) => !STORY_MARKER_RE.test(l.trim()));
156
+ const out = withoutEmptySections([...clean(main), ...clean(lateAfter)]);
157
+ if (humanKept.some((l) => l.trim() !== ""))
158
+ out.push("", HUMAN_HEADING, ...humanKept);
159
+ // 空行の続きを 1 行に・前後の空行を落とす
160
+ const md = out.join("\n").replace(/\n{3,}/g, "\n\n").trim();
161
+ // 伏せて空になった日は、見出しだけのカードにしない
162
+ return { heading: heading, markdown: md || exports.STORY_EMPTY_LINE };
163
+ }
164
+ /**
165
+ * 案件のストーリーを読む。**載せる日が無ければ `null`**(タブも器も出さない)。
166
+ *
167
+ * @param renderMarkdown 本文の関所(build の marked → sanitize)。ここでは HTML を作らない
168
+ */
169
+ async function readStory(repoRoot, renderMarkdown,
170
+ // その日のファイルを GitHub で開く URL(リポジトリ相対パス → URL。リポジトリの URL が無い案件は null)
171
+ editUrlOf = () => null) {
172
+ let text;
173
+ try {
174
+ text = await node_fs_1.promises.readFile(node_path_1.default.join(repoRoot, exports.STORY_LEDGER_PATH), "utf8");
175
+ }
176
+ catch (e) {
177
+ // 無いのが既定(ストーリーの書き手の栓を入れた案件だけが持つ)。無いことは警告しない
178
+ if (e?.code !== "ENOENT") {
179
+ console.warn(`story-days.json を読めませんでした(ストーリーは出さずに続けます): ${String(e)}`);
180
+ }
181
+ return null;
182
+ }
183
+ // 空の帳簿(空文字・空白だけ)は engine と同じく「空」=無いのと同じ(警告しない)
184
+ if (text.trim() === "")
185
+ return null;
186
+ const ledger = parseStoryLedger(text);
187
+ if (ledger === "broken") {
188
+ // 壊れ・版違いは 1 行残す(「夜間がまだ書いていない」と切り分けるため)
189
+ console.warn("story-days.json の形式か version が想定と違います(ストーリーは出さずに続けます)");
190
+ return null;
191
+ }
192
+ const on = onDaysOf(ledger);
193
+ if (!on.size)
194
+ return null;
195
+ const root = node_path_1.default.join(repoRoot, exports.STORY_DAYS_DIR);
196
+ let months;
197
+ try {
198
+ months = (await node_fs_1.promises.readdir(root, { withFileTypes: true })).filter((d) => d.isDirectory() && MONTH_RE.test(d.name)).map((d) => d.name);
199
+ }
200
+ catch {
201
+ return null;
202
+ }
203
+ const files = [];
204
+ for (const m of months) {
205
+ let names;
206
+ try {
207
+ names = await node_fs_1.promises.readdir(node_path_1.default.join(root, m));
208
+ }
209
+ catch {
210
+ continue;
211
+ }
212
+ for (const n of names) {
213
+ const day = n.endsWith(".md") ? n.slice(0, -3) : "";
214
+ // ファイル名が日付の形で、月の置き場と合い、帳簿で on の日だけ(README などは出さない)
215
+ if (!DAY_RE.test(day) || day.slice(0, 7) !== m || !on.has(day))
216
+ continue;
217
+ const abs = node_path_1.default.join(root, m, n);
218
+ // 🔴 通常のファイルだけ(リンクは辿らない)・1 枚の上限を超えるものは読まない(分類 B)
219
+ try {
220
+ const st = await node_fs_1.promises.lstat(abs);
221
+ if (!st.isFile() || st.size > exports.STORY_MAX_FILE_BYTES)
222
+ continue;
223
+ }
224
+ catch {
225
+ continue;
226
+ }
227
+ files.push({ day: day, abs: abs, rel: `${exports.STORY_DAYS_DIR}/${m}/${n}` });
228
+ }
229
+ }
230
+ files.sort((a, b) => (a.day < b.day ? 1 : a.day > b.day ? -1 : 0));
231
+ const out = [];
232
+ for (const f of files.slice(0, exports.STORY_MAX_DAYS)) {
233
+ let raw;
234
+ try {
235
+ raw = await node_fs_1.promises.readFile(f.abs, "utf8");
236
+ }
237
+ catch {
238
+ continue;
239
+ }
240
+ const { heading, markdown } = storyDayMarkdown(raw, f.day);
241
+ const editUrl = editUrlOf(f.rel);
242
+ out.push({ date: f.day, heading: heading, bodyHtml: await renderMarkdown(markdown), ...(editUrl ? { editUrl: editUrl } : {}) });
243
+ }
244
+ return out.length ? out : null;
245
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.97",
3
+ "version": "0.12.98",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,