@horizon_works/banto 0.5.0 → 0.6.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/gate_core.js CHANGED
@@ -199,8 +199,14 @@ function summarize(r, dict, targetLabel) {
199
199
  dictionary_free_candidates: r.candidates.length,
200
200
  verdict: reasons.length ? '要確認' : 'この検査では検出なし',
201
201
  verdict_reasons: reasons,
202
+ // 🚨 ★フルパスを返さない(2026-08-21 実測で判明)。
203
+ // Windows の一時フォルダは C:\Users\<ユーザー名>\... なので、
204
+ // **利用者本人の名前がAIへ渡っていた**。本文は1文字も渡していないのに、
205
+ // 「件数と判定だけ」という言い方が正確でなくなる。
206
+ // ★便利にしようとした1行が、設計を崩す(exposure-boundary §4)。
202
207
  detail_report: reportPath
203
- ? '詳細は ' + reportPath + ' に書きました。★人が手元で開いてください(AIは読みません)'
208
+ ? '詳細は、お使いのパソコンの一時フォルダ(Windowsなら %TEMP%)の '
209
+ + path.basename(reportPath) + ' に書きました。★人が手元で開いてください(AIは読みません)'
204
210
  : null,
205
211
  note:
206
212
  '★AIへ返しているのは件数と判定だけです。本文・該当行・該当語は返していません。' +
package/license/client.js CHANGED
@@ -135,4 +135,80 @@ function decide(c, now) {
135
135
  message: 'この鍵が確認できませんでした。お手数ですがご連絡ください' };
136
136
  }
137
137
 
