@takagaki/cortex-decisions-viewer 0.12.142 → 0.12.145

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/repo.js CHANGED
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.resolveRepo = resolveRepo;
4
- exports.latestGeneratedPaths = latestGeneratedPaths;
5
4
  exports.normalizeToHttps = normalizeToHttps;
6
5
  exports.classifyCommit = classifyCommit;
7
6
  exports.lastSubstantiveUpdate = lastSubstantiveUpdate;
@@ -18,64 +17,6 @@ function resolveRepo(opts) {
18
17
  const remote = tryGitRemote(opts.cwd);
19
18
  return { baseUrl: remote ? normalizeToHttps(remote) : null, branch };
20
19
  }
21
- /**
22
- * 直近のGold昇格で追加されたレコードのパス集合を返す。
23
- *
24
- * 「最新の生成物」をビューアで区別するために使う。レコードの frontmatter の `date` は
25
- * **決定日**(過去の議事録から起こした決定は古い日付になる)なので、生成のタイミングは
26
- * git 側からしか分からない。
27
- *
28
- * 判定: 夜間パイプラインのコミット(メッセージが「自動追記」を含む)のうち最新の1回を探し、
29
- * そのコミットで **追加(A)** されたファイルを返す。同じ夜に複数フェーズが別コミットで入るため、
30
- * 最新コミットと同じ「日」のパイプラインコミットまでをまとめて1回の実行として扱う。
31
- *
32
- * clone が浅い(--depth 1 等)・git が無い環境では**空集合を返す**(ラベルが出ないだけで壊れない)。
33
- *
34
- * 🔴 **パスは `-z`(NUL区切り)で取り、NFC に正規化してから集合に入れる。**
35
- * - `-z` … 既定の git(`core.quotePath=true`)は非ASCIIを含むパスを
36
- * `"Cortex/Rules/records/\345\261\245…md"` と引用符+8進エスケープで出す。レコードは
37
- * **日本語のファイル名**なので、そのまま入れると照合側(readdir の生 UTF-8)と
38
- * 一致せず「最新」ラベルが**1件も出なくなる**(#328 で実際に踏んだ)。
39
- * - NFC … リポジトリのパスは NFC が正(cortex-engine PR #211
40
- * https://github.com/classmethod-internal/cortex-engine/pull/211)。#211 以前に
41
- * NFD で入ったコミットが履歴に残っているので、照合の手前で揃える。
42
- */
43
- function latestGeneratedPaths(cwd) {
44
- const empty = new Set();
45
- try {
46
- // 夜間パイプラインのコミットを新しい順に数件。--grep はコミットメッセージの規約に依存するため、
47
- // 変わっても「ラベルが出ない」だけで済むように例外は握る。
48
- const log = (0, node_child_process_1.execFileSync)("git", ["log", "-n", "20", "--grep=自動追記", "--pretty=format:%H %cI", "--", "Cortex/"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
49
- if (!log)
50
- return empty;
51
- const rows = log.split("\n").map((l) => {
52
- const [sha, iso] = l.trim().split(/\s+/);
53
- return { sha, day: (iso || "").slice(0, 10) };
54
- });
55
- const latestDay = rows[0].day;
56
- if (!latestDay)
57
- return empty;
58
- const out = new Set();
59
- for (const r of rows) {
60
- if (r.day !== latestDay)
61
- break; // 直近の実行日ぶんだけ
62
- const files = (0, node_child_process_1.execFileSync)("git", ["show", "-z", "--no-color", "--diff-filter=A", "--name-only", "--pretty=format:", r.sha], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
63
- for (const f of files.split("\0")) {
64
- // 空の `--pretty=format:` では先頭に改行は出ない(git 2.39 で実測)が、format が
65
- // 非空だと `<ヘッダ>\n<最初のパス>` と改行で区切られる。将来ヘッダを足しても
66
- // パスが壊れないよう**先頭の改行だけ**落とす(trim だと名前の前後の空白まで削り、
67
- // その名前のレコードが照合できなくなる)
68
- const p = f.replace(/^\n+/, "");
69
- if (p)
70
- out.add(p.normalize("NFC"));
71
- }
72
- }
73
- return out;
74
- }
75
- catch {
76
- return empty; // 浅いclone・git無し・メッセージ規約の変更 → ラベルなしで続行
77
- }
78
- }
79
20
  function tryGitRemote(cwd) {
80
21
  try {
81
22
  return (0, node_child_process_1.execSync)("git config --get remote.origin.url", {
@@ -148,9 +89,7 @@ function classifyCommit(id) {
148
89
  // 出る(この関数が防ごうとしている誤帰属の、ちょうど逆向き)。
149
90
  // 規約側は「Gold自動追記: …」「週次整理: …」の形なので、接頭辞で足りる。
150
91
  //
151
- // ⚠️ **見るのは件名(%s)だけ。** latestGeneratedPaths(このファイル上部)の
152
- // `--grep` は**メッセージ本文も**対象なので、判定の範囲が揃っていない。
153
- // 本文まで足すと複数行になって1行1コミットのパースが壊れるため、ここは件名に留める。
92
+ // ⚠️ **見るのは件名(%s)だけ。** 本文まで足すと複数行になって1行1コミットのパースが壊れるため、件名に留める。
154
93
  // 件名に規約が無く本文にだけある形で engine が回っても、**規則3(bot 名義)で拾える**
155
94
  // ——夜間パイプラインは GitHub Actions の bot 名義でコミットするため。
156
95
  const scope = /^([^::]{1,40})[::]/.exec(subject);
package/dist/story.js CHANGED
@@ -7,6 +7,9 @@ exports.STORY_DIGEST_EMPTY_LINE = exports.STORY_MAX_MONTHS = exports.STORY_MAX_W
7
7
  exports.parseStoryLedger = parseStoryLedger;
8
8
  exports.partialDaysOf = partialDaysOf;
9
9
  exports.onDaysOf = onDaysOf;
10
+ exports.successDaysOf = successDaysOf;
11
+ exports.nightResultOf = nightResultOf;
12
+ exports.splitFrontmatter = splitFrontmatter;
10
13
  exports.storyDayMarkdown = storyDayMarkdown;
11
14
  exports.isEngineQuietDay = isEngineQuietDay;
12
15
  exports.todayJst = todayJst;
@@ -133,6 +136,32 @@ function onDaysOf(ledger) {
133
136
  }
134
137
  return out;
135
138
  }
139
+ function successDaysOf(ledger) {
140
+ const partial = partialDaysOf(ledger);
141
+ return new Set([...onDaysOf(ledger)].filter((d) => !partial.has(d)));
142
+ }
143
+ function nightResultOf(ledger) {
144
+ const on = onDaysOf(ledger);
145
+ if (!on.size)
146
+ return null;
147
+ return { firstOn: [...on].reduce((a, d) => (d < a ? d : a)), ok: successDaysOf(ledger) };
148
+ }
149
+ /**
150
+ * 日・週・月のページの先頭の frontmatter(`---` で囲んだ塊)を本文から分ける(純関数)。
151
+ * 日のページは `date` と `relations` を持ちうる(B-story)。画面には出さない。frontmatter が無い・閉じていないページは本文のまま返す。
152
+ * YAML としては読まない(壊れた YAML のページでも本文を落とさない)
153
+ */
154
+ function splitFrontmatter(content) {
155
+ const text = String(content ?? "").replace(/^\ufeff/, "");
156
+ const m = text.match(/^---[ \t]*\r?\n(?:([\s\S]*?)\r?\n)?---[ \t]*(?:\r?\n|$)/);
157
+ if (!m)
158
+ return { frontmatter: null, body: text };
159
+ // 先頭が水平線の `---` のページを frontmatter と取り違えて本文を落とさない: 中身が空か、最初の空でない行が `key:` の形のときだけ分ける
160
+ const first = (m[1] ?? "").split(/\r?\n/).find((l) => l.trim() !== "");
161
+ if (first !== undefined && !/^[A-Za-z_][\w-]*\s*:/.test(first))
162
+ return { frontmatter: null, body: text };
163
+ return { frontmatter: m[1] ?? "", body: text.slice(m[0].length) };
164
+ }
136
165
  /** 曜日つきの見出し(`# 2026-09-24(木)`)の文字。無ければ日付だけ */
137
166
  function headingOf(lines, day) {
138
167
  for (const l of lines) {
@@ -175,13 +204,13 @@ function withoutEmptyBoldHeads(lines) {
175
204
  /**
176
205
  * Days 1 枚の本文を、画面に出す Markdown にする(純関数)。
177
206
  *
178
- * - 先頭の `# 日付(曜)` は見出しとして別に使うので本文から除く
179
- * - 印の行は捨てる。`## 人の追記` の下(後から届いた記録の塊を除く)が空なら見出しごと出さない
207
+ * - 先頭の frontmatter(`date`・`relations`)は本文から分けて出さない。先頭の `# 日付(曜)` は見出しとして別に使うので本文から除く
208
+ * - 印の行は捨てる。`## 人の追記` の見出しは出さない(古いファイルに節が残っていれば、中身だけ本文の続きに出す)
180
209
  * - 人の追記の後ろに足された「後から届いた記録」(人が機械の塊を直した日)は、人の追記より前に寄せる
181
210
  * - 機械の語が紛れた行は行ごと伏せる(機械の行だけ)。中身の無い見出しは落とし、空になった日は 1 行の案内にする
182
211
  */
183
212
  function storyDayMarkdown(content, day) {
184
- const lines = String(content ?? "").replace(/^\ufeff/, "").split("\n").map((l) => l.replace(/\r$/, ""));
213
+ const lines = splitFrontmatter(content).body.split("\n").map((l) => l.replace(/\r$/, ""));
185
214
  const heading = headingOf(lines, day);
186
215
  // 先頭の `# ` 見出し(最初の空でない行だけ)を除く
187
216
  const first = lines.findIndex((l) => l.trim() !== "");
@@ -216,8 +245,9 @@ function storyDayMarkdown(content, day) {
216
245
  // 🔴 人の追記: 落とすのは**印の行だけ**(人が書いた行を画面が黙って消さない・実装レビュー 🟠2)
217
246
  const humanKept = human.filter((l) => !STORY_MARKER_RE.test(l.trim()));
218
247
  const out = withoutEmptySections(withoutEmptyBoldHeads([...clean(main), ...clean(lateAfter)]));
248
+ // 「人の追記」の節は置かない(仕様 7-5)。古いファイルに節が残っていても、見出しだけ外して中身は出す
219
249
  if (humanKept.some((l) => l.trim() !== ""))
220
- out.push("", HUMAN_HEADING, ...humanKept);
250
+ out.push("", ...humanKept);
221
251
  // 空行の続きを 1 行に・前後の空行を落とす
222
252
  const md = out.join("\n").replace(/\n{3,}/g, "\n\n").trim();
223
253
  // 伏せて空になった日は、見出しだけのカードにしない
@@ -225,7 +255,7 @@ function storyDayMarkdown(content, day) {
225
255
  }
226
256
  /** 「大きな動きなし」の 1 行だけの日か(人の追記が 1 行でもあれば違う。機械の印・空行は数えない) */
227
257
  function isEngineQuietDay(content) {
228
- const lines = String(content ?? "").replace(/^\ufeff/, "").split("\n").map((l) => l.replace(/\r$/, "").trim());
258
+ const lines = splitFrontmatter(content).body.split("\n").map((l) => l.replace(/\r$/, "").trim());
229
259
  const at = lines.findIndex((l) => l === HUMAN_HEADING);
230
260
  const main = (at < 0 ? lines : lines.slice(0, at)).filter((l) => l !== "" && !STORY_MARKER_RE.test(l) && !/^#\s/.test(l));
231
261
  const human = at < 0 ? [] : lines.slice(at + 1).filter((l) => l !== "" && !STORY_MARKER_RE.test(l));
@@ -246,19 +276,23 @@ const mdOf = (day) => `${Number(day.slice(5, 7))}/${Number(day.slice(8, 10))}`;
246
276
  * 動きのない日の行(純関数)。`from`〜`to`(新しい方が `to`)を 1 行にする。1 日なら `2026-09-18(金)`、
247
277
  * 続くなら `9/10〜9/18`。文言(「は動きはありませんでした」)は画面が付ける
248
278
  */
249
- function quietStoryRow(from, to, pending = false) {
279
+ function quietStoryRow(from, to, failed = false) {
250
280
  const heading = from === to ? `${to}(${WEEKDAYS[new Date(utcMs(to)).getUTCDay()]})` : `${mdOf(from)}〜${mdOf(to)}`;
251
- return { date: to, ...(from === to ? {} : { fromDate: from }), heading: heading, bodyHtml: "", quiet: true, ...(pending ? { pending: true } : {}) };
281
+ return { date: to, ...(from === to ? {} : { fromDate: from }), heading: heading, bodyHtml: "", quiet: true, ...(failed ? { failed: true } : {}) };
252
282
  }
253
283
  /**
254
284
  * ページのある日(`pages`)の間と、一番古いページから `today` までの日付の抜けを、
255
285
  * 動きのない日の行で埋めて新しい順に返す。連続する `STORY_COLLAPSE_QUIET_DAYS` 日以上は 1 行に畳む。
256
286
  * 🔴 ページより新しい日(今日まで)も埋める。`pages` が空なら空(タブを出さない)
257
287
  *
258
- * `lastDay`=日の書き手が処理し終えた最後の日(帳簿の最後の日)。これより後の空の日は「動きがなかった」のではなく
259
- * 「まだ書けていない」日なので `pending` の行にする(動きのない日の行とは畳まない)。分からない(`null`)ときは全部今までどおり
288
+ * `night`=夜の更新の結果(`nightResultOf`)。ページの無い日は、更新が**成功した日**なら「動きはありませんでした」、
289
+ * 成功していない日(帳簿に行が無い・shadow・途中まで。今日は除く)なら「この日の更新に失敗しています」(`failed`・仕様 7-4)。
290
+ * 🔴 今日(`today`)にページが無く、帳簿にも成功の行がないときは、行を出さない(その日の夜の更新の前で、失敗でも動きなしでもない)。
291
+ * 昨日以前は今までどおり。
292
+ * 例外(仕様に書かれていない入力): 帳簿が読めない(`null`)ときは全部今までどおり「動きはありませんでした」。
293
+ * 帳簿で on の一番古い日より前の日は「失敗」と出さない(まだ始めていなかった日)
260
294
  */
261
- function withQuietDays(pages, today, lastDay = null) {
295
+ function withQuietDays(pages, today, night = null) {
262
296
  if (!pages.length)
263
297
  return [];
264
298
  const have = new Map(pages.map((p) => [p.date, p]));
@@ -267,17 +301,17 @@ function withQuietDays(pages, today, lastDay = null) {
267
301
  const newest = dates.reduce((a, d) => (d > a ? d : a));
268
302
  const last = today > newest ? today : newest;
269
303
  const out = [];
270
- let run = []; // 新しい日から並ぶ動きのない日(まだ書けていない日とは混ぜない)
271
- const pendingOf = (d) => lastDay !== null && d > lastDay;
304
+ let run = []; // 新しい日から並ぶ動きのない日(失敗した日とは混ぜない)
305
+ const failedOf = (d) => night !== null && d >= night.firstOn && !night.ok.has(d);
272
306
  const flush = () => {
273
307
  if (!run.length)
274
308
  return;
275
- const pending = pendingOf(run[0]);
309
+ const failed = failedOf(run[0]);
276
310
  if (run.length >= exports.STORY_COLLAPSE_QUIET_DAYS)
277
- out.push(quietStoryRow(run[run.length - 1], run[0], pending));
311
+ out.push(quietStoryRow(run[run.length - 1], run[0], failed));
278
312
  else
279
313
  for (const d of run)
280
- out.push(quietStoryRow(d, d, pending));
314
+ out.push(quietStoryRow(d, d, failed));
281
315
  run = [];
282
316
  };
283
317
  for (let ms = utcMs(last); ms >= utcMs(oldest); ms -= 86400000) {
@@ -287,8 +321,12 @@ function withQuietDays(pages, today, lastDay = null) {
287
321
  flush();
288
322
  out.push(page);
289
323
  }
324
+ else if (night !== null && d === today && !night.ok.has(d)) {
325
+ // 今日(JST)はその日の夜の更新の前なので、失敗とも「動きはありませんでした」とも言わない(行を出さない)
326
+ flush();
327
+ }
290
328
  else {
291
- if (run.length && pendingOf(run[0]) !== pendingOf(d))
329
+ if (run.length && failedOf(run[0]) !== failedOf(d))
292
330
  flush();
293
331
  run.push(d);
294
332
  }
@@ -397,11 +435,8 @@ ledgerDir) {
397
435
  out.push({ date: f.day, heading: heading, bodyHtml: await renderMarkdown(markdown), ...(editUrl ? { editUrl: editUrl } : {}) });
398
436
  }
399
437
  // 一番古い「動きのある日」から今日まで、日付の抜けを動きのない日の行で埋める。動きのある日が 1 つも無ければ出さない
400
- // 最後の日=帳簿で on の最後の日(日のファイルの日付ではない。末尾に静かな日が続いても、帳簿には処理した印が残る)
401
- // 途中まで書いた日(partial_at つき)は数えない。その日のファイルがあれば本文は出る
402
- const partial = partialDaysOf(ledger);
403
- const lastDay = [...on].filter((d) => !partial.has(d)).reduce((a, d) => (a === null || d > a ? d : a), null);
404
- return out.length ? withQuietDays(out, today, lastDay) : null;
438
+ // 抜けの日は、帳簿の「成功した日」(行があり・on で・途中まででない日)なら動きなし、そうでなければ更新の失敗として出す
439
+ return out.length ? withQuietDays(out, today, nightResultOf(ledger)) : null;
405
440
  }
406
441
  // ---------------------------------------------------------------------------
407
442
  // 週・月のあらすじ(#901・engine の週・月の書き手が書く `Cortex/Story/Weeks`・`Cortex/Story/Months`)
@@ -455,7 +490,7 @@ function withoutHiddenBoldGroups(lines) {
455
490
  * - `**この週の局面**: …` の行を局面(文字)として抜き出し、残りを流れ(Markdown)にする
456
491
  */
457
492
  function storyDigestParts(content) {
458
- const lines = String(content ?? "").replace(/^/, "").split("\n").map((l) => l.replace(/\r$/, ""));
493
+ const lines = splitFrontmatter(content).body.split("\n").map((l) => l.replace(/\r$/, ""));
459
494
  const first = lines.findIndex((l) => l.trim() !== "");
460
495
  const body = first >= 0 && /^#\s/.test(lines[first]) ? lines.slice(first + 1) : lines;
461
496
  // 🔴 人の追記は、機械の塊の**終わりの印より後ろ**の `## 人の追記` からだけ取る(塊の中に見出しの行が紛れても、
package/dist/view-dir.js CHANGED
@@ -32,7 +32,7 @@ exports.viewCountsLine = viewCountsLine;
32
32
  * ## 名前(engine の宣言表 `publish: "viewer"` の行・V10a の `VIEW_HELD`)
33
33
  *
34
34
  * - 読む 11 本のうち、記憶側が置くのは 8 本+順位 1 本(`VIEW_NAMES`)
35
- * - 名寄せの提案(`gold-proposals`・A1 の答えまで)と確認の帳簿(`gold-confirmation`・B9a で人の手の表に寄せる)は置かれないので、いつも `.cortex` から読む
35
+ * - 名寄せの提案(`gold-proposals`・A1 の答えまで)と確認の帳簿(`gold-confirmation`・再導出の帯の読み手 `readRederivationLedgers` が読む)は置かれないので、いつも `.cortex` から読む
36
36
  */
37
37
  const node_child_process_1 = require("node:child_process");
38
38
  const node_crypto_1 = require("node:crypto");
@@ -55,7 +55,7 @@ exports.VIEW_NAMES = Object.freeze([
55
55
  /** 置かれない名前(いつも `.cortex` から読む)と理由 */
56
56
  exports.VIEW_HELD = Object.freeze({
57
57
  "gold-proposals": "名寄せの提案のカード(A1 の答えまで .cortex)",
58
- "gold-confirmation": "削除の確認の一言(記憶 v2 の案件では人の手の表に寄せる・B9a)",
58
+ "gold-confirmation": "人が確認した版と機械の書き直し(再導出の帯の材料・readRederivationLedgers)",
59
59
  });
60
60
  /** 目次の名前(engine `VIEW_INDEX_NAME`) */
61
61
  exports.VIEW_INDEX_FILE = "view-index.json";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.142",
3
+ "version": "0.12.145",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,