@takagaki/cortex-decisions-viewer 0.12.174 → 0.12.177

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
@@ -2210,7 +2210,7 @@ function evidenceRowsOf(section, items, keyOf, refs) {
2210
2210
  const key = it.url ? keyOf(it.url) : `text:${it.text_ref}`;
2211
2211
  // 言い直し(restated)は決めた場面(decided)と同じ根拠でも 1 行に溶かさない(日を言い直しの日に替えない)。照らしの鍵 `key` は同じまま
2212
2212
  const groupKey = `${it.kind === "restated" ? "restated" : "main"}\u0000${key}`;
2213
- const quotes = it.quotes.map((q) => (q.context ? { speaker: q.speaker, text: q.text, context: true } : { speaker: q.speaker, text: q.text }));
2213
+ const quotes = it.quotes.map((q) => ({ speaker: q.speaker, text: q.text, ...(q.time ? { time: q.time } : {}), ...(q.context ? { context: true } : {}) }));
2214
2214
  const hit = byKey.get(groupKey);
2215
2215
  if (hit) {
2216
2216
  for (const q of quotes) {
@@ -2219,9 +2219,15 @@ function evidenceRowsOf(section, items, keyOf, refs) {
2219
2219
  }
2220
2220
  if (it.at > hit.at)
2221
2221
  hit.at = it.at;
2222
+ // 名前は先に積まれた項目のものを使う(後の項目で上書きしない)。先の項目に無ければ後の名前を採る
2223
+ if (it.url && it.label && hit.url && hit.label === (refs.resolve(hit.url).label || hit.url))
2224
+ hit.label = it.label;
2222
2225
  continue;
2223
2226
  }
2224
- const label = it.url ? refs.resolve(it.url).label || it.url : String(it.text_ref);
2227
+ // 台帳の名前(会議名・課題の名前)を先に使い、無いときだけ帳の題名・URL の種類の名前(`common.evidence-label`)
2228
+ // 課題キーだけの名前は、帳の題名が引けるなら帳を先に使う(キーが題名より劣らない)
2229
+ const bookLabel = it.url ? refs.resolve(it.url).label : "";
2230
+ const label = it.url ? (it.label && ISSUE_KEY_ONLY_RE.test(it.label) && /backlog\.|github\.com\//i.test(it.url) ? bookLabel || it.label : it.label || bookLabel) || it.url : String(it.text_ref);
2225
2231
  const row = { at: it.at, kind: it.kind, label, url: it.url, key, quotes };
2226
2232
  byKey.set(groupKey, row);
2227
2233
  rows.push(row);
@@ -2538,8 +2544,10 @@ async function buildSite(opts, views) {
2538
2544
  // 記録ページの「更新履歴」。engine が毎晩書く帳簿(ledger-dir の gold-change-log.json)だけを読む(git は読まない)。
2539
2545
  // 無い・壊れ・版違いは null で、画面は「更新履歴はまだありません」の 1 つの形になる
2540
2546
  const changeLog = await (0, change_log_1.readChangeLog)(repoRoot, ledgerDir);
2541
- // ストーリー(#901 V2-5)の日本時間の今日。日・週・月の3つで同じ日にそろえるため1回だけ計算する
2542
- const storyToday = (0, story_1.todayJst)(new Date(opts.generatedAt));
2547
+ // ストーリー(#901 V2-5)の日本時間の今日(6 時区切り・`story.screen-quiet`)。日・週・月の3つで同じ日にそろえるため1回だけ計算する
2548
+ const storyToday = (0, story_1.storyTodayJst)(new Date(opts.generatedAt));
2549
+ // 日々の動きを埋める終点=夜の更新の晩の日(日本時間 6 時区切り・engine と同じ)。0〜6 時は前の日の晩がまだ仕上がっていない時間なので、前の日を「更新に失敗しています」と出さない
2550
+ const storyNightDay = (0, story_1.nightDayJst)(new Date(opts.generatedAt));
2543
2551
  // ストーリーの日々の動き(#901 V2-5)。帳簿で on の日のファイルが無ければ null(キーごと出さない)
2544
2552
  const shownIds = new Set([...decisions.map((d) => d.id), ...sections.flatMap((s) => s.records.map((r) => r.id))]);
2545
2553
  const idByRepoPath = new Map();
@@ -2547,8 +2555,8 @@ async function buildSite(opts, views) {
2547
2555
  if (shownIds.has(id))
2548
2556
  idByRepoPath.set(rel, id);
2549
2557
  const story = await (0, story_1.readStory)(repoRoot, renderGroundingSection, (p) => repo.baseUrl ? `${repo.baseUrl}/edit/${repo.branch}/${encodePath(p)}` : null,
2550
- // 動きのない日を埋める終点=ビルドの時刻の日本時間の日付(時計を注入できる)
2551
- storyToday, ledgerDir,
2558
+ // 動きのない日を埋める終点=ビルドの時刻の夜の更新の晩の日(日本時間 6 時区切り・時計を注入できる)
2559
+ storyNightDay, ledgerDir,
2552
2560
  // 名前のリンク(story.screen-name-links): 日の relations の関係者・用語・ルールを、画面にある記録の表示名・別名・同義語で本文に当てる
2553
2561
  (targets) => (0, story_1.storyNameLinks)(targets, storyNameSources(sections)),
2554
2562
  // 本文の相対リンク(決定の記録など)の飛び先: 画面に載る記録(決定・各区画)のパス → id
@@ -45,6 +45,7 @@ const DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
45
45
  // engine の台帳の検め(`^https?://`)と揃える。空白を含む URL も項目ごと捨てない
46
46
  const HTTP_RE = /^https?:\/\/\S/i;
47
47
  const isObj = (v) => !!v && typeof v === "object" && !Array.isArray(v);
48
+ const TIME_RE = /^\d{1,2}:[0-5]\d(?::[0-5]\d)?$/;
48
49
  const isText = (v) => typeof v === "string" && v.trim() !== "";
49
50
  /** 引用 1 つ。崩れていれば null */
50
51
  function quoteOf(v) {
@@ -57,6 +58,9 @@ function quoteOf(v) {
57
58
  q.material = v.material;
58
59
  if (typeof v.line === "number" && Number.isFinite(v.line))
59
60
  q.line = v.line;
61
+ // 時刻は形の合うものだけ(画面に出る文字・合わなければ時刻だけ捨てて引用は残す)
62
+ if (typeof v.time === "string" && TIME_RE.test(v.time.trim()))
63
+ q.time = v.time.trim();
60
64
  if (v.context === true)
61
65
  q.context = true;
62
66
  return q;
@@ -85,6 +89,12 @@ function memoryEvidenceItemOf(v) {
85
89
  item.night = v.night;
86
90
  if (typeof v.cand === "string" && v.cand)
87
91
  item.cand = v.cand;
92
+ // 名前は人に見せる文字なので整える(改行・連続の空白を 1 つに・80 字まで)。文字列でない・空は捨てる(項目は残す)
93
+ if (isText(v.label)) {
94
+ const label = [...v.label.replace(/\s+/g, " ").trim()].slice(0, 80).join("");
95
+ if (label)
96
+ item.label = label;
97
+ }
88
98
  return item;
89
99
  }
90
100
  /** 中身を読む(投げない)。形が違う・版が違う → `null`。崩れた記録・項目は捨てる */
@@ -123,9 +123,17 @@ function pendingSlackChannelId(input) {
123
123
  */
124
124
  function pendingSlackUrl(input, id) {
125
125
  const cid = String(id == null ? "" : id);
126
- const m = String(input == null ? "" : input).trim().match(/^https:\/\/([a-z0-9][a-z0-9-]*)\.slack\.com\/archives\/([CG][A-Z0-9]{6,})(?:[/?#]|$)/);
127
- if (m && m[2] === cid && m[1] !== "app")
128
- return "https://" + m[1] + ".slack.com/archives/" + cid;
126
+ const m = String(input == null ? "" : input).trim().match(/^https:\/\/([a-z0-9][a-z0-9.-]*\.slack\.com)\/archives\/([CG][A-Z0-9]{6,})(?:[/?#]|$)/);
127
+ // ワークスペースの住所の検査はサーバ logic.mjs の `slackWorkspaceHost` の写し(入れ子の `x.enterprise.slack.com` も受ける)。
128
+ // 🔴 この関数は render.ts が toString() で画面へ流し込むので、外の定数・関数を参照しない(自分の中で完結させる)
129
+ if (m && m[2] === cid) {
130
+ const labels = m[1].split(".");
131
+ const nonWorkspace = ["app", "www", "api", "files", "hooks", "join", "status", "edgeapi"];
132
+ const ok = labels.length >= 3 && !(labels.length === 3 && nonWorkspace.indexOf(labels[0]) >= 0)
133
+ && labels.slice(0, -2).every((l) => /^[a-z0-9-]+$/.test(l) && l.charAt(0) !== "-" && l.charAt(l.length - 1) !== "-");
134
+ if (ok)
135
+ return "https://" + m[1] + "/archives/" + cid;
136
+ }
129
137
  return cid ? "https://slack.com/archives/" + cid : "";
130
138
  }
131
139
  /**
@@ -171,9 +179,10 @@ function pendingRepo(input) {
171
179
  }
172
180
  function pendingFigmaKey(input) {
173
181
  const s = String(input == null ? "" : input).trim();
174
- const m = s.match(/figma\.com\/(?:design|file|proto)\/([A-Za-z0-9_-]{8,})/);
182
+ // ブランチ(`/design/<ファイルのキー>/branch/<ブランチのキー>`)はブランチのキー(サーバ logic.mjs の `figmaFileKey` の写し)
183
+ const m = s.match(/figma\.com\/(?:design|file|proto)\/([A-Za-z0-9_-]{8,})(?:\/branch\/([A-Za-z0-9_-]{8,}))?/);
175
184
  if (m)
176
- return m[1];
185
+ return m[2] || m[1];
177
186
  return /^[A-Za-z0-9_-]{8,}$/.test(s) ? s : null;
178
187
  }
179
188
  function pendingDriveFolderId(input) {
@@ -403,7 +412,7 @@ function sanitizePendingOps(raw, projectKey) {
403
412
  }
404
413
  else if (f === "url") {
405
414
  // href に入る値。localStorage から読み戻すので、Slack のチャンネル URL の形だけ通す
406
- if (typeof src[f] === "string" && /^https:\/\/(?:[a-z0-9][a-z0-9-]*\.)?slack\.com\/archives\/[CG][A-Z0-9]{6,}$/.test(src[f]))
415
+ if (typeof src[f] === "string" && /^https:\/\/(?:(?:[a-z0-9][a-z0-9-]*\.)+)?slack\.com\/archives\/[CG][A-Z0-9]{6,}$/.test(src[f]))
407
416
  payload[f] = src[f];
408
417
  }
409
418
  else if (typeof src[f] === "string")
package/dist/render.js CHANGED
@@ -6052,6 +6052,7 @@ button.ops-card-btn:disabled { opacity: .6; cursor: default; }
6052
6052
  .evidence-text-ref { color: var(--text); }
6053
6053
  .evidence-quotes { margin: 4px 0 2px; padding-left: 10px; border-left: 3px solid var(--border); }
6054
6054
  .evidence-quote { font-size: 13.5px; color: var(--text); line-height: 1.6; margin: 2px 0; white-space: pre-wrap; word-break: break-word; }
6055
+ .evidence-quote-time { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
6055
6056
  .evidence-quote.is-context { font-size: 12px; color: var(--muted); }
6056
6057
  .evidence-context-tag { display: inline-block; margin-right: 6px; padding: 0 6px; font-size: 11px; border: 1px solid var(--border); border-radius: 999px; color: var(--muted); }
6057
6058
  .evidence-more, .evidence-fold-btn { border: none; background: none; padding: 0 2px; font-size: 12.5px; color: var(--accent); cursor: pointer; }
@@ -6983,6 +6984,8 @@ ${story_1.splitByNameLinks.toString()}
6983
6984
  function evidenceQuote(q) {
6984
6985
  var box = el("div", { class: "evidence-quote" + (q.context ? " is-context" : "") });
6985
6986
  if (q.context) box.appendChild(el("span", { class: "evidence-context-tag", text: "前の発言" }));
6987
+ // 会議の発言の場所(録画の頭からの経過)。読めた発言だけ「28:15 ごろ」を頭に添える
6988
+ if (typeof q.time === "string" && /^\\d{1,2}:[0-5]\\d(:[0-5]\\d)?$/.test(q.time)) box.appendChild(el("span", { class: "evidence-quote-time", text: q.time + " ごろ " }));
6986
6989
  var who = q.speaker ? String(q.speaker) + ":" : "";
6987
6990
  var chars = Array.from(String(q.text || ""));
6988
6991
  var body = el("span", { class: "evidence-quote-text" });
@@ -7795,6 +7798,13 @@ ${story_1.splitByNameLinks.toString()}
7795
7798
  (mfInflight[w.id] = mfInflight[w.id] || []).push(w.box);
7796
7799
  });
7797
7800
  if (!ids.length) return;
7801
+ mfRequest(ids, false);
7802
+ }
7803
+ /**
7804
+ * member-faces を 1 回頼む(200 人まで)。頼んだ人は mfAsked に入れ、答え(または諦め)で待っている丸を最後の形へ。
7805
+ * preload=起動時の先読み。画像もブラウザのキャッシュに載せておく
7806
+ */
7807
+ function mfRequest(ids, preload) {
7798
7808
  ids.forEach(function (id) { mfAsked[id] = 1; });
7799
7809
  var finished = false;
7800
7810
  // 答えが出た(または諦めた)人の待ち行列を空にして、全部の丸を最後の形へ
@@ -7807,7 +7817,10 @@ ${story_1.splitByNameLinks.toString()}
7807
7817
  };
7808
7818
  var give = function (j) {
7809
7819
  (j.people || []).forEach(function (one) {
7810
- if (one && typeof one.userId === "string" && typeof one.avatarUrl === "string" && MF_AVATAR.test(one.avatarUrl)) mfFaces[one.userId] = one.avatarUrl;
7820
+ if (one && typeof one.userId === "string" && typeof one.avatarUrl === "string" && MF_AVATAR.test(one.avatarUrl)) {
7821
+ mfFaces[one.userId] = one.avatarUrl;
7822
+ if (preload) mfWarm(one.avatarUrl);
7823
+ }
7811
7824
  });
7812
7825
  ids.forEach(function (id) { if (!Object.prototype.hasOwnProperty.call(mfFaces, id)) mfFaces[id] = null; });
7813
7826
  if (!finished) { finished = true; clearTimeout(giveUp); drain(); }
@@ -7827,6 +7840,39 @@ ${story_1.splitByNameLinks.toString()}
7827
7840
  else forget();
7828
7841
  }, forget);
7829
7842
  }
7843
+ /** 先読みした画像のパス -> 読み込み済み(読み込み済みなら関係者タブでスケルトンを経ずに写真から出す) */
7844
+ var mfReady = {};
7845
+ function mfWarm(path) {
7846
+ if (Object.prototype.hasOwnProperty.call(mfReady, path)) return;
7847
+ mfReady[path] = false;
7848
+ var img = new Image();
7849
+ img.setAttribute("referrerpolicy", "no-referrer");
7850
+ img.addEventListener("load", function () { mfReady[path] = true; });
7851
+ img.addEventListener("error", function () { delete mfReady[path]; });
7852
+ img.src = PORTAL_ORIGIN + path;
7853
+ }
7854
+ /**
7855
+ * 起動時に 1 回だけ、全部の関係者の portal.id を 200 人ずつ頼んで写真を先読みする(loadLiveFleet と同じ作り)。
7856
+ * 🔴 route() の後に置く(popstate は route だけ通るので、戻る/進むで何度も走らない)。券・intake が無ければ何もしない。
7857
+ * 先読みの飛行中に関係者タブを開いた丸は mfInflight に並び、先読みの答えで同じ流れ(スケルトン → 写真)に乗る。
7858
+ * 失敗は覚えない(mfRequest の forget)。タブを開いたときにもう一度頼める
7859
+ */
7860
+ function mfPreload() {
7861
+ if (!SITE.intake || !actorTicket()) return;
7862
+ var ids = [];
7863
+ (SITE.sections || []).forEach(function (sec) {
7864
+ (sec.records || []).forEach(function (r) {
7865
+ if (!isMemberRec(r)) return;
7866
+ var id = mfPortalId(r);
7867
+ if (id && ids.indexOf(id) < 0 && !Object.prototype.hasOwnProperty.call(mfAsked, id) && !Object.prototype.hasOwnProperty.call(mfFaces, id)) ids.push(id);
7868
+ });
7869
+ });
7870
+ for (var i = 0; i < ids.length; i += 200) {
7871
+ var chunk = ids.slice(i, i + 200);
7872
+ chunk.forEach(function (id) { mfAsked[id] = 1; });
7873
+ mfRequest(chunk, true);
7874
+ }
7875
+ }
7830
7876
  function mfWant(id, box) {
7831
7877
  mfWaiting.push({ id: id, box: box });
7832
7878
  if (!mfTimer) mfTimer = setTimeout(mfFlush, 0);
@@ -7869,6 +7915,32 @@ ${story_1.splitByNameLinks.toString()}
7869
7915
  });
7870
7916
  // 行ごと(カード・詳細)の click に伝えない(カードを押した扱いで詳細へ移らない)
7871
7917
  a.addEventListener("click", function (ev) { ev.stopPropagation(); });
7918
+ // 先読みで答えが分かっている人は、スケルトンを経ずに最後の形から出す(写真は読み込み済みのときだけ)
7919
+ if (Object.prototype.hasOwnProperty.call(mfFaces, id)) {
7920
+ var known = mfFaces[id];
7921
+ if (!known) {
7922
+ a.classList.remove("mface-loading");
7923
+ a.setAttribute("data-state", "initial");
7924
+ a.appendChild(el("span", { class: "mface-initial", text: name }));
7925
+ return a;
7926
+ }
7927
+ if (mfReady[known] === true) {
7928
+ var ready = document.createElement("img");
7929
+ ready.setAttribute("alt", "");
7930
+ ready.setAttribute("referrerpolicy", "no-referrer");
7931
+ ready.addEventListener("error", function () {
7932
+ // 先読みで読めていたはずの画像が壊れたとき(キャッシュ切れ・ポータル不調)も頭文字に落とす
7933
+ if (ready.parentNode) ready.parentNode.removeChild(ready);
7934
+ a.setAttribute("data-state", "initial");
7935
+ a.appendChild(el("span", { class: "mface-initial", text: name }));
7936
+ });
7937
+ ready.setAttribute("src", PORTAL_ORIGIN + known);
7938
+ a.classList.remove("mface-loading");
7939
+ a.setAttribute("data-state", "photo");
7940
+ a.appendChild(ready);
7941
+ return a;
7942
+ }
7943
+ }
7872
7944
  mfWant(id, a);
7873
7945
  return a;
7874
7946
  }
@@ -21776,6 +21848,8 @@ ${story_1.splitByNameLinks.toString()}
21776
21848
  // ビルド時には存在しないので、**焼いた href は起動の時点で既に古いことがある**。
21777
21849
  // 🔴 route() の後に置く——ヘッダー以外の導線は route() が描くまで DOM に無い
21778
21850
  syncAiLinks();
21851
+ /** 関係者の写真の先読み(mfPreload の項)。loadLiveFleet と同じく起動時に 1 回だけ */
21852
+ mfPreload();
21779
21853
  /**
21780
21854
  * 接続状況の先読み(#264)。**開いたタブに関係なく、起動時に1回だけ取りに行く。**
21781
21855
  *
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_DIGEST_EMPTY_LINE = exports.STORY_MAX_MONTHS = exports.STORY_MAX_WEEKS = exports.STORY_MONTHS_DIR = exports.STORY_WEEKS_DIR = exports.STORY_LINK_MIN_NAME = exports.STORY_LINK_KINDS = 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_FILE = 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.NIGHT_CUTOFF_HOURS = exports.STORY_NIGHT_CUT_HOUR = exports.STORY_LINK_MIN_NAME = exports.STORY_LINK_KINDS = 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_FILE = void 0;
7
7
  exports.parseStoryLedger = parseStoryLedger;
8
8
  exports.partialDaysOf = partialDaysOf;
9
9
  exports.onDaysOf = onDaysOf;
@@ -17,6 +17,8 @@ exports.splitByNameLinks = splitByNameLinks;
17
17
  exports.storyDayMarkdown = storyDayMarkdown;
18
18
  exports.isEngineQuietDay = isEngineQuietDay;
19
19
  exports.todayJst = todayJst;
20
+ exports.storyTodayJst = storyTodayJst;
21
+ exports.nightDayJst = nightDayJst;
20
22
  exports.quietStoryRow = quietStoryRow;
21
23
  exports.withQuietDays = withQuietDays;
22
24
  exports.linkRecordRefs = linkRecordRefs;
@@ -387,6 +389,25 @@ function todayJst(now = new Date()) {
387
389
  now = new Date();
388
390
  return dayOfMs(now.getTime() + 9 * 3600 * 1000);
389
391
  }
392
+ /** 夜の更新の晩を切る日本時間の時(engine の夜の更新の区切りと同じ・`story.screen-quiet`・`flow.common.night-cut`) */
393
+ exports.STORY_NIGHT_CUT_HOUR = 6;
394
+ /**
395
+ * ストーリーの「今日」(`YYYY-MM-DD`)。日本時間の 6 時で切る(0〜6 時はまだ前の日)。
396
+ * 夜の更新が済む前の朝に、前の日を「この日の更新に失敗しています」と出さないため。日・週・月が同じ日を見る
397
+ */
398
+ function storyTodayJst(now = new Date()) {
399
+ if (Number.isNaN(now.getTime()))
400
+ now = new Date();
401
+ return todayJst(new Date(now.getTime() - exports.STORY_NIGHT_CUT_HOUR * 3600 * 1000));
402
+ }
403
+ /**
404
+ * 夜の更新の晩の日(`YYYY-MM-DD`)。`storyTodayJst` と同じ規則(日本時間 6 時区切り)の別名で、「夜の更新の晩」を指す呼び出しで読みやすくする。
405
+ * 🔴 区切りの数(6)は engine の `NIGHT_CUTOFF_HOURS`(`scripts/memory-v2/night-result.mjs`)と必ずそろえる
406
+ */
407
+ exports.NIGHT_CUTOFF_HOURS = exports.STORY_NIGHT_CUT_HOUR;
408
+ function nightDayJst(now = new Date()) {
409
+ return storyTodayJst(now);
410
+ }
390
411
  const mdOf = (day) => `${Number(day.slice(5, 7))}/${Number(day.slice(8, 10))}`;
391
412
  /**
392
413
  * 動きのない日の行(純関数)。`from`〜`to`(新しい方が `to`)を 1 行にする。1 日なら `2026-09-18(金)`、
@@ -403,6 +424,7 @@ function quietStoryRow(from, to, failed = false) {
403
424
  *
404
425
  * `night`=夜の更新の結果(`nightResultOf`)。ページの無い日は、更新が**成功した日**なら「動きはありませんでした」、
405
426
  * 成功していない日(帳簿に行が無い・shadow・途中まで。今日は除く)なら「この日の更新に失敗しています」(`failed`・仕様 7-4)。
427
+ * `today` は夜の更新の晩の日(`nightDayJst`・日本時間 6 時区切り)で渡す。
406
428
  * 🔴 今日(`today`)にページが無く、帳簿にも成功の行がないときは、行を出さない(その日の夜の更新の前で、失敗でも動きなしでもない)。
407
429
  * 昨日以前は今までどおり。
408
430
  * 例外(仕様に書かれていない入力): 帳簿が読めない(`null`)ときは全部今までどおり「動きはありませんでした」。
@@ -515,7 +537,7 @@ async function readStory(repoRoot, renderMarkdown,
515
537
  // その日のファイルを GitHub で開く URL(リポジトリ相対パス → URL。リポジトリの URL が無い案件は null)
516
538
  editUrlOf = () => null,
517
539
  // 日本時間の今日(テストは時計を注入する)
518
- today = todayJst(),
540
+ today = storyTodayJst(),
519
541
  // 帳簿の置き場(既定は `<repoRoot>/.cortex`)
520
542
  ledgerDir,
521
543
  // 日の `relations` の target から名前のリンクの表を作る口(build が記録の表を握って渡す。無ければリンクは付けない)
@@ -711,7 +733,7 @@ const isMonth = (key) => MONTH_RE.test(key) && Number(key.slice(5, 7)) >= 1 && N
711
733
  *
712
734
  * @param today 日本時間の今日(画面の「今週」の終わり。テストは時計を注入する)
713
735
  */
714
- async function readStoryDigest(repoRoot, renderMarkdown, editUrlOf = () => null, today = todayJst(), ledgerDir) {
736
+ async function readStoryDigest(repoRoot, renderMarkdown, editUrlOf = () => null, today = storyTodayJst(), ledgerDir) {
715
737
  const ledger = await loadStoryLedger(repoRoot, false, ledgerDir);
716
738
  if (!ledger)
717
739
  return null;
@@ -29,6 +29,8 @@ exports.retiredIdsOf = retiredIdsOf;
29
29
  * 3. **置き換えられた(もう有効でない・retired)=確定した置き換えが 1 本以上**
30
30
  * 4. 置き換え先は**全部・新しい順**(決定日の降順 → ID の降順・同じ ID は 1 回)
31
31
  * 5. 自分自身を指す・実在しない ID は数えない
32
+ * 6. **新しい側の決定日が古い側の決定日より前なら数えない**(`decision.supersede-direction`・向きが逆の置き換えは成り立たない)。
33
+ * 同じ日は数える。どちらかの日が読めないときも数える(engine の規則 6 と同じ)
32
34
  */
33
35
  const node_path_1 = __importDefault(require("node:path"));
34
36
  /**
@@ -88,6 +90,8 @@ function supersedesIndex(decisions) {
88
90
  const old = byKey.get(target);
89
91
  if (!old || old.key === d.key)
90
92
  continue; // 実在しない・自分自身
93
+ if (d.date && old.date && d.date < old.date)
94
+ continue; // 規則 6: 新しい側が日付で前なら向きが逆
91
95
  const entry = out.get(old.key) || { id: old.id, by: [], retired: false };
92
96
  if (entry.by.some((x) => decisionKeyOf(x.id) === d.key))
93
97
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.174",
3
+ "version": "0.12.177",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,