@takagaki/cortex-decisions-viewer 0.12.73 → 0.12.75

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
@@ -57,9 +57,35 @@ const marked_1 = require("marked");
57
57
  const sanitize_html_1 = __importDefault(require("sanitize-html"));
58
58
  const repo_1 = require("./repo");
59
59
  const rederivation_1 = require("./rederivation");
60
+ const person_redact_1 = require("./person-redact");
60
61
  // 直近のGold昇格で追加されたレコードのパス集合。レコード生成の各所から参照するのでモジュールスコープに置く
61
62
  // (引数で引き回すと呼び出し側の改修が広範囲になる)。build() の冒頭で1回だけ解決する。
62
63
  let LATEST_PATHS = new Set();
64
+ /**
65
+ * メンバーの記録から落としたメールアドレス・外の ID の件数(#681)。
66
+ *
67
+ * **ビルドログに出す。** 実データ(案件リポジトリ)はこのリポジトリから触れないので、
68
+ * 「どれだけ隠したか」を数えられる口はここしかない。配布後に Actions のログから
69
+ * 全案件ぶんが拾える(発注書 3 の「見つけた数だけ報告する」)。
70
+ */
71
+ const memberRedaction = {
72
+ records: 0,
73
+ dropped: 0,
74
+ reset() {
75
+ this.records = 0;
76
+ this.dropped = 0;
77
+ },
78
+ record(dropped) {
79
+ this.records++;
80
+ this.dropped += dropped;
81
+ },
82
+ report() {
83
+ // 落とすものが 1 つも無かった案件では黙る(毎晩「0 個落としました」を出さない)
84
+ if (!this.dropped)
85
+ return;
86
+ console.warn(`メンバーの記録 ${this.records} 件から、メールアドレスと外の ID を ${this.dropped} 個落としました(画面にも配るデータにも出しません)`);
87
+ },
88
+ };
63
89
  /**
64
90
  * このレコードが「直近のGold昇格で追加されたもの」か。
65
91
  *
@@ -676,13 +702,16 @@ function attachRederived(records, recordsRel, ctx, nowIso) {
676
702
  if (!ctx)
677
703
  return records;
678
704
  const pathOf = (r) => (recordsRel ? `${recordsRel}/${r.fileName}` : r.fileName);
705
+ // 🔴 **「確認済み」は `status` だけで決まる**(#667)。帯も並びも同じ `rstatus` を見る
706
+ // ——ここで別のものを渡すと、画面の札と食い違う(「未確認」の札が付いた記録に帯が出る)
707
+ const statusOf = (r) => r.rstatus;
679
708
  for (const r of records) {
680
- const band = (0, rederivation_1.rederivedBandOf)(pathOf(r), ctx.ledgers, ctx.identities, ctx.miss);
709
+ const band = (0, rederivation_1.rederivedBandOf)(pathOf(r), statusOf(r), ctx.ledgers, ctx.identities, ctx.miss);
681
710
  // **帯が立たないレコードにはキーごと付けない**(空の器を通さない)
682
711
  if (band)
683
712
  r.rederived = band;
684
713
  }
685
- return (0, rederivation_1.orderByActivity)(records, pathOf, ctx.ledgers, nowIso);
714
+ return (0, rederivation_1.orderByActivity)(records, pathOf, statusOf, ctx.ledgers, nowIso);
686
715
  }
687
716
  /**
688
717
  * AIからの更新提案(`.cortex/gold-proposals.json`)を読む。
@@ -917,7 +946,10 @@ async function loadGenericRecords(dirAbs, exclude, repoBaseUrl, branch) {
917
946
  if (exclude.has(e.name.toLowerCase()))
918
947
  continue;
919
948
  const raw = await node_fs_1.promises.readFile(path.join(dirAbs, e.name), "utf8");
920
- records.push(await parseGeneric(raw, e.name, repoRelDir, repoBaseUrl, branch));
949
+ // 区画のディレクトリ名(`records` の親)を**絶対パスから**渡す。メンバーの判定に使う
950
+ // ——`repoRelDir` は `process.cwd()` 相対で、cwd 次第で前置きが落ちる(#681)
951
+ const sectionDir = path.basename(path.dirname(path.resolve(dirAbs)));
952
+ records.push(await parseGeneric(raw, e.name, repoRelDir, repoBaseUrl, branch, sectionDir));
921
953
  }
922
954
  // 日付降順 → タイトル昇順(用語集のように日付が同一でも安定して並ぶ)
923
955
  records.sort((a, b) => {
@@ -927,7 +959,7 @@ async function loadGenericRecords(dirAbs, exclude, repoBaseUrl, branch) {
927
959
  });
928
960
  return records;
929
961
  }
930
- async function parseGeneric(raw, fileName, repoRelDir, repoBaseUrl, branch) {
962
+ async function parseGeneric(raw, fileName, repoRelDir, repoBaseUrl, branch, sectionDir) {
931
963
  const { data, content } = (0, gray_matter_1.default)(raw);
932
964
  const h1 = content.match(/^#\s+(.+)$/m);
933
965
  const id = String(data.id ?? fileName.replace(/\.md$/i, ""));
@@ -947,6 +979,14 @@ async function parseGeneric(raw, fileName, repoRelDir, repoBaseUrl, branch) {
947
979
  const editUrl = repoBaseUrl
948
980
  ? `${repoBaseUrl}/edit/${branch}/${encodePath(filePath)}`
949
981
  : null;
982
+ // メンバーの記録は、メールアドレスと外の ID を**配るデータに載せない**(#681)。
983
+ // 画面側ではなくここで落とす——`fm` は `JSON.stringify` でページのソースに丸ごと埋まる
984
+ const sanitized = sanitizeFm(data);
985
+ const isMember = (0, person_redact_1.isMemberRecord)(data.type, data.id, sectionDir);
986
+ const redacted = isMember ? (0, person_redact_1.redactMemberFm)(sanitized) : null;
987
+ if (redacted)
988
+ memberRedaction.record(redacted.dropped);
989
+ const proposals = takeProposals(filePath);
950
990
  return {
951
991
  id,
952
992
  title,
@@ -961,11 +1001,13 @@ async function parseGeneric(raw, fileName, repoRelDir, repoBaseUrl, branch) {
961
1001
  rtype: data.type != null ? String(data.type) : "",
962
1002
  rstatus: data.status != null ? String(data.status) : "",
963
1003
  isLatest: isLatestPath(filePath),
964
- proposals: takeProposals(filePath),
1004
+ proposals: isMember && proposals ? (0, person_redact_1.redactMemberProposals)(proposals) : proposals,
1005
+ // 🔴 `relations` / `source` には形の判定を当てない。中身は内側の参照(`minute:` 等)で、
1006
+ // 当てるとメンバー → 議事録・共有資料・デザインの線がまるごと消える(設計レビュー🔴2)
965
1007
  relations: toRelations(data.relations),
966
1008
  source: data.source != null ? String(data.source) : "",
967
1009
  bodyLinks: extractBodyLinks(content),
968
- fm: sanitizeFm(data),
1010
+ fm: redacted ? redacted.fm : sanitized,
969
1011
  fmErrors: validateFm(data),
970
1012
  };
971
1013
  }
@@ -1898,6 +1940,7 @@ async function build(opts) {
1898
1940
  // 同一プロセスで2回ビルドしても前回の値が残らないよう、毎回入れ替える
1899
1941
  PROPOSALS_BY_PATH = await readGoldProposals(repoRoot);
1900
1942
  PROPOSAL_PATHS_MATCHED = new Set();
1943
+ memberRedaction.reset();
1901
1944
  // 再導出の 3 帳簿(#665 便 12b)。**帳簿が無い案件では帯も並び替えも作らない**ので、
1902
1945
  // 読めたときだけ git の素性を取りに行く(帳簿を持たない 44 案件で git の呼び出しを増やさない)
1903
1946
  const ledgers = await (0, rederivation_1.readRederivationLedgers)(repoRoot);
@@ -1927,7 +1970,8 @@ async function build(opts) {
1927
1970
  if (knowledgeRoot) {
1928
1971
  try {
1929
1972
  const raw = await node_fs_1.promises.readFile(path.join(knowledgeRoot, "Home.md"), "utf8");
1930
- home = await parseGeneric(raw, "Home.md", toRepoRelative(knowledgeRoot), repo.baseUrl, repo.branch);
1973
+ // Home.md は区画の下ではなく Gold の根に置く(=メンバーの記録では絶対に無い)
1974
+ home = await parseGeneric(raw, "Home.md", toRepoRelative(knowledgeRoot), repo.baseUrl, repo.branch, path.basename(path.resolve(knowledgeRoot)));
1931
1975
  // 自己記述の節を取り出し、**本文からは抜く**(最上部に出すので、残すと2度出る)。
1932
1976
  // 旧い使い方の説明もここで落ちる(説明はビューアが持つようになった)
1933
1977
  const grounding = await buildHomeGrounding(raw);
@@ -1993,6 +2037,7 @@ async function build(opts) {
1993
2037
  `(浅い clone か履歴の窓の外。人か機械か見分けられないので帯は出しません): ` +
1994
2038
  shas.slice(0, 3).map((s) => s.slice(0, 8)).join(", ") + (shas.length > 3 ? " ほか" : ""));
1995
2039
  }
2040
+ memberRedaction.report();
1996
2041
  const unmatched = [...PROPOSALS_BY_PATH.keys()].filter((k) => !PROPOSAL_PATHS_MATCHED.has(k));
1997
2042
  if (unmatched.length) {
1998
2043
  console.warn(`gold-proposals.json: レコードに突き合わない提案が ${unmatched.length} 件ありました(表示しません): ` +
@@ -2010,7 +2055,10 @@ async function build(opts) {
2010
2055
  // git が無い・浅い clone・該当0件なら空で、履歴の節が出ないだけ(他の表示は壊れない)。
2011
2056
  // 走るのは latestGeneratedPaths と同じ **リポジトリルート**(process.cwd())——
2012
2057
  // Decisions ディレクトリで実行すると pathspec に一致せず常に空になる
2013
- const aiHistory = (0, repo_1.collectAiChangeHistory)(process.cwd());
2058
+ // 🔴 **伏せるのは repo.ts ではなくここ。** `isFrontmatterKeyRename` は before/after の値が
2059
+ // 全部一致したらエントリを丸ごと捨てるので、収集の側で伏せると**別々の ID が全部
2060
+ // 「(伏せた値)」になって一致し、本物の書き換えが履歴から黙って消える**(#681)
2061
+ const aiHistory = (0, person_redact_1.redactAiHistory)((0, repo_1.collectAiChangeHistory)(process.cwd()));
2014
2062
  // 案件アイコン(#348)。Cortex構成でなければ undefined(旧レイアウトには置き場が無い)
2015
2063
  const projectIcon = await findProjectIcon(knowledgeRoot);
2016
2064
  const site = {
@@ -0,0 +1,244 @@
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
+ }
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.emptyLedgers = exports.LEDGER_VERSION = void 0;
7
+ exports.isConfirmedStatus = isConfirmedStatus;
7
8
  exports.parseLedger = parseLedger;
8
9
  exports.readRederivationLedgers = readRederivationLedgers;
9
10
  exports.parseInjectedNow = parseInjectedNow;
@@ -49,6 +50,24 @@ const ACTR_DECAY = 0.5;
49
50
  * 0.5 日=「今日触れられた」を半日前として数える(ACT-R の実装で普通に使う丸め)。
50
51
  */
