@takagaki/cortex-decisions-viewer 0.12.75 → 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 +66 -56
- package/dist/external-id.js +148 -0
- package/dist/rederivation.js +104 -2
- package/dist/render.js +696 -124
- package/dist/repo.js +80 -0
- package/package.json +1 -1
- package/dist/person-redact.js +0 -244
package/dist/repo.js
CHANGED
|
@@ -8,6 +8,7 @@ exports.classifyCommit = classifyCommit;
|
|
|
8
8
|
exports.isFrontmatterKeyRename = isFrontmatterKeyRename;
|
|
9
9
|
exports.collectAiChangeHistory = collectAiChangeHistory;
|
|
10
10
|
exports.collectGoldCommitIdentities = collectGoldCommitIdentities;
|
|
11
|
+
exports.readBlobsAt = readBlobsAt;
|
|
11
12
|
exports.lastSubstantiveUpdate = lastSubstantiveUpdate;
|
|
12
13
|
const node_child_process_1 = require("node:child_process");
|
|
13
14
|
/**
|
|
@@ -825,6 +826,85 @@ function collectGoldCommitIdentities(cwd) {
|
|
|
825
826
|
}
|
|
826
827
|
return map;
|
|
827
828
|
}
|
|
829
|
+
/**
|
|
830
|
+
* 指定のコミットのファイルの中身を**まとめて 1 回**読む(`git cat-file --batch`)。
|
|
831
|
+
*
|
|
832
|
+
* 🔴 **レコードごとにプロセスを起こさない。** 帯(印)の判定は「人が確認した版と今の版で
|
|
833
|
+
* 説明文・本文が変わったか」を見るので、確認の基準コミットのレコードを読む必要がある。
|
|
834
|
+
* ここで `git show` をレコードごとに叩くと、用語が 1400 件ある案件でビルドが 1400 回
|
|
835
|
+
* git を起こす——`collectGoldCommitIdentities` と同じく「git の呼び出しはレコード数に
|
|
836
|
+
* 依存しない」の規律を保つため、key を stdin に流し込む `--batch` を 1 回だけ使う。
|
|
837
|
+
*
|
|
838
|
+
* @param keys `<commit>:<path>` の列(重複は畳む)。**空なら git を起こさない**。
|
|
839
|
+
* 🔴 **改行を含む key は git に渡す前に落とす**(`--batch` の問い合わせは 1 行 1 件なので、
|
|
840
|
+
* 1 つでも混ざると以降の key が**全部 1 つずつずれ**、`a.md` の中身が `b.md` として返る)
|
|
841
|
+
* @returns 引けた key → 中身(utf8)。**引けなかった key は入れない**——呼び出し側は
|
|
842
|
+
* 「入っていない = 引けなかった(窓の外・改名・削除)」として扱うこと。
|
|
843
|
+
* `missing` と「引けたが空」を言い分けるために、キーの有無で表す
|
|
844
|
+
*/
|
|
845
|
+
function readBlobsAt(cwd, keys) {
|
|
846
|
+
const out = new Map();
|
|
847
|
+
// 🔴 **改行(LF / CR)を含む key を git に渡さない。** `cat-file --batch` の問い合わせは
|
|
848
|
+
// 「1 行 1 件」で、応答も**問い合わせた順**に並ぶ前提でここは読んでいる。改行入りの key が
|
|
849
|
+
// 1 つ混ざると git はそれを 2 件として扱い、**そこから先の対応がすべて 1 つずれる**
|
|
850
|
+
// ——実測で `c.md` の欄に `b.md` の中身が入った(=印の判定を他人の版と比べる)。
|
|
851
|
+
// ファイル名に改行を入れられる案件は稀だが、**壊れ方が「引けない」ではなく「別人の中身」**
|
|
852
|
+
// なので、落として「引けなかった」に倒す(呼び出し側は印を出さない側へ倒れる)
|
|
853
|
+
const wanted = [...new Set(keys.filter((k) => k !== "" && !/[\r\n]/.test(k)))];
|
|
854
|
+
if (!wanted.length)
|
|
855
|
+
return out;
|
|
856
|
+
let buf;
|
|
857
|
+
try {
|
|
858
|
+
buf = (0, node_child_process_1.execFileSync)("git",
|
|
859
|
+
// 出力の形を読む人の git 設定に左右させない(gitLog と同じ理由)。
|
|
860
|
+
// `--batch` は素の中身をそのまま出すので、ここで要るのは色と引用の抑止だけ
|
|
861
|
+
["-c", "core.quotepath=false", "-c", "color.ui=false", "cat-file", "--batch"], {
|
|
862
|
+
cwd,
|
|
863
|
+
// 🔴 **`input` を渡すので `gitLog` は使えない**(あちらは stdin を ignore している)。
|
|
864
|
+
//
|
|
865
|
+
// ⚠️ **末尾の改行は「無いと最後の 1 件が読まれない」ではない**(2026-09-20 に実測で訂正)。
|
|
866
|
+
// git 2.43 は EOF で最後の行を読むので、付けても付けなくても応答は 1 バイトも変わらない
|
|
867
|
+
// ——**テストで守れる不変条件ではない**(変異を当てても緑のまま)。それでも付けるのは、
|
|
868
|
+
// `--batch` の入力が「1 行 1 件」だと綴りの上で言い切るため(行の途中で EOF にしない)
|
|
869
|
+
input: wanted.join("\n") + "\n",
|
|
870
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
871
|
+
// 🔴 **Buffer で受ける。** ヘッダの size は**バイト数**なので、文字で数えると
|
|
872
|
+
// 日本語のレコード(実データはほぼ全部)で切り出し位置がずれる
|
|
873
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
catch (e) {
|
|
877
|
+
// git が無い(ENOENT)・リポジトリでない(128)は既定の状態なので警告しない
|
|
878
|
+
//(`gitLog` と同じ規律)。戻り値は空=「1 件も引けなかった」で、呼び出し側は
|
|
879
|
+
// 印を立てない側に倒れる(誤報より無言)
|
|
880
|
+
const err = e;
|
|
881
|
+
if (err.code !== "ENOENT" && err.status !== 128) {
|
|
882
|
+
console.warn(`人が確認した版の中身を git から取れませんでした(印は出さずに続けます): ${String(e)}`);
|
|
883
|
+
}
|
|
884
|
+
return out;
|
|
885
|
+
}
|
|
886
|
+
// 応答は**問い合わせた順**に並ぶ。引けた行は `<oid> blob <size>` + 中身、
|
|
887
|
+
// 引けない行は `<key> missing`(key をそのまま返すので oid の形に当たらない)
|
|
888
|
+
let pos = 0;
|
|
889
|
+
for (const key of wanted) {
|
|
890
|
+
if (pos >= buf.length)
|
|
891
|
+
break;
|
|
892
|
+
const nl = buf.indexOf(0x0a, pos);
|
|
893
|
+
if (nl < 0)
|
|
894
|
+
break;
|
|
895
|
+
const header = buf.toString("utf8", pos, nl);
|
|
896
|
+
pos = nl + 1;
|
|
897
|
+
const m = /^([0-9a-f]{40,64}) (\S+) (\d+)$/.exec(header);
|
|
898
|
+
// `missing` / `ambiguous` の行は中身が続かないので、次の key へ進むだけ
|
|
899
|
+
if (!m)
|
|
900
|
+
continue;
|
|
901
|
+
const size = Number(m[3]);
|
|
902
|
+
if (m[2] === "blob")
|
|
903
|
+
out.set(key, buf.toString("utf8", pos, pos + size));
|
|
904
|
+
pos += size + 1; // 中身の後ろに git が付ける改行
|
|
905
|
+
}
|
|
906
|
+
return out;
|
|
907
|
+
}
|
|
828
908
|
function lastSubstantiveUpdate(cwd, repoPath) {
|
|
829
909
|
// 🔴 **浅い clone を最初に弾く。** 浅い clone では境界コミットが親を持たないため、
|
|
830
910
|
// このファイルを触っていない直近のコミットまで `A <path>` として現れる(実測)。
|
package/package.json
CHANGED
package/dist/person-redact.js
DELETED
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.REDACTED = void 0;
|
|
4
|
-
exports.isExternalId = isExternalId;
|
|
5
|
-
exports.redactText = redactText;
|
|
6
|
-
exports.isMemberRecord = isMemberRecord;
|
|
7
|
-
exports.isMemberRecordPath = isMemberRecordPath;
|
|
8
|
-
exports.redactMemberFm = redactMemberFm;
|
|
9
|
-
exports.redactMemberProposals = redactMemberProposals;
|
|
10
|
-
exports.redactAiHistory = redactAiHistory;
|
|
11
|
-
/**
|
|
12
|
-
* このリポジトリの内側の呼び名。外の ID と見なさない。
|
|
13
|
-
*
|
|
14
|
-
* 前半は記録の id の種類(`build.ts` の `GOLD_TYPES` と同じ)、後半は Silver の参照
|
|
15
|
-
* (`build.ts` の `classifyTarget` が正本の URL に解決する 3 つ)。
|
|
16
|
-
*/
|
|
17
|
-
const INSIDE_KINDS = new Set([
|
|
18
|
-
"decision", "term", "member", "rule", "report", "overview",
|
|
19
|
-
"minute", "material", "design",
|
|
20
|
-
]);
|
|
21
|
-
/**
|
|
22
|
-
* 欄ごと落とす(中身が外の ID の形でなくても落とす)。
|
|
23
|
-
*
|
|
24
|
-
* 形の判定だけに任せない——`email: "しらべ中"` のような書きかけの値は形に当たらないが、
|
|
25
|
-
* **この欄は配らない**という決まりは中身に依らず効かせる(変異検証 M1)。
|
|
26
|
-
*/
|
|
27
|
-
const DROP_KEYS = new Set(["email"]);
|
|
28
|
-
/**
|
|
29
|
-
* 形の判定を当てない欄。
|
|
30
|
-
*
|
|
31
|
-
* 🔴 **`relations` / `references` / `source` の行き先には当てない。** `classifyTarget`
|
|
32
|
-
* (`build.ts`)は `https://…` を外部ノードとして正本の URL に解決しており、外の ID の形の
|
|
33
|
-
* 判定は `https` も外の ID と見なす——当てると**外部リンクの線が図から消える**。
|
|
34
|
-
* `id` を外すのは、落とすと「レコード情報」から id の行だけが黙って消え、
|
|
35
|
-
* **配るデータの `id` とは食い違う**ため(落とすなら両方・残すなら両方)。
|
|
36
|
-
*/
|
|
37
|
-
const KEEP_KEYS = new Set(["id", "relations", "references", "source"]);
|
|
38
|
-
/** 伏せた値の代わりに出す字。**空欄にしない**(消えたのか元から無いのかが読めなくなる) */
|
|
39
|
-
exports.REDACTED = "(伏せた値)";
|
|
40
|
-
/**
|
|
41
|
-
* `<種類>:<値>` 1 つぶん。
|
|
42
|
-
*
|
|
43
|
-
* 🔴 **コロンの直後に空白があるものは当てない。** `[a-z_]+:` + 1 文字以上は YAML の
|
|
44
|
-
* `キー: 値` そのものの形なので、空白を許すと差分の `title: 山田 太郎` まで潰して、
|
|
45
|
-
* 「何が変わったのか」が読めなくなる(設計レビュー🔴3)。
|
|
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
|
-
* ⚠ **寄せるのは区切りだけ**(`redactText` は元の字面を返すので、全部を寄せると
|
|
57
|
-
* 差分の見た目まで書き換わる)。種類まで全角の `slack_user:…` は、
|
|
58
|
-
* 文のなかでは取りこぼす——欄の要素としては `isExternalId` が全角ごと寄せて当てる
|
|
59
|
-
*/
|
|
60
|
-
function normalizeSeparators(s) {
|
|
61
|
-
return s.replace(/:/g, ":");
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* その文字列ぜんたいが外の ID か(`aliases` の 1 要素の判定)。
|
|
65
|
-
*
|
|
66
|
-
* 判定に使うだけで中身を書き戻さないので、ここでは**全角を丸ごと半角に寄せて**から当てる
|
|
67
|
-
* (`slack_user:U0X` のように、種類まで全角で書かれた行を取りこぼさない)。
|
|
68
|
-
* 文のなかを伏せる `redactText` は元の字面を返す必要があるので、そちらはコロンだけを寄せる。
|
|
69
|
-
*/
|
|
70
|
-
function isExternalId(v) {
|
|
71
|
-
if (typeof v !== "string")
|
|
72
|
-
return false;
|
|
73
|
-
const s = v.normalize("NFKC").trim();
|
|
74
|
-
if (s === "")
|
|
75
|
-
return false;
|
|
76
|
-
// 外のサービスの呼び名を `@名前` の形で書いたもの
|
|
77
|
-
if (/^@\S+$/.test(s))
|
|
78
|
-
return true;
|
|
79
|
-
// ドットの無い社内アドレス(`taro@example`)も隠す側に入れる
|
|
80
|
-
if (EMAIL_WHOLE.test(s))
|
|
81
|
-
return true;
|
|
82
|
-
const m = TYPED_ID_WHOLE.exec(s);
|
|
83
|
-
return m != null && !INSIDE_KINDS.has(m[1].toLowerCase());
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* 文のなかの外の ID とメールアドレスを、**種類を残したまま**伏せる。
|
|
87
|
-
*
|
|
88
|
-
* 🔴 **`<種類>:<値>` を先に当てる。** 先にメールアドレスを当てると `email:taro@…` が
|
|
89
|
-
* まるごと消えて、「どの欄の話か」まで読めなくなる。
|
|
90
|
-
*/
|
|
91
|
-
function redactText(s) {
|
|
92
|
-
const typed = normalizeSeparators(s).replace(new RegExp(TYPED_ID_SRC, "g"), (whole, kind) => (INSIDE_KINDS.has(kind.toLowerCase()) ? whole : kind + ":" + exports.REDACTED));
|
|
93
|
-
return typed.replace(new RegExp(EMAIL_SRC, "g"), exports.REDACTED);
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* メンバーの記録か。
|
|
97
|
-
*
|
|
98
|
-
* 🔴 **パスの文字列だけで判定しない。** `toRepoRelative` は `process.cwd()` 相対で、
|
|
99
|
-
* cwd がリポジトリルートでないと `records` に倒れる——判定が外れると**静かに漏れる側**に
|
|
100
|
-
* 倒れる(設計レビュー🟡1)。だから cwd に依存しない印を 3 つ見る。
|
|
101
|
-
*
|
|
102
|
-
* @param type frontmatter の `type`
|
|
103
|
-
* @param id frontmatter の `id`(engine は `member:{氏名}` を必須にしている)
|
|
104
|
-
* @param sectionDir 区画のディレクトリ名(`records` の親。絶対パスから取る)
|
|
105
|
-
*/
|
|
106
|
-
function isMemberRecord(type, id, sectionDir) {
|
|
107
|
-
if (typeof type === "string" && type.trim() === "member")
|
|
108
|
-
return true;
|
|
109
|
-
if (typeof id === "string" && id.startsWith("member:"))
|
|
110
|
-
return true;
|
|
111
|
-
return sectionDir === "Members";
|
|
112
|
-
}
|
|
113
|
-
/** そのリポジトリ相対パスがメンバーの記録の置き場か(git のパスは cwd に依存しない) */
|
|
114
|
-
function isMemberRecordPath(repoPath) {
|
|
115
|
-
return /(^|\/)Members\/records\/[^/]+\.md$/.test(repoPath);
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* 値の中を再帰して外の ID を落とす(`sanitizeFm` と同じ形で降りる)。
|
|
119
|
-
*
|
|
120
|
-
* 🔴 **欄の名前で拾わない。** 欄を列挙すると、engine が欄を足した晩に静かに漏れる
|
|
121
|
-
* ——`not_aliases` 自体が後から足された欄で、同じことは必ずまた起きる。リストの要素も
|
|
122
|
-
* 入れ子の値も同じ形の判定で落とす。
|
|
123
|
-
*/
|
|
124
|
-
function redactValue(v) {
|
|
125
|
-
if (Array.isArray(v)) {
|
|
126
|
-
const out = [];
|
|
127
|
-
let dropped = 0;
|
|
128
|
-
for (const e of v) {
|
|
129
|
-
if (isExternalId(e)) {
|
|
130
|
-
dropped++;
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
const r = redactValue(e);
|
|
134
|
-
dropped += r.dropped;
|
|
135
|
-
out.push(r.value);
|
|
136
|
-
}
|
|
137
|
-
return { value: out, dropped };
|
|
138
|
-
}
|
|
139
|
-
if (v && typeof v === "object") {
|
|
140
|
-
const out = {};
|
|
141
|
-
let dropped = 0;
|
|
142
|
-
for (const [k, val] of Object.entries(v)) {
|
|
143
|
-
if (isExternalId(val)) {
|
|
144
|
-
dropped++;
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
const r = redactValue(val);
|
|
148
|
-
dropped += r.dropped;
|
|
149
|
-
out[k] = r.value;
|
|
150
|
-
}
|
|
151
|
-
return { value: out, dropped };
|
|
152
|
-
}
|
|
153
|
-
return { value: v, dropped: 0 };
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* メンバーの frontmatter から、メールアドレスと外の ID を落とす。
|
|
157
|
-
*
|
|
158
|
-
* @returns 落としたあとの frontmatter と、落とした要素の数(ビルドログの件数に使う)
|
|
159
|
-
*/
|
|
160
|
-
function redactMemberFm(fm) {
|
|
161
|
-
const out = {};
|
|
162
|
-
let dropped = 0;
|
|
163
|
-
for (const [k, v] of Object.entries(fm)) {
|
|
164
|
-
if (KEEP_KEYS.has(k)) {
|
|
165
|
-
out[k] = v;
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
if (DROP_KEYS.has(k)) {
|
|
169
|
-
if (v != null && String(v).trim() !== "")
|
|
170
|
-
dropped++;
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
if (isExternalId(v)) {
|
|
174
|
-
dropped++;
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
const r = redactValue(v);
|
|
178
|
-
dropped += r.dropped;
|
|
179
|
-
// **中身が全部落ちたリストは欄ごと出さない**(`aliases` が外の ID だけだった形)。
|
|
180
|
-
// もとから空だった欄はそのまま残す——消えたのか元から無いのかを変えない
|
|
181
|
-
if (Array.isArray(v) && v.length > 0 && Array.isArray(r.value) && r.value.length === 0)
|
|
182
|
-
continue;
|
|
183
|
-
out[k] = r.value;
|
|
184
|
-
}
|
|
185
|
-
return { fm: out, dropped };
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* メンバーの提案(`field: "aliases"` 等)の値を伏せる。**提案そのものは落とさない**
|
|
189
|
-
* ——落とすと「機械が何を結ぼうとしているのか」が画面から消える。
|
|
190
|
-
*
|
|
191
|
-
* 🔴 伏せた提案には `redacted` の印を付ける。画面はこの印を見て**「AIで適用」を出さない**。
|
|
192
|
-
* 伏せたまま適用させると、AI が `(伏せた値)` という文字列を `aliases` に書き、engine が
|
|
193
|
-
* それを `<id_kind>:<id>` の宣言として読む——**台帳のいちばん強い段に偽の結び付きが立つ**
|
|
194
|
-
* (設計レビュー🔴4)。
|
|
195
|
-
*/
|
|
196
|
-
function redactMemberProposals(ps) {
|
|
197
|
-
return ps.map((p) => {
|
|
198
|
-
const current = redactText(p.current);
|
|
199
|
-
const proposed = redactText(p.proposed);
|
|
200
|
-
const reason = redactText(p.reason);
|
|
201
|
-
const evidence = p.evidence.map((e) => ({
|
|
202
|
-
source: redactText(e.source),
|
|
203
|
-
quote: e.quote == null ? e.quote : redactText(e.quote),
|
|
204
|
-
}));
|
|
205
|
-
const changed = current !== p.current ||
|
|
206
|
-
proposed !== p.proposed ||
|
|
207
|
-
reason !== p.reason ||
|
|
208
|
-
evidence.some((e, i) => e.source !== p.evidence[i].source || e.quote !== p.evidence[i].quote);
|
|
209
|
-
// **短縮記法にしない**(fleet-passthrough のテストが `名前:` の字面を走査している。
|
|
210
|
-
// 画面が読む欄をこのファイルで組み立てているので、走査の対象にも入れてある)
|
|
211
|
-
return {
|
|
212
|
-
field: p.field,
|
|
213
|
-
current: current,
|
|
214
|
-
proposed: proposed,
|
|
215
|
-
evidence: evidence,
|
|
216
|
-
reason: reason,
|
|
217
|
-
createdAt: p.createdAt,
|
|
218
|
-
// 空の器は通さない——伏せていない提案では `undefined` にして、配る JSON から
|
|
219
|
-
// キーごと落とす(画面は「印があるか」だけを見る)
|
|
220
|
-
redacted: changed ? true : undefined,
|
|
221
|
-
};
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
/**
|
|
225
|
-
* メンバーの記録の git 差分から、値だけを伏せる(どの欄が変わったかは残す)。
|
|
226
|
-
*
|
|
227
|
-
* 鍵は git が出すリポジトリ相対パスなので、`process.cwd()` に依存しない。
|
|
228
|
-
*/
|
|
229
|
-
function redactAiHistory(history) {
|
|
230
|
-
const out = {};
|
|
231
|
-
for (const [p, entries] of Object.entries(history)) {
|
|
232
|
-
out[p] = isMemberRecordPath(p)
|
|
233
|
-
? entries.map((e) => ({
|
|
234
|
-
...e,
|
|
235
|
-
// 件名も同じ行に置く——engine の名簿のコミットは今は定型文だが、
|
|
236
|
-
// 「`<種類>:<値>` を結合」のような件名になった日に、ここだけ素で残る
|
|
237
|
-
subject: redactText(e.subject),
|
|
238
|
-
before: redactText(e.before),
|
|
239
|
-
after: redactText(e.after),
|
|
240
|
-
}))
|
|
241
|
-
: entries;
|
|
242
|
-
}
|
|
243
|
-
return out;
|
|
244
|
-
}
|