naider 1.19.0 → 1.21.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,1660 @@
1
+ import { Lexer } from './lexer.js';
2
+ import { Parser } from './parser.js';
3
+ import { readFileSync as _readFS, writeFileSync as _writeFS, existsSync as _existsFS } from 'node:fs';
4
+
5
+ // ── Vocabulary ──────────────────────────────────────────────
6
+
7
+ const INTENT_WORDS = {
8
+ server: ['server', 'api', 'rest', 'restful', 'endpoint', 'web', 'http', 'https', 'backend', 'app', 'application', 'service', 'microservice'],
9
+ schema: ['schema', 'model', 'entity', 'struct', 'table', 'type', 'data', 'record', 'resource', 'object', 'definition'],
10
+ bot: ['bot', 'chatbot', 'robot'],
11
+ cli: ['cli', 'command', 'terminal', 'console', 'argv', 'args'],
12
+ page: ['page', 'html', 'website', 'site', 'frontend', 'ui', 'view', 'template', 'render', 'layout'],
13
+ test: ['test', 'testing', 'spec', 'unit', 'e2e', 'integration', 'assert', 'expect'],
14
+ database: ['database', 'db', 'storage', 'persist', 'store', 'repository', 'sqlite', 'postgres', 'postgresql', 'json-db', 'mongo'],
15
+ ai: ['ai', 'llm', 'gpt', 'claude', 'openai', 'prompt', 'embedding', 'rag'],
16
+ crud: ['crud', 'create', 'read', 'update', 'delete', 'listing', 'list', 'index'],
17
+ auth: ['auth', 'jwt', 'login', 'authentication', 'authorization', 'signup', 'signin', 'sign-in', 'sign-up',
18
+ 'password', 'token', 'session', 'protect', 'permission', 'role', 'rbac', 'oauth', 'register', 'member', 'membership', 'account'],
19
+ cors: ['cors', 'cross-origin'],
20
+ websocket: ['websocket', 'ws', 'realtime', 'real-time', 'socket', 'live', 'push-notification', 'pubsub', 'pub-sub'],
21
+ mail: ['mail', 'smtp', 'mailer', 'sendmail', 'newsletter', 'notification'],
22
+ graphql: ['graphql', 'gql', 'query', 'mutation', 'resolver', 'subscription'],
23
+ };
24
+
25
+ const JA_INTENTS = {
26
+ server: ['サーバー', 'エンドポイント', 'バックエンド', 'アプリ', 'ウェブアプリ', 'Webアプリ', 'webアプリ',
27
+ 'API', 'サービス', 'マイクロサービス', 'ウェブサービス'],
28
+ schema: ['スキーマ', 'モデル', 'テーブル', 'データ型', '型定義', 'エンティティ', 'データモデル', 'リソース'],
29
+ bot: ['ボット', 'チャットボット', 'Bot', 'bot'],
30
+ cli: ['コマンド', 'ツール', 'ターミナル', 'コマンドライン', 'CLI', 'コンソール'],
31
+ page: ['ページ', 'サイト', 'ウェブ', 'フロントエンド', '画面', 'UI', '表示', 'テンプレート', 'ビュー', '見た目'],
32
+ test: ['テスト', '検証', 'テストコード', '単体テスト', '結合テスト', 'ユニットテスト'],
33
+ database: ['データベース', 'ストレージ', '保存', '永続化', 'DB', 'db', 'データ保存', '記録'],
34
+ ai: ['プロンプト', 'LLM', 'llm', '生成AI', '人工知能'],
35
+ auth: ['認証', 'ログイン', 'パスワード', '保護', '権限', '会員', 'サインイン', 'サインアップ',
36
+ '登録', 'ユーザー認証', 'アカウント', 'メンバー', '会員制', '会員機能', 'ログインできる',
37
+ 'ログイン機能', '認証機能', '認証付き', 'パスワード保護', 'アクセス制御', 'セキュリティ'],
38
+ crud: ['CRUD', 'crud', '作成', '取得', '更新', '削除', '一覧', '追加', '編集', '管理',
39
+ '管理機能', '一覧表示', 'リスト', '操作'],
40
+ websocket: ['ウェブソケット', 'リアルタイム', 'ソケット', 'WebSocket', 'websocket',
41
+ 'リアルタイム通信', 'プッシュ通知', 'ライブ'],
42
+ mail: ['メール', 'メール送信', 'メール機能', 'SMTP', '通知メール', 'メール通知'],
43
+ };
44
+
45
+ const JA_ENTITIES = {
46
+ 'ユーザー': 'User', 'ユーザ': 'User', 'ユーザ管理': 'User', 'ユーザー管理': 'User',
47
+ '会員': 'User', 'メンバー': 'User', 'アカウント': 'User', '利用者': 'User',
48
+ '顧客': 'Customer', 'お客様': 'Customer', 'クライアント': 'Customer',
49
+ '商品': 'Product', 'プロダクト': 'Product', '製品': 'Product', '品物': 'Product',
50
+ '記事': 'Post', '投稿': 'Post', 'ブログ記事': 'Post',
51
+ 'タスク': 'Task', 'やること': 'Task', '作業': 'Task',
52
+ 'メッセージ': 'Message', 'チャットメッセージ': 'Message',
53
+ '注文': 'Order', 'オーダー': 'Order', '受注': 'Order', '発注': 'Order',
54
+ 'コメント': 'Comment', '感想': 'Comment', 'レビュー': 'Review', '評価': 'Review',
55
+ 'ファイル': 'File', 'アップロード': 'File',
56
+ 'カテゴリ': 'Category', 'カテゴリー': 'Category', '分類': 'Category',
57
+ 'イベント': 'Event', '予定': 'Event', 'スケジュール': 'Event', '予約': 'Reservation',
58
+ 'プロジェクト': 'Project', '案件': 'Project',
59
+ '書籍': 'Book', '本': 'Book', '図書': 'Book',
60
+ 'アイテム': 'Item', 'リスト': 'List', 'ノート': 'Note', 'メモ': 'Note',
61
+ '通知': 'Notification', 'お知らせ': 'Notification',
62
+ '決済': 'Payment', '支払い': 'Payment', '課金': 'Payment',
63
+ '設定': 'Setting', '環境設定': 'Setting', '構成': 'Setting',
64
+ '問い合わせ': 'Contact', 'お問い合わせ': 'Contact', '連絡': 'Contact',
65
+ '掲示板': 'Post', 'スレッド': 'Thread', 'トピック': 'Topic',
66
+ 'レシピ': 'Recipe', '料理': 'Recipe',
67
+ '在庫': 'Inventory', '倉庫': 'Inventory',
68
+ '社員': 'Employee', '従業員': 'Employee', 'スタッフ': 'Staff',
69
+ 'チケット': 'Ticket', 'チーム': 'Team', 'グループ': 'Group',
70
+ 'ログ': 'Log', '履歴': 'Log', 'アクティビティ': 'Activity',
71
+ };
72
+
73
+ const PLATFORM_WORDS = {
74
+ discord: ['discord', 'ディスコード'],
75
+ slack: ['slack', 'スラック'],
76
+ telegram: ['telegram', 'テレグラム'],
77
+ line: ['line', 'ライン'],
78
+ };
79
+
80
+ const COMPOSITE_WORDS = {
81
+ fullstack: ['fullstack', 'full-stack', 'フルスタック', '管理システム', '管理アプリ', '管理画面'],
82
+ todo: ['todo', 'todos', 'to-do', 'タスク管理', 'todoアプリ', 'やることリスト', 'todo管理'],
83
+ blog: ['blog', 'ブログ', '記事管理', '投稿管理', 'ブログサイト', 'ブログアプリ'],
84
+ chat: ['chat', 'チャットアプリ', 'メッセンジャー', 'チャット機能', 'チャットルーム'],
85
+ shop: ['shop', 'ecommerce', 'e-commerce', 'ショップ', 'ストア', '通販', 'ECサイト', 'EC', 'オンラインショップ', '通販サイト'],
86
+ board: ['board', '掲示板', 'フォーラム', 'BBS', 'bbs'],
87
+ crm: ['crm', 'CRM', '顧客管理', '顧客管理システム', 'カスタマー管理'],
88
+ inventory: ['inventory', '在庫管理', '倉庫管理', '在庫システム'],
89
+ booking: ['booking', 'reservation', '予約システム', '予約管理', '予約アプリ', '予約サイト'],
90
+ sns: ['sns', 'SNS', 'ソーシャル', 'social', 'social-media', 'ソーシャルメディア'],
91
+ };
92
+
93
+ const NOISE = new Set([
94
+ 'a', 'an', 'the', 'for', 'with', 'and', 'or', 'on', 'at', 'to', 'of', 'in', 'by',
95
+ 'that', 'which', 'this', 'it', 'its', 'my', 'is', 'are', 'has', 'have', 'do', 'does',
96
+ 'make', 'create', 'build', 'generate', 'add', 'implement', 'simple', 'basic', 'new',
97
+ 'please', 'want', 'need', 'like', 'just', 'some', 'using', 'use', 'based',
98
+ 'setup', 'set', 'up', 'get', 'should', 'can', 'could', 'would',
99
+ 'の', 'を', 'に', 'で', 'は', 'が', 'と', 'も', 'から', 'まで', 'より', 'って',
100
+ 'な', 'ような', 'みたいな', 'っぽい', 'ような感じ',
101
+ '作って', '作る', '作成', '生成', '追加', '実装', 'ください', 'して', 'つけて',
102
+ '付き', '付きの', 'ある', 'ない', 'する', 'できる', 'ほしい', 'したい',
103
+ '機能', '仕組み', 'もの', 'こと', 'やつ', '感じ', '的な',
104
+ '簡単な', 'シンプルな', '基本的な', '新しい',
105
+ ]);
106
+
107
+ // ── Field Normalization ─────────────────────────────────────
108
+
109
+ const FIELD_SYNONYMS = {
110
+ 'e-mail': 'email', 'email_address': 'email', 'mail_address': 'email', 'emailaddress': 'email', 'mail': 'email',
111
+ 'user_name': 'username', 'login_name': 'username', 'loginname': 'username', 'login': 'username',
112
+ 'first_name': 'firstname', 'firstname': 'name', 'last_name': 'lastname',
113
+ 'display_name': 'name', 'full_name': 'name', 'fullname': 'name',
114
+ 'phone_number': 'phone', 'tel': 'phone', 'telephone': 'phone', 'phonenumber': 'phone', 'mobile': 'phone',
115
+ 'zip_code': 'zipcode', 'postal_code': 'zipcode', 'postalcode': 'zipcode',
116
+ 'birth_date': 'birthdate', 'birthday': 'birthdate', 'date_of_birth': 'birthdate',
117
+ 'created_at': 'created', 'creation_date': 'created', 'createdat': 'created', 'date_created': 'created',
118
+ 'updated_at': 'updated', 'modification_date': 'updated', 'updatedat': 'updated', 'date_updated': 'updated', 'modified_at': 'updated',
119
+ 'is_active': 'active', 'isactive': 'active',
120
+ 'is_deleted': 'deleted', 'isdeleted': 'deleted',
121
+ 'is_public': 'public', 'ispublic': 'public',
122
+ 'is_admin': 'admin', 'isadmin': 'admin',
123
+ 'is_verified': 'verified', 'isverified': 'verified',
124
+ 'is_done': 'done', 'isdone': 'done', 'is_completed': 'completed',
125
+ 'is_published': 'published', 'ispublished': 'published',
126
+ 'is_enabled': 'enabled', 'isenabled': 'enabled',
127
+ 'num_items': 'count', 'item_count': 'count',
128
+ 'total_price': 'total', 'total_amount': 'total',
129
+ 'img': 'image', 'pic': 'image', 'picture': 'image', 'photo': 'image', 'thumbnail': 'image', 'avatar': 'image',
130
+ 'desc': 'description', 'memo': 'description', 'detail': 'description', 'details': 'description',
131
+ 'pwd': 'password', 'passwd': 'password', 'pass': 'password', 'pw': 'password',
132
+ 'qty': 'quantity', 'amt': 'amount', 'addr': 'address', 'msg': 'message',
133
+ 'cat': 'category', 'tags': 'tag', 'lbl': 'label',
134
+ };
135
+
136
+ function normalizeField(name) {
137
+ const lower = name.toLowerCase();
138
+ if (FIELD_SYNONYMS[lower]) return FIELD_SYNONYMS[lower];
139
+ const n = lower.replace(/-/g, '_');
140
+ return FIELD_SYNONYMS[n] || n;
141
+ }
142
+
143
+ // ── Field Type Inference ────────────────────────────────────
144
+
145
+ const FIELD_TYPES = {
146
+ auto: ['id', 'created', 'updated', 'timestamp'],
147
+ bool: ['active', 'done', 'completed', 'enabled', 'published', 'verified', 'admin', 'public',
148
+ 'visible', 'archived', 'deleted', 'premium', 'banned', 'muted', 'online', 'featured',
149
+ 'approved', 'blocked', 'pinned', 'starred', 'read'],
150
+ int: ['age', 'count', 'quantity', 'amount', 'price', 'cost', 'total', 'score', 'rating',
151
+ 'level', 'order', 'index', 'size', 'width', 'height', 'weight', 'year', 'month',
152
+ 'day', 'port', 'priority', 'views', 'likes', 'followers', 'stock', 'points',
153
+ 'duration', 'position', 'rank', 'votes'],
154
+ };
155
+
156
+ function inferType(rawName) {
157
+ const n = normalizeField(rawName);
158
+ for (const [type, names] of Object.entries(FIELD_TYPES)) {
159
+ if (names.includes(n)) return type;
160
+ }
161
+ if (n.endsWith('_id') || n.endsWith('id') && n !== 'id') return 'int';
162
+ if (n.startsWith('is_') || n.startsWith('has_') || n.startsWith('can_')) return 'bool';
163
+ if (n.endsWith('count') || n.endsWith('num') || n.endsWith('number')) return 'int';
164
+ if (n.includes('price') || n.includes('cost') || n.includes('amount')) return 'int';
165
+ if (n.includes('date') || n.includes('time') || n === 'birthdate') return 'str';
166
+ return 'str';
167
+ }
168
+
169
+ function inferModifiers(rawName, type) {
170
+ const n = normalizeField(rawName);
171
+ if (type === 'auto') return [];
172
+ const mods = [];
173
+ if (!['bool', 'auto'].includes(type)) {
174
+ if (['name', 'title', 'email', 'password', 'username', 'content', 'body', 'phone', 'address'].includes(n)) mods.push('required');
175
+ }
176
+ if (n === 'email') mods.push('email');
177
+ if (n === 'name' || n === 'title') { mods.push('min(1)'); mods.push('max(200)'); }
178
+ if (n === 'password') mods.push('min(8)');
179
+ return mods;
180
+ }
181
+
182
+ // ── Default Schema Fields ───────────────────────────────────
183
+
184
+ const SCHEMA_PRESETS = {
185
+ user: [['id','auto'], ['name','str','required'], ['email','str','required','email'], ['password','str','required','min(8)']],
186
+ customer: [['id','auto'], ['name','str','required'], ['email','str','required','email'], ['phone','str']],
187
+ post: [['id','auto'], ['title','str','required','min(1)','max(200)'], ['body','str','required'], ['published','bool'], ['created','auto']],
188
+ product: [['id','auto'], ['name','str','required'], ['price','int','required'], ['stock','int'], ['description','str']],
189
+ todo: [['id','auto'], ['title','str','required','min(1)'], ['done','bool']],
190
+ task: [['id','auto'], ['title','str','required'], ['description','str'], ['status','str'], ['done','bool']],
191
+ comment: [['id','auto'], ['body','str','required'], ['author','str','required'], ['created','auto']],
192
+ review: [['id','auto'], ['body','str','required'], ['rating','int'], ['author','str','required'], ['created','auto']],
193
+ message: [['id','auto'], ['content','str','required'], ['sender','str','required'], ['created','auto']],
194
+ order: [['id','auto'], ['total','int','required'], ['status','str'], ['created','auto']],
195
+ event: [['id','auto'], ['title','str','required'], ['date','str','required'], ['description','str']],
196
+ reservation:[['id','auto'], ['date','str','required'], ['name','str','required'], ['status','str'], ['created','auto']],
197
+ note: [['id','auto'], ['title','str','required'], ['content','str'], ['created','auto']],
198
+ book: [['id','auto'], ['title','str','required'], ['author','str','required'], ['price','int'], ['published','bool']],
199
+ category: [['id','auto'], ['name','str','required'], ['description','str']],
200
+ file: [['id','auto'], ['name','str','required'], ['path','str','required'], ['size','int'], ['created','auto']],
201
+ project: [['id','auto'], ['name','str','required'], ['description','str'], ['status','str'], ['created','auto']],
202
+ contact: [['id','auto'], ['name','str','required'], ['email','str','required','email'], ['message','str','required'], ['created','auto']],
203
+ notification:[['id','auto'], ['title','str','required'], ['body','str'], ['read','bool'], ['created','auto']],
204
+ payment: [['id','auto'], ['amount','int','required'], ['status','str'], ['method','str'], ['created','auto']],
205
+ setting: [['id','auto'], ['key','str','required'], ['value','str','required']],
206
+ ticket: [['id','auto'], ['title','str','required'], ['description','str'], ['status','str'], ['priority','int'], ['created','auto']],
207
+ employee: [['id','auto'], ['name','str','required'], ['email','str','required','email'], ['role','str']],
208
+ staff: [['id','auto'], ['name','str','required'], ['email','str','required','email'], ['role','str']],
209
+ thread: [['id','auto'], ['title','str','required'], ['body','str','required'], ['author','str','required'], ['created','auto']],
210
+ recipe: [['id','auto'], ['title','str','required'], ['description','str'], ['ingredients','str'], ['created','auto']],
211
+ inventory:[['id','auto'], ['name','str','required'], ['quantity','int','required'], ['location','str']],
212
+ log: [['id','auto'], ['action','str','required'], ['detail','str'], ['created','auto']],
213
+ activity: [['id','auto'], ['type','str','required'], ['description','str'], ['created','auto']],
214
+ item: [['id','auto'], ['name','str','required'], ['created','auto']],
215
+ };
216
+
217
+ function defaultFields(schemaName) {
218
+ const key = schemaName.toLowerCase();
219
+ const preset = SCHEMA_PRESETS[key];
220
+ if (preset) return preset.map(f => ({ name: f[0], type: f[1], modifiers: f.slice(2) }));
221
+ return [
222
+ { name: 'id', type: 'auto', modifiers: [] },
223
+ { name: 'name', type: 'str', modifiers: ['required'] },
224
+ { name: 'created', type: 'auto', modifiers: [] },
225
+ ];
226
+ }
227
+
228
+ // ── Relationship Detection ─────────────────────────────────
229
+
230
+ const PARENT_CHILD = {
231
+ User: ['Post', 'Comment', 'Order', 'Message', 'Review', 'Task', 'Notification', 'File',
232
+ 'Payment', 'Ticket', 'Note', 'Reservation', 'Activity', 'Log', 'Thread', 'Recipe'],
233
+ Category: ['Product', 'Post', 'Item'],
234
+ Product: ['Order', 'Review', 'Inventory'],
235
+ Post: ['Comment', 'Review'],
236
+ Project: ['Task', 'Ticket'],
237
+ Team: ['Employee', 'Staff', 'Project'],
238
+ Order: ['Payment'],
239
+ Event: ['Reservation'],
240
+ };
241
+
242
+ function detectRelationships(entities) {
243
+ const rels = [];
244
+ const names = entities.map(e => e.name);
245
+ for (let i = 0; i < names.length; i++) {
246
+ for (let j = 0; j < names.length; j++) {
247
+ if (i === j) continue;
248
+ const children = PARENT_CHILD[names[i]];
249
+ if (children && children.includes(names[j])) {
250
+ rels.push({ parent: names[i], child: names[j], fk: names[i].toLowerCase() + '_id' });
251
+ }
252
+ }
253
+ }
254
+ return rels;
255
+ }
256
+
257
+ function applyRelationships(entities, relationships) {
258
+ for (const rel of relationships) {
259
+ const child = entities.find(e => e.name === rel.child);
260
+ if (child && !child.fields.some(f => f.name === rel.fk)) {
261
+ const insertIdx = child.fields.findIndex(f => f.type !== 'auto' || f.name !== 'id');
262
+ const fkField = { name: rel.fk, type: 'int', modifiers: ['required'] };
263
+ if (insertIdx > 0) child.fields.splice(insertIdx, 0, fkField);
264
+ else child.fields.push(fkField);
265
+ }
266
+ }
267
+ }
268
+
269
+ // ── Utilities ───────────────────────────────────────────────
270
+
271
+ function singularize(w) {
272
+ if (w.endsWith('ies')) return w.slice(0, -3) + 'y';
273
+ if (w.endsWith('ses') || w.endsWith('xes') || w.endsWith('zes') || w.endsWith('ches') || w.endsWith('shes')) return w.slice(0, -2);
274
+ if (w.endsWith('s') && !w.endsWith('ss') && !w.endsWith('us') && w.length > 3) return w.slice(0, -1);
275
+ return w;
276
+ }
277
+
278
+ function capitalize(w) {
279
+ return w.charAt(0).toUpperCase() + w.slice(1);
280
+ }
281
+
282
+ function pluralize(w) {
283
+ if (w.endsWith('y') && !/[aeiou]y$/i.test(w)) return w.slice(0, -1) + 'ies';
284
+ if (w.endsWith('s') || w.endsWith('x') || w.endsWith('z') || w.endsWith('ch') || w.endsWith('sh')) return w + 'es';
285
+ return w + 's';
286
+ }
287
+
288
+ const RESERVED = new Set([
289
+ 'get', 'post', 'put', 'del', 'on', 'in', 'as', 'is', 'isnt', 'if', 'for', 'each', 'while',
290
+ 'use', 'fn', 'ret', 'mut', 'pub', 'elif', 'else', 'match', 'try', 'fail', 'server', 'bot',
291
+ 'page', 'cli', 'mail', 'db', 'log', 'new', 'not', 'and', 'or', 'then', 'from', 'break',
292
+ 'continue', 'throw', 'schema', 'crud', 'auth', 'cors', 'model', 'self', 'test', 'assert',
293
+ 'await', 'enum', 'swap', 'repeat', 'until', 'unless', 'extends', 'print', 'static',
294
+ 'true', 'false', 'null', 'str', 'int', 'num', 'bool', 'list', 'map', 'any', 'json', 'void', 'auto',
295
+ 'search', 'push', 'image', 'csv', 'graphql', 'desktop', 'screen', 'validate', 'prompt',
296
+ 'every', 'watch', 'limit', 'env', 'session', 'cookie', 'upload', 'view', 'cache', 'patch',
297
+ 'queue', 'job', 'ws', 'sse', 'group', 'storage', 'oauth', 'pay', 'pdf', 'i18n',
298
+ ]);
299
+
300
+ function safeName(name) {
301
+ if (RESERVED.has(name.toLowerCase())) return name + 'App';
302
+ return name;
303
+ }
304
+
305
+ function align(rows) {
306
+ if (rows.length === 0) return [];
307
+ const cols = rows[0].length;
308
+ const widths = Array(cols).fill(0);
309
+ for (const row of rows) {
310
+ for (let i = 0; i < Math.min(cols, row.length); i++) {
311
+ widths[i] = Math.max(widths[i], (row[i] || '').length);
312
+ }
313
+ }
314
+ return rows.map(row =>
315
+ row.map((cell, i) => i < cols - 1 ? (cell || '').padEnd(widths[i]) : (cell || '')).join(' ').trimEnd()
316
+ );
317
+ }
318
+
319
+ // ── Instruction Parsing ─────────────────────────────────────
320
+
321
+ function tokenize(instruction) {
322
+ return instruction
323
+ .replace(/[.,!?;:()'"\[\]{}]/g, ' ')
324
+ .replace(/\s+/g, ' ')
325
+ .trim()
326
+ .split(' ')
327
+ .filter(w => w.length > 0);
328
+ }
329
+
330
+ function classify(words, raw) {
331
+ const lower = raw.toLowerCase();
332
+ const intents = new Set();
333
+ const mods = {};
334
+ let matchStrength = 0;
335
+
336
+ // English keyword matching
337
+ for (const [intent, kws] of Object.entries(INTENT_WORDS)) {
338
+ for (const w of words) {
339
+ const wl = w.toLowerCase();
340
+ if (kws.includes(wl)) { intents.add(intent); matchStrength++; break; }
341
+ }
342
+ }
343
+
344
+ // Japanese substring matching
345
+ for (const [intent, kws] of Object.entries(JA_INTENTS)) {
346
+ for (const kw of kws) {
347
+ if (raw.includes(kw)) { intents.add(intent); matchStrength++; break; }
348
+ }
349
+ }
350
+
351
+ // Composite detection (match both lowercase and original for mixed JP/EN keywords)
352
+ for (const [comp, kws] of Object.entries(COMPOSITE_WORDS)) {
353
+ if (kws.some(k => lower.includes(k.toLowerCase()) || raw.includes(k))) {
354
+ matchStrength += 2;
355
+ if (comp === 'fullstack') { intents.add('server'); intents.add('schema'); intents.add('crud'); intents.add('auth'); intents.add('database'); }
356
+ if (comp === 'todo') { intents.add('server'); intents.add('schema'); intents.add('crud'); intents.add('database'); mods.preset = 'todo'; }
357
+ if (comp === 'blog') { intents.add('server'); intents.add('schema'); intents.add('crud'); intents.add('database'); mods.preset = 'post'; }
358
+ if (comp === 'chat') { intents.add('server'); intents.add('schema'); intents.add('websocket'); intents.add('database'); mods.preset = 'message'; }
359
+ if (comp === 'shop') { intents.add('server'); intents.add('schema'); intents.add('crud'); intents.add('auth'); intents.add('database'); mods.preset = 'product'; mods.multiEntity = ['User', 'Product', 'Order']; }
360
+ if (comp === 'board') { intents.add('server'); intents.add('schema'); intents.add('crud'); intents.add('database'); mods.preset = 'post'; }
361
+ if (comp === 'crm') { intents.add('server'); intents.add('schema'); intents.add('crud'); intents.add('auth'); intents.add('database'); mods.multiEntity = ['Customer', 'Contact', 'Note']; }
362
+ if (comp === 'inventory') { intents.add('server'); intents.add('schema'); intents.add('crud'); intents.add('database'); mods.multiEntity = ['Product', 'Inventory']; }
363
+ if (comp === 'booking') { intents.add('server'); intents.add('schema'); intents.add('crud'); intents.add('auth'); intents.add('database'); mods.multiEntity = ['User', 'Reservation']; }
364
+ if (comp === 'sns') { intents.add('server'); intents.add('schema'); intents.add('crud'); intents.add('auth'); intents.add('websocket'); intents.add('database'); mods.multiEntity = ['User', 'Post', 'Comment']; }
365
+ }
366
+ }
367
+
368
+ // Platform detection
369
+ for (const [plat, kws] of Object.entries(PLATFORM_WORDS)) {
370
+ if (kws.some(k => lower.includes(k))) mods.platform = plat;
371
+ }
372
+
373
+ // DB type detection
374
+ if (lower.includes('sqlite')) mods.dbType = 'sqlite';
375
+ else if (lower.includes('postgres')) mods.dbType = 'postgres';
376
+
377
+ // Detect entity-like words (nouns that could be schema names)
378
+ const allKnown = new Set([...Object.values(INTENT_WORDS).flat(), ...Object.values(PLATFORM_WORDS).flat(),
379
+ ...Object.values(COMPOSITE_WORDS).flat()]);
380
+ const hasEntity = words.some(w => {
381
+ const wl = w.toLowerCase();
382
+ return !NOISE.has(wl) && !allKnown.has(wl) && !/^\d+$/.test(wl) && wl.length > 2 && /^[a-z]+s?$/i.test(wl);
383
+ }) || Object.keys(JA_ENTITIES).some(ja => raw.includes(ja));
384
+
385
+ // Complex NL phrase detection
386
+ if (/(?:users?\s+can\s+(?:comment|post|review|write)|コメントできる|投稿できる|レビューできる)/i.test(raw)) {
387
+ intents.add('auth'); intents.add('crud');
388
+ }
389
+ if (/(?:admin.*(?:delete|manage|moderate|ban)|管理者.*(?:削除|管理|モデレート))/i.test(raw)) {
390
+ intents.add('auth');
391
+ mods.needsAdmin = true;
392
+ }
393
+ if (/(?:upload|file\s+upload|画像.*アップ|ファイル.*アップ)/i.test(raw)) {
394
+ intents.add('crud');
395
+ }
396
+ if (/(?:search|検索|フィルタ|filter|sort|ソート)/i.test(raw)) {
397
+ intents.add('crud');
398
+ }
399
+ if (/(?:notification|通知|push|プッシュ|alert|アラート)/i.test(raw) && !intents.has('mail')) {
400
+ intents.add('websocket');
401
+ }
402
+ if (/(?:deploy|デプロイ|docker|コンテナ|container)/i.test(raw)) {
403
+ mods.needsDocker = true;
404
+ }
405
+ if (/(?:pagina|ページネーション|ページ送り|一覧表示)/i.test(raw)) {
406
+ intents.add('crud');
407
+ }
408
+ if (/(?:seed|初期データ|サンプルデータ|テストデータ|ダミーデータ)/i.test(raw)) {
409
+ mods.needsSeed = true;
410
+ }
411
+
412
+ // "for X" pattern implies schema + crud
413
+ if (hasEntity && intents.has('server')) {
414
+ intents.add('schema'); intents.add('crud');
415
+ }
416
+ if (lower.includes(' for ') && intents.has('server') && !intents.has('schema')) {
417
+ intents.add('schema'); intents.add('crud');
418
+ }
419
+
420
+ // Implicit intent rules
421
+ if (intents.has('crud') && !intents.has('server')) intents.add('server');
422
+ if (intents.has('crud') && !intents.has('schema')) intents.add('schema');
423
+ if (intents.has('auth') && !intents.has('server')) intents.add('server');
424
+ if (intents.has('schema') && intents.has('server')) intents.add('database');
425
+ if (intents.has('bot') && !mods.platform) mods.platform = 'discord';
426
+
427
+ // Default fallback: if nothing detected, server + schema
428
+ if (intents.size === 0) {
429
+ intents.add('server'); intents.add('schema'); intents.add('crud'); intents.add('database');
430
+ mods.fallback = true;
431
+ }
432
+
433
+ mods.matchStrength = matchStrength;
434
+ return { intents, mods };
435
+ }
436
+
437
+ function extractParams(words, raw, intents, mods) {
438
+ const params = { port: 3000, schemaName: null, fields: [], entities: [], relationships: [],
439
+ features: intents, platform: mods.platform || null, dbType: mods.dbType || null, commands: [] };
440
+
441
+ // Extract port number
442
+ for (let i = 0; i < words.length; i++) {
443
+ const n = parseInt(words[i]);
444
+ if (!isNaN(n) && n >= 80 && n <= 65535) params.port = n;
445
+ if (words[i].toLowerCase() === 'port' && i + 1 < words.length) {
446
+ const pn = parseInt(words[i + 1]);
447
+ if (!isNaN(pn)) params.port = pn;
448
+ }
449
+ }
450
+
451
+ // ── Multi-entity extraction ──
452
+ const entityNames = [];
453
+
454
+ // Japanese entities (longest match first, collect ALL matches)
455
+ const jaEntries = Object.entries(JA_ENTITIES).sort((a, b) => b[0].length - a[0].length);
456
+ const jaUsed = new Set();
457
+ for (const [ja, en] of jaEntries) {
458
+ if (raw.includes(ja) && !jaUsed.has(en)) {
459
+ entityNames.push(en);
460
+ jaUsed.add(en);
461
+ }
462
+ }
463
+
464
+ // English entities: extract nouns from the full instruction, splitting on "and" / ","
465
+ if (entityNames.length === 0) {
466
+ const allKnown = new Set([...Object.values(INTENT_WORDS).flat(), ...Object.values(PLATFORM_WORDS).flat(),
467
+ ...Object.values(COMPOSITE_WORDS).flat()]);
468
+ const lower = raw.toLowerCase();
469
+ const segments = lower.split(/\s+(?:and|&)\s+|,\s*/);
470
+ for (const seg of segments) {
471
+ const segWords = seg.trim().split(/\s+/).filter(w => w !== 'with');
472
+ for (const w of segWords) {
473
+ const wl = w.toLowerCase().replace(/[^a-z0-9]/g, '');
474
+ if (!wl || NOISE.has(wl) || allKnown.has(wl) || /^\d+$/.test(wl) || wl.length <= 2) continue;
475
+ const name = capitalize(singularize(wl));
476
+ if (!entityNames.includes(name)) entityNames.push(name);
477
+ break;
478
+ }
479
+ }
480
+ }
481
+
482
+ // Composite multi-entity presets (shop, crm, booking, sns, etc.)
483
+ if (mods.multiEntity && entityNames.length <= 1) {
484
+ for (const e of mods.multiEntity) {
485
+ if (!entityNames.includes(e)) entityNames.push(e);
486
+ }
487
+ }
488
+
489
+ // Preset override
490
+ if (mods.preset && entityNames.length === 0) {
491
+ entityNames.push(capitalize(mods.preset));
492
+ }
493
+
494
+ // Default
495
+ if (entityNames.length === 0 && intents.has('schema')) entityNames.push('Item');
496
+
497
+ // Primary entity (backward compat)
498
+ params.schemaName = entityNames[0] || null;
499
+
500
+ // Extract fields from "with" clause (applies to primary entity)
501
+ // Only treat words after "with" as fields if they look like field names (not entity names or intent keywords)
502
+ const lower = raw.toLowerCase();
503
+ let primaryFields = [];
504
+ const withIdx = lower.indexOf(' with ');
505
+ if (withIdx !== -1) {
506
+ const afterWith = lower.slice(withIdx + 6);
507
+ const primaryIntents = new Set(['server', 'api', 'rest', 'bot', 'cli', 'page', 'test', 'database',
508
+ 'auth', 'crud', 'websocket', 'mail', 'graphql', 'sqlite', 'postgres']);
509
+ const entityLike = new Set(entityNames.map(e => e.toLowerCase()));
510
+ const entityLikePlural = new Set(entityNames.map(e => pluralize(e.toLowerCase())));
511
+ const candidateWords = afterWith
512
+ .split(/[\s,、]+/)
513
+ .map(w => w.replace(/[^a-zA-Z0-9_\-]/g, ''))
514
+ .filter(w => w.length > 1 && !NOISE.has(w) && !primaryIntents.has(w) && !/^\d+$/.test(w)
515
+ && !entityLike.has(w) && !entityLikePlural.has(w) && w !== 'and');
516
+ // Only use as fields if they look like actual field names (not entity names)
517
+ const knownFieldish = new Set([...Object.keys(FIELD_SYNONYMS), ...Object.keys(FIELD_TYPES).flatMap(k => FIELD_TYPES[k])]);
518
+ const hasFieldNames = candidateWords.some(w => knownFieldish.has(normalizeField(w)));
519
+ if (candidateWords.length > 0 && hasFieldNames) {
520
+ primaryFields = [{ name: 'id', type: 'auto', modifiers: [] }];
521
+ for (const rawName of candidateWords) {
522
+ const name = normalizeField(rawName);
523
+ const type = inferType(name);
524
+ primaryFields.push({ name, type, modifiers: inferModifiers(name, type) });
525
+ }
526
+ }
527
+ }
528
+
529
+ // Build entities array
530
+ for (let i = 0; i < entityNames.length; i++) {
531
+ const name = entityNames[i];
532
+ const fields = (i === 0 && primaryFields.length > 0) ? primaryFields : defaultFields(name);
533
+ params.entities.push({ name, fields });
534
+ }
535
+
536
+ // Backward compat: primary entity fields
537
+ params.fields = params.entities.length > 0 ? params.entities[0].fields : [];
538
+
539
+ // Detect relationships between entities
540
+ if (params.entities.length > 1) {
541
+ params.relationships = detectRelationships(params.entities);
542
+ applyRelationships(params.entities, params.relationships);
543
+ }
544
+
545
+ // Extract bot commands
546
+ const cmdMatch = raw.match(/command[s]?\s+(.+)/i);
547
+ if (cmdMatch) {
548
+ params.commands = cmdMatch[1].split(/[\s,、and]+/).filter(c => c.length > 1).map(c => ({
549
+ trigger: c.startsWith('!') ? c : '!' + c,
550
+ response: `${capitalize(c.replace('!', ''))}!`,
551
+ }));
552
+ }
553
+
554
+ return params;
555
+ }
556
+
557
+ // ── Template Engine ─────────────────────────────────────────
558
+
559
+ function schemaBlock(name, fields) {
560
+ const rows = fields.map(f => [f.name, f.type, ...f.modifiers]);
561
+ const aligned = align(rows);
562
+ return `schema ${name}:\n` + aligned.map(l => ' ' + l).join('\n');
563
+ }
564
+
565
+ function dbBlock(type) {
566
+ if (type === 'sqlite') return 'db.sql "sqlite"';
567
+ if (type === 'postgres') return 'db.sql "postgres"';
568
+ return 'db "data/"';
569
+ }
570
+
571
+ function serverBlock(name, port, sections) {
572
+ return `server ${name} port ${port}:\n` + sections.map(l => ' ' + l).join('\n');
573
+ }
574
+
575
+ function botBlock(platform, commands) {
576
+ const token = platform.toUpperCase() + '_TOKEN';
577
+ const lines = [`bot mybot type "${platform}" token ${token} prefix "!":`];
578
+ if (commands.length === 0) {
579
+ commands = [
580
+ { trigger: '!hello', response: 'Hello!' },
581
+ { trigger: '!help', response: 'Commands: !hello, !help' },
582
+ ];
583
+ }
584
+ for (const cmd of commands) {
585
+ lines.push(` on "${cmd.trigger}" (msg):`);
586
+ lines.push(` msg.reply("${cmd.response}")`);
587
+ }
588
+ return lines.join('\n');
589
+ }
590
+
591
+ function cliBlock(name) {
592
+ return `cli ${safeName(name || 'app')}:
593
+ flag "-n" "--name" str "Name"
594
+ flag "-o" "--output" str "Output file"
595
+ run (args):
596
+ log("Name: " + args.name)
597
+ log("Output: " + args.output)`;
598
+ }
599
+
600
+ function testBlock(name) {
601
+ return `test "${name || 'app'} tests":
602
+ assert 1 + 1 == 2
603
+ assert "hello".length == 5
604
+ assert [1, 2, 3].length == 3`;
605
+ }
606
+
607
+ function entityTestBlock(entity) {
608
+ const name = entity.name;
609
+ const lower = name.toLowerCase();
610
+ const plural = pluralize(lower);
611
+ const store = name + 'Store';
612
+ const requiredFields = entity.fields.filter(f => f.modifiers.includes('required') && f.type !== 'auto');
613
+ const sampleObj = requiredFields.map(f => {
614
+ if (f.type === 'int') return `${f.name}: 1`;
615
+ if (f.type === 'bool') return `${f.name}: true`;
616
+ return `${f.name}: "test"`;
617
+ }).join(', ');
618
+
619
+ return `test "${lower} CRUD":
620
+ any created = ${store}.create({${sampleObj}})
621
+ assert created.id
622
+
623
+ any found = ${store}.find(created.id)
624
+ assert found.id == created.id
625
+
626
+ list all = ${store}.all()
627
+ assert all.length > 0
628
+
629
+ ${store}.remove(created.id)
630
+ any deleted = ${store}.find(created.id)
631
+ assert not deleted`;
632
+ }
633
+
634
+ function nestedRouteBlock(parent, child, fk) {
635
+ const parentLower = parent.toLowerCase();
636
+ const childLower = child.toLowerCase();
637
+ const childPlural = pluralize(childLower);
638
+ const childStore = child + 'Store';
639
+ return [
640
+ `get "/api/${pluralize(parentLower)}/:${fk.replace('_id', 'Id')}/${childPlural}" (req, res):`,
641
+ ` list items = ${childStore}.findAll("${fk}", req.params.${fk.replace('_id', 'Id')})`,
642
+ ` ret items`,
643
+ ];
644
+ }
645
+
646
+ function paginatedListBlock(entity) {
647
+ const lower = entity.name.toLowerCase();
648
+ const plural = pluralize(lower);
649
+ const store = entity.name + 'Store';
650
+ return [
651
+ `get "/api/${plural}" (req, res):`,
652
+ ` int page = req.query.page || 1`,
653
+ ` int limit = req.query.limit || 20`,
654
+ ` str search = req.query.search || ""`,
655
+ ` list all = ${store}.all()`,
656
+ ` if search:`,
657
+ ` all = all.filter((item) => JSON.stringify(item).includes(search))`,
658
+ ` int total = all.length`,
659
+ ` int start = (page - 1) * limit`,
660
+ ` list items = all.slice(start, start + limit)`,
661
+ ` ret {items, total, page, limit}`,
662
+ ];
663
+ }
664
+
665
+ function seedBlock(entities) {
666
+ const lines = ['fn seed():'];
667
+ for (const entity of entities) {
668
+ const store = entity.name + 'Store';
669
+ const seen = new Set();
670
+ const sampleFields = [];
671
+ for (const f of entity.fields) {
672
+ if (f.type === 'auto' || seen.has(f.name)) continue;
673
+ seen.add(f.name);
674
+ if (f.name === 'password') { sampleFields.push('password: "hashed_demo"'); continue; }
675
+ if (!f.modifiers.includes('required') && !f.name.endsWith('_id')) continue;
676
+ if (f.name.endsWith('_id')) sampleFields.push(`${f.name}: 1`);
677
+ else if (f.type === 'int') sampleFields.push(`${f.name}: ${f.name === 'price' ? 1000 : f.name === 'total' ? 2500 : 1}`);
678
+ else if (f.type === 'bool') sampleFields.push(`${f.name}: false`);
679
+ else if (f.name === 'email') sampleFields.push(`${f.name}: "demo@example.com"`);
680
+ else sampleFields.push(`${f.name}: "Sample ${entity.name}"`);
681
+ }
682
+ lines.push(` ${store}.create({${sampleFields.join(', ')}})`);
683
+ }
684
+ lines.push(' log("Seed data inserted")');
685
+ return lines.join('\n');
686
+ }
687
+
688
+ function graphqlBlock() {
689
+ return `graphql "/graphql"`;
690
+ }
691
+
692
+ function pageBlock(title) {
693
+ return `page "index.html":
694
+ h1 "${title || 'Welcome'}"
695
+ p "Built with NAIDE"`;
696
+ }
697
+
698
+ function mailBlock() {
699
+ return `mail "smtp.example.com" 587:
700
+ user SMTP_USER
701
+ pass SMTP_PASS`;
702
+ }
703
+
704
+ // ── Middleware & Utility Templates ──────────────────────────
705
+
706
+ function utilityFunctions(intents) {
707
+ const fns = [];
708
+ if (intents.has('server')) {
709
+ fns.push(`fn ok(any data, any meta):
710
+ ret {ok: true, data, error: null, meta}`);
711
+ fns.push(`fn err(str message, int code):
712
+ ret {ok: false, data: null, error: message, code}`);
713
+ fns.push(`fn sanitize(str input):
714
+ ret input.replace("<", "&lt;").replace(">", "&gt;").replace("'", "&#39;")`);
715
+ }
716
+ return fns;
717
+ }
718
+
719
+ function softDeleteRoutes(entity) {
720
+ const lower = entity.name.toLowerCase();
721
+ const plural = pluralize(lower);
722
+ const store = entity.name + 'Store';
723
+ return [
724
+ `del "/api/${plural}/:id/soft" (req, res):`,
725
+ ` ${store}.update(req.params.id, {deleted: true})`,
726
+ ` ret ok({archived: true}, null)`,
727
+ ``,
728
+ `put "/api/${plural}/:id/restore" (req, res):`,
729
+ ` ${store}.update(req.params.id, {deleted: false})`,
730
+ ` ret ok({restored: true}, null)`,
731
+ ];
732
+ }
733
+
734
+ function batchRoutes(entity) {
735
+ const lower = entity.name.toLowerCase();
736
+ const plural = pluralize(lower);
737
+ const store = entity.name + 'Store';
738
+ return [
739
+ `post "/api/${plural}/batch" (req, res):`,
740
+ ` list results = []`,
741
+ ` each item in req.body.items:`,
742
+ ` any created = ${store}.create(item)`,
743
+ ` results.push(created)`,
744
+ ` ret ok({created: results.length, items: results}, null)`,
745
+ ``,
746
+ `del "/api/${plural}/batch" (req, res):`,
747
+ ` each id in req.body.ids:`,
748
+ ` ${store}.remove(id)`,
749
+ ` ret ok({deleted: req.body.ids.length}, null)`,
750
+ ];
751
+ }
752
+
753
+ function uploadRoute() {
754
+ return [
755
+ `post "/api/upload" (req, res):`,
756
+ ` any file = req.files.file`,
757
+ ` if not file:`,
758
+ ` ret.status(400) err("No file provided", 400)`,
759
+ ` str path = "uploads/" + file.name`,
760
+ ` file.mv(path)`,
761
+ ` ret ok({path, name: file.name, size: file.size}, null)`,
762
+ ];
763
+ }
764
+
765
+ function auditSchema() {
766
+ return `schema AuditLog:
767
+ id auto
768
+ action str required
769
+ entity str required
770
+ entity_id int
771
+ user_id int
772
+ detail str
773
+ created auto`;
774
+ }
775
+
776
+ function auditFn() {
777
+ return `fn auditLog(str action, str entity, int entityId, int userId):
778
+ AuditLogStore.create({action, entity, entity_id: entityId, user_id: userId})`;
779
+ }
780
+
781
+ function webhookRoute() {
782
+ return [
783
+ `post "/api/webhooks" (req, res):`,
784
+ ` str event = req.body.event`,
785
+ ` any payload = req.body.payload`,
786
+ ` log("Webhook received: " + event)`,
787
+ ` ret ok({received: true, event}, null)`,
788
+ ``,
789
+ `get "/api/webhooks/health" (req, res):`,
790
+ ` ret ok({status: "listening"}, null)`,
791
+ ];
792
+ }
793
+
794
+ function serverMiddlewareHints(intents, params) {
795
+ const hints = [];
796
+ hints.push('# ── Middleware (install if needed) ──');
797
+ hints.push('# npm install express-rate-limit → rateLimit({windowMs: 900000, max: 100})');
798
+ hints.push('# npm install helmet → helmet() for security headers');
799
+ hints.push('# npm install compression → compression() for gzip');
800
+ if (intents.has('auth')) {
801
+ hints.push('# npm install cors → cors({origin: "https://yourdomain.com"})');
802
+ }
803
+ return hints.join('\n');
804
+ }
805
+
806
+ function openApiSpec(entities, intents, params, relationships) {
807
+ const paths = {};
808
+ for (const entity of entities) {
809
+ const plural = pluralize(entity.name.toLowerCase());
810
+ const requiredFields = entity.fields.filter(f => f.modifiers.includes('required') && f.type !== 'auto');
811
+ const properties = {};
812
+ for (const f of entity.fields) {
813
+ properties[f.name] = { type: f.type === 'auto' ? 'integer' : f.type === 'int' ? 'integer' : f.type === 'bool' ? 'boolean' : 'string' };
814
+ }
815
+ paths[`/api/${plural}`] = {
816
+ get: { summary: `List ${plural} (paginated)`, parameters: [
817
+ { name: 'page', in: 'query', schema: { type: 'integer', default: 1 } },
818
+ { name: 'limit', in: 'query', schema: { type: 'integer', default: 20 } },
819
+ { name: 'search', in: 'query', schema: { type: 'string' } },
820
+ ], responses: { '200': { description: `List of ${plural}` } } },
821
+ post: { summary: `Create ${entity.name.toLowerCase()}`, requestBody: { content: { 'application/json': { schema: {
822
+ type: 'object', required: requiredFields.map(f => f.name),
823
+ properties: Object.fromEntries(requiredFields.map(f => [f.name, properties[f.name]])),
824
+ } } } }, responses: { '201': { description: 'Created' } } },
825
+ };
826
+ paths[`/api/${plural}/{id}`] = {
827
+ get: { summary: `Get ${entity.name.toLowerCase()} by ID`, responses: { '200': { description: entity.name } } },
828
+ put: { summary: `Update ${entity.name.toLowerCase()}`, responses: { '200': { description: 'Updated' } } },
829
+ delete: { summary: `Delete ${entity.name.toLowerCase()}`, responses: { '200': { description: 'Deleted' } } },
830
+ };
831
+ paths[`/api/${plural}/batch`] = {
832
+ post: { summary: `Batch create ${plural}`, responses: { '200': { description: 'Batch created' } } },
833
+ delete: { summary: `Batch delete ${plural}`, responses: { '200': { description: 'Batch deleted' } } },
834
+ };
835
+ paths[`/api/${plural}/{id}/soft`] = {
836
+ delete: { summary: `Soft delete ${entity.name.toLowerCase()}`, responses: { '200': { description: 'Archived' } } },
837
+ };
838
+ paths[`/api/${plural}/{id}/restore`] = {
839
+ put: { summary: `Restore ${entity.name.toLowerCase()}`, responses: { '200': { description: 'Restored' } } },
840
+ };
841
+ }
842
+ for (const rel of relationships) {
843
+ const parentPlural = pluralize(rel.parent.toLowerCase());
844
+ const childPlural = pluralize(rel.child.toLowerCase());
845
+ paths[`/api/${parentPlural}/{${rel.fk.replace('_id', 'Id')}}/${childPlural}`] = {
846
+ get: { summary: `${rel.child}s by ${rel.parent}`, responses: { '200': { description: `List of ${childPlural}` } } },
847
+ };
848
+ }
849
+ if (intents.has('auth')) {
850
+ paths['/api/auth/register'] = { post: { summary: 'Register', responses: { '200': { description: 'Token + user' } } } };
851
+ paths['/api/auth/login'] = { post: { summary: 'Login', responses: { '200': { description: 'Token + user' } } } };
852
+ paths['/api/auth/me'] = { get: { summary: 'Current user', security: [{ bearerAuth: [] }], responses: { '200': { description: 'User' } } } };
853
+ }
854
+ paths['/api/health'] = { get: { summary: 'Health check', responses: { '200': { description: 'Status' } } } };
855
+ paths['/api/upload'] = { post: { summary: 'Upload file', responses: { '200': { description: 'File info' } } } };
856
+
857
+ const spec = {
858
+ openapi: '3.0.3',
859
+ info: { title: (entities[0]?.name || 'App') + ' API', version: '1.0.0', description: 'Generated by NAIDE Agent Coder' },
860
+ servers: [{ url: `http://localhost:${params.port}` }],
861
+ paths,
862
+ };
863
+ if (intents.has('auth')) {
864
+ spec.components = { securitySchemes: { bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' } } };
865
+ }
866
+ return spec;
867
+ }
868
+
869
+ function adminPage(entities, intents) {
870
+ const lines = [];
871
+ lines.push('<!DOCTYPE html>');
872
+ lines.push('<html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">');
873
+ lines.push('<title>Admin Dashboard</title>');
874
+ lines.push('<style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui;background:#f5f5f5;padding:2rem}');
875
+ lines.push('h1{margin-bottom:1rem}.card{background:#fff;border-radius:8px;padding:1.5rem;margin-bottom:1rem;box-shadow:0 1px 3px rgba(0,0,0,.1)}');
876
+ lines.push('table{width:100%;border-collapse:collapse}th,td{padding:.5rem;text-align:left;border-bottom:1px solid #eee}');
877
+ lines.push('th{background:#f9f9f9;font-weight:600}button{padding:.4rem 1rem;border:none;border-radius:4px;cursor:pointer;background:#2563eb;color:#fff}');
878
+ lines.push('button:hover{background:#1d4ed8}button.danger{background:#dc2626}button.danger:hover{background:#b91c1c}');
879
+ lines.push('input,select{padding:.4rem;border:1px solid #ddd;border-radius:4px;margin-right:.5rem}');
880
+ lines.push('.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:1rem;margin-bottom:1.5rem}');
881
+ lines.push('.stat{background:#fff;border-radius:8px;padding:1.5rem;text-align:center;box-shadow:0 1px 3px rgba(0,0,0,.1)}');
882
+ lines.push('.stat h3{font-size:2rem;color:#2563eb}.stat p{color:#666;margin-top:.5rem}');
883
+ lines.push('#toast{position:fixed;top:1rem;right:1rem;background:#22c55e;color:#fff;padding:.8rem 1.5rem;border-radius:8px;display:none}');
884
+ lines.push('</style></head><body>');
885
+ lines.push('<h1>Admin Dashboard</h1>');
886
+ lines.push('<div id="toast"></div>');
887
+ lines.push('<div class="stats" id="stats"></div>');
888
+ for (const entity of entities) {
889
+ const lower = entity.name.toLowerCase();
890
+ const plural = pluralize(lower);
891
+ lines.push(`<div class="card"><h2>${entity.name}s</h2>`);
892
+ lines.push(`<div style="margin:1rem 0"><input id="${lower}-search" placeholder="Search..." oninput="${lower}Load()">`);
893
+ lines.push(`<button onclick="${lower}Load()">Refresh</button> <button onclick="${lower}ShowForm()">+ New</button></div>`);
894
+ lines.push(`<div id="${lower}-form" style="display:none;margin:1rem 0;padding:1rem;background:#f9f9f9;border-radius:4px">`);
895
+ for (const f of entity.fields) {
896
+ if (f.type === 'auto') continue;
897
+ const inputType = f.type === 'int' ? 'number' : f.type === 'bool' ? 'checkbox' : f.name === 'password' ? 'password' : f.name === 'email' ? 'email' : 'text';
898
+ if (f.type === 'bool') {
899
+ lines.push(`<label><input type="checkbox" id="${lower}-${f.name}"> ${f.name}</label> `);
900
+ } else {
901
+ lines.push(`<input type="${inputType}" id="${lower}-${f.name}" placeholder="${f.name}${f.modifiers.includes('required') ? ' *' : ''}">`);
902
+ }
903
+ }
904
+ lines.push(`<button onclick="${lower}Create()">Save</button> <button onclick="document.getElementById('${lower}-form').style.display='none'" class="danger">Cancel</button></div>`);
905
+ lines.push(`<table><thead><tr>${entity.fields.map(f => `<th>${f.name}</th>`).join('')}<th>Actions</th></tr></thead>`);
906
+ lines.push(`<tbody id="${lower}-table"></tbody></table></div>`);
907
+ }
908
+ lines.push('<script>');
909
+ lines.push('const API="";function toast(m){const t=document.getElementById("toast");t.textContent=m;t.style.display="block";setTimeout(()=>t.style.display="none",2000)}');
910
+ lines.push('async function loadStats(){try{const r=await fetch(API+"/api/health");const d=await r.json();const s=document.getElementById("stats");');
911
+ lines.push('s.innerHTML=Object.entries(d).filter(([k])=>k!=="status").map(([k,v])=>`<div class="stat"><h3>${v}</h3><p>${k}</p></div>`).join("")}catch(e){}}');
912
+ for (const entity of entities) {
913
+ const lower = entity.name.toLowerCase();
914
+ const plural = pluralize(lower);
915
+ const fields = entity.fields.filter(f => f.type !== 'auto');
916
+ lines.push(`async function ${lower}Load(){const s=document.getElementById("${lower}-search").value;`);
917
+ lines.push(`const r=await fetch(API+"/api/${plural}?search="+s);const d=await r.json();const items=d.items||d;`);
918
+ lines.push(`document.getElementById("${lower}-table").innerHTML=items.map(i=>"<tr>${entity.fields.map(f => `<td>"+i.${f.name}+"`).join('')}<td>"+`);
919
+ lines.push(`"<button onclick=\\"${lower}Del("+i.id+")\\">Del</button></td></tr>").join("")}`);
920
+ lines.push(`function ${lower}ShowForm(){document.getElementById("${lower}-form").style.display="block"}`);
921
+ lines.push(`async function ${lower}Create(){const body={${fields.map(f => {
922
+ if (f.type === 'bool') return `${f.name}:document.getElementById("${lower}-${f.name}").checked`;
923
+ if (f.type === 'int') return `${f.name}:+document.getElementById("${lower}-${f.name}").value`;
924
+ return `${f.name}:document.getElementById("${lower}-${f.name}").value`;
925
+ }).join(',')}};`);
926
+ lines.push(`await fetch(API+"/api/${plural}",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)});`);
927
+ lines.push(`document.getElementById("${lower}-form").style.display="none";toast("Created!");${lower}Load();loadStats()}`);
928
+ lines.push(`async function ${lower}Del(id){if(!confirm("Delete?"))return;await fetch(API+"/api/${plural}/"+id,{method:"DELETE"});toast("Deleted!");${lower}Load();loadStats()}`);
929
+ }
930
+ lines.push(`loadStats();${entities.map(e => pluralize(e.name.toLowerCase()).replace(/^./, c => c) && e.name.toLowerCase() + 'Load()').join(';')}`);
931
+ lines.push('</script></body></html>');
932
+ return lines.join('\n');
933
+ }
934
+
935
+ function apiClientCode(entities, intents, port) {
936
+ const lines = [];
937
+ lines.push('// Auto-generated API client — NAIDE Agent Coder');
938
+ lines.push(`const BASE = typeof window !== 'undefined' ? '' : 'http://localhost:${port}';`);
939
+ lines.push('let _token = null;');
940
+ lines.push('');
941
+ lines.push('function headers() {');
942
+ lines.push(" const h = { 'Content-Type': 'application/json' };");
943
+ lines.push(" if (_token) h['Authorization'] = 'Bearer ' + _token;");
944
+ lines.push(' return h;');
945
+ lines.push('}');
946
+ lines.push('');
947
+ lines.push('async function api(method, path, body) {');
948
+ lines.push(' const opts = { method, headers: headers() };');
949
+ lines.push(' if (body) opts.body = JSON.stringify(body);');
950
+ lines.push(' const res = await fetch(BASE + path, opts);');
951
+ lines.push(' return res.json();');
952
+ lines.push('}');
953
+ lines.push('');
954
+ if (intents.has('auth')) {
955
+ lines.push('export async function register(name, email, password) {');
956
+ lines.push(" const r = await api('POST', '/api/auth/register', { name, email, password });");
957
+ lines.push(' if (r.token) _token = r.token;');
958
+ lines.push(' return r;');
959
+ lines.push('}');
960
+ lines.push('');
961
+ lines.push('export async function login(email, password) {');
962
+ lines.push(" const r = await api('POST', '/api/auth/login', { email, password });");
963
+ lines.push(' if (r.token) _token = r.token;');
964
+ lines.push(' return r;');
965
+ lines.push('}');
966
+ lines.push('');
967
+ lines.push("export async function me() { return api('GET', '/api/auth/me'); }");
968
+ lines.push('export function setToken(t) { _token = t; }');
969
+ lines.push('');
970
+ }
971
+ for (const entity of entities) {
972
+ const lower = entity.name.toLowerCase();
973
+ const plural = pluralize(lower);
974
+ const Name = entity.name;
975
+ lines.push(`// ${Name}`);
976
+ lines.push(`export async function list${Name}s(page = 1, limit = 20, search = '') {`);
977
+ lines.push(` return api('GET', \`/api/${plural}?page=\${page}&limit=\${limit}&search=\${search}\`);`);
978
+ lines.push('}');
979
+ lines.push(`export async function get${Name}(id) { return api('GET', \`/api/${plural}/\${id}\`); }`);
980
+ lines.push(`export async function create${Name}(data) { return api('POST', '/api/${plural}', data); }`);
981
+ lines.push(`export async function update${Name}(id, data) { return api('PUT', \`/api/${plural}/\${id}\`, data); }`);
982
+ lines.push(`export async function remove${Name}(id) { return api('DELETE', \`/api/${plural}/\${id}\`); }`);
983
+ lines.push(`export async function archive${Name}(id) { return api('DELETE', \`/api/${plural}/\${id}/soft\`); }`);
984
+ lines.push(`export async function restore${Name}(id) { return api('PUT', \`/api/${plural}/\${id}/restore\`); }`);
985
+ lines.push(`export async function batchCreate${Name}s(items) { return api('POST', '/api/${plural}/batch', { items }); }`);
986
+ lines.push(`export async function batchRemove${Name}s(ids) { return api('DELETE', '/api/${plural}/batch', { ids }); }`);
987
+ lines.push('');
988
+ }
989
+ lines.push("export async function upload(file) { const fd = new FormData(); fd.append('file', file); const r = await fetch(BASE + '/api/upload', { method: 'POST', headers: _token ? { Authorization: 'Bearer ' + _token } : {}, body: fd }); return r.json(); }");
990
+ lines.push("export async function health() { return api('GET', '/api/health'); }");
991
+ return lines.join('\n') + '\n';
992
+ }
993
+
994
+ // ── Auth Route Templates ────────────────────────────────────
995
+
996
+ function authRoutesBlock(userEntity) {
997
+ const store = userEntity + 'Store';
998
+ return [
999
+ `post "/api/auth/register" (req, res):`,
1000
+ ` str hashed = await hash(req.body.password)`,
1001
+ ` any user = ${store}.create({name: req.body.name, email: req.body.email, password: hashed})`,
1002
+ ` str token = jwt.sign({id: user.id}, JWT_SECRET)`,
1003
+ ` ret {token, user: {id: user.id, name: user.name, email: user.email}}`,
1004
+ ``,
1005
+ `post "/api/auth/login" (req, res):`,
1006
+ ` any user = ${store}.findBy("email", req.body.email)`,
1007
+ ` if not user:`,
1008
+ ` ret.status(401) {error: "Invalid credentials"}`,
1009
+ ` bool valid = await verify(req.body.password, user.password)`,
1010
+ ` if not valid:`,
1011
+ ` ret.status(401) {error: "Invalid credentials"}`,
1012
+ ` str token = jwt.sign({id: user.id}, JWT_SECRET)`,
1013
+ ` ret {token, user: {id: user.id, name: user.name, email: user.email}}`,
1014
+ ``,
1015
+ `get "/api/auth/me" (req, res):`,
1016
+ ` ret {user: req.user}`,
1017
+ ];
1018
+ }
1019
+
1020
+ function envLines(intents, params) {
1021
+ const lines = [];
1022
+ if (intents.has('auth')) lines.push('# env JWT_SECRET "change-me-in-production"');
1023
+ if (params.dbType === 'postgres') lines.push('# env DATABASE_URL "postgres://localhost:5432/app"');
1024
+ if (intents.has('mail')) {
1025
+ lines.push('# env SMTP_HOST "smtp.example.com"');
1026
+ lines.push('# env SMTP_USER "user@example.com"');
1027
+ lines.push('# env SMTP_PASS "password"');
1028
+ }
1029
+ if (intents.has('bot')) {
1030
+ const token = (params.platform || 'discord').toUpperCase() + '_TOKEN';
1031
+ lines.push(`# env ${token} "your-bot-token"`);
1032
+ }
1033
+ if (intents.has('ai')) lines.push('# env OPENAI_API_KEY "your-api-key"');
1034
+ return lines;
1035
+ }
1036
+
1037
+ // ── Code Composer ───────────────────────────────────────────
1038
+
1039
+ function compose(intents, params) {
1040
+ const sections = [];
1041
+ const entities = params.entities.length > 0 ? params.entities : [{ name: params.schemaName || 'Item', fields: params.fields }];
1042
+ const primaryName = entities[0].name;
1043
+ const primaryLower = primaryName.toLowerCase();
1044
+ const relationships = params.relationships || [];
1045
+ const hasAuth = intents.has('auth');
1046
+ const userEntity = entities.find(e => ['User', 'Customer', 'Employee', 'Staff'].includes(e.name));
1047
+
1048
+ // Middleware hints
1049
+ if (intents.has('server')) {
1050
+ sections.push(serverMiddlewareHints(intents, params));
1051
+ }
1052
+
1053
+ // Env hints
1054
+ const env = envLines(intents, params);
1055
+ if (env.length > 0) sections.push(env.join('\n'));
1056
+
1057
+ // Utility functions (envelope, sanitize)
1058
+ const utils = utilityFunctions(intents);
1059
+ if (utils.length > 0) sections.push(...utils);
1060
+
1061
+ // Schema blocks for all entities (add role + deleted fields)
1062
+ if (intents.has('schema')) {
1063
+ for (const entity of entities) {
1064
+ if (entity.fields.length > 0) {
1065
+ if (hasAuth && userEntity && entity.name === userEntity.name && !entity.fields.some(f => f.name === 'role')) {
1066
+ entity.fields.push({ name: 'role', type: 'str', modifiers: [] });
1067
+ }
1068
+ if (intents.has('crud') && !entity.fields.some(f => f.name === 'deleted')) {
1069
+ entity.fields.push({ name: 'deleted', type: 'bool', modifiers: [] });
1070
+ }
1071
+ sections.push(schemaBlock(entity.name, entity.fields));
1072
+ }
1073
+ }
1074
+ // Audit log schema when auth is present
1075
+ if (hasAuth) {
1076
+ sections.push(auditSchema());
1077
+ }
1078
+ }
1079
+
1080
+ if (intents.has('database')) {
1081
+ sections.push(dbBlock(params.dbType));
1082
+ }
1083
+
1084
+ if (intents.has('server')) {
1085
+ const body = [];
1086
+ body.push('cors "*"');
1087
+
1088
+ // CRUD for each entity
1089
+ if (intents.has('crud') && intents.has('schema')) {
1090
+ for (const entity of entities) {
1091
+ const ep = pluralize(entity.name.toLowerCase());
1092
+ body.push(`crud "/api/${ep}" ${entity.name}`);
1093
+ }
1094
+ }
1095
+
1096
+ // Auth: full routes + role-based access
1097
+ if (hasAuth) {
1098
+ body.push('auth JWT_SECRET:');
1099
+ body.push(' protect "/api/*"');
1100
+ body.push(' public "/api/auth/*"');
1101
+ body.push(' public "/api/health"');
1102
+ if (userEntity) {
1103
+ body.push('');
1104
+ body.push(...authRoutesBlock(userEntity.name));
1105
+ // Role-based admin check route
1106
+ body.push('');
1107
+ body.push('get "/api/admin/stats" (req, res):');
1108
+ body.push(' if req.user.role != "admin":');
1109
+ body.push(' ret.status(403) {error: "Admin access required"}');
1110
+ if (entities.length > 0) {
1111
+ const counts = entities.map(e => `${pluralize(e.name.toLowerCase())}: ${e.name}Store.count()`).join(', ');
1112
+ body.push(` ret {${counts}}`);
1113
+ } else {
1114
+ body.push(' ret {status: "ok"}');
1115
+ }
1116
+ }
1117
+ }
1118
+
1119
+ // Paginated list endpoints (override default CRUD list)
1120
+ if (intents.has('crud') && intents.has('schema')) {
1121
+ body.push('');
1122
+ for (const entity of entities) {
1123
+ body.push(...paginatedListBlock(entity));
1124
+ body.push('');
1125
+ }
1126
+ }
1127
+
1128
+ // Nested routes for relationships
1129
+ for (const rel of relationships) {
1130
+ body.push(...nestedRouteBlock(rel.parent, rel.child, rel.fk));
1131
+ body.push('');
1132
+ }
1133
+
1134
+ // WebSocket
1135
+ if (intents.has('websocket')) {
1136
+ body.push('ws "/ws":');
1137
+ body.push(' on "connection" (socket):');
1138
+ body.push(' log("Client connected")');
1139
+ body.push(' on "message" (data, socket):');
1140
+ body.push(' socket.send(data)');
1141
+ body.push(' on "close" (socket):');
1142
+ body.push(' log("Client disconnected")');
1143
+ body.push('');
1144
+ }
1145
+
1146
+ // AI endpoint
1147
+ if (intents.has('ai')) {
1148
+ body.push('post "/api/ask" (req, res):');
1149
+ body.push(' if not req.body.prompt:');
1150
+ body.push(' ret.status(400) {error: "prompt is required"}');
1151
+ body.push(' str answer = await ai.ask(req.body.prompt)');
1152
+ body.push(' ret {answer}');
1153
+ body.push('');
1154
+ }
1155
+
1156
+ // Soft delete + batch for each entity
1157
+ if (intents.has('crud') && intents.has('schema')) {
1158
+ for (const entity of entities) {
1159
+ body.push(...softDeleteRoutes(entity));
1160
+ body.push('');
1161
+ body.push(...batchRoutes(entity));
1162
+ body.push('');
1163
+ }
1164
+ }
1165
+
1166
+ // File upload
1167
+ if (intents.has('crud')) {
1168
+ body.push(...uploadRoute());
1169
+ body.push('');
1170
+ }
1171
+
1172
+ // Webhook
1173
+ if (intents.has('server')) {
1174
+ body.push(...webhookRoute());
1175
+ body.push('');
1176
+ }
1177
+
1178
+ // Health check
1179
+ body.push('get "/api/health" (req, res):');
1180
+ if (intents.has('schema') && intents.has('crud') && entities.length > 0) {
1181
+ const counts = entities.map(e => `${pluralize(e.name.toLowerCase())}: ${e.name}Store.count()`).join(', ');
1182
+ body.push(` ret {status: "ok", ${counts}}`);
1183
+ } else {
1184
+ body.push(' ret {status: "ok"}');
1185
+ }
1186
+
1187
+ const serverName = safeName(primaryLower === 'item' ? 'app' : primaryLower);
1188
+ sections.push(serverBlock(serverName, params.port, body));
1189
+ }
1190
+
1191
+ // Seed function
1192
+ if (intents.has('database') && intents.has('schema') && entities.length > 0) {
1193
+ sections.push(seedBlock(entities));
1194
+ }
1195
+
1196
+ // Audit log function
1197
+ if (hasAuth) {
1198
+ sections.push(auditFn());
1199
+ }
1200
+
1201
+ if (intents.has('bot')) {
1202
+ sections.push(botBlock(params.platform || 'discord', params.commands));
1203
+ }
1204
+
1205
+ if (intents.has('cli')) {
1206
+ sections.push(cliBlock(primaryLower));
1207
+ }
1208
+
1209
+ if (intents.has('graphql')) {
1210
+ sections.push(graphqlBlock());
1211
+ }
1212
+
1213
+ if (intents.has('page')) {
1214
+ sections.push(pageBlock(primaryName));
1215
+ }
1216
+
1217
+ if (intents.has('mail')) {
1218
+ sections.push(mailBlock());
1219
+ }
1220
+
1221
+ // Auto-generate entity tests when test intent or when entities exist
1222
+ if (intents.has('test') || (intents.has('schema') && intents.has('crud') && entities.length > 0)) {
1223
+ for (const entity of entities) {
1224
+ sections.push(entityTestBlock(entity));
1225
+ }
1226
+ }
1227
+
1228
+ return sections.join('\n\n') + '\n';
1229
+ }
1230
+
1231
+ // ── Self-Healing Validator ──────────────────────────────────
1232
+
1233
+ function validate(code) {
1234
+ let current = code;
1235
+ let fixed = false;
1236
+ const repairs = [];
1237
+
1238
+ for (let attempt = 0; attempt < 5; attempt++) {
1239
+ try {
1240
+ const lexer = new Lexer(current);
1241
+ const tokens = lexer.tokenize();
1242
+ const parser = new Parser(tokens);
1243
+ parser.parse();
1244
+ return { code: current, valid: true, fixed, repairs };
1245
+ } catch (err) {
1246
+ const before = current;
1247
+ const repair = heal(current, err);
1248
+ current = repair.code;
1249
+ if (current === before) return { code: current, valid: false, fixed, repairs };
1250
+ if (repair.description) repairs.push(repair.description);
1251
+ fixed = true;
1252
+ }
1253
+ }
1254
+
1255
+ return { code: current, valid: false, fixed, repairs };
1256
+ }
1257
+
1258
+ function heal(code, err) {
1259
+ const msg = err.message;
1260
+ const lines = code.split('\n');
1261
+ const lineMatch = msg.match(/line (\d+)/);
1262
+ if (!lineMatch) return { code, description: null };
1263
+
1264
+ const lineIdx = parseInt(lineMatch[1]) - 1;
1265
+ if (lineIdx < 0 || lineIdx >= lines.length) return { code, description: null };
1266
+ const line = lines[lineIdx];
1267
+
1268
+ // Missing colon at end of block header
1269
+ if (msg.includes('COLON') || msg.includes('Expected ":"')) {
1270
+ if (!line.trimEnd().endsWith(':')) {
1271
+ lines[lineIdx] = line.trimEnd() + ':';
1272
+ return { code: lines.join('\n'), description: `L${lineIdx + 1}: added missing ':' at end of block header` };
1273
+ }
1274
+ }
1275
+
1276
+ // Unexpected indent — previous line probably needs a colon
1277
+ if (msg.includes('INDENT') && lineIdx > 0) {
1278
+ const prev = lines[lineIdx - 1];
1279
+ if (!prev.trimEnd().endsWith(':') && !prev.trimEnd().endsWith(',')) {
1280
+ lines[lineIdx - 1] = prev.trimEnd() + ':';
1281
+ return { code: lines.join('\n'), description: `L${lineIdx}: added ':' to previous line to open block` };
1282
+ }
1283
+ }
1284
+
1285
+ // Reserved keyword used as identifier — rename it
1286
+ const reserved = ['get', 'post', 'put', 'del', 'on', 'in', 'as', 'is', 'log', 'new', 'not', 'and', 'or', 'if', 'for', 'use', 'fn', 'ret', 'mut'];
1287
+ if (msg.includes('Unexpected') || msg.includes('Expected IDENT')) {
1288
+ for (const kw of reserved) {
1289
+ const fieldPattern = new RegExp(`^(\\s+)${kw}(\\s+(?:str|int|num|bool|auto))`, 'm');
1290
+ if (fieldPattern.test(line)) {
1291
+ lines[lineIdx] = line.replace(fieldPattern, `$1${kw}_field$2`);
1292
+ return { code: lines.join('\n'), description: `L${lineIdx + 1}: renamed reserved word '${kw}' → '${kw}_field'` };
1293
+ }
1294
+ }
1295
+ }
1296
+
1297
+ // Unexpected token — try removing the problematic line (non-structural only)
1298
+ if (msg.includes('Unexpected') && !line.trim().match(/^(schema|server|bot|cli|page|test|fn|db)\b/)) {
1299
+ const removed = line.trim();
1300
+ lines.splice(lineIdx, 1);
1301
+ return { code: lines.join('\n'), description: `L${lineIdx + 1}: removed problematic line '${removed.slice(0, 40)}'` };
1302
+ }
1303
+
1304
+ return { code, description: null };
1305
+ }
1306
+
1307
+ // ── Suggestions ─────────────────────────────────────────────
1308
+
1309
+ const KEYWORD_CHEATSHEET = {
1310
+ 'Intent': 'server, api, rest, bot, cli, page, test, database, ai, crud, auth, websocket, mail, graphql',
1311
+ 'Composite': 'todo, blog, chat, shop, fullstack, board, crm, inventory, booking, sns',
1312
+ 'Platform': 'discord, slack, telegram, line',
1313
+ 'DB': 'sqlite, postgres',
1314
+ 'Japanese': 'サーバー, 認証, ログイン, 会員, CRUD, 管理, データベース, ボット, テスト, ページ',
1315
+ 'JP Entity': 'ユーザー, 商品, 記事, タスク, 注文, コメント, イベント, 問い合わせ, 通知, 掲示板',
1316
+ };
1317
+
1318
+ function buildSuggestion(mods) {
1319
+ if (!mods.fallback && mods.matchStrength >= 1) return null;
1320
+ const lines = [' Hint: use these keywords for better results:'];
1321
+ for (const [cat, kws] of Object.entries(KEYWORD_CHEATSHEET)) {
1322
+ lines.push(` ${cat.padEnd(10)} ${kws}`);
1323
+ }
1324
+ return lines.join('\n');
1325
+ }
1326
+
1327
+ // ── Project Generator ──────────────────────────────────────
1328
+
1329
+ export function generateProject(instruction, options = {}) {
1330
+ const result = generate(instruction, options);
1331
+ const files = [];
1332
+ const intents = new Set(result.intents);
1333
+ const entities = result.entities || [];
1334
+ const primaryName = (entities[0] || 'app').toLowerCase();
1335
+ const projectName = safeName(primaryName) + '-app';
1336
+
1337
+ // Main app file
1338
+ files.push({ path: 'app.naide', content: result.code });
1339
+
1340
+ // Package.json
1341
+ const deps = {};
1342
+ if (intents.has('server')) deps.express = '^4.21.0';
1343
+ if (intents.has('websocket')) deps.ws = '^8.18.0';
1344
+ if (intents.has('auth')) {
1345
+ deps.jsonwebtoken = '^9.0.2';
1346
+ deps.bcryptjs = '^2.4.3';
1347
+ }
1348
+ if (intents.has('mail')) deps.nodemailer = '^6.9.0';
1349
+ if (result.analysis.entities.some(() => true)) deps.naider = '^1.21.0';
1350
+ const pkg = {
1351
+ name: projectName,
1352
+ version: '1.0.0',
1353
+ type: 'module',
1354
+ scripts: {
1355
+ dev: 'naide app.naide',
1356
+ build: 'naide app.naide --emit -o dist/app.mjs',
1357
+ start: 'node dist/app.mjs',
1358
+ test: 'naide test app.naide',
1359
+ seed: 'naide app.naide --run-seed',
1360
+ },
1361
+ dependencies: deps,
1362
+ };
1363
+ files.push({ path: 'package.json', content: JSON.stringify(pkg, null, 2) + '\n' });
1364
+
1365
+ // .env file
1366
+ const envL = [];
1367
+ envL.push(`PORT=${result.port}`);
1368
+ if (intents.has('auth')) envL.push('JWT_SECRET=change-me-in-production');
1369
+ if (intents.has('bot')) envL.push(`${(options.platform || 'DISCORD').toUpperCase()}_TOKEN=your-bot-token`);
1370
+ if (intents.has('mail')) {
1371
+ envL.push('SMTP_HOST=smtp.example.com');
1372
+ envL.push('SMTP_USER=user@example.com');
1373
+ envL.push('SMTP_PASS=password');
1374
+ }
1375
+ if (intents.has('ai')) envL.push('OPENAI_API_KEY=your-api-key');
1376
+ files.push({ path: '.env', content: envL.join('\n') + '\n' });
1377
+
1378
+ // .gitignore
1379
+ files.push({ path: '.gitignore', content: 'node_modules/\ndist/\ndata/\n.env\n*.mjs\n' });
1380
+
1381
+ // Dockerfile
1382
+ if (intents.has('server')) {
1383
+ const dockerfile = `FROM node:22-slim
1384
+ WORKDIR /app
1385
+ COPY package*.json ./
1386
+ RUN npm ci --omit=dev
1387
+ RUN npm install -g naider
1388
+ COPY . .
1389
+ RUN naide app.naide --emit -o dist/app.mjs
1390
+ EXPOSE ${result.port}
1391
+ CMD ["node", "dist/app.mjs"]
1392
+ `;
1393
+ files.push({ path: 'Dockerfile', content: dockerfile });
1394
+
1395
+ files.push({ path: '.dockerignore', content: 'node_modules\ndist\ndata\n.env\n.git\n' });
1396
+ }
1397
+
1398
+ // README
1399
+ const readmeLines = [`# ${projectName}\n`];
1400
+ readmeLines.push(`Generated by NAIDE Agent Coder (~~)\n`);
1401
+ readmeLines.push(`## Features\n`);
1402
+ for (const step of result.analysis.plan) readmeLines.push(`- ${step}`);
1403
+ if (entities.length > 0) {
1404
+ readmeLines.push(`\n## Entities\n`);
1405
+ for (const e of result.analysis.entities) readmeLines.push(`- **${e.name}** (${e.fieldCount} fields)`);
1406
+ }
1407
+ if (result.analysis.relationships.length > 0) {
1408
+ readmeLines.push(`\n## Relationships\n`);
1409
+ for (const r of result.analysis.relationships) readmeLines.push(`- ${r.parent} → ${r.child} (${r.fk})`);
1410
+ }
1411
+ readmeLines.push(`\n## Quick Start\n`);
1412
+ readmeLines.push('```bash');
1413
+ readmeLines.push('npm install');
1414
+ readmeLines.push('npm run dev');
1415
+ readmeLines.push('```\n');
1416
+ if (intents.has('server')) {
1417
+ readmeLines.push(`## API Endpoints\n`);
1418
+ readmeLines.push(`- \`GET /api/health\` — Health check`);
1419
+ for (const e of entities) {
1420
+ const ep = pluralize(e.toLowerCase());
1421
+ readmeLines.push(`- \`GET /api/${ep}?page=1&limit=20&search=\` — List ${ep} (paginated)`);
1422
+ readmeLines.push(`- \`POST /api/${ep}\` — Create ${e.toLowerCase()}`);
1423
+ readmeLines.push(`- \`GET /api/${ep}/:id\` — Get ${e.toLowerCase()}`);
1424
+ readmeLines.push(`- \`PUT /api/${ep}/:id\` — Update ${e.toLowerCase()}`);
1425
+ readmeLines.push(`- \`DELETE /api/${ep}/:id\` — Delete ${e.toLowerCase()}`);
1426
+ readmeLines.push(`- \`DELETE /api/${ep}/:id/soft\` — Soft delete ${e.toLowerCase()}`);
1427
+ readmeLines.push(`- \`PUT /api/${ep}/:id/restore\` — Restore ${e.toLowerCase()}`);
1428
+ readmeLines.push(`- \`POST /api/${ep}/batch\` — Batch create ${ep}`);
1429
+ readmeLines.push(`- \`DELETE /api/${ep}/batch\` — Batch delete ${ep}`);
1430
+ }
1431
+ for (const r of result.analysis.relationships) {
1432
+ const parentPlural = pluralize(r.parent.toLowerCase());
1433
+ const childPlural = pluralize(r.child.toLowerCase());
1434
+ readmeLines.push(`- \`GET /api/${parentPlural}/:id/${childPlural}\` — ${r.child}s by ${r.parent}`);
1435
+ }
1436
+ readmeLines.push(`- \`POST /api/upload\` — File upload`);
1437
+ readmeLines.push(`- \`POST /api/webhooks\` — Register webhook`);
1438
+ readmeLines.push(`- \`GET /api/webhooks/health\` — Webhook health`);
1439
+ if (intents.has('auth')) {
1440
+ readmeLines.push(`- \`POST /api/auth/register\` — Register`);
1441
+ readmeLines.push(`- \`POST /api/auth/login\` — Login`);
1442
+ readmeLines.push(`- \`GET /api/auth/me\` — Current user`);
1443
+ readmeLines.push(`- \`GET /api/admin/stats\` — Admin stats (role: admin)`);
1444
+ }
1445
+ }
1446
+ if (intents.has('server')) {
1447
+ readmeLines.push(`\n## Docker\n`);
1448
+ readmeLines.push('```bash');
1449
+ readmeLines.push(`docker build -t ${projectName} .`);
1450
+ readmeLines.push(`docker run -p ${result.port}:${result.port} --env-file .env ${projectName}`);
1451
+ readmeLines.push('```');
1452
+ }
1453
+ files.push({ path: 'README.md', content: readmeLines.join('\n') + '\n' });
1454
+
1455
+ // Build full entity objects for generators
1456
+ const entityObjs = entities.map(name => {
1457
+ const preset = SCHEMA_PRESETS[name.toLowerCase()];
1458
+ const fields = preset ? preset.map(f => ({ name: f[0], type: f[1], modifiers: f.slice(2) })) : defaultFields(name);
1459
+ if (!fields.some(f => f.name === 'deleted')) fields.push({ name: 'deleted', type: 'bool', modifiers: [] });
1460
+ return { name, fields };
1461
+ });
1462
+
1463
+ // OpenAPI spec
1464
+ if (intents.has('server') && entityObjs.length > 0) {
1465
+ const spec = openApiSpec(entityObjs, intents, { port: result.port }, result.relationships || []);
1466
+ files.push({ path: 'openapi.json', content: JSON.stringify(spec, null, 2) + '\n' });
1467
+ }
1468
+
1469
+ // Admin dashboard
1470
+ if (intents.has('server') && entityObjs.length > 0) {
1471
+ files.push({ path: 'admin.html', content: adminPage(entityObjs, intents) });
1472
+ }
1473
+
1474
+ // API client
1475
+ if (intents.has('server')) {
1476
+ files.push({ path: 'client.mjs', content: apiClientCode(entityObjs, intents, result.port) });
1477
+ }
1478
+
1479
+ return { ...result, files };
1480
+ }
1481
+
1482
+ // ── File Write Mode ────────────────────────────────────────
1483
+
1484
+ export function writeToFile(filePath, instruction, options = {}) {
1485
+ const result = generate(instruction, options);
1486
+ const code = result.code;
1487
+
1488
+ if (!_existsFS(filePath)) {
1489
+ _writeFS(filePath, code, 'utf-8');
1490
+ return { ...result, action: 'created', path: filePath };
1491
+ }
1492
+
1493
+ const existing = _readFS(filePath, 'utf-8');
1494
+ const mode = options.mode || 'append';
1495
+
1496
+ if (mode === 'replace') {
1497
+ _writeFS(filePath, code, 'utf-8');
1498
+ return { ...result, action: 'replaced', path: filePath };
1499
+ }
1500
+
1501
+ // Append mode: merge intelligently
1502
+ const merged = mergeCode(existing, code);
1503
+ const validated = validate(merged);
1504
+ _writeFS(filePath, validated.code, 'utf-8');
1505
+ return { ...result, code: validated.code, valid: validated.valid, fixed: validated.fixed,
1506
+ repairs: validated.repairs, action: 'merged', path: filePath };
1507
+ }
1508
+
1509
+ function mergeCode(existing, generated) {
1510
+ const existingLines = existing.split('\n');
1511
+ const generatedLines = generated.split('\n');
1512
+
1513
+ const existingSchemas = new Set();
1514
+ const existingServers = new Set();
1515
+ for (const line of existingLines) {
1516
+ const schemaMatch = line.match(/^schema\s+(\w+)\s*:/);
1517
+ if (schemaMatch) existingSchemas.add(schemaMatch[1]);
1518
+ const serverMatch = line.match(/^server\s+(\w+)\s/);
1519
+ if (serverMatch) existingServers.add(serverMatch[1]);
1520
+ }
1521
+
1522
+ // Filter out duplicate top-level blocks from generated
1523
+ const filteredLines = [];
1524
+ let skipBlock = false;
1525
+ let blockIndent = -1;
1526
+ for (let i = 0; i < generatedLines.length; i++) {
1527
+ const line = generatedLines[i];
1528
+ const schemaMatch = line.match(/^schema\s+(\w+)\s*:/);
1529
+ const serverMatch = line.match(/^server\s+(\w+)\s/);
1530
+
1531
+ if (schemaMatch && existingSchemas.has(schemaMatch[1])) {
1532
+ skipBlock = true;
1533
+ blockIndent = 0;
1534
+ continue;
1535
+ }
1536
+ if (serverMatch && existingServers.has(serverMatch[1])) {
1537
+ // For server blocks, extract only new routes
1538
+ skipBlock = true;
1539
+ blockIndent = 0;
1540
+ continue;
1541
+ }
1542
+
1543
+ if (skipBlock) {
1544
+ const indent = line.match(/^(\s*)/)[1].length;
1545
+ if (line.trim() === '' || indent > blockIndent) continue;
1546
+ skipBlock = false;
1547
+ }
1548
+
1549
+ filteredLines.push(line);
1550
+ }
1551
+
1552
+ const newCode = filteredLines.join('\n').trim();
1553
+ if (!newCode) return existing;
1554
+
1555
+ return existing.trimEnd() + '\n\n' + newCode + '\n';
1556
+ }
1557
+
1558
+ // ── Pre-Processor (for ~~ in source files) ──────────────────
1559
+
1560
+ export function expandDirectives(source) {
1561
+ const lines = source.split('\n');
1562
+ const result = [];
1563
+ let expanded = false;
1564
+
1565
+ for (const line of lines) {
1566
+ const m = line.match(/^(\s*)~~\s+(?:"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)')\s*$/);
1567
+ if (m) {
1568
+ const indent = m[1];
1569
+ const instruction = m[2] ?? m[3];
1570
+ const gen = generate(instruction);
1571
+ const genLines = gen.code.trimEnd().split('\n');
1572
+ for (const gl of genLines) {
1573
+ result.push(indent + gl);
1574
+ }
1575
+ expanded = true;
1576
+ } else {
1577
+ result.push(line);
1578
+ }
1579
+ }
1580
+
1581
+ return { source: result.join('\n'), expanded };
1582
+ }
1583
+
1584
+ // ── Analysis Builder ───────────────────────────────────────
1585
+
1586
+ function buildAnalysis(instruction, intents, params) {
1587
+ const plan = [];
1588
+
1589
+ if (params.entities.length > 0) {
1590
+ for (const e of params.entities) {
1591
+ plan.push(`Schema "${e.name}" (${e.fields.map(f => f.name).join(', ')})`);
1592
+ }
1593
+ }
1594
+
1595
+ if (params.relationships.length > 0) {
1596
+ for (const r of params.relationships) {
1597
+ plan.push(`Relation: ${r.parent} → ${r.child} (${r.fk})`);
1598
+ }
1599
+ }
1600
+
1601
+ if (intents.has('database')) plan.push(`Database: ${params.dbType || 'file-based'}`);
1602
+ if (intents.has('server')) plan.push(`Server on port ${params.port}`);
1603
+ if (intents.has('crud')) plan.push(`CRUD endpoints for ${params.entities.map(e => pluralize(e.name.toLowerCase())).join(', ')} (paginated)`);
1604
+ if (intents.has('auth')) plan.push('Auth: JWT + register/login/me + role-based admin');
1605
+ if (params.relationships.length > 0) plan.push(`Nested routes: ${params.relationships.map(r => `/${pluralize(r.parent.toLowerCase())}/:id/${pluralize(r.child.toLowerCase())}`).join(', ')}`);
1606
+ if (intents.has('websocket'))plan.push('WebSocket: real-time messaging');
1607
+ if (intents.has('ai')) plan.push('AI endpoint: /api/ask');
1608
+ if (intents.has('bot')) plan.push(`Bot: ${params.platform || 'discord'}`);
1609
+ if (intents.has('mail')) plan.push('Mail: SMTP configuration');
1610
+ if (intents.has('graphql')) plan.push('GraphQL endpoint');
1611
+ if (intents.has('page')) plan.push('Page: HTML generation');
1612
+ if (intents.has('cli')) plan.push('CLI app with flags');
1613
+ if (intents.has('database') && intents.has('schema')) plan.push('Seed: sample data function');
1614
+ plan.push(`Tests: ${params.entities.length} entity CRUD test suites`);
1615
+
1616
+ return {
1617
+ instruction,
1618
+ entities: params.entities.map(e => ({ name: e.name, fieldCount: e.fields.length })),
1619
+ relationships: params.relationships,
1620
+ features: [...intents],
1621
+ plan,
1622
+ };
1623
+ }
1624
+
1625
+ // ── Public API ──────────────────────────────────────────────
1626
+
1627
+ export function generate(instruction, options = {}) {
1628
+ const words = tokenize(instruction);
1629
+ const { intents, mods } = classify(words, instruction);
1630
+ const params = extractParams(words, instruction, intents, mods);
1631
+
1632
+ if (options.port) params.port = options.port;
1633
+ if (options.schemaName) {
1634
+ params.schemaName = options.schemaName;
1635
+ if (params.entities.length > 0) params.entities[0].name = options.schemaName;
1636
+ }
1637
+ if (options.fields) {
1638
+ params.fields = options.fields;
1639
+ if (params.entities.length > 0) params.entities[0].fields = options.fields;
1640
+ }
1641
+
1642
+ let code = compose(intents, params);
1643
+ const result = validate(code);
1644
+ const analysis = buildAnalysis(instruction, intents, params);
1645
+
1646
+ return {
1647
+ code: result.code,
1648
+ valid: result.valid,
1649
+ fixed: result.fixed,
1650
+ repairs: result.repairs,
1651
+ intents: [...intents],
1652
+ schema: params.schemaName,
1653
+ entities: params.entities.map(e => e.name),
1654
+ relationships: params.relationships,
1655
+ port: params.port,
1656
+ suggestion: buildSuggestion(mods),
1657
+ fallback: !!mods.fallback,
1658
+ analysis,
1659
+ };
1660
+ }