51
52
  const MIN_ELAPSED_DAYS = 0.5;
53
+ /**
54
+ * その記録が「確認済み」か。**「AI生成・未確認」の札(`render.ts` の `draftBadge`)の裏返し**
55
+ * ——札が出ない記録が確認済み側(#667 のオーナー決定「確認済みは `status` だけで判断する」)。
56
+ *
57
+ * 🔴 **trim も小文字化もしない。** 札は `rec.rstatus === "draft"` の厳密比較なので、こちらを
58
+ * 1 文字でも緩めると「札は出ないのに未確認扱い」「札は出るのに確認済み扱い」が生まれる。
59
+ * `status` 欄が無い記録(`rstatus` が `""`)と `superseded` は**確認済み側**——札が出ないから。
60
+ *
61
+ * 🔴 **渡すのは `String()` 済みの `rstatus` だけ**(`build.ts:1048`)。生の frontmatter
62
+ * (`rec.fm.status`)を渡さない——YAML は真偽値も配列も返すので、札と同じ文字列にならない。
63
+ *
64
+ * ⚠️ **「確認済み」の門はもう 1 つある。** 削除の口の出し分けは `render.ts` の
65
+ * `humanConfirmedStatus`(`active|superseded|inactive` の列挙)で、`""` の扱いがここと逆。
66
+ * 揃えるかどうかは別の便(実データでは全 721 件が `status` を持つので今日の差は 0 件)。
67
+ */
68
+ function isConfirmedStatus(status) {
69
+ return status !== "draft";
70
+ }
52
71
  const emptyLedgers = () => ({
53
72
  confirmation: new Map(),
54
73
  rederivation: new Map(),
@@ -107,8 +126,11 @@ async function readRederivationLedgers(repoRoot) {
107
126
  if (!confirmed)
108
127
  continue;
109
128
  // 🔴 **基準コミットが空の行も持つ**(実装レビュー)。帯は作れない(差分も戻すも
110
- // 指す先が無い)が、**「人が確認した」という事実は並びが使う**——ここで落とすと、
111
- // 確認済みなのに先頭群から黙って外れる。帯の側(`rederivedBandOf`)で落とす
129
+ // 指す先が無い)が、行そのものは落とさずに持つ——落とす判断は帯の側
130
+ // (`rederivedBandOf`)に 1 か所で寄せる。
131
+ // ⚠️ #667 より前は「並びが『人が確認した』事実として使う」ためでもあったが、**並びは
132
+ // もう帳簿を見ない**(確認済みかどうかは `status` だけで決まる・`orderByActivity`)。
133
+ // この帳簿の役目は「どの版に戻すか」で、「確認済みかどうか」ではない
112
134
  const commit = confirmed.commit != null ? String(confirmed.commit) : "";
113
135
  const changedBy = Array.isArray(row?.changed_by)
114
136
  ? row.changed_by.map((s) => String(s)).filter((s) => s !== "")
@@ -233,6 +255,7 @@ function activityScore(entry, nowIso) {
233
255
  *
234
256
  * | 順 | 条件 | 結果 |
235
257
  * | --- | --- | --- |
258
+ * | **0** | **その記録の `status` が `draft`(=「AI生成・未確認」の札が出る)** | **立てない**(#667) |
236
259
  * | 1 | path が用語・メンバーのレコードでない | 立てない |
237
260
  * | 2 | `confirmation` に行が無い/基準コミットが空/確認日が日時として読めない | 立てない |
238
261
  * | 3 | `changed_by` が空 | 立てない |
@@ -246,9 +269,15 @@ function activityScore(entry, nowIso) {
246
269
  * | 6 | 根拠は、帳簿の行の `commit` が `changed_by` に入っているときだけ出す | 出す/出さない |
247
270
  * | 7 | 基準コミットが削除のコミットなら「戻す」を出さない(`baseDeleted`) | — |
248
271
  *
272
+ * @param status その記録の `rstatus`(`String()` 済み)。**2 番目の必須引数**にしてある——
273
+ * 末尾の任意引数にすると、渡し忘れが「行 0 の門が静かに開く」になる(設計レビュー 🔴3)
249
274
  * @param miss 立たなかった理由を数える器(呼び出し側で 1 つ作って全レコードで共有する)
250
275
  */
251
- function rederivedBandOf(recordPath, ledgers, identities, miss) {
276
+ function rederivedBandOf(recordPath, status, ledgers, identities, miss) {
277
+ // 0. 「AI生成・未確認」の札が出る記録には帯を重ねない(#667)。**綴りは 1 か所**
278
+ //(ここで `status === "draft"` と直書きすると、判定が 2 つに増えて一致が崩れる)
279
+ if (!isConfirmedStatus(status))
280
+ return null;
252
281
  if (!BAND_SECTION_RE.test(recordPath))
253
282
  return null;
254
283
  const conf = ledgers.confirmation.get(recordPath);
@@ -318,7 +347,11 @@ function rederivedBandOf(recordPath, ledgers, identities, miss) {
318
347
  const baseDeleted = base ? commit_classify_1.DELETE_SUBJECT_RE.test(base.subject) : false;
319
348
  return {
320
349
  confirmedAt: conf.at,
321
- confirmedBy: conf.by,
350
+ // 🔴 **券の無い確認では名前を器に入れない**(設計レビュー 🟡4)。画面が出さないだけだと、
351
+ // 名前は `site-data` の JSON として全案件のページに配られ続ける(`commit-classify.ts`
352
+ // の `Operated-By`「値そのものを持ち回さない」と同じ流儀)。しかも engine の `by` は
353
+ // `confirmer || authorName` の合成で、**券が無ければ「ただ触った人」でしかない**
354
+ confirmedBy: conf.verified ? conf.by : "",
322
355
  confirmedVerified: conf.verified,
323
356
  baseCommit: conf.commit,
324
357
  changedAt: newest.at,
@@ -329,13 +362,23 @@ function rederivedBandOf(recordPath, ledgers, identities, miss) {
329
362
  /**
330
363
  * 用語集の並び(#432 裁定 5-5)。**確認済みは先頭群・群の中は活性度の降順。**
331
364
  *
365
+ * 🔴 **「確認済み」は `status` だけで決まる**(#667 のオーナー決定)。**帳簿は見ない**
366
+ * ——先頭群に来るのは「AI生成・未確認」の札が付かない記録で、画面の札と 1 文字も同じ判定
367
+ * (`isConfirmedStatus`)を使う。前は `ledgers.confirmation.has(p)`(=人が 1 度でも触ったか)
368
+ * で決めていたので、**札は「未確認」なのに先頭群に居る**記録が出ていた(実測: cortex-context
369
+ * の用語集で 25 件)。帳簿の役目は「どの版に戻すか」だけになった。
370
+ *
332
371
  * - 活性度を持つレコードが 1 件も無い区画は **1 バイトも触らない**(帳簿が無い案件は従来順)
333
372
  * - 活性度を知らないレコードは群の末尾へ、**元の並びのまま**(安定)
334
373
  * - 用語集だけ(メンバーの並びは便 14・Decisions と Rules は対象外)
335
374
  *
375
+ * ℹ️ `ledgers` は活性度のために引き続き受け取る(`ledgers.activity` だけを渡す形にすると
376
+ * 「帳簿の判定に戻す」退行がコンパイルで止まる代わりに、**変異検証でそれを実演できなくなる**)。
377
+ *
336
378
  * @param records 区画のレコード(`repoPathOf` で引けるパスを持つもの。並びは変えずに新しい配列を返す)
379
+ * @param statusOf そのレコードの `rstatus`(`String()` 済み)を引く関数
337
380
  */
338
- function orderByActivity(records, pathOf, ledgers, nowIso) {
381
+ function orderByActivity(records, pathOf, statusOf, ledgers, nowIso) {
339
382
  if (!records.length)
340
383
  return records;
341
384
  // この区画が並べ替えの対象か(用語集で、活性度の行が 1 件以上あるか)
@@ -345,7 +388,7 @@ function orderByActivity(records, pathOf, ledgers, nowIso) {
345
388
  return {
346
389
  r: r,
347
390
  i: i,
348
- confirmed: inSection && ledgers.confirmation.has(p),
391
+ confirmed: inSection && isConfirmedStatus(statusOf(r)),
349
392
  score: inSection ? activityScore(ledgers.activity.get(p), nowIso) : null,
350
393
  };
351
394
  });
package/dist/render.js CHANGED
@@ -6236,13 +6236,20 @@ ${calendarAddUrl.toString()}
6236
6236
  * 「AI による最近の変更」が変わった行そのものを持っている。
6237
6237
  */
6238
6238
 
6239
- /** 帯に出す 1 行。券の無い確認(自己申告)は名前を権威として出さない(#432 5-A) */
6239
+ /**
6240
+ * 帯に出す 1 行。**帯は起きたこと(機械が書き直した日)を言う。**
6241
+ *
6242
+ * 🔴 **確認した人と日は、券のある確認(confirmedVerified)のときだけ添える**(#667)。
6243
+ * 「確認済み」は frontmatter の status だけで決まるようになり、その大半は
6244
+ * 「AIで編集」か直接編集で active になったもの=**画面から確認したわけではない**。
6245
+ * そこに「○○さんが 9/1 に確認」と書くと、起きていないことを言い切ることになる。
6246
+ * 券があるときだけ名前と日を出すので、「自己申告」の断り書きは要らなくなった。
6247
+ */
6240
6248
  function rederivedMetaText(b) {
6241
6249
  var parts = [];
6242
- var who = b.confirmedBy
6243
- ? "(" + b.confirmedBy + (b.confirmedVerified ? "" : "・自己申告") + ")"
6244
- : "";
6245
- parts.push("確認 " + ahDate(b.confirmedAt) + who);
6250
+ if (b.confirmedVerified) {
6251
+ parts.push("確認 " + ahDate(b.confirmedAt) + (b.confirmedBy ? "" + b.confirmedBy + ")" : ""));
6252
+ }
6246
6253
  parts.push("書き直し " + ahDate(b.changedAt));
6247
6254
  // **「根拠」は言えるときだけ**(この帯のコミットの記録が帳簿に無ければ出さない)
6248
6255
  if (typeof b.evidence === "number") parts.push("根拠 " + b.evidence + " 件");
@@ -6699,6 +6706,11 @@ ${calendarAddUrl.toString()}
6699
6706
  if (!g) return null;
6700
6707
  var ps = (rec && rec.proposals) || [];
6701
6708
  if (!ps.length) return null;
6709
+ // 🔴 **伏せた値をAIに渡さない**(#681)。メンバーの提案はメールアドレスと外の ID を
6710
+ // 伏せてあるので、この文面をそのまま適用させると **AIが「(伏せた値)」という文字列を
6711
+ // aliases に書き**、engine がそれを識別子の宣言として読む(台帳のいちばん強い段に
6712
+ // 偽の結び付きが立つ)。直すのは GitHub の記録の側(提案カードは読めるまま残す)
6713
+ if (ps.some(function (p) { return p.redacted; })) return null;
6702
6714
  var pathRef = repoPathOf(rec);
6703
6715
  var target = (pathRef ? pathRef + " と " : "") + g.dir + "/README.md";
6704
6716
  // 🔴 **提案データは境界宣言で挟む。** 引用(quote)は外部ソースの本文がそのまま入る値で、
@@ -6755,9 +6767,14 @@ ${calendarAddUrl.toString()}
6755
6767
  var apply = aiApplyBtn(rec, sectionName, rec.title);
6756
6768
  if (apply) head.appendChild(apply);
6757
6769
  box.appendChild(head);
6770
+ var hidden = ps.some(function (p) { return p.redacted; });
6758
6771
  box.appendChild(el("p", {
6759
6772
  class: "proposals-lead",
6760
- text: "夜間の取り込みで、この記録と食い違う内容が見つかりました。根拠を確かめたうえで、直すときは「AIで適用」から反映してください。",
6773
+ text: hidden
6774
+ // 伏せた提案には「AIで適用」が出ない(aiApplyBtn が null を返す)ので、
6775
+ // **どこで直せるのかを 1 行で言う**。帯は症状・行は一手(画面の文言の決まり)
6776
+ ? "夜間の取り込みで、この記録と食い違う内容が見つかりました。メールアドレスと外のIDは伏せてあるため、ここからは適用できません。直すときはGitHubでこの記録を開いてください。"
6777
+ : "夜間の取り込みで、この記録と食い違う内容が見つかりました。根拠を確かめたうえで、直すときは「AIで適用」から反映してください。",
6761
6778
  }));
6762
6779
  ps.forEach(function (p) {
6763
6780
  var item = el("div", { class: "proposal" }, [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.73",
3
+ "version": "0.12.75",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,