@takagaki/cortex-decisions-viewer 0.12.163 → 0.12.165

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
@@ -2367,11 +2367,18 @@ async function buildSite(opts, views) {
2367
2367
  // ストーリー(#901 V2-5)の日本時間の今日。日・週・月の3つで同じ日にそろえるため1回だけ計算する
2368
2368
  const storyToday = (0, story_1.todayJst)(new Date(opts.generatedAt));
2369
2369
  // ストーリーの日々の動き(#901 V2-5)。帳簿で on の日のファイルが無ければ null(キーごと出さない)
2370
+ const shownIds = new Set([...decisions.map((d) => d.id), ...sections.flatMap((s) => s.records.map((r) => r.id))]);
2371
+ const idByRepoPath = new Map();
2372
+ for (const [id, rel] of recordRelPaths)
2373
+ if (shownIds.has(id))
2374
+ idByRepoPath.set(rel, id);
2370
2375
  const story = await (0, story_1.readStory)(repoRoot, renderGroundingSection, (p) => repo.baseUrl ? `${repo.baseUrl}/edit/${repo.branch}/${encodePath(p)}` : null,
2371
2376
  // 動きのない日を埋める終点=ビルドの時刻の日本時間の日付(時計を注入できる)
2372
2377
  storyToday, ledgerDir,
2373
2378
  // 名前のリンク(story.screen-name-links): 日の relations の関係者・用語・ルールを、画面にある記録の表示名・別名・同義語で本文に当てる
2374
- (targets) => (0, story_1.storyNameLinks)(targets, storyNameSources(sections)));
2379
+ (targets) => (0, story_1.storyNameLinks)(targets, storyNameSources(sections)),
2380
+ // 本文の相対リンク(決定の記録など)の飛び先: 画面に載る記録(決定・各区画)のパス → id
2381
+ (repoPath) => idByRepoPath.get(repoPath));
2375
2382
  // 週・月のあらすじ(#901)。週・月のファイルが無ければ null(キーごと出さない=今までと同じ画面)
2376
2383
  const storyDigest = await (0, story_1.readStoryDigest)(repoRoot, renderGroundingSection, (p) => repo.baseUrl ? `${repo.baseUrl}/edit/${repo.branch}/${encodePath(p)}` : null, storyToday, ledgerDir);
2377
2384
  // 案件アイコン(#348)。Cortex構成でなければ undefined(旧レイアウトには置き場が無い)