138
- module.exports = { check };
138
+ // ═══════════════════════════════════════════════════════════════
139
+ // 型キーの統計(段1)★既定オフのオプトイン
140
+ //
141
+ // ★送るのは「型キーの名前」と「件数」だけ。記録の本文・顧客名・文章は一切送らない。
142
+ // → 送る中身は buildStats() が作る。**ここ以外で組み立てない**(口を1つにする)。
143
+ // ★1日1回だけ。相手が「はい」と言うまで、この関数は一度も呼ばれない。
144
+ // ═══════════════════════════════════════════════════════════════
145
+
146
+ // ★お手元のフォルダに置く。★パッケージの中に置くと、版が上がったとき npx が入れ替えて消える
147
+ // (=同じ日に2回送ってしまう。DB側は潰すので実害は無いが、印は残るところに置く)
148
+ const HOME = process.env.BANTO_HOME || path.join(require('os').homedir(), '.banto');
149
+ const STATS_CACHE = path.join(HOME, '.stats.json');
150
+
151
+ /** 送ることに同意しているか(★既定オフ。ファイルが無ければオフ) */
152
+ function isSharing() {
153
+ try { return JSON.parse(fs.readFileSync(path.join(HOME, 'share.json'), 'utf8')).stats === true; }
154
+ catch (_) { return false; }
155
+ }
156
+ /** 同意を切り替える(★人が明示的に呼んだときだけ) */
157
+ function setSharing(on) {
158
+ try { fs.mkdirSync(HOME, { recursive: true }); } catch (_) {}
159
+ const f = path.join(HOME, 'share.json');
160
+ const rec = { stats: on === true, changed_at: new Date().toISOString() };
161
+ fs.writeFileSync(f, JSON.stringify(rec, null, 2));
162
+ return rec;
163
+ }
164
+
165
+ /**
166
+ * review() の結果から、送る形だけを取り出す
167
+ * ★型キーの名前と数以外は、ここで落ちる(本文が通る経路を作らない)
168
+ */
169
+ function buildStats(review) {
170
+ if (!review) return [];
171
+ const out = [];
172
+ const push = (sig, total, afterRuled) => {
173
+ if (!sig) return;
174
+ out.push({ sig: String(sig).slice(0, 64), total: Number(total) || 0,
175
+ after_ruled: afterRuled === undefined || afterRuled === null ? null : Number(afterRuled) });
176
+ };
177
+ for (const r of review['ルールにする番'] || review.to_rule || []) push(r.sig, r.count, null);
178
+ for (const r of review['ルールが効いていない'] || review.not_working || []) push(r.sig, r.since_ruled, r.since_ruled);
179
+ for (const r of review['溜まりかけ'] || review.growing || []) push(r.sig, r.count, null);
180
+ return out;
181
+ }
182
+
183
+ /**
184
+ * 送る(★1日1回)
185
+ * @returns {Promise<{sent:boolean, reason:string, count:number}>}
186
+ */
187
+ async function reportStats(version, review) {
188
+ const { key, url, anonKey } = config();
189
+ if (!key || !url || !anonKey) return { sent: false, reason: 'unconfigured', count: 0 };
190
+
191
+ if (!isSharing()) return { sent: false, reason: 'opted-out', count: 0 };
192
+
193
+ const stats = buildStats(review);
194
+ if (!stats.length) return { sent: false, reason: 'nothing', count: 0 };
195
+
196
+ // ★1日1回(同じ日に何度起動しても増えない)
197
+ const today = new Date().toISOString().slice(0, 10);
198
+ let cache = null;
199
+ try { cache = JSON.parse(fs.readFileSync(STATS_CACHE, 'utf8')); } catch (_) {}
200
+ if (cache && cache.on === today) return { sent: false, reason: 'already-today', count: 0 };
201
+
202
+ const keyHash = crypto.createHash('sha256').update(key).digest('hex');
203
+ const rpc = url.replace(/verify_license\/?$/, 'report_stats');
204
+ try {
205
+ await post(rpc, { p_key_hash: keyHash, p_version: version || null, p_stats: stats }, anonKey);
206
+ try { fs.writeFileSync(STATS_CACHE, JSON.stringify({ on: today, count: stats.length }, null, 2)); } catch (_) {}
207
+ return { sent: true, reason: 'ok', count: stats.length };
208
+ } catch (e) {
209
+ // ★送れなくても止めない(相手の仕事が止まる理由にしない)
210
+ return { sent: false, reason: 'offline', count: 0 };
211
+ }
212
+ }
213
+
214
+ module.exports = { check, reportStats, buildStats, isSharing, setSharing };
package/package.json CHANGED
@@ -1,45 +1,45 @@
1
- {
2
- "name": "@horizon_works/banto",
3
- "version": "0.5.0",
4
- "description": "AI番頭のOS。ルール・記憶・検査を1本のMCPで配ります。中身をクラウドへ渡さずに検査できます。",
5
- "keywords": [
6
- "mcp",
7
- "model-context-protocol",
8
- "banto",
9
- "bantou",
10
- "ai-agent",
11
- "privacy",
12
- "japanese"
13
- ],
14
- "license": "UNLICENSED",
15
- "private": false,
16
- "author": "株式会社ホライズンワークス",
17
- "type": "commonjs",
18
- "bin": {
19
- "banto": "server.js"
20
- },
21
- "main": "server.js",
22
- "engines": {
23
- "node": ">=18"
24
- },
25
- "files": [
26
- "server.js",
27
- "gate_core.js",
28
- "privacy.js",
29
- "shelf.js",
30
- "gtasks.js",
31
- "env.js",
32
- "subconscious.js",
33
- "supplements.json",
34
- "payload/",
35
- "license/client.js",
36
- "README.md"
37
- ],
38
- "publishConfig": {
39
- "access": "public"
40
- },
41
- "optionalDependencies": {
42
- "googleapis": "^144.0.0",
43
- "@google-cloud/local-auth": "^3.0.1"
44
- }
45
- }
1
+ {
2
+ "name": "@horizon_works/banto",
3
+ "version": "0.6.0",
4
+ "description": "AI番頭のOS。ルール・記憶・検査を1本のMCPで配ります。中身をクラウドへ渡さずに検査できます。",
5
+ "keywords": [
6
+ "mcp",
7
+ "model-context-protocol",
8
+ "banto",
9
+ "bantou",
10
+ "ai-agent",
11
+ "privacy",
12
+ "japanese"
13
+ ],
14
+ "license": "UNLICENSED",
15
+ "private": false,
16
+ "author": "株式会社ホライズンワークス",
17
+ "type": "commonjs",
18
+ "bin": {
19
+ "banto": "server.js"
20
+ },
21
+ "main": "server.js",
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
25
+ "files": [
26
+ "server.js",
27
+ "gate_core.js",
28
+ "privacy.js",
29
+ "shelf.js",
30
+ "gtasks.js",
31
+ "env.js",
32
+ "subconscious.js",
33
+ "supplements.json",
34
+ "payload/",
35
+ "license/client.js",
36
+ "README.md"
37
+ ],
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "optionalDependencies": {
42
+ "googleapis": "^144.0.0",
43
+ "@google-cloud/local-auth": "^3.0.1"
44
+ }
45
+ }
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
 
package/server.js CHANGED
@@ -162,8 +162,11 @@ function inspect(targetPath) {
162
162
  },
163
163
  verdict: reasons.length ? '要確認' : 'この2つの検査では検出なし',
164
164
  verdict_reasons: reasons, // ★何で引っかかったかの「系統」。語は含まない
165
+ // 🚨 ★フルパスを返さない(2026-08-21 実測で判明。フリー版と同じ直し)
166
+ // 一時フォルダの道に **利用者本人の名前** が入っていた(C:\Users\<名前>\…)。
165
167
  detail_report: reportPath
