@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/build.js CHANGED
@@ -41,11 +41,8 @@ exports.sanitizeBody = sanitizeBody;
41
41
  exports.findProjectIcon = findProjectIcon;
42
42
  exports.readFleetSources = readFleetSources;
43
43
  exports.orderRecordsByLedgers = orderRecordsByLedgers;
44
- exports.humanTouchedOf = humanTouchedOf;
45
- exports.attachHumanTouched = attachHumanTouched;
46
44
  exports.rawFrontmatterDate = rawFrontmatterDate;
47
45
  exports.attachSupersedes = attachSupersedes;
48
- exports.attachRetractedBy = attachRetractedBy;
49
46
  exports.readGoldProposals = readGoldProposals;
50
47
  exports.buildHomeGrounding = buildHomeGrounding;
51
48
  exports.collectMeetings = collectMeetings;
@@ -58,7 +55,7 @@ exports.bodyStampId = bodyStampId;
58
55
  exports.bodyStampComment = bodyStampComment;
59
56
  exports.build = build;
60
57
  exports.decisionFirmnessOf = decisionFirmnessOf;
61
- exports.decisionTopicOf = decisionTopicOf;
58
+ exports.decisionStrandOf = decisionStrandOf;
62
59
  exports.decidedByOf = decidedByOf;
63
60
  exports.isRelatedUrl = isRelatedUrl;
64
61
  exports.relatedMaterialsRead = relatedMaterialsRead;
@@ -80,21 +77,8 @@ const story_1 = require("./story");
80
77
  const ranking_1 = require("./ranking");
81
78
  const hands_1 = require("./hands");
82
79
  const rederivation_1 = require("./rederivation");
83
- // 直近のGold昇格で追加されたレコードのパス集合。レコード生成の各所から参照するのでモジュールスコープに置く
84
- // (引数で引き回すと呼び出し側の改修が広範囲になる)。build() の冒頭で1回だけ解決する。
85
- let LATEST_PATHS = new Set();
86
- /**
87
- * このレコードが「直近のGold昇格で追加されたもの」か。
88
- *
89
- * **照合の手前で NFC に正規化する。** 集合側(git 由来)は NFC で入っているが、
90
- * こちらのパスは readdir 由来なので、ファイルシステムが NFD で返す環境(macOS の
91
- * 一部の経路)では**同じ日本語名でも一致しない**。正規化はこの1箇所に集める。
92
- */
93
- function isLatestPath(filePath) {
94
- return LATEST_PATHS.has(filePath.normalize("NFC"));
95
- }
96
80
  // AIからの更新提案(`.cortex/gold-proposals.json` 由来)を、レコードのパスで引ける形にしたもの。
97
- // **LATEST_PATHS と同じ置き方にしている**——どちらも「レコードを組み立てるその場で、
81
+ // **レコードを組み立てるその場で、
98
82
  // リポジトリルート相対のパスを鍵に引く」もので、引き回すと parseDecision / parseGeneric の
99
83
  // 呼び出し側を全部触ることになる。build() の冒頭で毎回入れ替える(同一プロセスで
100
84
  // 2回ビルドしても前回の値が残らないように)。
