@takagaki/cortex-decisions-viewer 0.12.85 → 0.12.87

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
@@ -36,16 +36,19 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.PROJECT_ICON_EXTS = void 0;
39
+ exports.PROJECT_ICON_EXTS = exports.MIRROR_RELS = void 0;
40
40
  exports.sanitizeBody = sanitizeBody;
41
41
  exports.findProjectIcon = findProjectIcon;
42
42
  exports.readFleetSources = readFleetSources;
43
43
  exports.attachRederived = attachRederived;
44
44
  exports.attachDecisionFollowup = attachDecisionFollowup;
45
+ exports.rawFrontmatterDate = rawFrontmatterDate;
46
+ exports.attachSupersedes = attachSupersedes;
45
47
  exports.readGoldProposals = readGoldProposals;
46
48
  exports.buildHomeGrounding = buildHomeGrounding;
47
49
  exports.collectMeetings = collectMeetings;
48
50
  exports.findBacklogDir = findBacklogDir;
51
+ exports.figmaUrlFromRef = figmaUrlFromRef;
49
52
  exports.bodyFileSlug = bodyFileSlug;
50
53
  exports.bodyStampId = bodyStampId;
51
54
  exports.bodyStampComment = bodyStampComment;
@@ -97,6 +100,7 @@ function takeProposals(filePath) {
97
100
  }
98
101
  const external_id_1 = require("./external-id");
99
102
  const home_project_1 = require("./home-project");
103
+ const supersedes_1 = require("./supersedes");
100
104
  const render_1 = require("./render");
101
105
  /**
102
106
  * この画面を作ったパッケージの版。**package.json を唯一の出どころにする。**
@@ -113,6 +117,11 @@ const PKG_VERSION = (() => {
113
117
  }
114
118
  })();
115
119
  const DEFAULT_EXCLUDE = ["README.md", "template.md"];
120
+ /**
121
+ * グラフ・エクスプローラーに描かない関係(#699 F3)。`superseded_by` は engine の夜間の精製が古い側に書く
122
+ * `supersedes` の写しで、描くと同じ置き換えが逆向きにもう 1 本出る。frontmatter のパネルには今までどおり出る
123
+ */
124
+ exports.MIRROR_RELS = new Set(["superseded_by"]);
116
125
  /** frontmatterのrelationsを安全に正規化する */
