naider 1.18.0 → 1.20.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/src/gen.js ADDED
@@ -0,0 +1,731 @@
1
+ import { Lexer } from './lexer.js';
2
+ import { Parser } from './parser.js';
3
+
4
+ // ── Vocabulary ──────────────────────────────────────────────
5
+
6
+ const INTENT_WORDS = {
7
+ server: ['server', 'api', 'rest', 'restful', 'endpoint', 'web', 'http', 'https', 'backend', 'app', 'application', 'service', 'microservice'],
8
+ schema: ['schema', 'model', 'entity', 'struct', 'table', 'type', 'data', 'record', 'resource', 'object', 'definition'],
9
+ bot: ['bot', 'chatbot', 'robot'],
10
+ cli: ['cli', 'command', 'terminal', 'console', 'argv', 'args'],
11
+ page: ['page', 'html', 'website', 'site', 'frontend', 'ui', 'view', 'template', 'render', 'layout'],
12
+ test: ['test', 'testing', 'spec', 'unit', 'e2e', 'integration', 'assert', 'expect'],
13
+ database: ['database', 'db', 'storage', 'persist', 'store', 'repository', 'sqlite', 'postgres', 'postgresql', 'json-db', 'mongo'],
14
+ ai: ['ai', 'llm', 'gpt', 'claude', 'openai', 'prompt', 'embedding', 'rag'],
15
+ crud: ['crud', 'create', 'read', 'update', 'delete', 'listing', 'list', 'index'],
16
+ auth: ['auth', 'jwt', 'login', 'authentication', 'authorization', 'signup', 'signin', 'sign-in', 'sign-up',
17
+ 'password', 'token', 'session', 'protect', 'permission', 'role', 'rbac', 'oauth', 'register', 'member', 'membership', 'account'],
18
+ cors: ['cors', 'cross-origin'],
19
+ websocket: ['websocket', 'ws', 'realtime', 'real-time', 'socket', 'live', 'push-notification', 'pubsub', 'pub-sub'],
20
+ mail: ['mail', 'smtp', 'mailer', 'sendmail', 'newsletter', 'notification'],
21
+ graphql: ['graphql', 'gql', 'query', 'mutation', 'resolver', 'subscription'],
22
+ };
23
+
24
+ const JA_INTENTS = {
25
+ server: ['サーバー', 'エンドポイント', 'バックエンド', 'アプリ', 'ウェブアプリ', 'Webアプリ', 'webアプリ',
26
+ 'API', 'サービス', 'マイクロサービス', 'ウェブサービス'],
27
+ schema: ['スキーマ', 'モデル', 'テーブル', 'データ型', '型定義', 'エンティティ', 'データモデル', 'リソース'],
28
+ bot: ['ボット', 'チャットボット', 'Bot', 'bot'],
29
+ cli: ['コマンド', 'ツール', 'ターミナル', 'コマンドライン', 'CLI', 'コンソール'],
30
+ page: ['ページ', 'サイト', 'ウェブ', 'フロントエンド', '画面', 'UI', '表示', 'テンプレート', 'ビュー', '見た目'],
31
+ test: ['テスト', '検証', 'テストコード', '単体テスト', '結合テスト', 'ユニットテスト'],
32
+ database: ['データベース', 'ストレージ', '保存', '永続化', 'DB', 'db', 'データ保存', '記録'],
33
+ ai: ['プロンプト', 'LLM', 'llm', '生成AI', '人工知能'],
34
+ auth: ['認証', 'ログイン', 'パスワード', '保護', '権限', '会員', 'サインイン', 'サインアップ',
35
+ '登録', 'ユーザー認証', 'アカウント', 'メンバー', '会員制', '会員機能', 'ログインできる',
36
+ 'ログイン機能', '認証機能', '認証付き', 'パスワード保護', 'アクセス制御', 'セキュリティ'],
37
+ crud: ['CRUD', 'crud', '作成', '取得', '更新', '削除', '一覧', '追加', '編集', '管理',
38
+ '管理機能', '一覧表示', 'リスト', '操作'],
39
+ websocket: ['ウェブソケット', 'リアルタイム', 'ソケット', 'WebSocket', 'websocket',
40
+ 'リアルタイム通信', 'プッシュ通知', 'ライブ'],
41
+ mail: ['メール', 'メール送信', 'メール機能', 'SMTP', '通知メール', 'メール通知'],
42
+ };
43
+
44
+ const JA_ENTITIES = {
45
+ 'ユーザー': 'User', 'ユーザ': 'User', 'ユーザ管理': 'User', 'ユーザー管理': 'User',
46
+ '会員': 'User', 'メンバー': 'User', 'アカウント': 'User', '利用者': 'User',
47
+ '顧客': 'Customer', 'お客様': 'Customer', 'クライアント': 'Customer',
48
+ '商品': 'Product', 'プロダクト': 'Product', '製品': 'Product', '品物': 'Product',
49
+ '記事': 'Post', '投稿': 'Post', 'ブログ記事': 'Post',
50
+ 'タスク': 'Task', 'やること': 'Task', '作業': 'Task',
51
+ 'メッセージ': 'Message', 'チャットメッセージ': 'Message',
52
+ '注文': 'Order', 'オーダー': 'Order', '受注': 'Order', '発注': 'Order',
53
+ 'コメント': 'Comment', '感想': 'Comment', 'レビュー': 'Review', '評価': 'Review',
54
+ 'ファイル': 'File', 'アップロード': 'File',
55
+ 'カテゴリ': 'Category', 'カテゴリー': 'Category', '分類': 'Category',
56
+ 'イベント': 'Event', '予定': 'Event', 'スケジュール': 'Event', '予約': 'Reservation',
57
+ 'プロジェクト': 'Project', '案件': 'Project',
58
+ '書籍': 'Book', '本': 'Book', '図書': 'Book',
59
+ 'アイテム': 'Item', 'リスト': 'List', 'ノート': 'Note', 'メモ': 'Note',
60
+ '通知': 'Notification', 'お知らせ': 'Notification',
61
+ '決済': 'Payment', '支払い': 'Payment', '課金': 'Payment',
62
+ '設定': 'Setting', '環境設定': 'Setting', '構成': 'Setting',
63
+ '問い合わせ': 'Contact', 'お問い合わせ': 'Contact', '連絡': 'Contact',
64
+ '掲示板': 'Post', 'スレッド': 'Thread', 'トピック': 'Topic',
65
+ 'レシピ': 'Recipe', '料理': 'Recipe',
66
+ '在庫': 'Inventory', '倉庫': 'Inventory',
67
+ '社員': 'Employee', '従業員': 'Employee', 'スタッフ': 'Staff',
68
+ 'チケット': 'Ticket', 'チーム': 'Team', 'グループ': 'Group',
69
+ 'ログ': 'Log', '履歴': 'Log', 'アクティビティ': 'Activity',
70
+ };
71
+
72
+ const PLATFORM_WORDS = {
73
+ discord: ['discord', 'ディスコード'],
74
+ slack: ['slack', 'スラック'],
75
+ telegram: ['telegram', 'テレグラム'],
76
+ line: ['line', 'ライン'],
77
+ };
78
+
79
+ const COMPOSITE_WORDS = {
80
+ fullstack: ['fullstack', 'full-stack', 'フルスタック', '管理システム', '管理アプリ', '管理画面'],
81
+ todo: ['todo', 'todos', 'to-do', 'タスク管理', 'todoアプリ', 'やることリスト', 'todo管理'],
82
+ blog: ['blog', 'ブログ', '記事管理', '投稿管理', 'ブログサイト', 'ブログアプリ'],
83
+ chat: ['chat', 'チャットアプリ', 'メッセンジャー', 'チャット機能', 'チャットルーム'],
84
+ shop: ['shop', 'ecommerce', 'e-commerce', 'ショップ', 'ストア', '通販', 'ECサイト', 'EC', 'オンラインショップ'],
85
+ board: ['board', '掲示板', 'フォーラム', 'BBS', 'bbs'],
86
+ };
87
+
88
+ const NOISE = new Set([
89
+ 'a', 'an', 'the', 'for', 'with', 'and', 'or', 'on', 'at', 'to', 'of', 'in', 'by',
90
+ 'that', 'which', 'this', 'it', 'its', 'my', 'is', 'are', 'has', 'have', 'do', 'does',
91
+ 'make', 'create', 'build', 'generate', 'add', 'implement', 'simple', 'basic', 'new',
92
+ 'please', 'want', 'need', 'like', 'just', 'some', 'using', 'use', 'based',
93
+ 'setup', 'set', 'up', 'get', 'should', 'can', 'could', 'would',
94
+ 'の', 'を', 'に', 'で', 'は', 'が', 'と', 'も', 'から', 'まで', 'より', 'って',
95
+ 'な', 'ような', 'みたいな', 'っぽい', 'ような感じ',
96
+ '作って', '作る', '作成', '生成', '追加', '実装', 'ください', 'して', 'つけて',
97
+ '付き', '付きの', 'ある', 'ない', 'する', 'できる', 'ほしい', 'したい',
98
+ '機能', '仕組み', 'もの', 'こと', 'やつ', '感じ', '的な',
99
+ '簡単な', 'シンプルな', '基本的な', '新しい',
100
+ ]);
101
+
102
+ // ── Field Normalization ─────────────────────────────────────
103
+
104
+ const FIELD_SYNONYMS = {
105
+ 'e-mail': 'email', 'email_address': 'email', 'mail_address': 'email', 'emailaddress': 'email', 'mail': 'email',
106
+ 'user_name': 'username', 'login_name': 'username', 'loginname': 'username', 'login': 'username',
107
+ 'first_name': 'firstname', 'firstname': 'name', 'last_name': 'lastname',
108
+ 'display_name': 'name', 'full_name': 'name', 'fullname': 'name',
109
+ 'phone_number': 'phone', 'tel': 'phone', 'telephone': 'phone', 'phonenumber': 'phone', 'mobile': 'phone',
110
+ 'zip_code': 'zipcode', 'postal_code': 'zipcode', 'postalcode': 'zipcode',
111
+ 'birth_date': 'birthdate', 'birthday': 'birthdate', 'date_of_birth': 'birthdate',
112
+ 'created_at': 'created', 'creation_date': 'created', 'createdat': 'created', 'date_created': 'created',
113
+ 'updated_at': 'updated', 'modification_date': 'updated', 'updatedat': 'updated', 'date_updated': 'updated', 'modified_at': 'updated',
114
+ 'is_active': 'active', 'isactive': 'active',
115
+ 'is_deleted': 'deleted', 'isdeleted': 'deleted',
116
+ 'is_public': 'public', 'ispublic': 'public',
117
+ 'is_admin': 'admin', 'isadmin': 'admin',
118
+ 'is_verified': 'verified', 'isverified': 'verified',
119
+ 'is_done': 'done', 'isdone': 'done', 'is_completed': 'completed',
120
+ 'is_published': 'published', 'ispublished': 'published',
121
+ 'is_enabled': 'enabled', 'isenabled': 'enabled',
122
+ 'num_items': 'count', 'item_count': 'count',
123
+ 'total_price': 'total', 'total_amount': 'total',
124
+ 'img': 'image', 'pic': 'image', 'picture': 'image', 'photo': 'image', 'thumbnail': 'image', 'avatar': 'image',
125
+ 'desc': 'description', 'memo': 'description', 'detail': 'description', 'details': 'description',
126
+ 'pwd': 'password', 'passwd': 'password', 'pass': 'password', 'pw': 'password',
127
+ 'qty': 'quantity', 'amt': 'amount', 'addr': 'address', 'msg': 'message',
128
+ 'cat': 'category', 'tags': 'tag', 'lbl': 'label',
129
+ };
130
+
131
+ function normalizeField(name) {
132
+ const lower = name.toLowerCase();
133
+ if (FIELD_SYNONYMS[lower]) return FIELD_SYNONYMS[lower];
134
+ const n = lower.replace(/-/g, '_');
135
+ return FIELD_SYNONYMS[n] || n;
136
+ }
137
+
138
+ // ── Field Type Inference ────────────────────────────────────
139
+
140
+ const FIELD_TYPES = {
141
+ auto: ['id', 'created', 'updated', 'timestamp'],
142
+ bool: ['active', 'done', 'completed', 'enabled', 'published', 'verified', 'admin', 'public',
143
+ 'visible', 'archived', 'deleted', 'premium', 'banned', 'muted', 'online', 'featured',
144
+ 'approved', 'blocked', 'pinned', 'starred', 'read'],
145
+ int: ['age', 'count', 'quantity', 'amount', 'price', 'cost', 'total', 'score', 'rating',
146
+ 'level', 'order', 'index', 'size', 'width', 'height', 'weight', 'year', 'month',
147
+ 'day', 'port', 'priority', 'views', 'likes', 'followers', 'stock', 'points',
148
+ 'duration', 'position', 'rank', 'votes'],
149
+ };
150
+
151
+ function inferType(rawName) {
152
+ const n = normalizeField(rawName);
153
+ for (const [type, names] of Object.entries(FIELD_TYPES)) {
154
+ if (names.includes(n)) return type;
155
+ }
156
+ if (n.endsWith('_id') || n.endsWith('id') && n !== 'id') return 'int';
157
+ if (n.startsWith('is_') || n.startsWith('has_') || n.startsWith('can_')) return 'bool';
158
+ if (n.endsWith('count') || n.endsWith('num') || n.endsWith('number')) return 'int';
159
+ if (n.includes('price') || n.includes('cost') || n.includes('amount')) return 'int';
160
+ if (n.includes('date') || n.includes('time') || n === 'birthdate') return 'str';
161
+ return 'str';
162
+ }
163
+
164
+ function inferModifiers(rawName, type) {
165
+ const n = normalizeField(rawName);
166
+ if (type === 'auto') return [];
167
+ const mods = [];
168
+ if (!['bool', 'auto'].includes(type)) {
169
+ if (['name', 'title', 'email', 'password', 'username', 'content', 'body', 'phone', 'address'].includes(n)) mods.push('required');
170
+ }
171
+ if (n === 'email') mods.push('email');
172
+ if (n === 'name' || n === 'title') { mods.push('min(1)'); mods.push('max(200)'); }
173
+ if (n === 'password') mods.push('min(8)');
174
+ return mods;
175
+ }
176
+
177
+ // ── Default Schema Fields ───────────────────────────────────
178
+
179
+ const SCHEMA_PRESETS = {
180
+ user: [['id','auto'], ['name','str','required'], ['email','str','required','email'], ['password','str','required','min(8)']],
181
+ customer: [['id','auto'], ['name','str','required'], ['email','str','required','email'], ['phone','str']],
182
+ post: [['id','auto'], ['title','str','required','min(1)','max(200)'], ['body','str','required'], ['published','bool'], ['created','auto']],
183
+ product: [['id','auto'], ['name','str','required'], ['price','int','required'], ['stock','int'], ['description','str']],
184
+ todo: [['id','auto'], ['title','str','required','min(1)'], ['done','bool']],
185
+ task: [['id','auto'], ['title','str','required'], ['description','str'], ['status','str'], ['done','bool']],
186
+ comment: [['id','auto'], ['body','str','required'], ['author','str','required'], ['created','auto']],
187
+ review: [['id','auto'], ['body','str','required'], ['rating','int'], ['author','str','required'], ['created','auto']],
188
+ message: [['id','auto'], ['content','str','required'], ['sender','str','required'], ['created','auto']],
189
+ order: [['id','auto'], ['total','int','required'], ['status','str'], ['created','auto']],
190
+ event: [['id','auto'], ['title','str','required'], ['date','str','required'], ['description','str']],
191
+ reservation:[['id','auto'], ['date','str','required'], ['name','str','required'], ['status','str'], ['created','auto']],
192
+ note: [['id','auto'], ['title','str','required'], ['content','str'], ['created','auto']],
193
+ book: [['id','auto'], ['title','str','required'], ['author','str','required'], ['price','int'], ['published','bool']],
194
+ category: [['id','auto'], ['name','str','required'], ['description','str']],
195
+ file: [['id','auto'], ['name','str','required'], ['path','str','required'], ['size','int'], ['created','auto']],
196
+ project: [['id','auto'], ['name','str','required'], ['description','str'], ['status','str'], ['created','auto']],
197
+ contact: [['id','auto'], ['name','str','required'], ['email','str','required','email'], ['message','str','required'], ['created','auto']],
198
+ notification:[['id','auto'], ['title','str','required'], ['body','str'], ['read','bool'], ['created','auto']],
199
+ payment: [['id','auto'], ['amount','int','required'], ['status','str'], ['method','str'], ['created','auto']],
200
+ setting: [['id','auto'], ['key','str','required'], ['value','str','required']],
201
+ ticket: [['id','auto'], ['title','str','required'], ['description','str'], ['status','str'], ['priority','int'], ['created','auto']],
202
+ employee: [['id','auto'], ['name','str','required'], ['email','str','required','email'], ['role','str']],
203
+ staff: [['id','auto'], ['name','str','required'], ['email','str','required','email'], ['role','str']],
204
+ thread: [['id','auto'], ['title','str','required'], ['body','str','required'], ['author','str','required'], ['created','auto']],
205
+ recipe: [['id','auto'], ['title','str','required'], ['description','str'], ['ingredients','str'], ['created','auto']],
206
+ inventory:[['id','auto'], ['name','str','required'], ['quantity','int','required'], ['location','str']],
207
+ log: [['id','auto'], ['action','str','required'], ['detail','str'], ['created','auto']],
208
+ activity: [['id','auto'], ['type','str','required'], ['description','str'], ['created','auto']],
209
+ item: [['id','auto'], ['name','str','required'], ['created','auto']],
210
+ };
211
+
212
+ function defaultFields(schemaName) {
213
+ const key = schemaName.toLowerCase();
214
+ const preset = SCHEMA_PRESETS[key];
215
+ if (preset) return preset.map(f => ({ name: f[0], type: f[1], modifiers: f.slice(2) }));
216
+ return [
217
+ { name: 'id', type: 'auto', modifiers: [] },
218
+ { name: 'name', type: 'str', modifiers: ['required'] },
219
+ { name: 'created', type: 'auto', modifiers: [] },
220
+ ];
221
+ }
222
+
223
+ // ── Utilities ───────────────────────────────────────────────
224
+
225
+ function singularize(w) {
226
+ if (w.endsWith('ies')) return w.slice(0, -3) + 'y';
227
+ if (w.endsWith('ses') || w.endsWith('xes') || w.endsWith('zes') || w.endsWith('ches') || w.endsWith('shes')) return w.slice(0, -2);
228
+ if (w.endsWith('s') && !w.endsWith('ss') && !w.endsWith('us') && w.length > 3) return w.slice(0, -1);
229
+ return w;
230
+ }
231
+
232
+ function capitalize(w) {
233
+ return w.charAt(0).toUpperCase() + w.slice(1);
234
+ }
235
+
236
+ function pluralize(w) {
237
+ if (w.endsWith('y') && !/[aeiou]y$/i.test(w)) return w.slice(0, -1) + 'ies';
238
+ if (w.endsWith('s') || w.endsWith('x') || w.endsWith('z') || w.endsWith('ch') || w.endsWith('sh')) return w + 'es';
239
+ return w + 's';
240
+ }
241
+
242
+ const RESERVED = new Set([
243
+ 'get', 'post', 'put', 'del', 'on', 'in', 'as', 'is', 'isnt', 'if', 'for', 'each', 'while',
244
+ 'use', 'fn', 'ret', 'mut', 'pub', 'elif', 'else', 'match', 'try', 'fail', 'server', 'bot',
245
+ 'page', 'cli', 'mail', 'db', 'log', 'new', 'not', 'and', 'or', 'then', 'from', 'break',
246
+ 'continue', 'throw', 'schema', 'crud', 'auth', 'cors', 'model', 'self', 'test', 'assert',
247
+ 'await', 'enum', 'swap', 'repeat', 'until', 'unless', 'extends', 'print', 'static',
248
+ 'true', 'false', 'null', 'str', 'int', 'num', 'bool', 'list', 'map', 'any', 'json', 'void', 'auto',
249
+ 'search', 'push', 'image', 'csv', 'graphql', 'desktop', 'screen', 'validate', 'prompt',
250
+ 'every', 'watch', 'limit', 'env', 'session', 'cookie', 'upload', 'view', 'cache', 'patch',
251
+ 'queue', 'job', 'ws', 'sse', 'group', 'storage', 'oauth', 'pay', 'pdf', 'i18n',
252
+ ]);
253
+
254
+ function safeName(name) {
255
+ if (RESERVED.has(name.toLowerCase())) return name + 'App';
256
+ return name;
257
+ }
258
+
259
+ function align(rows) {
260
+ if (rows.length === 0) return [];
261
+ const cols = rows[0].length;
262
+ const widths = Array(cols).fill(0);
263
+ for (const row of rows) {
264
+ for (let i = 0; i < Math.min(cols, row.length); i++) {
265
+ widths[i] = Math.max(widths[i], (row[i] || '').length);
266
+ }
267
+ }
268
+ return rows.map(row =>
269
+ row.map((cell, i) => i < cols - 1 ? (cell || '').padEnd(widths[i]) : (cell || '')).join(' ').trimEnd()
270
+ );
271
+ }
272
+
273
+ // ── Instruction Parsing ─────────────────────────────────────
274
+
275
+ function tokenize(instruction) {
276
+ return instruction
277
+ .replace(/[.,!?;:()'"\[\]{}]/g, ' ')
278
+ .replace(/\s+/g, ' ')
279
+ .trim()
280
+ .split(' ')
281
+ .filter(w => w.length > 0);
282
+ }
283
+
284
+ function classify(words, raw) {
285
+ const lower = raw.toLowerCase();
286
+ const intents = new Set();
287
+ const mods = {};
288
+ let matchStrength = 0;
289
+
290
+ // English keyword matching
291
+ for (const [intent, kws] of Object.entries(INTENT_WORDS)) {
292
+ for (const w of words) {
293
+ const wl = w.toLowerCase();
294
+ if (kws.includes(wl)) { intents.add(intent); matchStrength++; break; }
295
+ }
296
+ }
297
+
298
+ // Japanese substring matching
299
+ for (const [intent, kws] of Object.entries(JA_INTENTS)) {
300
+ for (const kw of kws) {
301
+ if (raw.includes(kw)) { intents.add(intent); matchStrength++; break; }
302
+ }
303
+ }
304
+
305
+ // Composite detection
306
+ for (const [comp, kws] of Object.entries(COMPOSITE_WORDS)) {
307
+ if (kws.some(k => lower.includes(k))) {
308
+ matchStrength += 2;
309
+ if (comp === 'fullstack') { intents.add('server'); intents.add('schema'); intents.add('crud'); intents.add('auth'); intents.add('database'); }
310
+ if (comp === 'todo') { intents.add('server'); intents.add('schema'); intents.add('crud'); intents.add('database'); mods.preset = 'todo'; }
311
+ if (comp === 'blog') { intents.add('server'); intents.add('schema'); intents.add('crud'); intents.add('database'); mods.preset = 'post'; }
312
+ if (comp === 'chat') { intents.add('server'); intents.add('schema'); intents.add('websocket'); intents.add('database'); mods.preset = 'message'; }
313
+ if (comp === 'shop') { intents.add('server'); intents.add('schema'); intents.add('crud'); intents.add('auth'); intents.add('database'); mods.preset = 'product'; }
314
+ if (comp === 'board') { intents.add('server'); intents.add('schema'); intents.add('crud'); intents.add('database'); mods.preset = 'post'; }
315
+ }
316
+ }
317
+
318
+ // Platform detection
319
+ for (const [plat, kws] of Object.entries(PLATFORM_WORDS)) {
320
+ if (kws.some(k => lower.includes(k))) mods.platform = plat;
321
+ }
322
+
323
+ // DB type detection
324
+ if (lower.includes('sqlite')) mods.dbType = 'sqlite';
325
+ else if (lower.includes('postgres')) mods.dbType = 'postgres';
326
+
327
+ // Detect entity-like words (nouns that could be schema names)
328
+ const allKnown = new Set([...Object.values(INTENT_WORDS).flat(), ...Object.values(PLATFORM_WORDS).flat(),
329
+ ...Object.values(COMPOSITE_WORDS).flat()]);
330
+ const hasEntity = words.some(w => {
331
+ const wl = w.toLowerCase();
332
+ return !NOISE.has(wl) && !allKnown.has(wl) && !/^\d+$/.test(wl) && wl.length > 2 && /^[a-z]+s?$/i.test(wl);
333
+ }) || Object.keys(JA_ENTITIES).some(ja => raw.includes(ja));
334
+
335
+ // "for X" pattern implies schema + crud
336
+ if (hasEntity && intents.has('server')) {
337
+ intents.add('schema'); intents.add('crud');
338
+ }
339
+ if (lower.includes(' for ') && intents.has('server') && !intents.has('schema')) {
340
+ intents.add('schema'); intents.add('crud');
341
+ }
342
+
343
+ // Implicit intent rules
344
+ if (intents.has('crud') && !intents.has('server')) intents.add('server');
345
+ if (intents.has('crud') && !intents.has('schema')) intents.add('schema');
346
+ if (intents.has('auth') && !intents.has('server')) intents.add('server');
347
+ if (intents.has('schema') && intents.has('server')) intents.add('database');
348
+ if (intents.has('bot') && !mods.platform) mods.platform = 'discord';
349
+
350
+ // Default fallback: if nothing detected, server + schema
351
+ if (intents.size === 0) {
352
+ intents.add('server'); intents.add('schema'); intents.add('crud'); intents.add('database');
353
+ mods.fallback = true;
354
+ }
355
+
356
+ mods.matchStrength = matchStrength;
357
+ return { intents, mods };
358
+ }
359
+
360
+ function extractParams(words, raw, intents, mods) {
361
+ const params = { port: 3000, schemaName: null, fields: [], features: intents, platform: mods.platform || null, dbType: mods.dbType || null, commands: [] };
362
+
363
+ // Extract port number
364
+ for (let i = 0; i < words.length; i++) {
365
+ const n = parseInt(words[i]);
366
+ if (!isNaN(n) && n >= 80 && n <= 65535) params.port = n;
367
+ if (words[i].toLowerCase() === 'port' && i + 1 < words.length) {
368
+ const pn = parseInt(words[i + 1]);
369
+ if (!isNaN(pn)) params.port = pn;
370
+ }
371
+ }
372
+
373
+ // Extract entity name — Japanese first (longest match wins)
374
+ const jaEntries = Object.entries(JA_ENTITIES).sort((a, b) => b[0].length - a[0].length);
375
+ for (const [ja, en] of jaEntries) {
376
+ if (raw.includes(ja)) { params.schemaName = en; break; }
377
+ }
378
+
379
+ // Extract entity name — English (find nouns that aren't keywords)
380
+ if (!params.schemaName) {
381
+ const allKnown = new Set([...Object.values(INTENT_WORDS).flat(), ...Object.values(PLATFORM_WORDS).flat(),
382
+ ...Object.values(COMPOSITE_WORDS).flat()]);
383
+ for (const w of words) {
384
+ const wl = w.toLowerCase();
385
+ if (NOISE.has(wl) || allKnown.has(wl) || /^\d+$/.test(wl) || wl.length <= 2) continue;
386
+ params.schemaName = capitalize(singularize(wl));
387
+ break;
388
+ }
389
+ }
390
+
391
+ // Preset override
392
+ if (mods.preset && !params.schemaName) {
393
+ params.schemaName = capitalize(mods.preset);
394
+ }
395
+
396
+ // Default schema name
397
+ if (!params.schemaName && intents.has('schema')) params.schemaName = 'Item';
398
+
399
+ // Extract fields from "with" clause
400
+ const lower = raw.toLowerCase();
401
+ const withIdx = lower.indexOf(' with ');
402
+ if (withIdx !== -1) {
403
+ const afterWith = lower.slice(withIdx + 6);
404
+ const stopWords = new Set(Object.values(INTENT_WORDS).flat());
405
+ const fieldNames = afterWith
406
+ .split(/[\s,、]+/)
407
+ .map(w => w.replace(/[^a-zA-Z0-9_\-]/g, ''))
408
+ .filter(w => w.length > 1 && !NOISE.has(w) && !stopWords.has(w) && !/^\d+$/.test(w));
409
+ if (fieldNames.length > 0) {
410
+ params.fields = [{ name: 'id', type: 'auto', modifiers: [] }];
411
+ for (const rawName of fieldNames) {
412
+ const name = normalizeField(rawName);
413
+ const type = inferType(name);
414
+ params.fields.push({ name, type, modifiers: inferModifiers(name, type) });
415
+ }
416
+ }
417
+ }
418
+
419
+ // Use preset fields if none extracted
420
+ if (params.fields.length === 0 && params.schemaName) {
421
+ params.fields = defaultFields(params.schemaName);
422
+ }
423
+
424
+ // Extract bot commands
425
+ const cmdMatch = raw.match(/command[s]?\s+(.+)/i);
426
+ if (cmdMatch) {
427
+ params.commands = cmdMatch[1].split(/[\s,、and]+/).filter(c => c.length > 1).map(c => ({
428
+ trigger: c.startsWith('!') ? c : '!' + c,
429
+ response: `${capitalize(c.replace('!', ''))}!`,
430
+ }));
431
+ }
432
+
433
+ return params;
434
+ }
435
+
436
+ // ── Template Engine ─────────────────────────────────────────
437
+
438
+ function schemaBlock(name, fields) {
439
+ const rows = fields.map(f => [f.name, f.type, ...f.modifiers]);
440
+ const aligned = align(rows);
441
+ return `schema ${name}:\n` + aligned.map(l => ' ' + l).join('\n');
442
+ }
443
+
444
+ function dbBlock(type) {
445
+ if (type === 'sqlite') return 'db.sql "sqlite"';
446
+ if (type === 'postgres') return 'db.sql "postgres"';
447
+ return 'db "data/"';
448
+ }
449
+
450
+ function serverBlock(name, port, sections) {
451
+ return `server ${name} port ${port}:\n` + sections.map(l => ' ' + l).join('\n');
452
+ }
453
+
454
+ function botBlock(platform, commands) {
455
+ const token = platform.toUpperCase() + '_TOKEN';
456
+ const lines = [`bot mybot type "${platform}" token ${token} prefix "!":`];
457
+ if (commands.length === 0) {
458
+ commands = [
459
+ { trigger: '!hello', response: 'Hello!' },
460
+ { trigger: '!help', response: 'Commands: !hello, !help' },
461
+ ];
462
+ }
463
+ for (const cmd of commands) {
464
+ lines.push(` on "${cmd.trigger}" (msg):`);
465
+ lines.push(` msg.reply("${cmd.response}")`);
466
+ }
467
+ return lines.join('\n');
468
+ }
469
+
470
+ function cliBlock(name) {
471
+ return `cli ${safeName(name || 'app')}:
472
+ flag "-n" "--name" str "Name"
473
+ flag "-o" "--output" str "Output file"
474
+ run (args):
475
+ log("Name: " + args.name)
476
+ log("Output: " + args.output)`;
477
+ }
478
+
479
+ function testBlock(name) {
480
+ return `test "${name || 'app'} tests":
481
+ assert 1 + 1 == 2
482
+ assert "hello".length == 5
483
+ assert [1, 2, 3].length == 3`;
484
+ }
485
+
486
+ function graphqlBlock() {
487
+ return `graphql "/graphql"`;
488
+ }
489
+
490
+ function pageBlock(title) {
491
+ return `page "index.html":
492
+ h1 "${title || 'Welcome'}"
493
+ p "Built with NAIDE"`;
494
+ }
495
+
496
+ function mailBlock() {
497
+ return `mail "smtp.example.com" 587:
498
+ user SMTP_USER
499
+ pass SMTP_PASS`;
500
+ }
501
+
502
+ // ── Code Composer ───────────────────────────────────────────
503
+
504
+ function compose(intents, params) {
505
+ const sections = [];
506
+ const schemaName = params.schemaName || 'Item';
507
+ const nameLower = schemaName.toLowerCase();
508
+ const namePlural = pluralize(nameLower);
509
+
510
+ if (intents.has('schema') && params.fields.length > 0) {
511
+ sections.push(schemaBlock(schemaName, params.fields));
512
+ }
513
+
514
+ if (intents.has('database')) {
515
+ sections.push(dbBlock(params.dbType));
516
+ }
517
+
518
+ if (intents.has('server')) {
519
+ const body = [];
520
+ body.push('cors "*"');
521
+
522
+ if (intents.has('crud') && intents.has('schema')) {
523
+ body.push(`crud "/api/${namePlural}" ${schemaName}`);
524
+ }
525
+
526
+ if (intents.has('auth')) {
527
+ body.push('auth JWT_SECRET:');
528
+ body.push(' protect "/api/*"');
529
+ body.push(' public "/api/auth/*"');
530
+ }
531
+
532
+ if (intents.has('websocket')) {
533
+ body.push('');
534
+ body.push('ws "/ws":');
535
+ body.push(' on "message" (data):');
536
+ body.push(' socket.send(data)');
537
+ }
538
+
539
+ if (intents.has('ai')) {
540
+ body.push('');
541
+ body.push('post "/api/ask" (req, res):');
542
+ body.push(' str answer = await ai.ask(req.body.prompt)');
543
+ body.push(' ret {answer}');
544
+ }
545
+
546
+ body.push('');
547
+ body.push('get "/api/health" (req, res):');
548
+ if (intents.has('schema') && intents.has('crud')) {
549
+ body.push(` ret {status: "ok", ${namePlural}: ${schemaName}Store.count()}`);
550
+ } else {
551
+ body.push(' ret {status: "ok"}');
552
+ }
553
+
554
+ sections.push(serverBlock(safeName(nameLower === 'item' ? 'app' : nameLower), params.port, body));
555
+ }
556
+
557
+ if (intents.has('bot')) {
558
+ sections.push(botBlock(params.platform || 'discord', params.commands));
559
+ }
560
+
561
+ if (intents.has('cli')) {
562
+ sections.push(cliBlock(nameLower));
563
+ }
564
+
565
+ if (intents.has('graphql')) {
566
+ sections.push(graphqlBlock());
567
+ }
568
+
569
+ if (intents.has('page')) {
570
+ sections.push(pageBlock(schemaName));
571
+ }
572
+
573
+ if (intents.has('mail')) {
574
+ sections.push(mailBlock());
575
+ }
576
+
577
+ if (intents.has('test')) {
578
+ sections.push(testBlock(nameLower));
579
+ }
580
+
581
+ return sections.join('\n\n') + '\n';
582
+ }
583
+
584
+ // ── Self-Healing Validator ──────────────────────────────────
585
+
586
+ function validate(code) {
587
+ let current = code;
588
+ let fixed = false;
589
+ const repairs = [];
590
+
591
+ for (let attempt = 0; attempt < 5; attempt++) {
592
+ try {
593
+ const lexer = new Lexer(current);
594
+ const tokens = lexer.tokenize();
595
+ const parser = new Parser(tokens);
596
+ parser.parse();
597
+ return { code: current, valid: true, fixed, repairs };
598
+ } catch (err) {
599
+ const before = current;
600
+ const repair = heal(current, err);
601
+ current = repair.code;
602
+ if (current === before) return { code: current, valid: false, fixed, repairs };
603
+ if (repair.description) repairs.push(repair.description);
604
+ fixed = true;
605
+ }
606
+ }
607
+
608
+ return { code: current, valid: false, fixed, repairs };
609
+ }
610
+
611
+ function heal(code, err) {
612
+ const msg = err.message;
613
+ const lines = code.split('\n');
614
+ const lineMatch = msg.match(/line (\d+)/);
615
+ if (!lineMatch) return { code, description: null };
616
+
617
+ const lineIdx = parseInt(lineMatch[1]) - 1;
618
+ if (lineIdx < 0 || lineIdx >= lines.length) return { code, description: null };
619
+ const line = lines[lineIdx];
620
+
621
+ // Missing colon at end of block header
622
+ if (msg.includes('COLON') || msg.includes('Expected ":"')) {
623
+ if (!line.trimEnd().endsWith(':')) {
624
+ lines[lineIdx] = line.trimEnd() + ':';
625
+ return { code: lines.join('\n'), description: `L${lineIdx + 1}: added missing ':' at end of block header` };
626
+ }
627
+ }
628
+
629
+ // Unexpected indent — previous line probably needs a colon
630
+ if (msg.includes('INDENT') && lineIdx > 0) {
631
+ const prev = lines[lineIdx - 1];
632
+ if (!prev.trimEnd().endsWith(':') && !prev.trimEnd().endsWith(',')) {
633
+ lines[lineIdx - 1] = prev.trimEnd() + ':';
634
+ return { code: lines.join('\n'), description: `L${lineIdx}: added ':' to previous line to open block` };
635
+ }
636
+ }
637
+
638
+ // Reserved keyword used as identifier — rename it
639
+ const reserved = ['get', 'post', 'put', 'del', 'on', 'in', 'as', 'is', 'log', 'new', 'not', 'and', 'or', 'if', 'for', 'use', 'fn', 'ret', 'mut'];
640
+ if (msg.includes('Unexpected') || msg.includes('Expected IDENT')) {
641
+ for (const kw of reserved) {
642
+ const fieldPattern = new RegExp(`^(\\s+)${kw}(\\s+(?:str|int|num|bool|auto))`, 'm');
643
+ if (fieldPattern.test(line)) {
644
+ lines[lineIdx] = line.replace(fieldPattern, `$1${kw}_field$2`);
645
+ return { code: lines.join('\n'), description: `L${lineIdx + 1}: renamed reserved word '${kw}' → '${kw}_field'` };
646
+ }
647
+ }
648
+ }
649
+
650
+ // Unexpected token — try removing the problematic line (non-structural only)
651
+ if (msg.includes('Unexpected') && !line.trim().match(/^(schema|server|bot|cli|page|test|fn|db)\b/)) {
652
+ const removed = line.trim();
653
+ lines.splice(lineIdx, 1);
654
+ return { code: lines.join('\n'), description: `L${lineIdx + 1}: removed problematic line '${removed.slice(0, 40)}'` };
655
+ }
656
+
657
+ return { code, description: null };
658
+ }
659
+
660
+ // ── Suggestions ─────────────────────────────────────────────
661
+
662
+ const KEYWORD_CHEATSHEET = {
663
+ 'Intent': 'server, api, rest, bot, cli, page, test, database, ai, crud, auth, websocket, mail, graphql',
664
+ 'Composite': 'todo, blog, chat, shop, fullstack, board',
665
+ 'Platform': 'discord, slack, telegram, line',
666
+ 'DB': 'sqlite, postgres',
667
+ 'Japanese': 'サーバー, 認証, ログイン, 会員, CRUD, 管理, データベース, ボット, テスト, ページ',
668
+ 'JP Entity': 'ユーザー, 商品, 記事, タスク, 注文, コメント, イベント, 問い合わせ, 通知, 掲示板',
669
+ };
670
+
671
+ function buildSuggestion(mods) {
672
+ if (!mods.fallback && mods.matchStrength >= 1) return null;
673
+ const lines = [' Hint: use these keywords for better results:'];
674
+ for (const [cat, kws] of Object.entries(KEYWORD_CHEATSHEET)) {
675
+ lines.push(` ${cat.padEnd(10)} ${kws}`);
676
+ }
677
+ return lines.join('\n');
678
+ }
679
+
680
+ // ── Pre-Processor (for ~~ in source files) ──────────────────
681
+
682
+ export function expandDirectives(source) {
683
+ const lines = source.split('\n');
684
+ const result = [];
685
+ let expanded = false;
686
+
687
+ for (const line of lines) {
688
+ const m = line.match(/^(\s*)~~\s+(?:"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)')\s*$/);
689
+ if (m) {
690
+ const indent = m[1];
691
+ const instruction = m[2] ?? m[3];
692
+ const gen = generate(instruction);
693
+ const genLines = gen.code.trimEnd().split('\n');
694
+ for (const gl of genLines) {
695
+ result.push(indent + gl);
696
+ }
697
+ expanded = true;
698
+ } else {
699
+ result.push(line);
700
+ }
701
+ }
702
+
703
+ return { source: result.join('\n'), expanded };
704
+ }
705
+
706
+ // ── Public API ──────────────────────────────────────────────
707
+
708
+ export function generate(instruction, options = {}) {
709
+ const words = tokenize(instruction);
710
+ const { intents, mods } = classify(words, instruction);
711
+ const params = extractParams(words, instruction, intents, mods);
712
+
713
+ if (options.port) params.port = options.port;
714
+ if (options.schemaName) params.schemaName = options.schemaName;
715
+ if (options.fields) params.fields = options.fields;
716
+
717
+ let code = compose(intents, params);
718
+ const result = validate(code);
719
+
720
+ return {
721
+ code: result.code,
722
+ valid: result.valid,
723
+ fixed: result.fixed,
724
+ repairs: result.repairs,
725
+ intents: [...intents],
726
+ schema: params.schemaName,
727
+ port: params.port,
728
+ suggestion: buildSuggestion(mods),
729
+ fallback: !!mods.fallback,
730
+ };
731
+ }