@@ -118,7 +102,6 @@ function takeProposals(filePath) {
118
102
  const external_id_1 = require("./external-id");
119
103
  const home_project_1 = require("./home-project");
120
104
  const supersedes_1 = require("./supersedes");
121
- const confirmed_1 = require("./confirmed");
122
105
  const minify_1 = require("./minify");
123
106
  const render_1 = require("./render");
124
107
  const ledger_dir_1 = require("./ledger-dir");
@@ -730,45 +713,6 @@ function orderRecordsByLedgers(records, recordsDir, ctx, nowIso) {
730
713
  const pathOf = (r) => (recordsRel ? `${recordsRel}/${r.fileName}` : r.fileName);
731
714
  return (0, rederivation_1.orderByActivity)(records, pathOf, (r) => r.rstatus, ctx.ledgers, nowIso);
732
715
  }
733
- /**
734
- * 記録に人の手があるか(cortex-engine #1094 段 4・削除の確認の文面を替える材料)。
735
- *
736
- * 次のどちらかで真:
737
- * 1. status が人の値(口の `humanOwned`・区画の語彙。旧い記録の「確認済み」)
738
- * 2. 確認の帳簿(`.cortex/gold-confirmation.json`)にその path の `confirmed` の行がある(消した行を除く)
739
- *
740
- * 🔴 **ビルドの git は使わない**(確定版 0 章 7)。cortex-bot の夜のコミットを人と数え、浅い clone では
741
- * 全部が人になるため。engine の人の手の表がビューアに届く段 5 で差し替える。
742
- * 取りこぼしの代償は「確認の一言が出ない」だけ(消せる範囲は変わらない)。
743
- *
744
- * 記憶 v2 の案件で人の手の表の写し(`memory-hands`)が読めたときは、2 の確認の帳簿の代わりに表の `touched` を見る
745
- * (cortex-engine #1094 鎖 3 V10b・B9a。確認の帳簿は記憶 v2 では書かれなくなる)。表が読めない晩は今の確認の帳簿に倒す。
746
- *
747
- * @param p リポジトリルート相対の path(`Cortex/<区画>/records/…`)。帳簿の鍵は NFC なので NFC でも引く
748
- * @param status その記録の `rstatus`(`String()` 済み)
749
- * @param hands 人の手の表(path → touched)。渡したときは確認の帳簿を見ない
750
- */
751
- function humanTouchedOf(p, status, confirmation, hands = null) {
752
- if ((0, confirmed_1.humanOwned)(p, status))
753
- return true;
754
- if (hands)
755
- return hands.get(p) === true || hands.get(p.normalize("NFC")) === true;
756
- const row = confirmation.get(p) ?? confirmation.get(p.normalize("NFC"));
757
- return !!row && row.deleted !== true;
758
- }
759
- /**
760
- * 区画の記録に人の手の印(`humanTouched: true`)を載せる。**人の手が無い記録にはキーごと付けない**
761
- * (今の SiteData の形を保つ)。path は `repoRoot` 起点で作る(帳簿の鍵はリポジトリルート相対)。
762
- * 置き換えの逆引き(`attachSupersedes`)より前に呼ぶ。
763
- */
764
- function attachHumanTouched(records, recordsDir, repoRoot, confirmation, hands = null) {
765
- const recordsRel = path.relative(repoRoot, recordsDir).split(path.sep).join("/");
766
- for (const r of records) {
767
- const p = recordsRel ? `${recordsRel}/${r.fileName}` : r.fileName;
768
- if (humanTouchedOf(p, r.rstatus, confirmation, hands))
769
- r.humanTouched = true;
770
- }
771
- }
772
716
  /**
773
717
  * 決定に置き換えの逆引き(置き換え先・置き換えた決定)を載せる(cortex-engine #699 F3・A-1')。
774
718
  *
@@ -800,7 +744,7 @@ function rawFrontmatterDate(raw) {
800
744
  function attachSupersedes(decisions, rawDateOf = (d) => d.fm.date, pathOf = () => undefined) {
801
745
  const input = decisions.map((d) => ({
802
746
  id: d.id,
803
- // 口(`humanOwned`)に区画を伝える path。無ければ写しが決定の区画で聞く
747
+ // 記録の path(engine と入力の形を揃えるだけで、逆引きには使わない)
804
748
  path: pathOf(d),
805
749
  // 🔴 **frontmatter に書かれたままの date の文字列**(`rawFrontmatterDate`)。gray-matter は時刻・時差つきの
806
750
  // date を Date に読み、sanitizeFm は UTC の日付にするので、`2026-07-01T01:00:00+09:00` が 6/30 にずれる。
@@ -844,35 +788,6 @@ function attachSupersedes(decisions, rawDateOf = (d) => d.fm.date, pathOf = () =
844
788
  d.supersedesIds = replaced;
845
789
  }
846
790
  }
847
- /**
848
- * ルールに取り下げ(`relations[retracted_by]`・cortex-engine #1094 便 4-2d)を載せる。
849
- *
850
- * 取り下げ=この線が 1 本以上(決定の「置き換えられた」と同じ導出・engine の目次と同じ)。
851
- * target は取り下げを決めた決定の ID。実在する決定なら記録の綴りと決定日を添える(無ければ ID のまま・日付は空)。
852
- * **取り下げの無いルール・ルール以外の区画にはキーごと付けない**(今までと同じ SiteData)
853
- */
854
- function attachRetractedBy(records, decisions, rawDateOf = (d) => d.date) {
855
- const byKey = new Map();
856
- for (const d of decisions) {
857
- const k = (0, supersedes_1.decisionKeyOf)(d.id);
858
- if (!byKey.has(k))
859
- byKey.set(k, d);
860
- }
861
- for (const r of records) {
862
- const out = [];
863
- for (const rel of r.relations) {
864
- if (rel.rel !== "retracted_by")
865
- continue;
866
- const k = (0, supersedes_1.decisionKeyOf)(rel.target);
867
- if (!k || out.some((x) => (0, supersedes_1.decisionKeyOf)(x.id) === k))
868
- continue;
869
- const d = byKey.get(k);
870
- out.push({ id: d ? d.id : k, date: d ? (0, supersedes_1.decisionDateOf)(rawDateOf(d), d.id) : "" });
871
- }
872
- if (out.length)
873
- r.retractedBy = out;
874
- }
875
- }
876
791
  /**
877
792
  * AIからの更新提案(`.cortex/gold-proposals.json`)を読む。
878
793
  *
@@ -1198,7 +1113,6 @@ async function parseGeneric(raw, fileName, repoRelDir, repoBaseUrl, branch, sect
1198
1113
  editUrl,
1199
1114
  rtype: data.type != null ? String(data.type) : "",
1200
1115
  rstatus: data.status != null ? String(data.status) : "",
1201
- isLatest: isLatestPath(filePath),
1202
1116
  proposals: takeProposals(filePath),
1203
1117
  relations: toRelations(data.relations),
1204
1118
  source: data.source != null ? String(data.source) : "",
@@ -2413,11 +2327,6 @@ async function buildSite(opts, views) {
2413
2327
  branch: opts.branch,
2414
2328
  cwd: dirAbs,
2415
2329
  });
2416
- // 直近のGold昇格で追加されたレコードを git から解決する(レコードの date は決定日なので生成時期が分からない)。
2417
- // git が返すのはリポジトリルート相対のパス(`Cortex/...`)なので、判定も**リポジトリルート**で行う
2418
- // (Decisionsディレクトリで実行すると `-- Cortex/` に一致せず常に空になる)。
2419
- // 浅いclone・git無しの環境では空集合が返り、「最新」ラベルが出ないだけで他の表示は壊れない。
2420
- LATEST_PATHS = (0, repo_1.latestGeneratedPaths)(process.cwd());
2421
2330
  // Cortex構成なら、ホームと「records/ を持つディレクトリ」をすべて読み込む(旧構成では空)
2422
2331
  // ディレクトリ名はハードコードしない: 案件が新しいレコード種別のディレクトリを足せば、そのままタブとして表示される
2423
2332
  const knowledgeRoot = resolveKnowledgeRoot(dirAbs);
@@ -2458,7 +2367,7 @@ async function buildSite(opts, views) {
2458
2367
  // 用語集の並びの帳簿(活性度・強さ #790)。**帳簿が無い案件では並び替えを作らない**。
2459
2368
  // 🔴 **強さ(`state` が on のときだけ中身が入る)だけの案件でも器を作る**——作らないと、活性度の帳簿を
2460
2369
  // 持たない案件で用語集が目次(engine の `termRows`)と違う順になる。
2461
- // 確認の帳簿(`confirmation`)は削除の確認の一言(`humanTouchedOf`)が使うが、並びには関係しない
2370
+ // 確認の帳簿(`confirmation`)は並びには関係しない
2462
2371
  const ledgers = await (0, rederivation_1.readRederivationLedgers)(repoRoot, ledgerDir);
2463
2372
  const rederive = ledgers.activity.size || ledgers.strength.size
2464
2373
  ? { ledgers: ledgers, repoRoot: repoRoot }
@@ -2472,9 +2381,6 @@ async function buildSite(opts, views) {
2472
2381
  // 「記憶 v2 の案件か」は 1 本の栓 `memory_v2_writer` の値で決める(cortex-engine #1094 鎖 3 V10b)。橋の間は写しとの OR
2473
2382
  const memoryHands = await (0, hands_1.readMemoryHands)(repoRoot, ledgerDir);
2474
2383
  const memoryV2 = (0, hands_1.isMemoryV2)(opts.memoryV2Writer, memoryHands !== null);
2475
- // 削除の確認の一言の材料(B9a): 記憶 v2 の案件で表が読めたときは表、ほかは今の確認の帳簿
2476
- // (表が読めた案件は必ず記憶 v2 なので、表そのものを渡せばよい)
2477
- const touchedBy = memoryHands;
2478
2384
  // 記録の id → リポジトリルート相対のパス(順位のファイルの鍵と突き合わせる。`repoRoot` 起点・cwd 起点にしない)
2479
2385
  const recordRelPaths = new Map();
2480
2386
  const relOf = (dir, fileName) => {
@@ -2493,8 +2399,6 @@ async function buildSite(opts, views) {
2493
2399
  if (rd !== undefined)
2494
2400
  rawDates.set(rec, rd);
2495
2401
  }
2496
- // 人の手(削除の確認の文面を替える材料・cortex-engine #1094 段 4)。**置き換えより前に・repoRoot で**
2497
- attachHumanTouched(decisions, dirAbs, repoRoot, ledgers.confirmation, touchedBy);
2498
2402
  // 置き換えの逆引き(#699 F3)。**置き換えの無い案件では 1 件も付かない**
2499
2403
  attachSupersedes(decisions, (d) => rawDates.get(d) ?? d.fm.date, (d) => relOf(dirAbs, d.fileName));
2500
2404
  let home = null;
@@ -2535,14 +2439,13 @@ async function buildSite(opts, views) {
2535
2439
  if (!e.isDirectory() || e.name === "Decisions")
2536
2440
  continue;
2537
2441
  const recordsDir = path.join(knowledgeRoot, e.name, "records");
2538
- const records = await loadGenericRecords(recordsDir, exclude, repo.baseUrl, repo.branch);
2539
- // 人の手(削除の確認の文面を替える材料・cortex-engine #1094 段 4)
2540
- attachHumanTouched(records, recordsDir, repoRoot, ledgers.confirmation, touchedBy);
2442
+ let records = await loadGenericRecords(recordsDir, exclude, repo.baseUrl, repo.branch);
2443
+ // ルールは取り下げたらファイルを消す(cortex-engine #1094)。案件の旧い記録の片付けより画面が先に出ても
2444
+ // 旧い取り下げが普通に見えないよう、`retracted_by` を 1 本以上持つルールは一覧(と件数・つながり)から外す
2445
+ if (e.name === "Rules")
2446
+ records = records.filter((r) => !r.relations.some((x) => x.rel === "retracted_by"));
2541
2447
  // 関連の資料(cortex-engine #1094 F8)。ルール・用語だけ・欄の無い記録には何も付かない
2542
2448
  attachRelatedMaterials(e.name, records, refs);
2543
- // ルールの取り下げ(`retracted_by`・cortex-engine #1094 便 4-2d)。ルールの区画だけ・取り下げの無い案件では何も付かない
2544
- if (e.name === "Rules")
2545
- attachRetractedBy(records, decisions, (d) => rawDates.get(d) ?? d.fm.date);
2546
2449
  // 用語集の並び(活性度・強さ)。**帳簿を持たない案件では並びもそのまま**
2547
2450
  const rederived = orderRecordsByLedgers(records, recordsDir, rederive, opts.generatedAt);
2548
2451
  // 関係者だけ、関与の多い順(engine の目次と同じ `order`・便 14b)。用語集の並び
@@ -2742,17 +2645,13 @@ async function parseDecision(raw, fileName, repoRelDir, repoBaseUrl, branch, ref
2742
2645
  const id = String(data.id ?? fileName.replace(/\.md$/i, ""));
2743
2646
  const title = String(data.title ?? stripIdPrefix(fileName));
2744
2647
  const date = normalizeDate(data.date);
2745
- const sprint = data.sprint != null ? String(data.sprint) : "";
2746
- const category = data.category != null ? String(data.category) : "";
2747
2648
  const summary = data.description != null ? String(data.description) : data.summary != null ? String(data.summary) : ""; // description優先(旧summaryは移行期互換)
2748
- const deciders = toStringArray(data.deciders);
2749
2649
  const referencesRaw = toStringArray(data.references);
2750
2650
  const references = referencesRaw.map((r) => refs.resolve(r));
2751
2651
  const relations = toRelations(data.relations);
2752
- const followupHuman = (0, rederivation_1.humanFollowupOf)(data.followup);
2753
2652
  // 記憶 v2 の任意の欄(#1094)。**欄が無い・語彙外の記録ではキーごと載せない**(埋め込みの形を今と同じに保つ)
2754
2653
  const firmness = decisionFirmnessOf(data.firmness);
2755
- const topic = decisionTopicOf(data.topic);
2654
+ const strand = decisionStrandOf(data.strand);
2756
2655
  const decidedBy = decidedByOf(data.decided_by);
2757
2656
  // 関連の資料(#1094 F8)。通る URL が 0 本ならキーごと載せない
2758
2657
  const relatedMaterials = relatedMaterialsView(data, refs);
@@ -2766,9 +2665,6 @@ async function parseDecision(raw, fileName, repoRelDir, repoBaseUrl, branch, ref
2766
2665
  id,
2767
2666
  title,
2768
2667
  date,
2769
- sprint,
2770
- category,
2771
- deciders,
2772
2668
  summary,
2773
2669
  references,
2774
2670
  relations,
@@ -2778,16 +2674,13 @@ async function parseDecision(raw, fileName, repoRelDir, repoBaseUrl, branch, ref
2778
2674
  bodyId: "",
2779
2675
  searchText: toSearchText(content),
2780
2676
  rstatus: data.status != null ? String(data.status) : "",
2781
- isLatest: isLatestPath(filePath),
2782
2677
  proposals: takeProposals(filePath),
2783
2678
  fileName,
2784
2679
  editUrl,
2785
2680
  fm: sanitizeFm(data),
2786
2681
  fmErrors: validateFm(data),
2787
- // 担当者の答え(#705)。**語彙外・形が違うものはキーごと載せない**
2788
- ...(followupHuman ? { followupHuman: followupHuman } : {}),
2789
2682
  ...(firmness ? { firmness } : {}),
2790
- ...(topic ? { topic } : {}),
2683
+ ...(strand ? { strand } : {}),
2791
2684
  ...(decidedBy ? { decidedBy } : {}),
2792
2685
  ...(relatedMaterials ? { relatedMaterials } : {}),
2793
2686
  };
@@ -2810,8 +2703,8 @@ const isText = (v) => typeof v === "string" && v.trim() !== "";
2810
2703
  function decisionFirmnessOf(value) {
2811
2704
  return exports.DECISION_FIRMNESS.includes(value) ? value : undefined;
2812
2705
  }
2813
- /** frontmatter の `topic` → 論点の札の名前(前後の空白を落とす)。空・文字列でないものは undefined */
2814
- function decisionTopicOf(value) {
2706
+ /** frontmatter の `strand` → 筋の札の名前(前後の空白を落とす)。空・文字列でないものは undefined(`topic` は読まない) */
2707
+ function decisionStrandOf(value) {
2815
2708
  return isText(value) ? value.trim() : undefined;
2816
2709
  }
2817
2710
  /** `decided_by` の要素 1 つ。読めなければ null(文字列・知らない鍵・member と name/url の同居・member: で始まらない・空の name・http(s) でない url) */
@@ -2835,7 +2728,7 @@ function decidedByEntryOf(entry) {
2835
2728
  }
2836
2729
  /**
2837
2730
  * frontmatter の `decided_by` → 決めた人の並び。🔴 形の崩れた要素は**飛ばす**。
2838
- * 欄がリストでなければ undefined(無いものとして扱い、旧い `deciders` を出す)。空のリストは []
2731
+ * 欄がリストでなければ undefined(無いものとして扱う)。空のリストは []
2839
2732
  */
2840
2733
  function decidedByOf(value) {
2841
2734
  if (!Array.isArray(value))
@@ -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.HUMAN_FOLLOWUP_STATES = exports.emptyLedgers = exports.TERM_STRENGTH_FILE = exports.LEDGER_VERSION = void 0;
6
+ exports.emptyLedgers = exports.TERM_STRENGTH_FILE = exports.LEDGER_VERSION = void 0;
7
7
  exports.isConfirmedStatus = isConfirmedStatus;
8
8
  exports.parseLedger = parseLedger;
9
9
  exports.readRederivationLedgers = readRederivationLedgers;
@@ -14,8 +14,6 @@ exports.comparableContent = comparableContent;
14
14
  exports.sameRederivedContent = sameRederivedContent;
15
15
  exports.contentShaOf = contentShaOf;
16
16
  exports.orderByActivity = orderByActivity;
17
- exports.followupDateOf = followupDateOf;
18
- exports.humanFollowupOf = humanFollowupOf;
19
17
  /**
20
18
  * 再導出の 3 帳簿(+用語の強さの帳簿)を読み、帯「人が確認した版から変わりました」と用語集の並びを作る(#665 便 12b)。
21
19
  *
@@ -70,10 +68,6 @@ const MIN_ELAPSED_DAYS = 0.5;
70
68
  * 🔴 **渡すのは `String()` 済みの `rstatus` だけ**(`build.ts:1048`)。生の frontmatter
71
69
  * (`rec.fm.status`)を渡さない——YAML は真偽値も配列も返すので、札と同じ文字列にならない
72
70
  * (口は文字列・null・undefined のほかを投げる)。
73
- *
74
- * ⚠️ **「人の手」は別にある。** 削除の確認の文面を替える `rec.humanTouched`(`build.ts` の
75
- * `humanTouchedOf`)は口の `humanOwned`(区画の語彙)か確認の帳簿の行で、`""` の扱いがここと逆
76
- * (人の手として**扱う**側は `humanOwned`、機械が触る前の**守り**は `!machineOwned` という割り当て)。
77
71
  */
78
72
  function isConfirmedStatus(status, recordPath = null) {
79
73
  return !(0, confirmed_1.machineOwned)(recordPath, status);
@@ -596,54 +590,3 @@ function breakTiesByStrength(records, pathOf, ledgers, nowIso) {
596
590
  }
597
591
  return out;
598
592
  }
599
- /* ---------------------------------------------------------------------------
600
- * 決定の「その後」(人の答え)
601
- * ---------------------------------------------------------------------------
602
- *
603
- * 画面が読むのは、人が frontmatter に書いた `followup:` の 1 行だけ(#1094 35d)。
604
- * 夜間の帳簿(`.cortex/gold-decision-followup.json`)は画面に出さない・読まない
605
- * (PR のマージを完了と数える差 97 を画面から外すため)。
606
- */
607
- /** 人が frontmatter に書く `followup.state` の語彙(intake `FOLLOWUP_STATES` と同じ 4 語) */
608
- exports.HUMAN_FOLLOWUP_STATES = ["confirmed", "done", "dropped", "ignore"];
609
- /**
610
- * 日付を `YYYY-MM-DD` に畳む。
611
- *
612
- * 🔴 **engine の `at` は日付とは限らない**(`decision-links.mjs` の `terminalStateOf` は
613
- * `merged_at` / `updated_at` の **ISO 日時**をそのまま入れる)。時刻つきのものは
614
- * **JST の日付**に直す——UTC の先頭 10 文字を切ると、日本の夜に閉じた課題が前日に見える。
615
- * 読めないもの(null・数・壊れた文字列)は空。
616
- */
617
- function followupDateOf(v) {
618
- if (v instanceof Date)
619
- return Number.isFinite(v.getTime()) ? v.toISOString().slice(0, 10) : "";
620
- const s = typeof v === "string" ? v.trim() : "";
621
- if (!s)
622
- return "";
623
- if (/^\d{4}-\d{2}-\d{2}$/.test(s))
624
- return s;
625
- if (!/^\d{4}-\d{2}-\d{2}T/.test(s))
626
- return "";
627
- const ms = Date.parse(s);
628
- if (!Number.isFinite(ms))
629
- return "";
630
- return new Date(ms + 9 * 3600 * 1000).toISOString().slice(0, 10);
631
- }
632
- /**
633
- * frontmatter の `followup:`(人が画面のボタンで書く 1 行)を読む。
634
- *
635
- * 形は `followup: { state: done, at: 2026-09-20 }`(intake `setFollowupLine` が書く)。
636
- * YAML は `at` を Date に読むので日付に戻す。**語彙外・形が違うものは `null`**(載せない)。
637
- */
638
- function humanFollowupOf(v) {
639
- const o = asRecord(v);
640
- if (!o)
641
- return null;
642
- const state = typeof o.state === "string" ? o.state : "";
643
- if (!exports.HUMAN_FOLLOWUP_STATES.includes(state))
644
- return null;
645
- const at = followupDateOf(o.at);
646
- if (!at)
647
- return null;
648
- return { state: state, at: at };
649
- }