@horizon_works/banto 0.5.0 → 0.7.0

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/privacy.js CHANGED
@@ -71,9 +71,52 @@ function setPolicy(dir, level) {
71
71
  // ───────────────────────────── 伏せる/戻す
72
72
  const MAPDIR = '.bantou-mask'; // ★対応表の置き場(手元だけ。AIには渡さない)
73
73
 
74
+ // 🚨 ★印の種類は、ここが唯一の正本(2026-08-21)。
75
+ // `tel` を足したとき、**数える側の正規表現に足し忘れた**。しかも数える側は2箇所あった。
76
+ // ★同じ辞書を2つ書かない。数える側は、この表から作る。
77
+ const 印の種類 = { '人名': 'name', '法人名': 'corp', '事務所名': 'office', '屋号': 'shop',
78
+ '電話': 'tel', '辞書': 'word' };
79
+ const 印の正規表現 = () =>
80
+ new RegExp('\\b(?:' + [...new Set(Object.values(印の種類))].concat('item').join('|') + ')\\d{3,6}\\b', 'g');
81
+
74
82
  function tokenFor(kind, n) {
75
- const k = { '人名': 'name', '法人名': 'corp', '事務所名': 'office', '屋号': 'shop', '辞書': 'word' }[kind] || 'item';
76
- return k + String(n).padStart(5, '0');
83
+ return (印の種類[kind] || 'item') + String(n).padStart(5, '0');
84
+ }
85
+
86
+ // 🚨 ★検出の「当たり付け」を、そのまま置き換えに使わない(2026-08-21 実測で判明)
87
+ //
88
+ // 検出側の法人名パターンは `株式会社` のあとを **12字まで貪欲に**取る。
89
+ // 候補を挙げるだけなら十分だが、**置き換えると文が壊れる**。実際にこうなった:
90
+ // 元 : 株式会社みどり工務店の締めは20日。
91
+ // 伏せ: name00001日。 ← ★「の締めは20」まで食べた
92
+ //
93
+ // ★「見つける」と「置き換える」では、要る精度が違う。同じ形を使い回さない。
94
+ const 助詞など = /[のはがをにでともへやかなど、。「」『』()()\s]/;
95
+
96
+ // 先に付く法人格(★これは「語尾」ではないので、ここで切ってはいけない)
97
+ const 法人格 = /^(?:株式会社|合同会社|有限会社|一般社団法人|一般財団法人|公益社団法人|公益財団法人|特定非営利活動法人|社会福祉法人|医療法人|学校法人|宗教法人|社労士法人|税理士法人)/;
98
+ // うしろに付く語尾(★いちばん後ろまで取る=貪欲)
99
+ const 語尾 = /^(.*(?:株式会社|工務店|商店|工業所|製作所|鉄工所|事務所|法人|会|園|店))/;
100
+
101
+ function 実体だけにする(s) {
102
+ const t = String(s);
103
+ // 🚨 最初に書いた `^(.*?(?:株式会社|…))` は、**先頭の「株式会社」自身で止まった**
104
+ // (非貪欲なので最初の一致で終わる)。結果「株式会社」だけを伏せて社名が残った。
105
+ // ★頭と尻は別に扱う。
106
+ const 頭 = (t.match(法人格) || [''])[0];
107
+ const 残り = t.slice(頭.length);
108
+ const m = 残り.match(語尾);
109
+ if (m && m[1]) return 頭 + m[1];
110
+ const i = 残り.split('').findIndex((c) => 助詞など.test(c) || /[0-90-9]/.test(c));
111
+ return 頭 + (i > 0 ? 残り.slice(0, i) : 残り);
112
+ }
113
+
114
+ function 種別(s) {
115
+ const t = String(s);
116
+ if (/(株式会社|合同会社|有限会社|社労士法人|税理士法人|医療法人|学校法人)/.test(t)) return '法人名';
117
+ if (/(事務所)$/.test(t)) return '事務所名';
118
+ if (/(工務店|商店|工業所|製作所|鉄工所)$/.test(t)) return '屋号';
119
+ return '人名';
77
120
  }
78
121
 
79
122
  /**
@@ -85,6 +128,7 @@ function maskFile(FREE, target, opts) {
85
128
  if (!fs.existsSync(src)) throw new Error('見つかりません: ' + src);
86
129
  const text = fs.readFileSync(src, 'utf8');
87
130
  const dir = path.dirname(src);
131
+ // (実体だけにする/種別 は下に定義)
88
132
 
89
133
  const always = scanAlways(text);
90
134
  const dict = FREE.loadDict();
@@ -93,7 +137,9 @@ function maskFile(FREE, target, opts) {
93
137
  // 伏せる対象=辞書の語 + 辞書に無い固有名の候補(★長いものから置換。部分一致で壊さない)
94
138
  const targets = [];
95
139
  for (const h of found.dictHits) targets.push({ text: h.word, kind: '辞書' });
96
- for (const c of found.candidates) targets.push({ text: c.text, kind: '人名' });
140
+ for (const c of found.candidates) targets.push({ text: 実体だけにする(c.text), kind: 種別(c.text) });
141
+ // ★形で分かるもの(電話番号)も伏せる。辞書が無くても効く
142
+ for (const t of (text.match(/0\d{1,4}[-(]?\d{1,4}[-)]?\d{3,4}/g) || [])) targets.push({ text: t, kind: '電話' });
97
143
  targets.sort((a, b) => b.text.length - a.text.length);
98
144
 
99
145
  const map = {}; // token → 実物
@@ -104,10 +150,16 @@ function maskFile(FREE, target, opts) {
104
150
  if (rev[t.text]) continue;
105
151
  n++;
106
152
  const tok = tokenFor(t.kind, n);
153
+ // 🚨 ★置き換わらなかったものを、印として台帳に載せない(2026-08-21 実測)
154
+ // 長い語を先に置換するので、短い語(「株式会社」だけ等)は本文から消えている。
155
+ // それでも印を作ると、戻すときに「**使われなかった印がある**」と鳴って往復が止まる。
156
+ // ★「作った数」ではなく「**実際に置き換わった数**」を数える。
157
+ const 置換後 = out.split(t.text).join(tok);
158
+ if (置換後 === out) { n--; continue; }
107
159
  rev[t.text] = tok;
108
160
  map[tok] = t.text;
109
161
  byKind[t.kind] = (byKind[t.kind] || 0) + 1;
110
- out = out.split(t.text).join(tok);
162
+ out = 置換後;
111
163
  }
112
164
 
113
165
  // ★絶対層は「伏せる」ではなく「消す」。戻す必要が無いものを持ち回らない
@@ -128,7 +180,11 @@ function maskFile(FREE, target, opts) {
128
180
  fs.writeFileSync(masked, '<!-- bantou-mask:' + id + ' -->\n' + out, 'utf8');
129
181
 
130
182
  return {
131
- 伏せた版: masked,
183
+ // 🚨 ★フルパスを返さない(2026-08-22 新しい関所が捕まえた)。
184
+ // Windows の道は C:\Users\<利用者名>\… なので、**利用者本人の名前がAIへ渡る**。
185
+ // ★AIは元のファイルの場所を自分で渡しているので、**ファイル名だけで足りる**。
186
+ 伏せた版: path.basename(masked),
187
+ 場所: '★お渡しいただいた正本と同じフォルダです',
132
188
  伏せた語の数: n,
133
189
  種別ごとの数: byKind, // ★数だけ。語は返さない
134
190
  削除した機密: always.length ? always : undefined, // ★種別と数だけ
@@ -158,7 +214,8 @@ function unmaskFile(FREE, maskedPath, outPath, force) {
158
214
  const body = text.replace(/<!--\s*bantou-mask:[0-9a-f]+\s*-->\n?/, '');
159
215
 
160
216
  // ① 表に無いトークンが混ざっていないか(★AIが壊した/作った)
161
- const used = body.match(/\b(?:name|corp|office|shop|word|item)\d{3,6}\b/g) || [];
217
+ // ★印の種類は 印の種類 表から作る(種類を足したら、ここは自動で追従する)
218
+ const used = body.match(印の正規表現()) || [];
162
219
  const unknown = [...new Set(used.filter((t) => !map[t]))];
163
220
 
164
221
  // ② 使われなかったトークン(★AIが消した)
@@ -168,7 +225,13 @@ function unmaskFile(FREE, maskedPath, outPath, force) {
168
225
  const dict = FREE.loadDict();
169
226
  const restored = Object.entries(map).reduce((s, [t, v]) => s.split(t).join(v), body);
170
227
  const found = FREE.inspectText(restored, dict);
171
- const newNames = found.candidates.filter((c) => !Object.values(map).includes(c.text));
228
+ // 🚨 ★完全一致で照らすと、同じ語の一部が「新しい名前」に見える(2026-08-21 実測)
229
+ // 例=伏せたのは「株式会社みどり工務店」、検出の当たり付けは「みどり」を返す。
230
+ // 別物ではないのに、**戻す関所が毎回鳴って往復が止まった**。
231
+ // ★どちらかがどちらかを含んでいれば、同じものとして扱う。
232
+ const 伏せた語 = Object.values(map);
233
+ const newNames = found.candidates.filter((c) =>
234
+ !伏せた語.some((v) => v === c.text || v.includes(c.text) || c.text.includes(v)));
172
235
 
173
236
  const problems = [];
174
237
  if (unknown.length) problems.push('表に無い印が ' + unknown.length + ' 種あります(AIが書き換えたか、作ったものです)');
@@ -199,7 +262,8 @@ function unmaskFile(FREE, maskedPath, outPath, force) {
199
262
  fs.writeFileSync(dst, restored, 'utf8');
200
263
  return {
201
264
  書きました: true,
202
- 書き先: dst,
265
+ 書き先: path.basename(dst), // ★フルパスを返さない(利用者名が入るため)
266
+ 場所: '★お渡しいただいた正本と同じフォルダです',
203
267
  戻した印の数: Object.keys(map).length - missing.length,
204
268
  注意: problems.length ? problems : undefined,
205
269
  note: '★元のファイルは .bak として残しています。中身はAIに渡していません。ご自身で開いて確かめてください。',
@@ -217,7 +281,8 @@ function render(FREE, maskId, text, outPath, dir) {
217
281
  if (!fs.existsSync(mapFile)) throw new Error('対応表が見つかりません: ' + mapFile);
218
282
  const { map } = JSON.parse(fs.readFileSync(mapFile, 'utf8'));
219
283
  const body = String(text || '');
220
- const used = body.match(/\b(?:name|corp|office|shop|word|item)\d{3,6}\b/g) || [];
284
+ // ★印の種類は 印の種類 表から作る(種類を足したら、ここは自動で追従する)
285
+ const used = body.match(印の正規表現()) || [];
221
286
  const unknown = [...new Set(used.filter((t) => !map[t]))];
222
287
  const restored = Object.entries(map).reduce((s, [t, v]) => s.split(t).join(v), body);
223
288