117
126
  function toRelations(value) {
118
127
  if (!Array.isArray(value))
@@ -707,30 +716,35 @@ readBlobs = repo_1.readBlobsAt) {
707
716
  // 🔴 **「確認済み」は `status` だけで決まる**(#667)。帯も並びも同じ `rstatus` を見る
708
717
  // ——ここで別のものを渡すと、画面の札と食い違う(「未確認」の札が付いた記録に帯が出る)
709
718
  const statusOf = (r) => r.rstatus;
710
- // 決定表の行 8(中身が変わったか)の材料。**印の候補になったレコードのぶんだけ**、
711
- // 今の版を読み、`<基準コミット>:<path>` をまとめて 1 回引く。
719
+ // 決定表の行 5〜7 の材料。**帯の候補になったレコードのぶんだけ**今の版を読み、指紋を作る。
720
+ // 帳簿に人が確認した版の指紋が無い候補だけ、`<基準コミット>:<path>` をまとめて 1 回 git から引く。
712
721
  // 🔴 **キーが 0 件の区画では git を起こさない**(この関数は区画ごとに呼ばれる)
713
722
  const keyOf = new Map();
714
723
  // 今の版は**作業ツリーから読む**——画面が描いているのはこのファイルであって HEAD ではない
715
724
  const now = new Map();
725
+ const nowSha = new Map();
716
726
  for (const r of records) {
717
727
  const key = (0, rederivation_1.rederivedContentKey)(pathOf(r), statusOf(r), ctx.ledgers);
718
728
  if (!key)
719
729
  continue;
720
730
  const p = pathOf(r);
731
+ let nowRaw;
721
732
  try {
722
- now.set(p, await node_fs_1.promises.readFile(path.join(ctx.repoRoot, p), "utf8"));
733
+ nowRaw = await node_fs_1.promises.readFile(path.join(ctx.repoRoot, p), "utf8");
723
734
  }
724
735
  catch {
725
- // 読めなければ「引けなかった」側(下の verdict が unknown に倒す)
736
+ // 読めなければ「読めなかった」側(`rederivedBandOf` が帯を出さずに数える)
737
+ continue;
726
738
  }
727
- // 🔴 **帳簿の指紋が今の中身と一致する記録は git の key に入れない**(engine #546)。
728
- // 答えは下の verdict で `same` になるので、基準版を引く意味が無い——ここで外さないと、
729
- // engine が指紋を書いても用語 1400 件の案件で毎晩 1400 個の blob を読み続ける。
730
- // 指紋が無い・食い違う・今の版が読めない記録は今までどおり git へ落とす
731
- const nowRaw = now.get(p);
732
- const sha = ctx.ledgers.confirmation.get(p)?.contentSha;
733
- if (nowRaw != null && sha && sha === (0, rederivation_1.contentShaOf)(nowRaw)) {
739
+ now.set(p, nowRaw);
740
+ const sha = (0, rederivation_1.contentShaOf)(nowRaw);
741
+ nowSha.set(p, sha);
742
+ const conf = ctx.ledgers.confirmation.get(p);
743
+ // 🔴 **今の版が機械の書き直しの版でなければ git を読まない**(帯は出ないと決まっている)。
744
+ // 人が確認した版の指紋が帳簿にあれば、それで決まるので git を読まない(engine #546)
745
+ if (sha !== conf?.rewritten?.contentSha)
746
+ continue;
747
+ if (conf.contentSha) {
734
748
  if (ctx.blobs)
735
749
  ctx.blobs.skipped++;
736
750
  continue;
@@ -742,28 +756,18 @@ readBlobs = repo_1.readBlobsAt) {
742
756
  const bases = keyOf.size ? readBlobs(ctx.repoRoot, [...keyOf.values()]) : new Map();
743
757
  const verdict = (p, baseCommit) => {
744
758
  const nowRaw = now.get(p);
745
- if (nowRaw == null) {
746
- ctx.miss.unreadable.add(p);
747
- return "unknown";
748
- }
749
- // 🔴 **帳簿が中身の指紋を持つならそれを先に使う**(engine #546 が書く。一致した記録は
750
- // 上で git の key から外してある)。
751
- // 一致したときだけ「変わっていない」と言い、食い違ったら git の二段目へ落とす——
752
- // engine が別の綴りで指紋を作っていた場合に、全レコードへ誤報が出るのを避ける
753
- const conf = ctx.ledgers.confirmation.get(p);
754
- if (conf && conf.contentSha && conf.contentSha === (0, rederivation_1.contentShaOf)(nowRaw))
755
- return "same";
756
759
  const baseRaw = bases.get(`${baseCommit}:${p}`);
757
- if (baseRaw == null) {
760
+ if (nowRaw == null || baseRaw == null) {
758
761
  // 浅い clone の窓の外・確認の後に改名された・基準コミットにこの path が無い。
759
- // **印は立てない**(誤報より無言)が、静かに消える経路なのでログの器に数える
762
+ // **帯は出さない**(誤報より無言)が、静かに消える経路なのでログの器に数える
760
763
  ctx.miss.unreadable.add(p);
761
764
  return "unknown";
762
765
  }
763
766
  return (0, rederivation_1.sameRederivedContent)(baseRaw, nowRaw) ? "same" : "changed";
764
767
  };
768
+ const nowShaOf = (p) => nowSha.get(p) ?? null;
765
769
  for (const r of records) {
766
- const band = (0, rederivation_1.rederivedBandOf)(pathOf(r), statusOf(r), ctx.ledgers, ctx.identities, verdict, ctx.miss);
770
+ const band = (0, rederivation_1.rederivedBandOf)(pathOf(r), statusOf(r), ctx.ledgers, nowShaOf, verdict, ctx.miss);
767
771
  // **印が立たないレコードにはキーごと付けない**(空の器を通さない)
768
772
  if (band)
769
773
  r.rederived = band;
@@ -795,6 +799,78 @@ function attachDecisionFollowup(decisions, recordsDir, repoRoot, ledger) {
795
799
  }
796
800
  return [...ledger.keys()].filter((k) => !matched.has(k.normalize("NFC")));
797
801
  }
802
+ /**
803
+ * 決定に置き換えの逆引き(置き換え先・置き換えた決定)を載せる(cortex-engine #699 F3・A-1')。
804
+ *
805
+ * 🔴 **逆引きは `supersedes.ts`(engine `scripts/gold/supersedes-index.mjs` の写し)だけで組む**。
806
+ * 材料は新しい側の `relations[supersedes]` だけで、古い側の `superseded_by`(夜間の精製が書く写し)は
807
+ * 読まない。確認済み=新しい側の `status` が `active`。
808
+ *
809
+ * 置き換えが無い決定には**キーごと付けない**(今までと同じ SiteData)
810
+ */
811
+ /**
812
+ * frontmatter に**書かれたままの** `date` の値(トップレベルの `date:` の行・引用符と行末のコメントを外す)。
813
+ * 無い・frontmatter が無ければ undefined。置き換えの逆引きの決定日だけに使う(engine と同じ日付にするため)
814
+ */
815
+ function rawFrontmatterDate(raw) {
816
+ const m = String(raw ?? "").replace(/^\uFEFF/, "").match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/);
817
+ if (!m)
818
+ return undefined;
819
+ const line = m[1].match(/^date:[ \t]*(.*)$/m);
820
+ if (!line)
821
+ return undefined;
822
+ let v = line[1].replace(/\r$/, "").trim();
823
+ const q = v.match(/^(["'])(.*)\1(?:\s+#.*)?$/);
824
+ if (q)
825
+ return q[2];
826
+ v = v.replace(/\s+#.*$/, "").trim();
827
+ return v;
828
+ }
829
+ function attachSupersedes(decisions, rawDateOf = (d) => d.fm.date) {
830
+ const input = decisions.map((d) => ({
831
+ id: d.id,
832
+ // 🔴 **frontmatter に書かれたままの date の文字列**(`rawFrontmatterDate`)。gray-matter は時刻・時差つきの
833
+ // date を Date に読み、sanitizeFm は UTC の日付にするので、`2026-07-01T01:00:00+09:00` が 6/30 にずれる。
834
+ // engine は日付を Date にしない読み方(CORE_SCHEMA)で書かれた文字列の先頭 8 桁を使う。
835
+ // 無ければ写しが ID の先頭 8 桁から取る
836
+ date: rawDateOf(d),
837
+ status: d.fm.status,
838
+ supersedes: d.relations.filter((r) => r.rel === "supersedes").map((r) => r.target),
839
+ }));
840
+ const index = (0, supersedes_1.supersedesIndex)(input);
841
+ const idOfKey = new Map();
842
+ for (const d of decisions) {
843
+ const k = (0, supersedes_1.decisionKeyOf)(d.id);
844
+ if (!idOfKey.has(k))
845
+ idOfKey.set(k, d.id);
846
+ }
847
+ for (const d of decisions) {
848
+ const key = (0, supersedes_1.decisionKeyOf)(d.id);
849
+ const entry = index.get(key);
850
+ if (entry && entry.by.length) {
851
+ d.supersededBy = {
852
+ by: entry.by.map((b) => ({ id: b.id, date: b.date, confirmed: b.confirmed })),
853
+ retired: entry.retired,
854
+ };
855
+ }
856
+ const replaced = [];
857
+ for (const r of d.relations) {
858
+ if (r.rel !== "supersedes")
859
+ continue;
860
+ const k = (0, supersedes_1.decisionKeyOf)(r.target);
861
+ // engine の規則と同じく、自分自身・実在しない決定は数えない
862
+ if (!k || k === key)
863
+ continue;
864
+ const id = idOfKey.get(k);
865
+ if (id === undefined)
866
+ continue;
867
+ if (!replaced.includes(id))
868
+ replaced.push(id);
869
+ }
870
+ if (replaced.length)
871
+ d.supersedesIds = replaced;
872
+ }
873
+ }
798
874
  /**
799
875
  * AIからの更新提案(`.cortex/gold-proposals.json`)を読む。
800
876
  *
@@ -1373,16 +1449,28 @@ async function buildTargetUrlMap(repoRoot, repoBaseUrl, branch) {
1373
1449
  // - 参照ID: `design:xxx:103:1836`
1374
1450
  // - [Figmaで開く](https://www.figma.com/design/...)
1375
1451
  // 以前は `data.type === "design"` を見ていたので、この解決は**一度も効いていなかった**。
1376
- for (const file of await walkMd(path.join(repoRoot, designDir, "inventory"))) {
1377
- try {
1378
- const body = await node_fs_1.promises.readFile(file, "utf8");
1379
- const id = body.match(/^- 参照ID: `(design:[^`]+)`\s*$/m);
1380
- const url = body.match(/^- \[Figmaで開く\]\((https?:[^)]+)\)\s*$/m);
1381
- if (id && url)
1382
- map.set(id[1], url[1]);
1383
- }
1384
- catch {
1385
- // 読めないファイルはスキップ
1452
+ //
1453
+ // #727 K10 で、コメントだけを取り込む案件(栓 `design_comments_only`)は inventory を持たず、
1454
+ // `デザイン/指摘/<ファイル>/README.md`(`design:{fileKey}` と「Figmaで開く」)と
1455
+ // 月ごとの `YYYY-MM.md`(スレッドごとの `figma-comment:` と「Figmaでこの指摘を開く」)になる。
1456
+ // 移行期は両方ありうるので**両方歩く**。inventory を先に歩き、同じ ID は先勝ち
1457
+ // (=inventory があれば今までと同じ URL。1バイトも変えない)。
1458
+ // 表に無い `design:` / `figma-comment:` は `figmaUrlFromRef` がキーから組む(解決器の側)。
1459
+ for (const sub of ["inventory", "指摘"]) {
1460
+ for (const file of await walkMd(path.join(repoRoot, designDir, sub))) {
1461
+ try {
1462
+ const body = await node_fs_1.promises.readFile(file, "utf8");
1463
+ const id = body.match(/^- 参照ID: `(design:[^`]+)`\s*$/m);
1464
+ const url = body.match(/^- \[Figmaで開く\]\((https?:[^)]+)\)\s*$/m);
1465
+ if (id && url && !map.has(id[1]))
1466
+ map.set(id[1], url[1]);
1467
+ for (const [cid, curl] of figmaCommentLinks(body))
1468
+ if (!map.has(cid))
1469
+ map.set(cid, curl);
1470
+ }
1471
+ catch {
1472
+ // 読めないファイルはスキップ
1473
+ }
1386
1474
  }
1387
1475
  }
1388
1476
  // minute:{定例名}:{YYYYMMDD} → 議事録ファイルのGitHubリンク
@@ -1405,6 +1493,70 @@ async function buildTargetUrlMap(repoRoot, repoBaseUrl, branch) {
1405
1493
  }
1406
1494
  return map;
1407
1495
  }
1496
+ /**
1497
+ * 指摘のスレッドから `figma-comment:{fileKey}:{commentId}` → 「Figmaでこの指摘を開く」の URL を拾う。
1498
+ *
1499
+ * engine の `render_thread` の形(`### 未解決: …` の見出し・`- 参照ID:`・`- [Figmaでこの指摘を開く](…)`)。
1500
+ * **対は同じスレッドの中でだけ組む。** 次の `### ` 見出しを越えて拾うと、リンクの欠けた
1501
+ * スレッドに隣のスレッドの URL を付けてしまう(押すと別の指摘へ飛ぶ——エラーの出ない壊れ方)。
1502
+ */
1503
+ function figmaCommentLinks(body) {
1504
+ const out = [];
1505
+ let pending = null;
1506
+ for (const line of body.split("\n")) {
1507
+ if (/^#{1,3}\s/.test(line)) {
1508
+ pending = null;
1509
+ continue;
1510
+ }
1511
+ const id = line.match(/^- 参照ID: `(figma-comment:[^`]+)`\s*$/);
1512
+ if (id) {
1513
+ pending = id[1];
1514
+ continue;
1515
+ }
1516
+ const url = line.match(/^- \[Figmaでこの指摘を開く\]\((https?:[^)]+)\)\s*$/);
1517
+ if (url && pending) {
1518
+ out.push([pending, url[1]]);
1519
+ pending = null;
1520
+ }
1521
+ }
1522
+ return out;
1523
+ }
1524
+ /** Figma のファイルキー(engine `sync_designs.py` の `REAL_FIGMA_KEY` と同じ形) */
1525
+ const FIGMA_KEY = /^[A-Za-z0-9_-]{8,}$/;
1526
+ /** Figma のノード ID(`103:1836`。URL では `103-1836` と書く) */
1527
+ const FIGMA_NODE = /^\d+:\d+$/;
1528
+ /**
1529
+ * `design:` / `figma-comment:` の参照から、**キーとノードだけで** Figma の URL を組む(#727 K10)。
1530
+ *
1531
+ * コメントだけを取り込む案件には画面ごとの inventory が無いので、Gold の
1532
+ * `design:{fileKey}:{nodeId}` を表から引けない。画面の本体は Figma にあるので、キーとノードが
1533
+ * 揃っていれば URL は決まる(スラグは Figma が読まないので付けない)。
1534
+ * - `design:{key}:{node}` → `https://www.figma.com/design/{key}?node-id={node の : を -}`
1535
+ * - `design:{key}` → `https://www.figma.com/design/{key}`
1536
+ * - `figma-comment:{key}:{id}` → `https://www.figma.com/design/{key}#{id}`
1537
+ * (フラグメントはサーバーに送られないので、外れてもファイルには着く。engine の `comment_link` と同じ倒し方)
1538
+ *
1539
+ * 🔴 **形の合わないものは組まない(null)。** キー・ノードに任意の文字列を通すと、
1540
+ * `design:../../x` や `design:abc:1:2?x=` から**押せそうで 404** の URL ができる。
1541
+ * 壊れたリンクは、無いより悪い。
1542
+ */
1543
+ function figmaUrlFromRef(ref) {
1544
+ const d = ref.match(/^design:([^:]+)(?::(.+))?$/);
1545
+ if (d) {
1546
+ const [, key, node] = d;
1547
+ if (!FIGMA_KEY.test(key))
1548
+ return null;
1549
+ if (node === undefined)
1550
+ return `https://www.figma.com/design/${key}`;
1551
+ if (!FIGMA_NODE.test(node))
1552
+ return null;
1553
+ return `https://www.figma.com/design/${key}?node-id=${node.replace(":", "-")}`;
1554
+ }
1555
+ const c = ref.match(/^figma-comment:([^:]+):(\d+)$/);
1556
+ if (c && FIGMA_KEY.test(c[1]))
1557
+ return `https://www.figma.com/design/${c[1]}#${c[2]}`;
1558
+ return null;
1559
+ }
1408
1560
  /** 画像・添付など、グラフのノードにしないリンクの拡張子 */
1409
1561
  const NON_DOC_LINK = /\.(png|jpe?g|gif|svg|webp|ico|zip|mp4|mov|heic)([?#]|$)/i;
1410
1562
  /** 本文markdownからhttp(s)リンクを抽出する(重複除去・画像等は除外) */
@@ -1509,7 +1661,8 @@ async function scanContent(repoRoot, goldDirName, repoBaseUrl, branch) {
1509
1661
  }
1510
1662
  }
1511
1663
  for (const r of toRelations(data.relations))
1512
- edges.push({ target: r.target, rel: r.rel });
1664
+ if (!exports.MIRROR_RELS.has(r.rel))
1665
+ edges.push({ target: r.target, rel: r.rel });
1513
1666
  }
1514
1667
  else {
1515
1668
  const keyMatch = raw.slice(0, 4000).match(/^- 課題キー: (\S+)$/m);
@@ -1586,9 +1739,13 @@ function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl,
1586
1739
  return resolved;
1587
1740
  let group = "phantom";
1588
1741
  let label = target;
1589
- if (target.startsWith("minute:") || target.startsWith("material:") || target.startsWith("design:")) {
1742
+ if (target.startsWith("minute:") ||
1743
+ target.startsWith("material:") ||
1744
+ target.startsWith("design:") ||
1745
+ target.startsWith("figma-comment:")) {
1590
1746
  group = "silver";
1591
- const url = targetUrls.get(target);
1747
+ // 表に無い design: / figma-comment: はキーから組む(詳細画面の resolve と同じ順)
1748
+ const url = targetUrls.get(target) ?? figmaUrlFromRef(target);
1592
1749
  if (url)
1593
1750
  return addNode({ id: target, label, group, url });
1594
1751
  }
@@ -1618,6 +1775,9 @@ function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl,
1618
1775
  };
1619
1776
  const addRelationEdges = (fromId, relations) => {
1620
1777
  for (const r of relations) {
1778
+ // 🔴 `superseded_by` は描かない(#699 F3)。新しい側の `supersedes` の写しで、描くと同じ置き換えが 2 本になる
1779
+ if (exports.MIRROR_RELS.has(r.rel))
1780
+ continue;
1621
1781
  edges.push({ from: fromId, to: classifyTarget(r.target), rel: r.rel });
1622
1782
  }
1623
1783
  };
@@ -2043,8 +2203,8 @@ async function build(opts) {
2043
2203
  // 同一プロセスで2回ビルドしても前回の値が残らないよう、毎回入れ替える
2044
2204
  PROPOSALS_BY_PATH = await readGoldProposals(repoRoot);
2045
2205
  PROPOSAL_PATHS_MATCHED = new Set();
2046
- // 再導出の 3 帳簿(#665 便 12b)。**帳簿が無い案件では帯も並び替えも作らない**ので、
2047
- // 読めたときだけ git の素性を取りに行く(帳簿を持たない 44 案件で git の呼び出しを増やさない)
2206
+ // 再導出の 3 帳簿(#665 便 12b)。**帳簿が無い案件では帯も並び替えも作らない**。
2207
+ // 人か機械かの見分けは engine が帳簿(`rewritten`)に書くので、画面は git の素性を読まない(#799)
2048
2208
  const ledgers = await (0, rederivation_1.readRederivationLedgers)(repoRoot);
2049
2209
  const rederive = ledgers.confirmation.size || ledgers.activity.size
2050
2210
  ? {
@@ -2052,10 +2212,9 @@ async function build(opts) {
2052
2212
  // 🔴 **`repoRoot` で走らせる**(`collectAiChangeHistory` の `process.cwd()` に倣わない)。
2053
2213
  // あちらは「リポジトリルートで実行すること」が前提の既存の制約だが、こちらは repoRoot を
2054
2214
  // 既に知っている(帳簿をそこから読んでいる)。Decisions ディレクトリから CLI を叩いた
2055
- // ときも pathspec が当たり、帯が黙って消えない
2215
+ // ときも今の版と基準版が引け、帯が黙って消えない
2056
2216
  repoRoot: repoRoot,
2057
- identities: ledgers.confirmation.size ? (0, repo_1.collectGoldCommitIdentities)(repoRoot) : new Map(),
2058
- miss: { unresolved: new Set(), unreadable: new Set() },
2217
+ miss: { unreadable: new Set() },
2059
2218
  blobs: { read: 0, skipped: 0 },
2060
2219
  }
2061
2220
  : null;
@@ -2063,11 +2222,18 @@ async function build(opts) {
2063
2222
  // **読めない・使えないなら `null`** で、関係者の並びは今までと 1 バイトも同じ
2064
2223
  const engagement = await (0, engagement_1.readEngagementLedger)(repoRoot);
2065
2224
  const decisions = [];
2225
+ const rawDates = new Map();
2066
2226
  for (const fileName of mdFiles) {
2067
2227
  const raw = await node_fs_1.promises.readFile(path.join(dirAbs, fileName), "utf8");
2068
2228
  const rec = await parseDecision(raw, fileName, repoRelDir, repo.baseUrl, repo.branch, refs);
2069
2229
  decisions.push(rec);
2230
+ // 置き換えの逆引きの決定日(書かれたままの文字列・SiteData には載せない)
2231
+ const rd = rawFrontmatterDate(raw);
2232
+ if (rd !== undefined)
2233
+ rawDates.set(rec, rd);
2070
2234
  }
2235
+ // 置き換えの逆引き(#699 F3)。**置き換えの無い案件では 1 件も付かない**
2236
+ attachSupersedes(decisions, (d) => rawDates.get(d) ?? d.fm.date);
2071
2237
  // 決定の「その後」(#705)。**帳簿を持たない案件では 1 件も付かず、絞り込みの印も立たない**
2072
2238
  // (`decisionFollowup` はキーごと無い=今までと 1 バイトも同じ画面)
2073
2239
  const followupLedger = await (0, rederivation_1.readDecisionFollowup)(repoRoot);
@@ -2152,30 +2318,20 @@ async function build(opts) {
2152
2318
  // **突き合わなかった提案の件数をビルドログに出す。** path が1文字違うだけで
2153
2319
  // 画面から静かに消えるので、出さないと「提案が出ない」の原因に辿り着けない。
2154
2320
  // 表示からは落とす(どのレコードの話か分からない提案は、出しても行動につながらない)
2155
- // 🔴 **帯が静かに消える経路を 1 行残す(設計レビュー 🟡10)。** `changed_by` の sha が
2156
- // git の窓(fetch-depth 2000)の外にあると、人か機械か見分けられない=帯が立たない。
2157
- // 画面には出さない(利用者が動ける症状ではない)が、ログに無いと「帯が出ない」の原因を
2158
- // 「夜間がまだ書いていない」と切り分けられない
2159
- if (rederive && rederive.miss.unresolved.size) {
2160
- const shas = [...rederive.miss.unresolved];
2161
- console.warn(`gold-confirmation.json: 変更のコミット ${shas.length} 件を git の履歴で引けませんでした` +
2162
- `(浅い clone か履歴の窓の外。人か機械か見分けられないので帯は出しません): ` +
2163
- shas.slice(0, 3).map((s) => s.slice(0, 8)).join(", ") + (shas.length > 3 ? " ほか" : ""));
2164
- }
2165
- // 🔴 **印が静かに消えるもう 1 つの経路を 1 行残す。** 人が確認した版の中身を git から
2166
- // 引けないと「説明文が変わったか」を言えないので、印は出さない(誤報より無言)。
2321
+ // 🔴 **帯が静かに消える経路を 1 行残す。** 今の版か、人が確認した版の中身を
2322
+ // 引けないと「説明文が変わったか」を言えないので、帯は出さない(誤報より無言)。
2167
2323
  // 出さないと「印が出ない」の原因を「中身が変わっていない」と切り分けられない
2168
2324
  if (rederive && rederive.miss.unreadable.size) {
2169
2325
  const paths = [...rederive.miss.unreadable];
2170
- console.warn(`gold-confirmation.json: 人が確認した版の中身を ${paths.length} 件引けませんでした` +
2171
- `(浅い clone か、確認の後に改名・削除された。変わったかを言えないので印は出しません): ` +
2326
+ console.warn(`gold-confirmation.json: 人が確認した版か今の版の中身を ${paths.length} 件引けませんでした` +
2327
+ `(浅い clone か、確認の後に改名・削除された。変わったかを言えないので帯は出しません): ` +
2172
2328
  paths.slice(0, 3).join(", ") + (paths.length > 3 ? " ほか" : ""));
2173
2329
  }
2174
2330
  // 🔴 **指紋(engine #546)が効いているかを 1 行残す。** 一致した記録は git を読まないので、
2175
2331
  // 「読んだ件数」が候補の数より減っていれば効いている。警告ではないので console.log
2176
2332
  if (rederive?.blobs && rederive.blobs.read + rederive.blobs.skipped > 0) {
2177
2333
  console.log(`gold-confirmation.json: 人が確認した版の中身を git から ${rederive.blobs.read} 件読みました` +
2178
- `(中身の指紋が一致した ${rederive.blobs.skipped} 件は読んでいません)`);
2334
+ `(中身の指紋で決めた ${rederive.blobs.skipped} 件は読んでいません)`);
2179
2335
  }
2180
2336
  const unmatched = [...PROPOSALS_BY_PATH.keys()].filter((k) => !PROPOSAL_PATHS_MATCHED.has(k));
2181
2337
  if (unmatched.length) {
@@ -2399,6 +2555,10 @@ function createRefResolver(opts) {
2399
2555
  const known = targetUrls.get(trimmed);
2400
2556
  if (known)
2401
2557
  return { label: trimmed, url: known };
2558
+ // inventory の無い案件(#727 K10)の design: / figma-comment: は、キーとノードから組む
2559
+ const figma = figmaUrlFromRef(trimmed);
2560
+ if (figma)
2561
+ return { label: trimmed, url: figma };
2402
2562
  // 実在するファイルだけ blob URL にする
2403
2563
  if (repoBaseUrl && exists(trimmed)) {
2404
2564
  return { label: trimmed, url: `${repoBaseUrl}/blob/${branch}/${encodePath(trimmed)}` };
@@ -50,7 +50,7 @@ exports.ENGAGEMENT_LEDGER_VERSION = 1;
50
50
  /** 半減期(日)。engine `weights.mjs` の `HALF_LIFE_DAYS`(正本 6-B「半減期 90 日固定」) */
51
51
  exports.HALF_LIFE_DAYS = 90;
52
52
  /**
53
- * 行為 1 件の重み。engine `weights.mjs` の `WEIGHTS` の写し(22 値・**表に無い `kind` は 0**)。
53
+ * 行為 1 件の重み。engine `weights.mjs` の `WEIGHTS` の写し(24 値・**表に無い `kind` は 0**)。
54
54
  *
55
55
  * 🔴 契約の表(fixture)は `message.posted` しか使っていないので、**ここの写し間違いは表では
56
56
  * 捕まらない**。重みの番人は `test/member-engagement-parity.test.mjs` の `[重み]` が持つ。
@@ -80,6 +80,9 @@ exports.WEIGHTS = Object.freeze({
80
80
  "record.deleted": 0,
81
81
  // 本文を取り込んだ(engine #695)。人の行為ではないので 0(話した人は `utterance.spoken` で数える)
82
82
  "body.captured": 0,
83
+ // 人の宣言の指紋(cortex-engine #699 F1)。宣言の取り込みが書く行で、関与には数えない(0)。
84
+ // 場は宣言専用(`decl:<owner/repo>`)で帳簿にもともと入らないが、網羅のため写す
85
+ "decl.recorded": 0,
83
86
  });
84
87
  const DAY_MS = 86400000;
85
88
  const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
@@ -106,8 +106,8 @@ function splitTypedId(v) {
106
106
  * 両方がこれを引く。
107
107
  *
108
108
  * 🔴 画面の中では `identities` と呼ばない(`linkedIds` / `.linked-ids` / `.li-*`)。
109
- * `identities` という語は、このリポジトリでは既に `GoldCommitIdentity`
110
- *(帯のコミットの筆者)の意味で使っている——同じ語を 2 つの意味で使わない。
109
+ * `identities` という語は、このリポジトリでは `GoldCommitIdentity`
110
+ *(帯のコミットの筆者・#799 で撤去)の意味で使っていた——同じ語を 2 つの意味で使わない。
111
111
  */
112
112
  exports.FM_LINKED_IDS = "identities";
113
113
  /**