package/dist/render.js CHANGED
@@ -2679,9 +2679,11 @@ function reflectedStateOf(rows, capId) {
2679
2679
  /**
2680
2680
  * ISO 時刻を「Gold の夜の日」の表示(`M/D`)にする(cortex-engine #968 設計 v1 §3)。
2681
2681
  *
2682
- * **engine の `nightDayOf`(−12 時間)と同じ切り方。** Gold は JST 22:00 に始まり日をまたいで
2683
- * 終わるので、素の日付だと同じ晩の記録が「9/28」と「9/29」に分かれる。12 時間引いてから
2684
- * JST の日付を読むと、22:00 に始まった晩はいつも開始した日の日付になる。
2682
+ * **engine の `NIGHT_CUTOFF_HOURS = 6`(`scripts/memory-v2/night-result.mjs`・日本時間 6 時)と
2683
+ * 同じ区切り。** Gold は JST 22:00 に始まり日をまたいで終わるので、素の日付だと同じ晩の記録が
2684
+ * 「9/28」と「9/29」に分かれる。6 時間引いてから JST の日付を読むと、JST 6:00 より前は前の日の
2685
+ * 晩、6:00 以降は当日の晩になる。🔴 区切りの数(6)はこの関数の `NIGHT_CUTOFF_HOURS` 1 か所だけに
2686
+ * 書く(engine と必ずそろえる。`.toString()` で渡すので関数の中に置く)。
2685
2687
  *
2686
2688
  * **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す)。
2687
2689
  * @returns `M/D`(ゼロ埋めなし)。読めない値は null
@@ -2691,10 +2693,11 @@ function jstNightLabel(iso) {
2691
2693
  if (!Number.isFinite(ms))
2692
2694
  return null;
2693
2695
  const JST_MS = 9 * 60 * 60 * 1000;
2694
- const d = new Date(ms - 12 * 60 * 60 * 1000 + JST_MS);
2696
+ const NIGHT_CUTOFF_HOURS = 6;
2697
+ const d = new Date(ms - NIGHT_CUTOFF_HOURS * 60 * 60 * 1000 + JST_MS);
2695
2698
  return (d.getUTCMonth() + 1) + "/" + d.getUTCDate();
2696
2699
  }
2697
- /** JST の暦日(`YYYY-MM-DD`)。`jstNightLabel` と違い −12 時間をしない(素の日付の比較用) */
2700
+ /** JST の暦日(`YYYY-MM-DD`)。`jstNightLabel` と違い夜の区切りで引かない(素の日付の比較用) */
2698
2701
  function jstCalendarDay(ms) {
2699
2702
  const JST_MS = 9 * 60 * 60 * 1000;
2700
2703
  const d = new Date(ms + JST_MS);
package/dist/story.js CHANGED
@@ -19,6 +19,7 @@ exports.isEngineQuietDay = isEngineQuietDay;
19
19
  exports.todayJst = todayJst;
20
20
  exports.quietStoryRow = quietStoryRow;
21
21
  exports.withQuietDays = withQuietDays;
22
+ exports.linkRecordRefs = linkRecordRefs;
22
23
  exports.readStory = readStory;
23
24
  exports.storyWeekHeading = storyWeekHeading;
24
25
  exports.storyMonthHeading = storyMonthHeading;
@@ -75,19 +76,29 @@ const MACHINE_WORD_RE = /urn:|cortex-engine|sha=|<!--|-->|\.md\b|Days\/|story-da
75
76
  * 門に掛ける前にリンクの文字だけに剝がす。外の URL(`https://…`・`<https://…>`)は対象外(そのまま残す)。
76
77
  * 本文に素で書かれた `foo.md` はここでは剝がさず、`MACHINE_WORD_RE` が行ごと伏せる
77
78
  */
78
- const RELATIVE_MD_LINK_RE = /\[([^\]\n]*)\]\(\s*<?(?![a-zA-Z][a-zA-Z0-9+.-]*:|\/\/|#)[^)\s<>]*\.md(?:#[^)\s<>]*)?>?\s*\)/g;
79
+ const RELATIVE_MD_LINK_RE = /\[([^\]\n]*)\]\(\s*<?(?![a-zA-Z][a-zA-Z0-9+.-]*:|\/\/|#)([^)\s<>]*\.md)(?:#[^)\s<>]*)?>?\s*\)/g;
79
80
  const LINK_OPEN_RE = /\[[^\]\n]*\]\([^)\s]*/g;
80
81
  /**
81
82
  * 相対 `.md` のリンクを文字に剝がす。剝がした結果、剝がす前の行に無かった `](` のリンクが現れる行(入れ子のリンクなど)は、
82
83
  * 剝がさず元のまま返す(`.md` が残るので `MACHINE_WORD_RE` が行ごと伏せる)。後ろの関所に頼らない
83
84
  */
84
- const unlinkRelativeMd = (line) => {
85
+ const unlinkRelativeMd = (line, keepRecordRefs = false) => {
85
86
  const out = line.replace(RELATIVE_MD_LINK_RE, "$1");
86
87
  if (out === line)
87
88
  return line;
88
89
  const before = new Set(line.match(LINK_OPEN_RE) ?? []);
89
- return (out.match(LINK_OPEN_RE) ?? []).some((m) => !before.has(m)) ? line : out;
90
+ if ((out.match(LINK_OPEN_RE) ?? []).some((m) => !before.has(m)))
91
+ return line;
92
+ if (!keepRecordRefs)
93
+ return out;
94
+ // 記録(`…/records/<名前>.md`)を指すものは、`.md` を落とした印のリンク `#rec:<相対パス>` にして残す(門の `.md` の伏せを通すため)。
95
+ // 画面の中の飛び先への差し替えは `linkRecordRefs`(build が記録の表を握って渡す)。それ以外の相対リンクは今までどおり文字に剝がす
96
+ return line.replace(RELATIVE_MD_LINK_RE, (_m, text, href) => RECORD_HREF_RE.test(href) ? `[${text}](${RECORD_REF_PREFIX}${href.slice(0, -3)})` : text);
90
97
  };
98
+ /** 記録の相対パス(`…/records/<名前>.md`) */
99
+ const RECORD_HREF_RE = /(?:^|\/)records\/[^/]+\.md$/;
100
+ /** 記録を指すリンクの印。門(`.md` の伏せ)と関所(相対 URL を許す)を通り、`linkRecordRefs` が飛び先に差し替える */
101
+ const RECORD_REF_PREFIX = "#rec:";
91
102
  /** 太字だけの 1 行(筋の名前の行) */
92
103
  const BOLD_ONLY_RE = /^\*\*[^*]+\*\*$/;
93
104
  /** 見出しの行(`##`〜`######`) */
@@ -346,7 +357,7 @@ function storyDayMarkdown(content, day) {
346
357
  }
347
358
  // 機械の行: 印と機械の語の行を伏せる
348
359
  // 決定の記録へのリンクは、門に掛ける前に文字だけに剝がす(行ごと伏せない)
349
- const clean = (ls) => ls.map(unlinkRelativeMd).filter((l) => !STORY_MARKER_RE.test(l.trim()) && !MACHINE_WORD_RE.test(l));
360
+ const clean = (ls) => ls.map((l) => unlinkRelativeMd(l, true)).filter((l) => !STORY_MARKER_RE.test(l.trim()) && !MACHINE_WORD_RE.test(l));
350
361
  // 🔴 人の追記: 落とすのは**印の行だけ**(人が書いた行を画面が黙って消さない・実装レビュー 🟠2)
351
362
  const humanKept = human.filter((l) => !STORY_MARKER_RE.test(l.trim()));
352
363
  const out = withoutEmptySections(withoutEmptyBoldHeads([...clean(main), ...clean(lateAfter)]));
@@ -466,6 +477,35 @@ async function loadStoryLedger(repoRoot, warn, ledgerDir) {
466
477
  }
467
478
  return ledger;
468
479
  }
480
+ /**
481
+ * 本文(関所を通した HTML)の相対リンクのうち、記録(`Cortex/<区画>/records/*.md`)を指すものを
482
+ * 画面の中のその記録への飛び先(人名・用語のリンクと同じ `a.wikilink[data-go]`)にする。
483
+ *
484
+ * - 相対パスはその日のファイルの置き場から解く(`../../../Decisions/records/x.md` → `Cortex/Decisions/records/x.md`)
485
+ * - 🔴 記録が見つからない(`idOf` が undefined)相対リンクは `<a>` を外して文字のまま残す(壊れたリンク・行き止まりを作らない)
486
+ * - 外の URL(http・https・mailto など `scheme:`・`//`)とページ内の `#…` は触らない
487
+ *
488
+ * 呼び手は**サニタイズ後**の HTML を渡す(属性値・文字はエスケープ済み。ここで新しい HTML は混ぜず、data-go の値だけ & と " を逃がす)
489
+ */
490
+ function linkRecordRefs(html, dayRepoPath, idOf) {
491
+ const baseDir = node_path_1.default.posix.dirname(dayRepoPath);
492
+ return html.replace(/<a href="([^"]*)">([\s\S]*?)<\/a>/g, (whole, href, inner) => {
493
+ const isRef = href.startsWith(RECORD_REF_PREFIX);
494
+ if (!isRef && (/^[a-z][a-z0-9+.-]*:/i.test(href) || href.startsWith("//") || href.startsWith("#")))
495
+ return whole;
496
+ let target = isRef ? href.slice(RECORD_REF_PREFIX.length).replace(/&amp;/g, "&") + ".md" : href.replace(/&amp;/g, "&").replace(/[?#].*$/, "");
497
+ try {
498
+ target = decodeURIComponent(target);
499
+ }
500
+ catch { /* 解けない綴りはそのまま照合して見つからなければ文字に落とす */ }
501
+ const resolved = target.startsWith("/") ? node_path_1.default.posix.normalize(target.slice(1)) : node_path_1.default.posix.join(baseDir, target);
502
+ const id = /^Cortex\/[^/]+\/records\/[^/]+\.md$/.test(resolved) ? idOf(resolved) : undefined;
503
+ if (!id)
504
+ return inner;
505
+ const attr = id.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
506
+ return `<a class="wikilink" data-go="${attr}">${inner}</a>`;
507
+ });
508
+ }
469
509
  /**
470
510
  * 案件のストーリーを読む。**載せる日が無ければ `null`**(タブも器も出さない)。
471
511
  *
@@ -479,7 +519,9 @@ today = todayJst(),
479
519
  // 帳簿の置き場(既定は `<repoRoot>/.cortex`)
480
520
  ledgerDir,
481
521
  // 日の `relations` の target から名前のリンクの表を作る口(build が記録の表を握って渡す。無ければリンクは付けない)
482
- linksOf = () => []) {
522
+ linksOf = () => [],
523
+ // 本文の相対リンクが指す記録(リポジトリ相対パス)の id。画面にある記録だけ返す(無ければリンクは文字に落とす)
524
+ recordIdOf = () => undefined) {
483
525
  const ledger = await loadStoryLedger(repoRoot, true, ledgerDir);
484
526
  if (!ledger)
485
527
  return null;
@@ -543,7 +585,7 @@ linksOf = () => []) {
543
585
  const links = linksOf(dayRelationTargets(fm));
544
586
  const partialHour = dayPartialHour(fm);
545
587
  out.push({
546
- date: f.day, heading: heading, bodyHtml: await renderMarkdown(markdown),
588
+ date: f.day, heading: heading, bodyHtml: linkRecordRefs(await renderMarkdown(markdown), f.rel, recordIdOf),
547
589
  ...(editUrl ? { editUrl: editUrl } : {}),
548
590
  ...(partialHour !== null ? { partialHour: partialHour } : {}),
549
591
  ...(links.length ? { links: links } : {}),
@@ -613,7 +655,7 @@ function storyDigestParts(content) {
613
655
  const end = body.findIndex((l) => DIGEST_END_RE.test(l.trim()));
614
656
  const at = body.findIndex((l, i) => i > end && l.trim() === HUMAN_HEADING);
615
657
  // 機械の部分の `#` の見出しの行は落とす(流れは段落だけの約束。見出しが紛れると後ろの文が別の節に見える)
616
- const main = withoutHiddenBoldGroups((at < 0 ? body : body.slice(0, at)).map(unlinkRelativeMd).filter((l) => !STORY_MARKER_RE.test(l.trim()) && !/^\s{0,3}#{1,6}(\s|$)/.test(l)))
658
+ const main = withoutHiddenBoldGroups((at < 0 ? body : body.slice(0, at)).map((l) => unlinkRelativeMd(l)).filter((l) => !STORY_MARKER_RE.test(l.trim()) && !/^\s{0,3}#{1,6}(\s|$)/.test(l)))
617
659
  .filter((l) => !MACHINE_WORD_RE.test(l));
618
660
  const human = (at < 0 ? [] : body.slice(at + 1)).filter((l) => !STORY_MARKER_RE.test(l.trim()));
619
661
  let phase = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.163",
3
+ "version": "0.12.165",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,