@takagaki/cortex-decisions-viewer 0.12.74 → 0.12.76
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 +60 -5
- package/dist/external-id.js +148 -0
- package/dist/rederivation.js +104 -2
- package/dist/render.js +696 -114
- package/dist/repo.js +80 -0
- package/package.json +1 -1
package/dist/build.js
CHANGED
|
@@ -672,16 +672,61 @@ async function readFleetSources(repoRoot) {
|
|
|
672
672
|
* 🔴 **突き合わせはビルド時にここで閉じる**(`proposals` と同じ)。画面側でパスを組み立て直すと、
|
|
673
673
|
* `repoPathOf` が `editUrl` 由来なので **`repoBaseUrl` が無い案件で帯が黙って消える**。
|
|
674
674
|
*/
|
|
675
|
-
function attachRederived(records,
|
|
675
|
+
async function attachRederived(records, recordsDir, ctx, nowIso) {
|
|
676
676
|
if (!ctx)
|
|
677
677
|
return records;
|
|
678
|
+
// 🔴 **レコードのパスは `repoRoot` から作る**(`toRepoRelative` の `process.cwd()` 起点で
|
|
679
|
+
// 作らない)。帳簿のキーは常にリポジトリルート相対(`Cortex/Glossary/records/…`)なので、
|
|
680
|
+
// Decisions ディレクトリから CLI を叩いた案件では cwd 起点だと 1 件も突き合わない
|
|
681
|
+
const recordsRel = path.relative(ctx.repoRoot, recordsDir).split(path.sep).join("/");
|
|
678
682
|
const pathOf = (r) => (recordsRel ? `${recordsRel}/${r.fileName}` : r.fileName);
|
|
679
683
|
// 🔴 **「確認済み」は `status` だけで決まる**(#667)。帯も並びも同じ `rstatus` を見る
|
|
680
684
|
// ——ここで別のものを渡すと、画面の札と食い違う(「未確認」の札が付いた記録に帯が出る)
|
|
681
685
|
const statusOf = (r) => r.rstatus;
|
|
686
|
+
// 決定表の行 8(中身が変わったか)の材料。**印の候補になったレコードのぶんだけ**、
|
|
687
|
+
// `<基準コミット>:<path>` をまとめて 1 回引く。
|
|
688
|
+
// 🔴 **キーが 0 件の区画では git を起こさない**(この関数は区画ごとに呼ばれる)
|
|
689
|
+
const keyOf = new Map();
|
|
682
690
|
for (const r of records) {
|
|
683
|
-
const
|
|
684
|
-
|
|
691
|
+
const key = (0, rederivation_1.rederivedContentKey)(pathOf(r), statusOf(r), ctx.ledgers);
|
|
692
|
+
if (key)
|
|
693
|
+
keyOf.set(r, key);
|
|
694
|
+
}
|
|
695
|
+
const bases = keyOf.size ? (0, repo_1.readBlobsAt)(ctx.repoRoot, [...keyOf.values()]) : new Map();
|
|
696
|
+
// 今の版は**作業ツリーから読む**——画面が描いているのはこのファイルであって HEAD ではない
|
|
697
|
+
const now = new Map();
|
|
698
|
+
for (const r of keyOf.keys()) {
|
|
699
|
+
try {
|
|
700
|
+
now.set(pathOf(r), await node_fs_1.promises.readFile(path.join(ctx.repoRoot, pathOf(r)), "utf8"));
|
|
701
|
+
}
|
|
702
|
+
catch {
|
|
703
|
+
// 読めなければ「引けなかった」側(下の verdict が unknown に倒す)
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
const verdict = (p, baseCommit) => {
|
|
707
|
+
const nowRaw = now.get(p);
|
|
708
|
+
if (nowRaw == null) {
|
|
709
|
+
ctx.miss.unreadable.add(p);
|
|
710
|
+
return "unknown";
|
|
711
|
+
}
|
|
712
|
+
// 🔴 **帳簿が中身の指紋を持つならそれを先に使う**(engine が後の便で書く予定)。
|
|
713
|
+
// 一致したときだけ「変わっていない」と言い、食い違ったら git の二段目へ落とす——
|
|
714
|
+
// engine が別の綴りで指紋を作っていた場合に、全レコードへ誤報が出るのを避ける
|
|
715
|
+
const conf = ctx.ledgers.confirmation.get(p);
|
|
716
|
+
if (conf && conf.contentSha && conf.contentSha === (0, rederivation_1.contentShaOf)(nowRaw))
|
|
717
|
+
return "same";
|
|
718
|
+
const baseRaw = bases.get(`${baseCommit}:${p}`);
|
|
719
|
+
if (baseRaw == null) {
|
|
720
|
+
// 浅い clone の窓の外・確認の後に改名された・基準コミットにこの path が無い。
|
|
721
|
+
// **印は立てない**(誤報より無言)が、静かに消える経路なのでログの器に数える
|
|
722
|
+
ctx.miss.unreadable.add(p);
|
|
723
|
+
return "unknown";
|
|
724
|
+
}
|
|
725
|
+
return (0, rederivation_1.sameRederivedContent)(baseRaw, nowRaw) ? "same" : "changed";
|
|
726
|
+
};
|
|
727
|
+
for (const r of records) {
|
|
728
|
+
const band = (0, rederivation_1.rederivedBandOf)(pathOf(r), statusOf(r), ctx.ledgers, ctx.identities, verdict, ctx.miss);
|
|
729
|
+
// **印が立たないレコードにはキーごと付けない**(空の器を通さない)
|
|
685
730
|
if (band)
|
|
686
731
|
r.rederived = band;
|
|
687
732
|
}
|
|
@@ -1911,8 +1956,9 @@ async function build(opts) {
|
|
|
1911
1956
|
// あちらは「リポジトリルートで実行すること」が前提の既存の制約だが、こちらは repoRoot を
|
|
1912
1957
|
// 既に知っている(帳簿をそこから読んでいる)。Decisions ディレクトリから CLI を叩いた
|
|
1913
1958
|
// ときも pathspec が当たり、帯が黙って消えない
|
|
1959
|
+
repoRoot: repoRoot,
|
|
1914
1960
|
identities: ledgers.confirmation.size ? (0, repo_1.collectGoldCommitIdentities)(repoRoot) : new Map(),
|
|
1915
|
-
miss: { unresolved: new Set() },
|
|
1961
|
+
miss: { unresolved: new Set(), unreadable: new Set() },
|
|
1916
1962
|
}
|
|
1917
1963
|
: null;
|
|
1918
1964
|
const decisions = [];
|
|
@@ -1959,7 +2005,7 @@ async function build(opts) {
|
|
|
1959
2005
|
const records = await loadGenericRecords(recordsDir, exclude, repo.baseUrl, repo.branch);
|
|
1960
2006
|
// 「人が確認した版から変わりました」の帯と、用語集の並び(#665 便 12b)。
|
|
1961
2007
|
// **帳簿を持たない案件では 1 件も付かず、並びもそのまま**(下の attachRederived)
|
|
1962
|
-
const ordered = attachRederived(records,
|
|
2008
|
+
const ordered = await attachRederived(records, recordsDir, rederive, opts.generatedAt);
|
|
1963
2009
|
// レコード0件でもタブは出す(空状態から「+AIで登録」できる導線になる)。
|
|
1964
2010
|
// records/ 自体が無いディレクトリ(その区画が未導入の案件)はタブにしない
|
|
1965
2011
|
if (records.length || (await isDirectory(recordsDir))) {
|
|
@@ -1996,6 +2042,15 @@ async function build(opts) {
|
|
|
1996
2042
|
`(浅い clone か履歴の窓の外。人か機械か見分けられないので帯は出しません): ` +
|
|
1997
2043
|
shas.slice(0, 3).map((s) => s.slice(0, 8)).join(", ") + (shas.length > 3 ? " ほか" : ""));
|
|
1998
2044
|
}
|
|
2045
|
+
// 🔴 **印が静かに消えるもう 1 つの経路を 1 行残す。** 人が確認した版の中身を git から
|
|
2046
|
+
// 引けないと「説明文が変わったか」を言えないので、印は出さない(誤報より無言)。
|
|
2047
|
+
// 出さないと「印が出ない」の原因を「中身が変わっていない」と切り分けられない
|
|
2048
|
+
if (rederive && rederive.miss.unreadable.size) {
|
|
2049
|
+
const paths = [...rederive.miss.unreadable];
|
|
2050
|
+
console.warn(`gold-confirmation.json: 人が確認した版の中身を ${paths.length} 件引けませんでした` +
|
|
2051
|
+
`(浅い clone か、確認の後に改名・削除された。変わったかを言えないので印は出しません): ` +
|
|
2052
|
+
paths.slice(0, 3).join(", ") + (paths.length > 3 ? " ほか" : ""));
|
|
2053
|
+
}
|
|
1999
2054
|
const unmatched = [...PROPOSALS_BY_PATH.keys()].filter((k) => !PROPOSAL_PATHS_MATCHED.has(k));
|
|
2000
2055
|
if (unmatched.length) {
|
|
2001
2056
|
console.warn(`gold-proposals.json: レコードに突き合わない提案が ${unmatched.length} 件ありました(表示しません): ` +
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 外の ID の「形」の判定(#686)。
|
|
4
|
+
*
|
|
5
|
+
* 🔴 **伏せるためではなく、`<種類>:<値>` の形の ID を見分けて画面で解釈するための判定。**
|
|
6
|
+
* 関係者の記録には `slack_user:U0…` のような外のサービスの ID が並ぶ。画面はこれを
|
|
7
|
+
* 「種類」と「値」に割って読ませたい(どのサービスの ID なのかが分からないと、人は
|
|
8
|
+
* 結び付きが正しいかを確かめられない)。そのための見分けをここに置く。
|
|
9
|
+
*
|
|
10
|
+
* #681 で入れた伏せの実装(`person-redact.ts`)は #686 で撤去した。**形の判定だけを残して
|
|
11
|
+
* ある**——判定そのものは伏せとは関係なく要るもので、書き直すと同じ取りこぼしを踏む。
|
|
12
|
+
*
|
|
13
|
+
* ## 種類を列挙しない
|
|
14
|
+
*
|
|
15
|
+
* 外の ID は `<種類>:<値>` の形で書かれる(engine `docs/observations.md` の `<id_kind>:<id>`)。
|
|
16
|
+
* **種類を列挙すると、engine が新しい種類を足した晩に見分けられなくなる**ので、形だけで
|
|
17
|
+
* 判定する。種類の大文字小文字は問わない。
|
|
18
|
+
*
|
|
19
|
+
* 逆に、**このリポジトリの内側の呼び名は外の ID と見なさない**(`INSIDE_KINDS`)。
|
|
20
|
+
* `member:山田太郎` は名前そのもので、`minute:` / `material:` / `design:` は
|
|
21
|
+
* つながりの図が正本の URL を引く行き先(`build.ts` の `classifyTarget`)。
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.INSIDE_KIND_LIST = exports.TYPED_ID_WHOLE_SRC = exports.FM_LINKED_IDS = void 0;
|
|
25
|
+
exports.normalizeSeparators = normalizeSeparators;
|
|
26
|
+
exports.isExternalId = isExternalId;
|
|
27
|
+
exports.splitTypedId = splitTypedId;
|
|
28
|
+
exports.isMemberRecord = isMemberRecord;
|
|
29
|
+
exports.isMemberRecordPath = isMemberRecordPath;
|
|
30
|
+
/**
|
|
31
|
+
* このリポジトリの内側の呼び名。外の ID と見なさない。
|
|
32
|
+
*
|
|
33
|
+
* 前半は記録の id の種類(`build.ts` の `GOLD_TYPES` と同じ)、後半は Silver の参照
|
|
34
|
+
* (`build.ts` の `classifyTarget` が正本の URL に解決する 3 つ)。
|
|
35
|
+
*/
|
|
36
|
+
const INSIDE_KINDS = new Set([
|
|
37
|
+
"decision", "term", "member", "rule", "report", "overview",
|
|
38
|
+
"minute", "material", "design",
|
|
39
|
+
]);
|
|
40
|
+
/**
|
|
41
|
+
* `<種類>:<値>` 1 つぶん。
|
|
42
|
+
*
|
|
43
|
+
* 🔴 **コロンの直後に空白があるものは当てない。** `[a-z_]+:` + 1 文字以上は YAML の
|
|
44
|
+
* `キー: 値` そのものの形なので、空白を許すと `title: 山田 太郎` のような行まで
|
|
45
|
+
* 「外の ID」と見なしてしまう。
|
|
46
|
+
*/
|
|
47
|
+
const TYPED_ID_SRC = "([A-Za-z][A-Za-z0-9_]*):([^\\s,\"'\\[\\]、。,))]+)";
|
|
48
|
+
/** メールアドレスの形(`email:` を付けずに書かれたもの) */
|
|
49
|
+
const EMAIL_SRC = "[^\\s,\"'<>\\[\\]、。,))]+@[^\\s,\"'<>\\[\\]、。,))]+";
|
|
50
|
+
const TYPED_ID_WHOLE = new RegExp("^" + TYPED_ID_SRC + "$");
|
|
51
|
+
const EMAIL_WHOLE = new RegExp("^" + EMAIL_SRC + "$");
|
|
52
|
+
/**
|
|
53
|
+
* 全角コロンを半角に寄せる。人が名簿を手で書く欄なので、日本語入力のまま
|
|
54
|
+
* `slack_user:…` と区切りだけ全角になった行を取りこぼさない。
|
|
55
|
+
*/
|
|
56
|
+
function normalizeSeparators(s) {
|
|
57
|
+
return s.replace(/:/g, ":");
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* その文字列ぜんたいが外の ID か(`aliases` の 1 要素の判定)。
|
|
61
|
+
*
|
|
62
|
+
* 判定に使うだけで中身を書き戻さないので、ここでは**全角を丸ごと半角に寄せて**から当てる
|
|
63
|
+
* (`slack_user:U0X` のように、種類まで全角で書かれた行を取りこぼさない)。
|
|
64
|
+
*/
|
|
65
|
+
function isExternalId(v) {
|
|
66
|
+
if (typeof v !== "string")
|
|
67
|
+
return false;
|
|
68
|
+
const s = v.normalize("NFKC").trim();
|
|
69
|
+
if (s === "")
|
|
70
|
+
return false;
|
|
71
|
+
// 外のサービスの呼び名を `@名前` の形で書いたもの
|
|
72
|
+
if (/^@\S+$/.test(s))
|
|
73
|
+
return true;
|
|
74
|
+
// ドットの無い社内アドレス(`taro@example`)も外の ID に入れる
|
|
75
|
+
if (EMAIL_WHOLE.test(s))
|
|
76
|
+
return true;
|
|
77
|
+
const m = TYPED_ID_WHOLE.exec(s);
|
|
78
|
+
return m != null && !INSIDE_KINDS.has(m[1].toLowerCase());
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* `<種類>:<値>` を種類と値に割る。画面が「どのサービスの ID か」を出すための口。
|
|
82
|
+
*
|
|
83
|
+
* 内側の呼び名(`member:` / `minute:` 等)と、形に当たらない値は `null` を返す
|
|
84
|
+
* ——外の ID として解釈してはいけないものを、呼び出し側が取り違えないようにする。
|
|
85
|
+
* 全角で書かれていても割れるよう、`isExternalId` と同じく先に半角へ寄せてから当てる
|
|
86
|
+
* (返す種類・値も寄せたあとの字面)。
|
|
87
|
+
*/
|
|
88
|
+
function splitTypedId(v) {
|
|
89
|
+
if (typeof v !== "string")
|
|
90
|
+
return null;
|
|
91
|
+
const m = TYPED_ID_WHOLE.exec(normalizeSeparators(v.normalize("NFKC").trim()));
|
|
92
|
+
if (!m)
|
|
93
|
+
return null;
|
|
94
|
+
if (INSIDE_KINDS.has(m[1].toLowerCase()))
|
|
95
|
+
return null;
|
|
96
|
+
return { kind: m[1], value: m[2] };
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* 関係者の記録で「つながっている ID」を載せるフロントマターの欄の名前(#686)。
|
|
100
|
+
*
|
|
101
|
+
* 🔴 **この綴りを他所に書かない。** 欄名は engine 側でまだ確定していない
|
|
102
|
+
*(`identities` か `linked_ids` か未裁定)。読む側(画面の「つながっている ID」の節と
|
|
103
|
+
* レコード情報のパネル)と、書く側(AIで編集の下書き)が別々に綴りを持つと、
|
|
104
|
+
* engine が改名した日に**片方だけが追随して静かに食い違う**。定数はここ 1 本にして、
|
|
105
|
+
* 両方がこれを引く。
|
|
106
|
+
*
|
|
107
|
+
* 🔴 画面の中では `identities` と呼ばない(`linkedIds` / `.linked-ids` / `.li-*`)。
|
|
108
|
+
* `identities` という語は、このリポジトリでは既に `GoldCommitIdentity`
|
|
109
|
+
*(帯のコミットの筆者)の意味で使っている——同じ語を 2 つの意味で使わない。
|
|
110
|
+
*/
|
|
111
|
+
exports.FM_LINKED_IDS = "identities";
|
|
112
|
+
/**
|
|
113
|
+
* 関係者(Members)の記録か。
|
|
114
|
+
*
|
|
115
|
+
* 🔴 **パスの文字列だけで判定しない。** `toRepoRelative` は `process.cwd()` 相対で、
|
|
116
|
+
* cwd がリポジトリルートでないと `records` に倒れる——判定が外れると**関係者の記録なのに
|
|
117
|
+
* 関係者として扱われない**側に倒れる。だから cwd に依存しない印を 3 つ見る。
|
|
118
|
+
*
|
|
119
|
+
* #681 の伏せの実装(`person-redact.ts`)は #686 で撤去したが、**この判定だけは残す**
|
|
120
|
+
* ——伏せとは関係なく「関係者の記録か」は要るもので、書き直すと同じ取りこぼしを踏む。
|
|
121
|
+
*
|
|
122
|
+
* @param type frontmatter の `type`
|
|
123
|
+
* @param id frontmatter の `id`(engine は `member:{氏名}` を必須にしている)
|
|
124
|
+
* @param sectionDir 区画のディレクトリ名(`records` の親。絶対パスから取る)
|
|
125
|
+
*/
|
|
126
|
+
function isMemberRecord(type, id, sectionDir) {
|
|
127
|
+
if (typeof type === "string" && type.trim() === "member")
|
|
128
|
+
return true;
|
|
129
|
+
if (typeof id === "string" && id.startsWith("member:"))
|
|
130
|
+
return true;
|
|
131
|
+
return sectionDir === "Members";
|
|
132
|
+
}
|
|
133
|
+
/** そのリポジトリ相対パスが関係者の記録の置き場か(git のパスは cwd に依存しない) */
|
|
134
|
+
function isMemberRecordPath(repoPath) {
|
|
135
|
+
return /(^|\/)Members\/records\/[^/]+\.md$/.test(repoPath);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* `<種類>:<値>` 1 つぶんの形(文字列ぜんたい)の**正規表現の素**(#686)。
|
|
139
|
+
*
|
|
140
|
+
* 🔴 **画面の埋め込み JS に同じ形を 2 度書かないために出す。** 名寄せの提案を
|
|
141
|
+
* 「同じ人ですか」の問いに変えてよいかの門は、この形だけで見分ける
|
|
142
|
+
*(`isExternalId` の「`@名前`」「素のメール」の枝は門に使わない——`aliases` には
|
|
143
|
+
* ハンドル名が普通に入るので、`@yamada` が門を通ると、engine が永久に読まない行を
|
|
144
|
+
* 記録に書くことになる)。綴りを写すと、engine の形が変わった日に画面だけが取り残される。
|
|
145
|
+
*/
|
|
146
|
+
exports.TYPED_ID_WHOLE_SRC = "^" + TYPED_ID_SRC + "$";
|
|
147
|
+
/** 内側の呼び名の一覧(同上。埋め込み JS が `splitTypedId` と同じ除外を持つため) */
|
|
148
|
+
exports.INSIDE_KIND_LIST = [...INSIDE_KINDS];
|
package/dist/rederivation.js
CHANGED
|
@@ -9,6 +9,10 @@ exports.parseLedger = parseLedger;
|
|
|
9
9
|
exports.readRederivationLedgers = readRederivationLedgers;
|
|
10
10
|
exports.parseInjectedNow = parseInjectedNow;
|
|
11
11
|
exports.activityScore = activityScore;
|
|
12
|
+
exports.comparableContent = comparableContent;
|
|
13
|
+
exports.sameRederivedContent = sameRederivedContent;
|
|
14
|
+
exports.contentShaOf = contentShaOf;
|
|
15
|
+
exports.rederivedContentKey = rederivedContentKey;
|
|
12
16
|
exports.rederivedBandOf = rederivedBandOf;
|
|
13
17
|
exports.orderByActivity = orderByActivity;
|
|
14
18
|
/**
|
|
@@ -34,8 +38,10 @@ exports.orderByActivity = orderByActivity;
|
|
|
34
38
|
* 引数で注入する(`generatedAt`)——`new Date()` を中で読むと、固定日付のテストが
|
|
35
39
|
* 実時間と比べられて**ある日から赤くなる**(作業規約「日付爆弾」)。
|
|
36
40
|
*/
|
|
41
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
37
42
|
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
38
43
|
const node_path_1 = __importDefault(require("node:path"));
|
|
44
|
+
const gray_matter_1 = __importDefault(require("gray-matter"));
|
|
39
45
|
const commit_classify_1 = require("./commit-classify");
|
|
40
46
|
/** 帳簿の版(違ったら空として扱う・engine `LEDGER_VERSION`) */
|
|
41
47
|
exports.LEDGER_VERSION = 1;
|
|
@@ -135,12 +141,15 @@ async function readRederivationLedgers(repoRoot) {
|
|
|
135
141
|
const changedBy = Array.isArray(row?.changed_by)
|
|
136
142
|
? row.changed_by.map((s) => String(s)).filter((s) => s !== "")
|
|
137
143
|
: [];
|
|
144
|
+
// 中身の指紋は**文字列のときだけ**持つ(engine がまだ書かないので、いまは常に無い)
|
|
145
|
+
const contentSha = typeof confirmed.content_sha === "string" ? confirmed.content_sha : "";
|
|
138
146
|
out.confirmation.set(p, {
|
|
139
147
|
at: confirmed.at != null ? String(confirmed.at) : "",
|
|
140
148
|
by: confirmed.by != null ? String(confirmed.by) : "",
|
|
141
149
|
verified: confirmed.verified === true,
|
|
142
150
|
commit: commit,
|
|
143
151
|
changedBy: changedBy,
|
|
152
|
+
...(contentSha ? { contentSha: contentSha } : {}),
|
|
144
153
|
});
|
|
145
154
|
}
|
|
146
155
|
}
|
|
@@ -250,6 +259,84 @@ function activityScore(entry, nowIso) {
|
|
|
250
259
|
// 未来の日付(時計のずれ・手で書いた帳簿)は上の下限で潰れるので、ここは最後の網
|
|
251
260
|
return Number.isFinite(score) ? score : null;
|
|
252
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* 比較のための正規化。**末尾の改行・行末の空白を落とし、NFC にそろえる。**
|
|
264
|
+
*
|
|
265
|
+
* 改行コード(CRLF / CR)も LF にそろえる——「説明文が変わりましたか」の問いに対して、
|
|
266
|
+
* 改行コードだけの差を「変わった」と答えるのは嘘に近い。
|
|
267
|
+
*/
|
|
268
|
+
function normalizeForCompare(text) {
|
|
269
|
+
return String(text ?? "")
|
|
270
|
+
.replace(/\r\n?/g, "\n")
|
|
271
|
+
.split("\n")
|
|
272
|
+
.map((line) => line.replace(/[ \t\u3000]+$/, ""))
|
|
273
|
+
.join("\n")
|
|
274
|
+
.replace(/\n+$/, "")
|
|
275
|
+
.normalize("NFC");
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* レコードの生テキストから、**人が読むところだけ**を取り出す(印の判定の材料)。
|
|
279
|
+
*
|
|
280
|
+
* 🔴 **`description` のフォールバックを使わない**(用語集の `synonyms` の補足行・旧 `summary`)。
|
|
281
|
+
* 画面の要約はそれらを継ぎ足して作るが、この印が言うのは「**説明文が変わった**」なので、
|
|
282
|
+
* `description` の欄そのものだけを見る。継ぎ足すと「別名を 1 つ足しただけ」で印が立つ。
|
|
283
|
+
*
|
|
284
|
+
* 🔴 **frontmatter の他の欄は 1 つも見ない。** 今回の誤報(関係者 11 人に出た回)の原因は
|
|
285
|
+
* 使われない 1 行(`side: cm`)が移行で消えたことだった。`identities` のような欄が
|
|
286
|
+
* 足された晩に、説明文が 1 文字も変わっていないのに印を立てない。
|
|
287
|
+
*/
|
|
288
|
+
function comparableContent(raw) {
|
|
289
|
+
let data = {};
|
|
290
|
+
let content = String(raw ?? "");
|
|
291
|
+
try {
|
|
292
|
+
const parsed = (0, gray_matter_1.default)(String(raw ?? ""));
|
|
293
|
+
data = (parsed.data ?? {});
|
|
294
|
+
content = parsed.content;
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
// frontmatter が壊れているレコードは**本文として丸ごと比べる**(読めないことを
|
|
298
|
+
// 「変わった」の側に倒さない。壊れた形のまま両方同じなら印は立たない)
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
description: normalizeForCompare(String(data.description ?? "")),
|
|
302
|
+
body: normalizeForCompare(content),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
/** 2 つの版で「説明文と本文」が同じか(frontmatter の他の欄の差は見ない) */
|
|
306
|
+
function sameRederivedContent(a, b) {
|
|
307
|
+
const x = comparableContent(a);
|
|
308
|
+
const y = comparableContent(b);
|
|
309
|
+
return x.description === y.description && x.body === y.body;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* 「説明文と本文」の指紋。**帳簿の `confirmed.content_sha` と突き合わせる綴り**。
|
|
313
|
+
*
|
|
314
|
+
* 🔴 engine がこの欄を書き始めるまで、この値と比べる相手は存在しない。**一致したときだけ
|
|
315
|
+
* 「変わっていない」と言い、食い違ったら git へ落とす**(`attachRederived`)——engine が
|
|
316
|
+
* 別の綴りで指紋を作っていた場合に、全レコードへ誤報が出るのを避けるため。
|
|
317
|
+
*/
|
|
318
|
+
function contentShaOf(raw) {
|
|
319
|
+
const c = comparableContent(raw);
|
|
320
|
+
return node_crypto_1.default.createHash("sha256").update(c.description + "\u0000" + c.body, "utf8").digest("hex");
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* そのレコードが印の**候補**なら、人が確認した版を git から引くための key
|
|
324
|
+
* (`<基準コミット>:<path>`)。候補でなければ null。
|
|
325
|
+
*
|
|
326
|
+
* 🔴 **決定表の行 0〜3 と同じ門**(`rederivedBandOf` の手前の安い行だけ)。ここで候補を
|
|
327
|
+
* 絞ってから `readBlobsAt` に**まとめて 1 回**渡す——全レコードぶんの key を作ると、
|
|
328
|
+
* 用語が 1400 件ある案件で毎晩 1400 個の blob を読むことになる。
|
|
329
|
+
*/
|
|
330
|
+
function rederivedContentKey(recordPath, status, ledgers) {
|
|
331
|
+
if (!isConfirmedStatus(status))
|
|
332
|
+
return null;
|
|
333
|
+
if (!BAND_SECTION_RE.test(recordPath))
|
|
334
|
+
return null;
|
|
335
|
+
const conf = ledgers.confirmation.get(recordPath);
|
|
336
|
+
if (!conf || !conf.commit || !conf.changedBy.length)
|
|
337
|
+
return null;
|
|
338
|
+
return `${conf.commit}:${recordPath}`;
|
|
339
|
+
}
|
|
253
340
|
/**
|
|
254
341
|
* そのレコードに帯を立てるか(#665 の**決定表**・上から順に評価し、当たった行で終わる)。
|
|
255
342
|
*
|
|
@@ -265,15 +352,24 @@ function activityScore(entry, nowIso) {
|
|
|
265
352
|
* | 4c | 分類が「人」または「不明」 | 落とす |
|
|
266
353
|
* | 4d | **どちらかの綴りで引っ越し** | 落とす |
|
|
267
354
|
* | 4e | author 日付が確認日**以前**(epoch 比較) | 落とす |
|
|
268
|
-
* | 5 | 残りのうち最も新しい author 日付を「書き直した日」にする |
|
|
355
|
+
* | 5 | 残りのうち最も新しい author 日付を「書き直した日」にする | 候補になる |
|
|
356
|
+
* | **8** | **人が確認した版から「説明文(`description`)か本文」が変わったか**(`verdict`) | **`changed` のときだけ立てる** |
|
|
269
357
|
* | 6 | 根拠は、帳簿の行の `commit` が `changed_by` に入っているときだけ出す | 出す/出さない |
|
|
270
358
|
* | 7 | 基準コミットが削除のコミットなら「戻す」を出さない(`baseDeleted`) | — |
|
|
271
359
|
*
|
|
360
|
+
* 🔴 **行 8 がこの機能の本丸**(2026-09-20 の誤報)。前はここが無く、「確認の基準コミットより
|
|
361
|
+
* 後に機械のコミットがその path に 1 件でもあるか」だけで立てていた——**ファイルの中身を
|
|
362
|
+
* 1 バイトも見ていなかった**ので、移行でフロントマターの使わない 1 行(`side: cm`)が
|
|
363
|
+
* 消えただけの回にも、関係者 11 人の画面に「変わりました」が出た。
|
|
364
|
+
*
|
|
272
365
|
* @param status その記録の `rstatus`(`String()` 済み)。**2 番目の必須引数**にしてある——
|
|
273
366
|
* 末尾の任意引数にすると、渡し忘れが「行 0 の門が静かに開く」になる(設計レビュー 🔴3)
|
|
367
|
+
* @param verdict 行 8 の答えを引く関数(`(recordPath, baseCommit) => ContentVerdict`)。
|
|
368
|
+
* 🔴 **必須引数**にしてある——任意にすると、渡し忘れが「中身を見ない昔の判定」に静かに戻る。
|
|
369
|
+
* `changed` 以外(`same` / `unknown`)は**立てない**
|
|
274
370
|
* @param miss 立たなかった理由を数える器(呼び出し側で 1 つ作って全レコードで共有する)
|
|
275
371
|
*/
|
|
276
|
-
function rederivedBandOf(recordPath, status, ledgers, identities, miss) {
|
|
372
|
+
function rederivedBandOf(recordPath, status, ledgers, identities, verdict, miss) {
|
|
277
373
|
// 0. 「AI生成・未確認」の札が出る記録には帯を重ねない(#667)。**綴りは 1 か所**
|
|
278
374
|
//(ここで `status === "draft"` と直書きすると、判定が 2 つに増えて一致が崩れる)
|
|
279
375
|
if (!isConfirmedStatus(status))
|
|
@@ -336,6 +432,12 @@ function rederivedBandOf(recordPath, status, ledgers, identities, miss) {
|
|
|
336
432
|
// いちばん重い壊れ方なので、既定(立てない側)に倒す。engine が次の夜に基準点を動かす
|
|
337
433
|
if (newestHuman > newest.ms)
|
|
338
434
|
return null;
|
|
435
|
+
// 8. 🔴 **中身が変わったか。** ここまでは「機械がこの path を触ったか」しか見ていない。
|
|
436
|
+
// 触っただけ(フロントマターの使わない欄が移行で消えた・`identities` の欄が足された)で
|
|
437
|
+
// 印を立てると、説明文が 1 文字も変わっていない回に「変わりました」が出る(2026-09-20 の誤報)。
|
|
438
|
+
// 引けないとき(`unknown`)も立てない——誤報より無言
|
|
439
|
+
if (verdict(recordPath, conf.commit) !== "changed")
|
|
440
|
+
return null;
|
|
339
441
|
// 6. 根拠の件数は「この帯のコミットの記録」から出す(別の夜の数字を並べない)
|
|
340
442
|
const red = ledgers.rederivation.get(recordPath);
|
|
341
443
|
const evidence = red && red.evidence != null && red.commit && conf.changedBy.includes(red.commit)
|