166
- ? '詳細は ' + reportPath + ' に書きました。★人が手元で開いてください(AIは読みません)'
168
+ ? '詳細は、お使いのパソコンの一時フォルダ(Windowsなら %TEMP%)の '
169
+ + path.basename(reportPath) + ' に書きました。★人が手元で開いてください(AIは読みません)'
167
170
  : null,
168
171
  note:
169
172
  'この結果は「この2つの検査で引っかかった数」です。辞書に無い綴りは辞書側では永遠に0件になります。' +
@@ -263,6 +266,19 @@ const TOOLS = [
263
266
  },
264
267
  },
265
268
  },
269
+ {
270
+ name: 'share_stats',
271
+ description:
272
+ '★型キーの統計を、番頭を作った側(ホライズンワークス)へ送るかどうかを決めます。**既定は送りません**。' +
273
+ '送るのは「型キーの名前と件数」だけで、記録の本文・顧客名・文章は一切送りません。' +
274
+ '★呼ぶと、いま実際に送られる中身がそのまま見えます。いつでも止められます。',
275
+ inputSchema: {
276
+ type: 'object',
277
+ properties: {
278
+ on: { type: 'boolean', description: 'true で送る/false で止める。省略すると今の設定と中身を見せるだけ' },
279
+ },
280
+ },
281
+ },
266
282
  {
267
283
  name: 'review',
268
284
  description:
@@ -1427,6 +1443,28 @@ function callTool(name, args) {
1427
1443
 
1428
1444
  if (name === 'review') return FREE.review(true);
1429
1445
 
1446
+ // ★型キーの統計を送るかどうか(既定オフのオプトイン)
1447
+ // ★この道具は「切り替え」と「中身を見せる」を必ず同時にやる。
1448
+ // 何が送られるか見えない同意は、同意ではないため。
1449
+ if (name === 'share_stats') {
1450
+ const LIC = (() => { try { return require('./license/client.js'); } catch (_) { return null; } })();
1451
+ if (!LIC || !LIC.setSharing) return { error: 'この構成では使えません(鍵の口がありません)' };
1452
+ if (args && typeof args.on === 'boolean') LIC.setSharing(args.on);
1453
+ const on = LIC.isSharing();
1454
+ const 送られるもの = LIC.buildStats(FREE.review(false));
1455
+ return {
1456
+ いまの設定: on ? '送ります' : '★送りません(既定)',
1457
+ 送るもの: '型キーの名前と件数だけ',
1458
+ 送らないもの: ['記録の本文', '検査した文章', '顧客名・事業所名', 'ファイルの中身', 'パソコンの中の何か'],
1459
+ 回数: '1日に1回まで',
1460
+ '★いま送られる中身': 送られるもの.length ? 送られるもの : '(まだ型キーが付いた記録がありません)',
1461
+ 切り替え方: 'share_stats を on: true / false で呼んでください。いつでも変えられます',
1462
+ note: on
1463
+ ? '★これは、同じ失敗が他の方でも起きているかを見て、助言に使うためのものです。中身は読めません。'
1464
+ : '★送らない設定のままでも、番頭の機能はすべて使えます。',
1465
+ };
1466
+ }
1467
+
1430
1468
  if (name === 'settle') {
1431
1469
  return FREE.settle(args && args.ids, args && args.sig, args && args.ruled_to);
1432
1470
  }
@@ -1637,13 +1675,23 @@ async function handle(req) {
1637
1675
  let LICENSE_STATE = { allowed: true, status: 'unconfigured', message: '', daysLeft: null };
1638
1676
  (async () => {
1639
1677
  try {
1640
- const { check } = require('./license/client.js');
1641
- LICENSE_STATE = await check('0.1.0');
1678
+ const LIC = require('./license/client.js');
1679
+ LICENSE_STATE = await check_(LIC);
1680
+
1681
+ // ★型キーの統計(段1)— **同意しているときだけ**。1日1回。
1682
+ // ここで送るのは型キーの名前と件数だけ(組み立ては client 側の buildStats に閉じてある)。
1683
+ // ★送れなくても、何も止めない。
1684
+ if (LICENSE_STATE.allowed && LIC.isSharing && LIC.isSharing()) {
1685
+ try { await LIC.reportStats(VERSION, FREE.review(false)); } catch (_) {}
1686
+ }
1642
1687
  } catch (_) {
1643
1688
  // license モジュールが無い構成でも動く
1644
1689
  }
1645
1690
  })();
1646
1691
 
1692
+ // ★版番号を固定で書かない(0.1.0 のままだった=どの版が動いているか台帳で分からなくなる)
1693
+ async function check_(LIC) { return LIC.check(VERSION); }
1694
+
1647
1695
  // ★1つずつ順に処理する(2026-08-21)。
1648
1696
  // 待てる形(async)にした途端、**要求が同時に走る**ようになり、
1649
1697
  // 「入れる」の途中で「一覧」が走って『未導入』と返った。