@takagaki/cortex-decisions-viewer 0.12.175 → 0.12.178

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
@@ -5123,12 +5123,6 @@ a.pager-btn:hover { text-decoration: none; }
5123
5123
  .li-basis.is-firm { color: #15803d; }
5124
5124
  .li-basis.is-machine { color: var(--muted); }
5125
5125
  .li-basis.is-plain { color: var(--text); }
5126
- /* 否定済み(not_aliases)の畳み。つながっている側より一段控えめに(#704) */
5127
- .li-denied { margin-top: 10px; font-size: 13px; }
5128
- .li-denied-summary { cursor: pointer; font-size: 12.5px; color: var(--muted); }
5129
- .li-denied-list { margin: 6px 0 0; padding-left: 20px; }
5130
- .li-denied-row { overflow-wrap: anywhere; color: var(--muted); }
5131
- .li-denied-kind { display: inline-block; min-width: 72px; margin-right: 8px; font-weight: 700; }
5132
5126
  /* 違っていたときにどうするかの断り。.ch-note と同じ置き方(節の末尾の小さな段落) */
5133
5127
  .li-note { font-size: 12px; color: var(--muted); line-height: 1.7; margin: 12px 0 0; }
5134
5128
  /* ---- レコードの削除(?op=delete-record)----
@@ -6052,6 +6046,7 @@ button.ops-card-btn:disabled { opacity: .6; cursor: default; }
6052
6046
  .evidence-text-ref { color: var(--text); }
6053
6047
  .evidence-quotes { margin: 4px 0 2px; padding-left: 10px; border-left: 3px solid var(--border); }
6054
6048
  .evidence-quote { font-size: 13.5px; color: var(--text); line-height: 1.6; margin: 2px 0; white-space: pre-wrap; word-break: break-word; }
6049
+ .evidence-quote-time { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
6055
6050
  .evidence-quote.is-context { font-size: 12px; color: var(--muted); }
6056
6051
  .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
6052
  .evidence-more, .evidence-fold-btn { border: none; background: none; padding: 0 2px; font-size: 12.5px; color: var(--accent); cursor: pointer; }
@@ -6908,14 +6903,6 @@ ${story_1.splitByNameLinks.toString()}
6908
6903
  el("span", { text: "この記録を確認済みにしました。一覧などの表示は、数分後に画面が作り直されたときに反映されます。" }),
6909
6904
  ]), note);
6910
6905
  }
6911
- /**
6912
- * 画面に出す提案(**答え済みの名寄せの提案を除く**・#704)。
6913
- *
6914
- * 🔴 claim_key の ID が、記録の identities(同じ人と答えた)か not_aliases(別人と答えた)に
6915
- * もう在るなら、そのカードは**出さない**——従来のカードにも落とさない。人が答えたあと、
6916
- * engine がカードを取り下げるまでの夜(最長 14 日)に同じ問いを出し続けると、
6917
- * [別人です]を押し直して宣言した行を黙って切る形になる
6918
- */
6919
6906
 
6920
6907
  // 状態の絞り込み。未確認のみ(draft)の選択肢だけを持つ
6921
6908
  function statusLabel(v) {
@@ -6983,6 +6970,8 @@ ${story_1.splitByNameLinks.toString()}
6983
6970
  function evidenceQuote(q) {
6984
6971
  var box = el("div", { class: "evidence-quote" + (q.context ? " is-context" : "") });
6985
6972
  if (q.context) box.appendChild(el("span", { class: "evidence-context-tag", text: "前の発言" }));
6973
+ // 会議の発言の場所(録画の頭からの経過)。読めた発言だけ「28:15 ごろ」を頭に添える
6974
+ 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
6975
  var who = q.speaker ? String(q.speaker) + ":" : "";
6987
6976
  var chars = Array.from(String(q.text || ""));
6988
6977
  var body = el("span", { class: "evidence-quote-text" });
@@ -19539,10 +19528,7 @@ ${story_1.splitByNameLinks.toString()}
19539
19528
  // 数値 id で組むと開いた先に誰も居ない(または別人が出る)
19540
19529
  var LI_BACKLOG_USER = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
19541
19530
  var LI_BACKLOG_DOMAIN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.backlog\\.(?:jp|com)$/i;
19542
- var LI_NOTE = "違うものがあれば「AIで編集」から「この ID は私ではない」と伝えると、翌晩から結ばなくなります。";
19543
- // 否定済み(not_aliases)の畳みの見出し。🔴 **中は値だけでリンクにしない**——
19544
- // 「私ではない」と言った先へ飛ばす口を出すと、つながっている側と見分けがつかなくなる
19545
- var LI_DENIED_TITLE = "これは私ではない(否定済み)";
19531
+ var LI_NOTE = "この ID があなたでない・この人でないときは、ポータルの人のページの ID 一覧で答えてください。";
19546
19532
 
19547
19533
  /** 種類 → 道具の読める名前。語彙の外はその綴りのまま */
19548
19534
  function liKindLabel(kind) {
@@ -19784,73 +19770,38 @@ ${story_1.splitByNameLinks.toString()}
19784
19770
  return out;
19785
19771
  }
19786
19772
  /**
19787
- * 否定済み(not_aliases)の行。**文字列の要素だけ**を、書かれた順のまま返す。
19788
- * <種類>:<値> の形なら道具の名前と値に割り、素の文字列はそのまま値として出す
19789
- */
19790
- function deniedIdRows(rec) {
19791
- var list = rec && rec.fm ? rec.fm.not_aliases : null;
19792
- if (!Array.isArray(list)) return [];
19793
- var out = [];
19794
- list.forEach(function (one) {
19795
- if (typeof one !== "string") return;
19796
- var whole = one.trim();
19797
- if (whole === "") return;
19798
- var typed = LI_TYPED_ID.exec(whole);
19799
- out.push({ kindLabel: typed ? liKindLabel(typed[1]) : "", value: typed ? typed[2] : whole });
19800
- });
19801
- return out;
19802
- }
19803
- /**
19804
- * 「つながっている ID」の折りたたみ。**関係者以外の記録と、つながっている ID も
19805
- * 否定済みも 0 件のときは null**(節ごと出さない=欄が無い記録は今までと同じに見える)。
19773
+ * 「つながっている ID」の折りたたみ。**関係者以外の記録と、つながっている ID が
19774
+ * 0 件のときは null**(節ごと出さない=欄が無い記録は今までと同じに見える)。
19806
19775
  *
19807
19776
  * 🔴 **表に table を使わない**(画面の部品に table は1つも無い)。.fm-grid と同じ
19808
19777
  * 書き方のグリッドで組む。値は全部 el() の text(textContent 経由)で入れる。
19809
- * 🔴 **否定済みは同じ節の中に、さらに畳んで出す**(開かなければ見えない)。
19810
- * つながっている ID が無く否定済みだけある記録にも節を出す——「前に否定したか」を
19811
- * 確かめる口が画面から消えないように
19778
+ * 🔴 **記録の not_aliases(否定済み)は読まない・画面に出さない**(member.no-email)。
19779
+ * 否定の答えを持つのはポータルで、見る・取り消すのはポータルの ID 一覧
19812
19780
  */
19813
19781
  function buildLinkedIds(rec) {
19814
19782
  if (!isMemberRec(rec)) return null;
19815
19783
  var rows = linkedIdRows(rec);
19816
- var denied = deniedIdRows(rec);
19817
- if (!rows.length && !denied.length) return null;
19818
- var head = "つながっている ID(" + rows.length + "件" + (denied.length ? "・否定済み " + denied.length + "件" : "") + ")";
19784
+ if (!rows.length) return null;
19785
+ var head = "つながっている ID(" + rows.length + "件)";
19819
19786
  var det = el("details", { class: "linked-ids" }, [
19820
19787
  el("summary", { class: "li-summary", text: head }),
19821
19788
  ]);
19822
- if (rows.length) {
19823
- var grid = el("div", { class: "li-grid" });
19824
- LI_COLS.forEach(function (col) {
19825
- grid.appendChild(el("div", { class: "li-cell li-head", text: col }));
19826
- });
19827
- rows.forEach(function (row) {
19828
- grid.appendChild(el("div", { class: "li-cell li-kind", text: row.kindLabel }));
19829
- var valueCell = el("div", { class: "li-cell li-value" });
19830
- valueCell.appendChild(row.href
19831
- ? el("a", { href: row.href, target: "_blank", rel: "noopener", text: row.value })
19832
- : el("span", { text: row.value }));
19833
- grid.appendChild(valueCell);
19834
- grid.appendChild(el("div", { class: "li-cell li-basis " + row.tone, text: row.basisText }));
19835
- grid.appendChild(el("div", { class: "li-cell li-since", text: row.since }));
19836
- grid.appendChild(el("div", { class: "li-cell li-scope", text: row.scope }));
19837
- });
19838
- det.appendChild(grid);
19839
- }
19840
- if (denied.length) {
19841
- var box = el("details", { class: "li-denied" }, [
19842
- el("summary", { class: "li-denied-summary", text: LI_DENIED_TITLE + "(" + denied.length + "件)" }),
19843
- ]);
19844
- var ul = el("ul", { class: "li-denied-list" });
19845
- denied.forEach(function (d) {
19846
- var li = el("li", { class: "li-denied-row" });
19847
- if (d.kindLabel) li.appendChild(el("span", { class: "li-denied-kind", text: d.kindLabel }));
19848
- li.appendChild(el("span", { class: "li-denied-value", text: d.value }));
19849
- ul.appendChild(li);
19850
- });
19851
- box.appendChild(ul);
19852
- det.appendChild(box);
19853
- }
19789
+ var grid = el("div", { class: "li-grid" });
19790
+ LI_COLS.forEach(function (col) {
19791
+ grid.appendChild(el("div", { class: "li-cell li-head", text: col }));
19792
+ });
19793
+ rows.forEach(function (row) {
19794
+ grid.appendChild(el("div", { class: "li-cell li-kind", text: row.kindLabel }));
19795
+ var valueCell = el("div", { class: "li-cell li-value" });
19796
+ valueCell.appendChild(row.href
19797
+ ? el("a", { href: row.href, target: "_blank", rel: "noopener", text: row.value })
19798
+ : el("span", { text: row.value }));
19799
+ grid.appendChild(valueCell);
19800
+ grid.appendChild(el("div", { class: "li-cell li-basis " + row.tone, text: row.basisText }));
19801
+ grid.appendChild(el("div", { class: "li-cell li-since", text: row.since }));
19802
+ grid.appendChild(el("div", { class: "li-cell li-scope", text: row.scope }));
19803
+ });
19804
+ det.appendChild(grid);
19854
19805
  // 違っていたときにどうするか。.ch-note と同じ置き方(節の末尾の小さな段落)
19855
19806
  det.appendChild(el("p", { class: "li-note", text: LI_NOTE }));
19856
19807
  return det;
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.175",
3
+ "version": "0.12.178",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,