@takagaki/cortex-decisions-viewer 0.12.79 → 0.12.80

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,6 +41,7 @@ exports.sanitizeBody = sanitizeBody;
41
41
  exports.findProjectIcon = findProjectIcon;
42
42
  exports.readFleetSources = readFleetSources;
43
43
  exports.attachRederived = attachRederived;
44
+ exports.attachDecisionFollowup = attachDecisionFollowup;
44
45
  exports.readGoldProposals = readGoldProposals;
45
46
  exports.buildHomeGrounding = buildHomeGrounding;
46
47
  exports.collectMeetings = collectMeetings;
@@ -761,6 +762,31 @@ readBlobs = repo_1.readBlobsAt) {
761
762
  }
762
763
  return (0, rederivation_1.orderByActivity)(records, pathOf, statusOf, ctx.ledgers, nowIso);
763
764
  }
765
+ /**
766
+ * 決定に「その後」(`.cortex/gold-decision-followup.json`・#705)を載せる。
767
+ *
768
+ * 🔴 **突き合わせはビルド時にここで閉じる**(`attachRederived` と同じ)。path は
769
+ * **`repoRoot` 起点**で作る——帳簿の鍵は常にリポジトリルート相対(`Cortex/Decisions/records/…`)。
770
+ * 画面に出す状態の結び付きが 1 件も無い決定には**キーごと付けない**(`shownFollowupOf`)。
771
+ *
772
+ * @returns 帳簿の行のうち、どの決定にも突き合わなかった path(ビルドログに 1 行出す材料)
773
+ */
774
+ function attachDecisionFollowup(decisions, recordsDir, repoRoot, ledger) {
775
+ const recordsRel = path.relative(repoRoot, recordsDir).split(path.sep).join("/");
776
+ const matched = new Set();
777
+ for (const d of decisions) {
778
+ // 帳簿の鍵(git 由来)は NFC。readdir が NFD で返す環境でも突き合うように揃える
779
+ const p = (recordsRel ? `${recordsRel}/${d.fileName}` : d.fileName).normalize("NFC");
780
+ const row = ledger.get(p);
781
+ if (!row)
782
+ continue;
783
+ matched.add(p);
784
+ const shown = (0, rederivation_1.shownFollowupOf)(row);
785
+ if (shown)
786
+ d.followup = shown;
787
+ }
788
+ return [...ledger.keys()].filter((k) => !matched.has(k.normalize("NFC")));
789
+ }
764
790
  /**
765
791
  * AIからの更新提案(`.cortex/gold-proposals.json`)を読む。
766
792
  *
@@ -2031,6 +2057,17 @@ async function build(opts) {
2031
2057
  const rec = await parseDecision(raw, fileName, repoRelDir, repo.baseUrl, repo.branch, refs);
2032
2058
  decisions.push(rec);
2033
2059
  }
2060
+ // 決定の「その後」(#705)。**帳簿を持たない案件では 1 件も付かず、絞り込みの印も立たない**
2061
+ // (`decisionFollowup` はキーごと無い=今までと 1 バイトも同じ画面)
2062
+ const followupLedger = await (0, rederivation_1.readDecisionFollowup)(repoRoot);
2063
+ if (followupLedger) {
2064
+ const unmatchedFollowup = attachDecisionFollowup(decisions, dirAbs, repoRoot, followupLedger);
2065
+ // **突き合わなかった行を 1 行残す**(path が 1 文字違うだけで画面から静かに消えるので)
2066
+ if (unmatchedFollowup.length) {
2067
+ console.warn(`gold-decision-followup.json: 決定に突き合わない行が ${unmatchedFollowup.length} 件ありました(表示しません): ` +
2068
+ unmatchedFollowup.slice(0, 3).join(", ") + (unmatchedFollowup.length > 3 ? " ほか" : ""));
2069
+ }
2070
+ }
2034
2071
  let home = null;
2035
2072
  let homeGrounding;
2036
2073
  let homeUpdatedAt;
@@ -2163,6 +2200,8 @@ async function build(opts) {
2163
2200
  projectKey: opts.projectKey,
2164
2201
  }
2165
2202
  : undefined,
2203
+ // 帳簿を読めた案件だけ true(無い・壊れ・版違いはキーごと出さない)
2204
+ decisionFollowup: followupLedger ? true : undefined,
2166
2205
  meetings,
2167
2206
  // **URLとトークンが両方そろったときだけ出す。** 片方だけでは貼れるURLを作れないので、
2168
2207
  // 手順だけが出て「専用URLが空」という一番混乱する形になる(intake と同じ扱い)
@@ -2263,6 +2302,7 @@ async function parseDecision(raw, fileName, repoRelDir, repoBaseUrl, branch, ref
2263
2302
  const referencesRaw = toStringArray(data.references);
2264
2303
  const references = referencesRaw.map((r) => refs.resolve(r));
2265
2304
  const relations = toRelations(data.relations);
2305
+ const followupHuman = (0, rederivation_1.humanFollowupOf)(data.followup);
2266
2306
  // 本文先頭の H1 はタイトルと重複するため除去(詳細画面ではタイトルを別途表示する)
2267
2307
  const bodyHtml = await renderRecordBody(content);
2268
2308
  const filePath = repoRelDir ? `${repoRelDir}/${fileName}` : fileName;
@@ -2291,6 +2331,8 @@ async function parseDecision(raw, fileName, repoRelDir, repoBaseUrl, branch, ref
2291
2331
  editUrl,
2292
2332
  fm: sanitizeFm(data),
2293
2333
  fmErrors: validateFm(data),
2334
+ // 担当者の答え(#705)。**語彙外・形が違うものはキーごと載せない**
2335
+ ...(followupHuman ? { followupHuman: followupHuman } : {}),
2294
2336
  };
2295
2337
  }
2296
2338
  function createRefResolver(opts) {
@@ -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.emptyLedgers = exports.LEDGER_VERSION = void 0;
6
+ exports.HUMAN_FOLLOWUP_STATES = exports.FOLLOWUP_EVIDENCE_VERDICTS = exports.FOLLOWUP_SHOWN_STATES = exports.DECISION_FOLLOWUP_FILE = exports.emptyLedgers = exports.LEDGER_VERSION = void 0;
7
7
  exports.isConfirmedStatus = isConfirmedStatus;
8
8
  exports.parseLedger = parseLedger;
9
9
  exports.readRederivationLedgers = readRederivationLedgers;
@@ -15,6 +15,10 @@ exports.contentShaOf = contentShaOf;
15
15
  exports.rederivedContentKey = rederivedContentKey;
16
16
  exports.rederivedBandOf = rederivedBandOf;
17
17
  exports.orderByActivity = orderByActivity;
18
+ exports.followupDateOf = followupDateOf;
19
+ exports.readDecisionFollowup = readDecisionFollowup;
20
+ exports.shownFollowupOf = shownFollowupOf;
21
+ exports.humanFollowupOf = humanFollowupOf;
18
22
  /**
19
23
  * 再導出の 3 帳簿を読み、帯「人が確認した版から変わりました」と用語集の並びを作る(#665 便 12b)。
20
24
  *
@@ -527,3 +531,164 @@ function orderByActivity(records, pathOf, statusOf, ledgers, nowIso) {
527
531
  })
528
532
  .map((s) => s.r);
529
533
  }
534
+ /* ---------------------------------------------------------------------------
535
+ * 決定の「その後」の帳簿(#705・engine #509 便 4)
536
+ * ---------------------------------------------------------------------------
537
+ *
538
+ * `.cortex/gold-decision-followup.json` は夜間(engine `scripts/gold/decision-followup.mjs`)が
539
+ * 型(`scripts/ledger/`)を通して書く。形は
540
+ * `{ version: 1, venues: [...], sections: { Decisions: { <path>: { decided_on, state, links, candidates, evidence, checked_on } } } }`。
541
+ *
542
+ * 🔴 **帳簿が無い・壊れている・版が違う案件は「今までと 1 バイトも同じ画面」**(上の 3 帳簿と同じ規律)。
543
+ * 読めないものは `null`(=帳簿を持たない案件と同じ)として続ける。
544
+ *
545
+ * 🔴 **便 3 の `evidence[]` は形が未確定**。知らない verdict・欠けた欄は行ごと捨てる
546
+ * (落ちない・知らないものを画面へ運ばない)。
547
+ */
548
+ /** 帳簿のファイル名(`.cortex/` の下) */
549
+ exports.DECISION_FOLLOWUP_FILE = "gold-decision-followup.json";
550
+ /**
551
+ * 画面に出す結び付きの状態(engine `decision-followup-run.mjs` の `LINKED_STATES` と同じ 6 語)。
552
+ *
553
+ * 🔴 **`venue_unavailable` / `link_unresolved` / `no_link` / `source_missing` は出さない**
554
+ * (発注書 #705)。「未接続のため」「記録なし」の文を画面に並べない——出来事だけを出す。
555
+ */
556
+ exports.FOLLOWUP_SHOWN_STATES = [
557
+ "issue_closed", "pr_merged", "issue_open", "superseded", "same_day", "pr_closed",
558
+ ];
559
+ /** 結び付きの種類(`material` は出典の資料で、`source_missing` にしか現れないので載せない) */
560
+ const FOLLOWUP_LINK_KINDS = ["issue", "pr", "gh", "decision"];
561
+ /** 便 3 の verdict のうち画面が知っているもの。**知らない値は行ごと捨てる** */
562
+ exports.FOLLOWUP_EVIDENCE_VERDICTS = ["done", "changed", "review"];
563
+ /** 人が frontmatter に書く `followup.state` の語彙(intake `FOLLOWUP_STATES` と同じ 4 語) */
564
+ exports.HUMAN_FOLLOWUP_STATES = ["confirmed", "done", "dropped", "ignore"];
565
+ /** http(s) の URL だけを通す(`javascript:` などを画面へ運ばない) */
566
+ const httpUrl = (v) => {
567
+ const s = typeof v === "string" ? v.trim() : "";
568
+ return /^https?:\/\//i.test(s) ? s : "";
569
+ };
570
+ /**
571
+ * 日付を `YYYY-MM-DD` に畳む。
572
+ *
573
+ * 🔴 **engine の `at` は日付とは限らない**(`decision-links.mjs` の `terminalStateOf` は
574
+ * `merged_at` / `updated_at` の **ISO 日時**をそのまま入れる)。時刻つきのものは
575
+ * **JST の日付**に直す——UTC の先頭 10 文字を切ると、日本の夜に閉じた課題が前日に見える。
576
+ * 読めないもの(null・数・壊れた文字列)は空。
577
+ */
578
+ function followupDateOf(v) {
579
+ if (v instanceof Date)
580
+ return Number.isFinite(v.getTime()) ? v.toISOString().slice(0, 10) : "";
581
+ const s = typeof v === "string" ? v.trim() : "";
582
+ if (!s)
583
+ return "";
584
+ if (/^\d{4}-\d{2}-\d{2}$/.test(s))
585
+ return s;
586
+ if (!/^\d{4}-\d{2}-\d{2}T/.test(s))
587
+ return "";
588
+ const ms = Date.parse(s);
589
+ if (!Number.isFinite(ms))
590
+ return "";
591
+ return new Date(ms + 9 * 3600 * 1000).toISOString().slice(0, 10);
592
+ }
593
+ /**
594
+ * 決定の「その後」の帳簿を読む。
595
+ *
596
+ * @returns path → 行。**帳簿が無い・壊れている・版が違うときは `null`**(=帳簿を持たない案件)。
597
+ * 読めたが行が 0 件のときは空の Map(帳簿を持つ案件として扱う=絞り込みの「記録なし」が出うる)
598
+ */
599
+ async function readDecisionFollowup(repoRoot) {
600
+ let raw;
601
+ try {
602
+ raw = await promises_1.default.readFile(node_path_1.default.join(repoRoot, ".cortex", exports.DECISION_FOLLOWUP_FILE), "utf8");
603
+ }
604
+ catch (e) {
605
+ // 無いのが既定(栓 `decision_followup` を入れた案件だけが持つ)。無いことは警告しない
606
+ if (e?.code !== "ENOENT") {
607
+ console.warn(`${exports.DECISION_FOLLOWUP_FILE} を読めませんでした(その後の記録は出さずに続けます): ${String(e)}`);
608
+ }
609
+ return null;
610
+ }
611
+ const parsed = parseLedger(raw);
612
+ if (!parsed) {
613
+ // 壊れ・版違いは 1 行残す(「夜間がまだ書いていない」と切り分けるため)
614
+ console.warn(`${exports.DECISION_FOLLOWUP_FILE} の形式か version が想定と違います(その後の記録は出さずに続けます)`);
615
+ return null;
616
+ }
617
+ const out = new Map();
618
+ const sections = asRecord(parsed.sections);
619
+ // 🔴 **区画は Decisions だけを見る**(engine の `SECTION`)。他の名前の節が増えても拾わない
620
+ const rows = asRecord(sections?.Decisions);
621
+ if (!rows)
622
+ return out;
623
+ for (const [p, v] of Object.entries(rows)) {
624
+ const row = asRecord(v);
625
+ if (!row)
626
+ continue;
627
+ const links = [];
628
+ if (Array.isArray(row.links)) {
629
+ for (const l of row.links) {
630
+ const link = asRecord(l);
631
+ if (!link)
632
+ continue;
633
+ const kind = typeof link.kind === "string" ? link.kind : "";
634
+ const key = typeof link.key === "string" ? link.key : "";
635
+ const state = typeof link.state === "string" ? link.state : "";
636
+ if (!FOLLOWUP_LINK_KINDS.includes(kind) || !key || !state)
637
+ continue;
638
+ links.push({ kind: kind, key: key, state: state, at: followupDateOf(link.at), url: httpUrl(link.url) });
639
+ }
640
+ }
641
+ const evidence = [];
642
+ if (Array.isArray(row.evidence)) {
643
+ for (const e of row.evidence) {
644
+ const ev = asRecord(e);
645
+ if (!ev)
646
+ continue;
647
+ const verdict = typeof ev.verdict === "string" ? ev.verdict : "";
648
+ // 🔴 **知らない verdict は捨てる**(便 3 の語彙は未確定。知らない語を画面の文にしない)
649
+ if (!exports.FOLLOWUP_EVIDENCE_VERDICTS.includes(verdict))
650
+ continue;
651
+ evidence.push({
652
+ verdict: verdict,
653
+ quote: typeof ev.quote === "string" ? ev.quote : "",
654
+ url: httpUrl(ev.url),
655
+ // engine の今の根拠行は `{id, venue, quote, verdict, judged_at}`(`occurred_at` を持たない)。
656
+ // 無ければ判定した日で代える——形は便 3 で合わせる(docs/product-decisions.md)
657
+ at: followupDateOf(ev.occurred_at) || followupDateOf(ev.judged_at),
658
+ });
659
+ }
660
+ }
661
+ out.set(p, { links: links, evidence: evidence });
662
+ }
663
+ return out;
664
+ }
665
+ /**
666
+ * 帳簿の行から、画面に出すぶんだけを残す。**出す結び付きが 1 件も無ければ `null`**
667
+ * (節を作らない・一覧の印も付かない)。
668
+ */
669
+ function shownFollowupOf(row) {
670
+ if (!row)
671
+ return null;
672
+ const links = row.links.filter((l) => exports.FOLLOWUP_SHOWN_STATES.includes(l.state));
673
+ if (!links.length)
674
+ return null;
675
+ return { links: links, evidence: row.evidence };
676
+ }
677
+ /**
678
+ * frontmatter の `followup:`(人が画面のボタンで書く 1 行)を読む。
679
+ *
680
+ * 形は `followup: { state: done, at: 2026-09-20 }`(intake `setFollowupLine` が書く)。
681
+ * YAML は `at` を Date に読むので日付に戻す。**語彙外・形が違うものは `null`**(載せない)。
682
+ */
683
+ function humanFollowupOf(v) {
684
+ const o = asRecord(v);
685
+ if (!o)
686
+ return null;
687
+ const state = typeof o.state === "string" ? o.state : "";
688
+ if (!exports.HUMAN_FOLLOWUP_STATES.includes(state))
689
+ return null;
690
+ const at = followupDateOf(o.at);
691
+ if (!at)
692
+ return null;
693
+ return { state: state, at: at };
694
+ }
package/dist/render.js CHANGED
@@ -4971,6 +4971,25 @@ button.site-icon[aria-busy="true"] .site-icon-plus { opacity: 0; }
4971
4971
  border: 1px solid rgba(194,94,63,0.95); white-space: nowrap;
4972
4972
  }
4973
4973
  .ai-apply-btn:hover { background: rgba(194,94,63,0.92); text-decoration: none; }
4974
+ /* 決定の「その後」(#705)。一覧の印は .badge の並び、詳細は提案の節の後。
4975
+ 🔴 節は .followup-section(.followup は一覧の印の .badge にも付くので、節の余白を当てない) */
4976
+ .badge.followup { background: #f0fdf4; color: #166534; border: 1px solid #bbf7d0; }
4977
+ .badge.followup-open { background: #eff6ff; color: #1e40af; border-color: #bfdbfe; }
4978
+ .badge.followup-superseded, .badge.followup-dropped { background: #f5f5f4; color: #57534e; border-color: #d6d3d1; }
4979
+ .followup-section { margin: 14px 0 4px; }
4980
+ .followup-human { font-size: 13.5px; margin: 0 0 6px; color: #166534; }
4981
+ .followup-list, .followup-evidence { list-style: none; margin: 0 0 8px; padding: 0; }
4982
+ .followup-row, .followup-evidence-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: baseline; font-size: 13.5px; line-height: 1.7; padding: 2px 0; }
4983
+ .followup-kind { font-size: 12px; color: var(--muted); border: 1px solid var(--border, #e5e7eb); border-radius: 4px; padding: 0 6px; }
4984
+ .followup-key, .followup-at { font-size: 12.5px; word-break: break-all; }
4985
+ .followup-at { color: var(--muted); }
4986
+ .followup-quote { color: var(--muted); }
4987
+ .followup-actions { margin-top: 6px; }
4988
+ .followup-actions-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 6px; }
4989
+ .followup-actions-row[hidden] { display: none; }
4990
+ .followup-btn { font-size: 12.5px; padding: 4px 12px; border-radius: 6px; border: 1px solid var(--border, #d1d5db); background: #fff; cursor: pointer; }
4991
+ .followup-btn:disabled { opacity: .6; cursor: default; }
4992
+ .followup-btn-ng { color: #92400e; }
4974
4993
  /* 押したあとの帯。draft(黄・要対応)と入れ替わるので、済んだと分かる緑にする */
4975
4994
  .promoted-note {
4976
4995
  display: flex; gap: 8px; align-items: flex-start;
@@ -6524,25 +6543,332 @@ ${calendarAddUrl.toString()}
6524
6543
  }
6525
6544
 
6526
6545
  // 状態の絞り込み。**未確認と提案ありを同じセレクトに並べる**(導線を増やさない)
6546
+ var FOLLOWUP_FILTER_LABEL = { "followup-done": "その後: 完了", "followup-open": "その後: 進行中", "followup-none": "その後: 記録なし" };
6527
6547
  function statusLabel(v) {
6548
+ if (Object.prototype.hasOwnProperty.call(FOLLOWUP_FILTER_LABEL, v)) return FOLLOWUP_FILTER_LABEL[v];
6528
6549
  return v === "draft" ? "未確認のみ" : v === "proposed" ? "提案ありのみ" : v;
6529
6550
  }
6530
- /** 一覧に出す選択肢。**該当が1件も無い状態は選択肢に出さない**(押しても0件の項目を並べない) */
6531
- function statusFilterOptions(items) {
6551
+ /**
6552
+ * 一覧に出す選択肢。**該当が1件も無い状態は選択肢に出さない**(押しても0件の項目を並べない)
6553
+ *
6554
+ * @param withFollowup 「その後: …」を足すか。**決定一覧で、帳簿を読めた案件だけ true**
6555
+ * (SITE.decisionFollowup)——帳簿の無い案件で「記録なし」を全件に当てない
6556
+ */
6557
+ function statusFilterOptions(items, withFollowup) {
6532
6558
  var opts = [];
6533
6559
  if (hasUnverified(items)) opts.push("draft");
6534
6560
  if ((items || []).some(hasProposals)) opts.push("proposed");
6561
+ if (withFollowup) {
6562
+ ["followup-done", "followup-open", "followup-none"].forEach(function (v) {
6563
+ if ((items || []).some(function (r) { return matchesFollowupFilter(r, v); })) opts.push(v);
6564
+ });
6565
+ }
6535
6566
  return opts;
6536
6567
  }
6537
6568
  /**
6538
6569
  * 絞り込みの判定。**"proposed" は frontmatter の status ではない**ので、
6539
6570
  * rstatus との比較に混ぜない(混ぜると status: proposed の記録だけが引っかかる)
6571
+ *
6572
+ * 「その後: …」(followup-*)は**決定一覧だけ**の絞り込み。withFollowup が無い一覧
6573
+ * (用語集など・帳簿の無い案件)では絞らない——タブを移っても全件が消えないように
6540
6574
  */
6541
- function matchesStatusFilter(rec) {
6575
+ function matchesStatusFilter(rec, withFollowup) {
6542
6576
  if (!listState.status) return true;
6543
6577
  if (listState.status === "proposed") return hasProposals(rec);
6578
+ if (Object.prototype.hasOwnProperty.call(FOLLOWUP_FILTER_LABEL, listState.status)) return withFollowup ? matchesFollowupFilter(rec, listState.status) : true;
6544
6579
  return rec.rstatus === listState.status;
6545
6580
  }
6581
+ /* -------------------------------------------------------------------------
6582
+ * 決定の「その後」(#705・.cortex/gold-decision-followup.json と frontmatter の followup:)
6583
+ * -------------------------------------------------------------------------
6584
+ * **出すかどうかはビルド側が決めてある**(rederivation.ts の shownFollowupOf が、画面に出す
6585
+ * 6 状態の結び付きが 1 件以上ある決定にだけ rec.followup を付ける)。画面はキーの有無だけを見る。
6586
+ *
6587
+ * 🔴 **帳簿が無い案件・行が無い決定では要素を 1 つも作らない**(今までと 1 バイトも同じ DOM)。
6588
+ * 🔴 「未接続のため」「記録なし」の文は出さない——出来事だけを出す(発注書 #705)。
6589
+ */
6590
+ var FOLLOWUP_KIND_LABEL = { issue: "課題", pr: "PR", gh: "GitHub", decision: "決定" };
6591
+ var FOLLOWUP_STATE_LABEL = {
6592
+ issue_closed: "完了",
6593
+ pr_merged: "完了",
6594
+ issue_open: "進行中",
6595
+ superseded: "置き換え済み",
6596
+ // 🔴 engine は同じ日の終端を種類を問わず same_day に畳む(マージされなかった PR も入る)。
6597
+ // だから「完了」と言わない・一覧の印と「その後: 完了」の集計にも入れない
6598
+ same_day: "決定と同じ日に閉じた",
6599
+ pr_closed: "閉じた(マージなし)",
6600
+ };
6601
+ /** 便 3 の verdict。**知らない値は出さない**(ビルド側でも捨てている・二重の網) */
6602
+ var FOLLOWUP_VERDICT_LABEL = {
6603
+ done: "実施したと述べています",
6604
+ changed: "やめた・変えたと述べています",
6605
+ review: "見直し条件に触れています",
6606
+ };
6607
+ /** 担当者の答え(frontmatter の followup:)の 1 行。日付は括弧で添える */
6608
+ var FOLLOWUP_HUMAN_LABEL = {
6609
+ confirmed: "担当者が正しいと確認",
6610
+ done: "担当者が実施済みにしました",
6611
+ dropped: "担当者が取りやめと記録",
6612
+ ignore: "担当者が追わなくてよいと記録",
6613
+ };
6614
+ /** 押したあとの 1 行(「数分後に反映」は markPromoted と同じ言い方) */
6615
+ var FOLLOWUP_SENT_LABEL = {
6616
+ confirmed: "この記録が正しいと記録しました。",
6617
+ done: "実施済みにしました。",
6618
+ dropped: "取りやめたと記録しました。",
6619
+ ignore: "追わなくてよいと記録しました。",
6620
+ };
6621
+
6622
+ /** 画面に出す状態の結び付きがあるか(ビルド側でも絞ってある・語彙の外は数えない二重の網) */
6623
+ /** 表からラベルを引く(__proto__ や toString のような鍵で Object の中身を拾わない) */
6624
+ function followupLabel(table, k) {
6625
+ return Object.prototype.hasOwnProperty.call(table, k) ? table[k] : "";
6626
+ }
6627
+ function hasFollowupLinks(rec) {
6628
+ var links = (rec && rec.followup && rec.followup.links) || [];
6629
+ return links.some(function (lk) { return !!(lk && followupLabel(FOLLOWUP_STATE_LABEL, lk.state)); });
6630
+ }
6631
+ function hasFollowupHuman(rec) {
6632
+ return !!(rec && rec.followupHuman && followupLabel(FOLLOWUP_HUMAN_LABEL, rec.followupHuman.state));
6633
+ }
6634
+ /**
6635
+ * 鍵の表示。gh:owner/repo#88 → owner/repo#88(PR も同じ綴りで来る)。
6636
+ * 課題キーは engine が小文字に畳むので、**課題のときだけ大文字に戻す**(PJ_X-142)
6637
+ */
6638
+ function followupKeyText(kind, key) {
6639
+ var k = String(key || "");
6640
+ if (k.indexOf("gh:") === 0) return k.slice(3);
6641
+ return kind === "issue" ? k.toUpperCase() : k;
6642
+ }
6643
+ /** 置き換え先の決定。**SITE にあれば題名で ?id= へ、無ければ id だけ** */
6644
+ function followupDecisionEl(id) {
6645
+ var hit = null;
6646
+ (SITE.decisions || []).forEach(function (dd) { if (!hit && dd.id === id) hit = dd; });
6647
+ if (!hit) return el("span", { class: "mono", text: id });
6648
+ var a = el("a", { href: "?id=" + encodeURIComponent(id), text: hit.title || id });
6649
+ a.addEventListener("click", function (ev) { ev.preventDefault(); go(id); });
6650
+ return a;
6651
+ }
6652
+ function followupLinkRow(lk) {
6653
+ var li = el("li", { class: "followup-row" }, [
6654
+ el("span", { class: "followup-kind", text: followupLabel(FOLLOWUP_KIND_LABEL, lk.kind) || lk.kind }),
6655
+ ]);
6656
+ if (lk.state === "superseded" && lk.kind === "decision") {
6657
+ li.appendChild(el("span", { class: "followup-state", text: FOLLOWUP_STATE_LABEL.superseded + " → " }));
6658
+ li.appendChild(followupDecisionEl(lk.key));
6659
+ } else {
6660
+ li.appendChild(el("span", { class: "mono followup-key", text: followupKeyText(lk.kind, lk.key) }));
6661
+ li.appendChild(el("span", { class: "followup-state", text: followupLabel(FOLLOWUP_STATE_LABEL, lk.state) }));
6662
+ }
6663
+ if (lk.at) li.appendChild(el("span", { class: "mono followup-at", text: lk.at }));
6664
+ // http(s) だけ(ビルド側で落としてある・ここでも見る)
6665
+ if (lk.url && /^https?:\\/\\//i.test(lk.url)) {
6666
+ li.appendChild(el("a", { class: "followup-open", href: lk.url, target: "_blank", rel: "noopener", text: "開く" }));
6667
+ }
6668
+ return li;
6669
+ }
6670
+ function followupEvidenceRow(ev) {
6671
+ var label = followupLabel(FOLLOWUP_VERDICT_LABEL, ev.verdict);
6672
+ if (!label) return null;
6673
+ var li = el("li", { class: "followup-evidence-row" }, [
6674
+ el("span", { class: "followup-verdict", text: label }),
6675
+ ]);
6676
+ if (ev.quote) li.appendChild(el("q", { class: "followup-quote", text: ev.quote }));
6677
+ if (ev.url && /^https?:\\/\\//i.test(ev.url)) {
6678
+ li.appendChild(el("a", { class: "followup-open", href: ev.url, target: "_blank", rel: "noopener", text: "発言を開く" }));
6679
+ }
6680
+ return li;
6681
+ }
6682
+ function followupHumanLine(fh) {
6683
+ return el("p", { class: "followup-human", text: followupLabel(FOLLOWUP_HUMAN_LABEL, fh.state) + "(" + fh.at + ")" });
6684
+ }
6685
+
6686
+ /**
6687
+ * 詳細の節「その後の記録」。**出す結び付きも担当者の答えも無ければ null**(要素を作らない)。
6688
+ */
6689
+ function followupSection(rec) {
6690
+ if (!hasFollowupLinks(rec) && !hasFollowupHuman(rec)) return null;
6691
+ var box = el("section", { class: "followup-section" }, [
6692
+ el("div", { class: "section-label", text: "その後の記録" }),
6693
+ ]);
6694
+ if (hasFollowupHuman(rec)) box.appendChild(followupHumanLine(rec.followupHuman));
6695
+ if (hasFollowupLinks(rec)) {
6696
+ var ul = el("ul", { class: "followup-list" });
6697
+ rec.followup.links.forEach(function (lk) {
6698
+ if (followupLabel(FOLLOWUP_STATE_LABEL, lk.state)) ul.appendChild(followupLinkRow(lk));
6699
+ });
6700
+ if (ul.children.length) box.appendChild(ul);
6701
+ var evs = el("ul", { class: "followup-evidence" });
6702
+ (rec.followup.evidence || []).forEach(function (ev) {
6703
+ var row = followupEvidenceRow(ev);
6704
+ if (row) evs.appendChild(row);
6705
+ });
6706
+ if (evs.children.length) box.appendChild(evs);
6707
+ }
6708
+ var actions = followupActions(rec, box);
6709
+ if (actions) box.appendChild(actions);
6710
+ return box;
6711
+ }
6712
+
6713
+ // 担当者が直せるボタンを出すパス。**Decisions の records だけ**(守りはサーバ側の isFollowupPath)
6714
+ function followupRecordPath(rec) {
6715
+ var path = promotableRecordPath(rec);
6716
+ return path && path.indexOf("Cortex/Decisions/records/") === 0 ? path : "";
6717
+ }
6718
+ /** ブラウザの今日(YYYY-MM-DD・ローカル時刻) */
6719
+ function followupToday() {
6720
+ var d = new Date();
6721
+ return d.getFullYear() + "-" + ("0" + (d.getMonth() + 1)).slice(-2) + "-" + ("0" + d.getDate()).slice(-2);
6722
+ }
6723
+ /**
6724
+ * 「この記録は正しい」「違う」。**SITE.intake がある案件だけ**。
6725
+ * 「違う」を押すと「実施済みにする」「取りやめた」「追わなくてよい」が出る。
6726
+ * どれも押すと確認のモーダルを挟む(promote と同じ・押した瞬間に書かない)。
6727
+ */
6728
+ function followupActions(rec, box) {
6729
+ var path = followupRecordPath(rec);
6730
+ if (!path) return null;
6731
+ var wrap = el("div", { class: "followup-actions" });
6732
+ var row = el("div", { class: "followup-actions-row" });
6733
+ var okBtn = el("button", { class: "followup-btn", type: "button", text: "この記録は正しい" });
6734
+ var ngBtn = el("button", { class: "followup-btn followup-btn-ng", type: "button", text: "違う" });
6735
+ row.appendChild(okBtn);
6736
+ row.appendChild(ngBtn);
6737
+ wrap.appendChild(row);
6738
+ var choices = el("div", { class: "followup-actions-row followup-choices", hidden: "hidden" });
6739
+ [["done", "実施済みにする"], ["dropped", "取りやめた"], ["ignore", "追わなくてよい"]].forEach(function (c) {
6740
+ var b = el("button", { class: "followup-btn", type: "button", text: c[1] });
6741
+ b.addEventListener("click", function () {
6742
+ openFollowupModal(path, c[0], function (state, at) { markFollowupSent(rec, box, wrap, state, at); });
6743
+ });
6744
+ choices.appendChild(b);
6745
+ });
6746
+ wrap.appendChild(choices);
6747
+ okBtn.addEventListener("click", function () {
6748
+ openFollowupModal(path, "confirmed", function (state, at) { markFollowupSent(rec, box, wrap, state, at); });
6749
+ });
6750
+ ngBtn.addEventListener("click", function () {
6751
+ if (choices.hasAttribute("hidden")) choices.removeAttribute("hidden");
6752
+ else choices.setAttribute("hidden", "hidden");
6753
+ });
6754
+ return wrap;
6755
+ }
6756
+
6757
+ var FOLLOWUP_MODAL_TITLE = {
6758
+ confirmed: "この記録は正しいと記録する",
6759
+ done: "実施済みにする",
6760
+ dropped: "取りやめたと記録する",
6761
+ ignore: "追わなくてよいと記録する",
6762
+ };
6763
+ function openFollowupModal(path, state, onDone) {
6764
+ var mk = makeModal(FOLLOWUP_MODAL_TITLE[state], false);
6765
+ var modal = mk.modal;
6766
+ modal.appendChild(el("p", { class: "modal-desc", text: "この決定の記録に、担当者の答えとして 1 行を書き足します。" }));
6767
+ modal.appendChild(el("div", { class: "promote-target mono", text: path }));
6768
+ var dateInput = null;
6769
+ if (state === "done") {
6770
+ dateInput = el("input", { type: "date", class: "modal-date" });
6771
+ dateInput.value = followupToday();
6772
+ modal.appendChild(fieldRow("実施日", dateInput));
6773
+ }
6774
+ var nameInput = el("input", { type: "text", class: "modal-name", placeholder: "記録した人として残ります(省略できます)" });
6775
+ nameInput.value = rememberedVerifier();
6776
+ modal.appendChild(fieldRow("お名前(任意)", nameInput));
6777
+ var status = el("div", { class: "modal-status" });
6778
+ var cancelBtn = el("button", { class: "modal-cancel", type: "button", text: "閉じる" });
6779
+ var okBtn = el("button", { class: "ops-card-btn", type: "button", text: FOLLOWUP_MODAL_TITLE[state] });
6780
+ modal.appendChild(status);
6781
+ modal.appendChild(el("div", { class: "modal-actions" }, [cancelBtn, okBtn]));
6782
+ mk.overlay.appendChild(modal);
6783
+ showModal(mk.overlay, modal);
6784
+ cancelBtn.addEventListener("click", function () { mk.close(); });
6785
+ okBtn.addEventListener("click", function () {
6786
+ var at = dateInput ? String(dateInput.value || "").trim() : followupToday();
6787
+ sendFollowup(path, state, at, nameInput.value, status, okBtn, function () { mk.close(); onDone(state, at); });
6788
+ });
6789
+ }
6790
+
6791
+ async function sendFollowup(path, state, at, name, status, btn, onOk) {
6792
+ var intake = SITE.intake;
6793
+ if (!intake) return;
6794
+ function setStatus(msg, isError) {
6795
+ status.className = "modal-status" + (isError ? " modal-status-error" : "");
6796
+ status.textContent = msg;
6797
+ }
6798
+ // 日付は押す前にここでも見る(守りはサーバ側の isFollowupDate)
6799
+ if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(at)) {
6800
+ setStatus("✗ 日付を YYYY-MM-DD の形で入れてください。", true);
6801
+ return;
6802
+ }
6803
+ var endBusy = beginBusy(btn, "記録中…");
6804
+ setStatus("", false);
6805
+ var who = String(name || "").trim();
6806
+ var res, j;
6807
+ try {
6808
+ res = await callIntake("set-followup", { projectKey: intake.projectKey, path: path, state: state, at: at, submitter: who });
6809
+ j = await res.json().catch(function () { return {}; });
6810
+ } catch (e) {
6811
+ endBusy();
6812
+ setStatus("✗ サーバーに接続できませんでした。通信環境を確認して、もう一度お試しください。", true);
6813
+ return;
6814
+ }
6815
+ // 同じ値の押し直しもサーバは 200(unchanged)で返す——押した人の望む状態にはなっている
6816
+ if (res.ok) {
6817
+ if (who) { try { localStorage.setItem(verifierStoreKey(), who); } catch (e2) {} }
6818
+ endBusy();
6819
+ onOk();
6820
+ return;
6821
+ }
6822
+ endBusy();
6823
+ setStatus("✗ " + (j.message || "記録できませんでした。時間をおいて再度お試しください。"), true);
6824
+ }
6825
+
6826
+ /**
6827
+ * 押した結果を節に映す(markPromoted と同じ「数分後に反映」の言い方)。
6828
+ * **元データも直す**——一覧へ戻って開き直しても元に戻らないように
6829
+ */
6830
+ function markFollowupSent(rec, box, actions, state, at) {
6831
+ rec.followupHuman = { state: state, at: at };
6832
+ var note = el("div", { class: "promoted-note followup-sent" }, [
6833
+ el("span", { text: "✅" }),
6834
+ el("span", { text: FOLLOWUP_SENT_LABEL[state] + "一覧などの表示は、数分後に画面が作り直されたときに反映されます。" }),
6835
+ ]);
6836
+ if (actions.parentNode === box) box.replaceChild(note, actions);
6837
+ else box.appendChild(note);
6838
+ }
6839
+
6840
+ /**
6841
+ * 一覧の印の種類。**担当者の答えを優先**(done→実施済み・dropped→取りやめ。
6842
+ * confirmed / ignore は帳簿に従う)。無ければ帳簿の出す結び付きから:
6843
+ * 置き換え済み → 完了(issue_closed / pr_merged)→ 進行中(issue_open)の順。
6844
+ * pr_closed・same_day だけの決定には印を付けない(same_day は閉じ方を問わない語なので「完了」と言えない)。
6845
+ */
6846
+ function followupMarkOf(rec) {
6847
+ var fh = hasFollowupHuman(rec) ? rec.followupHuman : null;
6848
+ if (fh && fh.state === "done") return "done";
6849
+ if (fh && fh.state === "dropped") return "dropped";
6850
+ if (!hasFollowupLinks(rec)) return "";
6851
+ var states = rec.followup.links.map(function (lk) { return lk.state; });
6852
+ if (states.indexOf("superseded") !== -1) return "superseded";
6853
+ if (states.indexOf("issue_closed") !== -1 || states.indexOf("pr_merged") !== -1) return "complete";
6854
+ if (states.indexOf("issue_open") !== -1) return "open";
6855
+ return "";
6856
+ }
6857
+ var FOLLOWUP_MARK_LABEL = { done: "実施済み", dropped: "取りやめ", superseded: "置き換え済み", complete: "完了", open: "進行中" };
6858
+ /** 一覧の印(proposalBadge と同じ形)。**印が無ければ null**(要素を作らない) */
6859
+ function followupBadge(rec) {
6860
+ var mark = followupMarkOf(rec);
6861
+ return mark ? el("span", { class: "badge followup followup-" + mark, text: FOLLOWUP_MARK_LABEL[mark] }) : null;
6862
+ }
6863
+ /** 絞り込み「その後: …」の判定(帳簿を持つ案件の決定一覧だけで使う) */
6864
+ function matchesFollowupFilter(rec, v) {
6865
+ var mark = followupMarkOf(rec);
6866
+ if (v === "followup-done") return mark === "complete" || mark === "done";
6867
+ if (v === "followup-open") return mark === "open";
6868
+ if (v === "followup-none") return !hasFollowupLinks(rec) && !hasFollowupHuman(rec);
6869
+ return false;
6870
+ }
6871
+
6546
6872
  // 未確認が1件も無い一覧では絞り込みを出さない
6547
6873
  function hasUnverified(items) {
6548
6874
  return (items || []).some(function (r) { return r.rstatus === "draft"; });
@@ -17384,7 +17710,7 @@ ${calendarAddUrl.toString()}
17384
17710
  filters.appendChild(buildSelect("すべての期間", SITE.months, listState.month, function (v) { listState.month = v; listState.page = 1; drawCards(); }, monthLabel));
17385
17711
  }
17386
17712
  filters.appendChild(buildSelect("すべてのカテゴリー", SITE.categories, listState.category, function (v) { listState.category = v; listState.page = 1; drawCards(); }));
17387
- var statusOptsD = statusFilterOptions(visibleRecords(SITE.decisions));
17713
+ var statusOptsD = statusFilterOptions(visibleRecords(SITE.decisions), !!SITE.decisionFollowup);
17388
17714
  if (statusOptsD.length) {
17389
17715
  filters.appendChild(buildSelect("すべての状態", statusOptsD, listState.status, function (v) { listState.status = v; listState.page = 1; drawCards(); }, statusLabel));
17390
17716
  }
@@ -17417,7 +17743,7 @@ ${calendarAddUrl.toString()}
17417
17743
  var items = all.filter(function (d) {
17418
17744
  if (listState.month && d.date.slice(0, 7) !== listState.month) return false;
17419
17745
  if (listState.category && d.category !== listState.category) return false;
17420
- if (!matchesStatusFilter(d)) return false;
17746
+ if (!matchesStatusFilter(d, !!SITE.decisionFollowup)) return false;
17421
17747
  if (q) {
17422
17748
  var hay = (d.title + " " + d.summary + " " + d.searchText + " " + d.deciders.join(" ")).toLowerCase();
17423
17749
  if (hay.indexOf(q) === -1) return false;
@@ -17443,6 +17769,7 @@ ${calendarAddUrl.toString()}
17443
17769
  var meta = el("div", { class: "card-meta" }, [
17444
17770
  draftBadge(d),
17445
17771
  proposalBadge(d),
17772
+ followupBadge(d),
17446
17773
  latestBadge(d),
17447
17774
  el("span", { class: "mono", text: d.date || "----" }),
17448
17775
  catBadge(d.category),
@@ -19035,6 +19362,10 @@ ${calendarAddUrl.toString()}
19035
19362
  var pr = proposalsSection(d, "Decisions");
19036
19363
  if (pr) app.appendChild(pr);
19037
19364
 
19365
+ // 決定の「その後」(#705)。提案の節の後・要約の前。**出すものが無ければ要素を作らない**
19366
+ var fu = followupSection(d);
19367
+ if (fu) app.appendChild(fu);
19368
+
19038
19369
  if (d.summary) {
19039
19370
  app.appendChild(el("div", { class: "section-label", text: "要約" }));
19040
19371
  app.appendChild(el("div", { class: "summary-box", text: d.summary }));
package/dist/repo.js CHANGED
@@ -165,8 +165,10 @@ const PATHSPEC_MATCH = /^Cortex\/.*\/records\/.*\.md$/;
165
165
  const RECORD_PATHSPEC = "Cortex/*/records/*.md";
166
166
  function classifyCommit(id) {
167
167
  const subject = id.subject || "";
168
- // 1. 人の操作の代理(infra/lambda/intake/logic.mjs の promoteCommitMessage / deleteCommitMessage)
169
- if (/^(Gold確認|レコード削除)[::]/.test(subject))
168
+ // 1. 人の操作の代理(infra/lambda/intake/logic.mjs の promoteCommitMessage / deleteCommitMessage /
169
+ // followupCommitMessage)。🔴 **intake に件名を足したらここにも足す**(無いと bot 名義の
170
+ // コミットとして「AIがこの記録を書き換えました」に倒れる・#705 の実装レビュー R1)
171
+ if (/^(Gold確認|レコード削除|決定のその後)[::]/.test(subject))
170
172
  return "human";
171
173
  // 2. データ構造の引っ越し。実データの件名は「エンジンマイグレーションを適用(schema_version更新)」。
172
174
  //
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.79",
3
+ "version": "0.12.80",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,