naider 1.21.1 → 1.23.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/bin/naide.js CHANGED
@@ -8,7 +8,7 @@ import { spawn } from 'child_process';
8
8
  import { createRequire } from 'module';
9
9
  const _require = createRequire(import.meta.url);
10
10
 
11
- const NAIDE_VERSION = '1.21.1';
11
+ const NAIDE_VERSION = '1.23.0';
12
12
 
13
13
  function crashReport(err, context = {}) {
14
14
  const info = [
@@ -279,22 +279,26 @@ if (files[0] === 'gen' || files[0] === '~~') {
279
279
  writeFileSync(fp, f.content, 'utf-8');
280
280
  }
281
281
  const a = result.analysis;
282
- const mode = isUpdate ? 'Updated' : 'Created';
282
+ const mode = result.action === 'removed' ? 'Removed' : isUpdate ? 'Updated' : 'Created';
283
283
  process.stderr.write(`\n ── NAIDE Agent (${mode}) ────────────────────────────────\n`);
284
- process.stderr.write(` Input: "${a.instruction}"\n`);
284
+ process.stderr.write(` Input: "${a.instruction || instruction}"\n`);
285
285
  if (a.entities.length > 0) {
286
286
  process.stderr.write(` Entities: ${a.entities.map(e => `${e.name} (${e.fieldCount} fields)`).join(', ')}\n`);
287
287
  }
288
288
  if (a.relationships && a.relationships.length > 0) {
289
289
  for (const r of a.relationships) process.stderr.write(` Relation: ${r.parent} 1→N ${r.child} (${r.fk})\n`);
290
290
  }
291
+ if (result.removed && result.removed.length > 0) {
292
+ process.stderr.write(` Removed: ${result.removed.join(', ')}\n`);
293
+ }
291
294
  if (a.added && a.added.length > 0) {
292
295
  process.stderr.write(` Added: ${a.added.join(', ')}\n`);
293
296
  }
294
297
  process.stderr.write(` Plan:\n`);
295
298
  for (const step of a.plan) process.stderr.write(` + ${step}\n`);
299
+ process.stderr.write(` Files: ${result.files.length} generated\n`);
296
300
  process.stderr.write(' ────────────────────────────────────────────────────────\n\n');
297
- console.log(` Project ${isUpdate ? 'updated' : 'created'}: ${dir}/`);
301
+ console.log(` Project ${mode.toLowerCase()}: ${dir}/`);
298
302
  for (const f of result.files) console.log(` ${f.path}`);
299
303
  if (!isUpdate) console.log(`\n cd ${dir} && npm install && npm run dev\n`);
300
304
  else console.log(`\n Files regenerated. Run: npm run dev\n`);
package/mcp/server.js CHANGED
@@ -192,7 +192,7 @@ async function handleMessage(msg) {
192
192
  respond(msg.id, {
193
193
  protocolVersion: '2024-11-05',
194
194
  capabilities: { tools: {} },
195
- serverInfo: { name: 'naide-mcp', version: '1.21.1' }
195
+ serverInfo: { name: 'naide-mcp', version: '1.23.0' }
196
196
  });
197
197
  break;
198
198
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "naider",
3
- "version": "1.21.1",
3
+ "version": "1.23.0",
4
4
  "description": "NAIDE - Simpler than Python, compiles to 15 targets. AI-specialized language with 35+ built-in functions, syntax sugar (unless/until/repeat/swap/is/isnt), and 47 features. Targets: Node.js, Python, TypeScript, C, C++, Java, Go, Rust, PHP, Ruby, Kotlin, Swift, Dart, C#, Bun.",
5
5
  "main": "src/index.js",
6
6
  "exports": {
package/src/gen.js CHANGED
@@ -93,7 +93,7 @@ const COMPOSITE_WORDS = {
93
93
  const NOISE = new Set([
94
94
  'a', 'an', 'the', 'for', 'with', 'and', 'or', 'on', 'at', 'to', 'of', 'in', 'by',
95
95
  'that', 'which', 'this', 'it', 'its', 'my', 'is', 'are', 'has', 'have', 'do', 'does',
96
- 'make', 'create', 'build', 'generate', 'add', 'implement', 'simple', 'basic', 'new',
96
+ 'make', 'create', 'build', 'generate', 'add', 'implement', 'simple', 'basic', 'new', 'remove', 'delete', 'drop',
97
97
  'please', 'want', 'need', 'like', 'just', 'some', 'using', 'use', 'based',
98
98
  'setup', 'set', 'up', 'get', 'should', 'can', 'could', 'would',
99
99
  'の', 'を', 'に', 'で', 'は', 'が', 'と', 'も', 'から', 'まで', 'より', 'って',
@@ -266,6 +266,65 @@ function applyRelationships(entities, relationships) {
266
266
  }
267
267
  }
268
268
 
269
+ // ── Many-to-Many Relationships ────────────────────────────
270
+
271
+ const MANY_TO_MANY = {
272
+ 'User-Product': { junction: 'Favorite', fkLeft: 'user_id', fkRight: 'product_id' },
273
+ 'User-Team': { junction: 'TeamMember', fkLeft: 'user_id', fkRight: 'team_id' },
274
+ 'Post-Category':{ junction: 'PostTag', fkLeft: 'post_id', fkRight: 'category_id' },
275
+ 'Product-Category':{ junction: 'ProductCategory', fkLeft: 'product_id', fkRight: 'category_id' },
276
+ 'User-Group': { junction: 'GroupMember', fkLeft: 'user_id', fkRight: 'group_id' },
277
+ };
278
+
279
+ function detectManyToMany(entityNames) {
280
+ const rels = [];
281
+ for (let i = 0; i < entityNames.length; i++) {
282
+ for (let j = i + 1; j < entityNames.length; j++) {
283
+ const key1 = entityNames[i] + '-' + entityNames[j];
284
+ const key2 = entityNames[j] + '-' + entityNames[i];
285
+ const match = MANY_TO_MANY[key1] || MANY_TO_MANY[key2];
286
+ if (match) rels.push({ left: entityNames[i], right: entityNames[j], ...match });
287
+ }
288
+ }
289
+ return rels;
290
+ }
291
+
292
+ function junctionSchemaBlock(rel) {
293
+ return `schema ${rel.junction}:\n id auto\n ${rel.fkLeft.padEnd(10)}int required\n ${rel.fkRight.padEnd(10)}int required\n created auto`;
294
+ }
295
+
296
+ function manyToManyRoutes(rel) {
297
+ const leftPlural = pluralize(rel.left.toLowerCase());
298
+ const rightPlural = pluralize(rel.right.toLowerCase());
299
+ const store = rel.junction + 'Store';
300
+ return [
301
+ `get "/api/${leftPlural}/:id/${rightPlural}" (req, res):`,
302
+ ` list links = ${store}.findAll("${rel.fkLeft}", req.params.id)`,
303
+ ` ret links`,
304
+ ``,
305
+ `post "/api/${leftPlural}/:id/${rightPlural}" (req, res):`,
306
+ ` any link = ${store}.create({${rel.fkLeft}: req.params.id, ${rel.fkRight}: req.body.${rel.fkRight}})`,
307
+ ` ret ok(link, null)`,
308
+ ``,
309
+ `del "/api/${leftPlural}/:id/${rightPlural}/:linkId" (req, res):`,
310
+ ` ${store}.remove(req.params.linkId)`,
311
+ ` ret ok({removed: true}, null)`,
312
+ ``,
313
+ `get "/api/${rightPlural}/:id/${leftPlural}" (req, res):`,
314
+ ` list links = ${store}.findAll("${rel.fkRight}", req.params.id)`,
315
+ ` ret links`,
316
+ ];
317
+ }
318
+
319
+ // ── Enum Fields ───────────────────────────────────────────
320
+
321
+ const ENUM_VALUES = {
322
+ status: ['pending', 'active', 'completed', 'cancelled'],
323
+ priority: ['low', 'medium', 'high', 'critical'],
324
+ role: ['user', 'admin', 'moderator'],
325
+ visibility: ['public', 'private', 'unlisted'],
326
+ };
327
+
269
328
  // ── Utilities ───────────────────────────────────────────────
270
329
 
271
330
  function singularize(w) {
@@ -382,6 +441,16 @@ function classify(words, raw) {
382
441
  return !NOISE.has(wl) && !allKnown.has(wl) && !/^\d+$/.test(wl) && wl.length > 2 && /^[a-z]+s?$/i.test(wl);
383
442
  }) || Object.keys(JA_ENTITIES).some(ja => raw.includes(ja));
384
443
 
444
+ // Entity removal detection (B8)
445
+ if (/(?:remove|delete|drop|取り除|削除)\s+/i.test(raw)) {
446
+ mods.removal = true;
447
+ }
448
+
449
+ // API versioning detection (B13)
450
+ if (/(?:version|v\d|versioning|バージョン)/i.test(raw)) {
451
+ mods.apiVersion = 'v1';
452
+ }
453
+
385
454
  // Complex NL phrase detection
386
455
  if (/(?:users?\s+can\s+(?:comment|post|review|write)|コメントできる|投稿できる|レビューできる)/i.test(raw)) {
387
456
  intents.add('auth'); intents.add('crud');
@@ -474,7 +543,6 @@ function extractParams(words, raw, intents, mods) {
474
543
  if (!wl || NOISE.has(wl) || allKnown.has(wl) || /^\d+$/.test(wl) || wl.length <= 2) continue;
475
544
  const name = capitalize(singularize(wl));
476
545
  if (!entityNames.includes(name)) entityNames.push(name);
477
- break;
478
546
  }
479
547
  }
480
548
  }
@@ -540,6 +608,7 @@ function extractParams(words, raw, intents, mods) {
540
608
  if (params.entities.length > 1) {
541
609
  params.relationships = detectRelationships(params.entities);
542
610
  applyRelationships(params.entities, params.relationships);
611
+ params.m2mRelationships = detectManyToMany(params.entities.map(e => e.name));
543
612
  }
544
613
 
545
614
  // Extract bot commands
@@ -649,16 +718,33 @@ function paginatedListBlock(entity) {
649
718
  const store = entity.name + 'Store';
650
719
  return [
651
720
  `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()`,
721
+ ` int page = req.query.page or 1`,
722
+ ` int limit = req.query.limit or 20`,
723
+ ` str search = req.query.search or ""`,
724
+ ` str sortBy = req.query.sort or "id"`,
725
+ ` str order = req.query.order or "desc"`,
726
+ ` mut list rows = ${store}.all()`,
656
727
  ` 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}`,
728
+ ` rows = rows.filter((item) => JSON.stringify(item).toLowerCase().includes(search.toLowerCase()))`,
729
+ ` each key in Object.keys(req.query):`,
730
+ ` if key.endsWith("_gte"):`,
731
+ ` str field = key.replace("_gte", "")`,
732
+ ` rows = rows.filter((item) => item[field] >= Number(req.query[key]))`,
733
+ ` if key.endsWith("_lte"):`,
734
+ ` str field = key.replace("_lte", "")`,
735
+ ` rows = rows.filter((item) => item[field] <= Number(req.query[key]))`,
736
+ ` if key.endsWith("_eq"):`,
737
+ ` str field = key.replace("_eq", "")`,
738
+ ` rows = rows.filter((item) => String(item[field]) == req.query[key])`,
739
+ ` if order == "asc":`,
740
+ ` rows = rows.sort((a, b) => String(a[sortBy]).localeCompare(String(b[sortBy])))`,
741
+ ` else:`,
742
+ ` rows = rows.sort((a, b) => String(b[sortBy]).localeCompare(String(a[sortBy])))`,
743
+ ` int total = rows.length`,
744
+ ` int offset = page - 1`,
745
+ ` int start = offset * limit`,
746
+ ` list items = rows.slice(start, start + limit)`,
747
+ ` ret {items, total, page, limit, sort: sortBy, order}`,
662
748
  ];
663
749
  }
664
750
 
@@ -817,7 +903,9 @@ function openApiSpec(entities, intents, params, relationships) {
817
903
  { name: 'page', in: 'query', schema: { type: 'integer', default: 1 } },
818
904
  { name: 'limit', in: 'query', schema: { type: 'integer', default: 20 } },
819
905
  { name: 'search', in: 'query', schema: { type: 'string' } },
820
- ], responses: { '200': { description: `List of ${plural}` } } },
906
+ { name: 'sort', in: 'query', schema: { type: 'string' }, description: 'Field to sort by' },
907
+ { name: 'order', in: 'query', schema: { type: 'string', enum: ['asc','desc'], default: 'asc' } },
908
+ ], responses: { '200': { description: `List of ${plural} with pagination` } } },
821
909
  post: { summary: `Create ${entity.name.toLowerCase()}`, requestBody: { content: { 'application/json': { schema: {
822
910
  type: 'object', required: requiredFields.map(f => f.name),
823
911
  properties: Object.fromEntries(requiredFields.map(f => [f.name, properties[f.name]])),
@@ -847,12 +935,24 @@ function openApiSpec(entities, intents, params, relationships) {
847
935
  };
848
936
  }
849
937
  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' } } } };
938
+ paths['/api/auth/register'] = { post: { summary: 'Register', requestBody: { content: { 'application/json': { schema: { type: 'object', required: ['email','password'], properties: { email:{type:'string'}, password:{type:'string'}, name:{type:'string'} } } } } }, responses: { '200': { description: 'Token + user' }, '400': { description: 'Validation error' } } } };
939
+ paths['/api/auth/login'] = { post: { summary: 'Login', requestBody: { content: { 'application/json': { schema: { type: 'object', required: ['email','password'], properties: { email:{type:'string'}, password:{type:'string'} } } } } }, responses: { '200': { description: 'Token + user' }, '401': { description: 'Invalid credentials' } } } };
852
940
  paths['/api/auth/me'] = { get: { summary: 'Current user', security: [{ bearerAuth: [] }], responses: { '200': { description: 'User' } } } };
941
+ paths['/api/auth/forgot-password'] = { post: { summary: 'Request password reset', requestBody: { content: { 'application/json': { schema: { type: 'object', required: ['email'], properties: { email:{type:'string'} } } } } }, responses: { '200': { description: 'Reset token sent' } } } };
942
+ paths['/api/auth/reset-password'] = { post: { summary: 'Reset password with token', requestBody: { content: { 'application/json': { schema: { type: 'object', required: ['token','password'], properties: { token:{type:'string'}, password:{type:'string'} } } } } }, responses: { '200': { description: 'Password reset' }, '400': { description: 'Invalid token' } } } };
943
+ paths['/api/auth/change-password'] = { post: { summary: 'Change password (authenticated)', security: [{ bearerAuth: [] }], requestBody: { content: { 'application/json': { schema: { type: 'object', required: ['oldPassword','newPassword'], properties: { oldPassword:{type:'string'}, newPassword:{type:'string'} } } } } }, responses: { '200': { description: 'Password changed' }, '401': { description: 'Wrong password' } } } };
944
+ }
945
+ const m2m = detectManyToMany(entities.map(e => e.name));
946
+ for (const rel of m2m) {
947
+ const lp = pluralize(rel.left.toLowerCase());
948
+ const rp = pluralize(rel.right.toLowerCase());
949
+ paths[`/api/${lp}/{id}/${rp}`] = { get: { summary: `${rel.right}s for ${rel.left}`, responses: { '200': { description: `List of ${rp}` } } } };
950
+ paths[`/api/${rp}/{id}/${lp}`] = { get: { summary: `${rel.left}s for ${rel.right}`, responses: { '200': { description: `List of ${lp}` } } } };
951
+ paths[`/api/${rel.junction.toLowerCase()}`] = { post: { summary: `Link ${rel.left}↔${rel.right}`, responses: { '201': { description: 'Linked' } } }, delete: { summary: `Unlink ${rel.left}↔${rel.right}`, responses: { '200': { description: 'Unlinked' } } } };
853
952
  }
854
953
  paths['/api/health'] = { get: { summary: 'Health check', responses: { '200': { description: 'Status' } } } };
855
954
  paths['/api/upload'] = { post: { summary: 'Upload file', responses: { '200': { description: 'File info' } } } };
955
+ paths['/docs'] = { get: { summary: 'Swagger UI documentation', responses: { '200': { description: 'HTML page' } } } };
856
956
 
857
957
  const spec = {
858
958
  openapi: '3.0.3',
@@ -991,6 +1091,373 @@ function apiClientCode(entities, intents, port) {
991
1091
  return lines.join('\n') + '\n';
992
1092
  }
993
1093
 
1094
+ // ── Input Validation (A4) ──────────────────────────────────
1095
+
1096
+ function validationBlock(entity) {
1097
+ const lines = [];
1098
+ lines.push(`fn validate${entity.name}(any body):`);
1099
+ lines.push(` list errors = []`);
1100
+ for (const f of entity.fields) {
1101
+ if (f.type === 'auto') continue;
1102
+ if (f.modifiers.includes('required')) {
1103
+ lines.push(` if not body.${f.name}:`);
1104
+ lines.push(` errors.push("${f.name} is required")`);
1105
+ }
1106
+ if (f.modifiers.includes('email')) {
1107
+ lines.push(` if body.${f.name} and not body.${f.name}.includes("@"):`);
1108
+ lines.push(` errors.push("${f.name} must be a valid email")`);
1109
+ }
1110
+ const minMod = f.modifiers.find(m => m.startsWith('min('));
1111
+ if (minMod) {
1112
+ const minVal = minMod.match(/\d+/)[0];
1113
+ if (f.type === 'str') {
1114
+ lines.push(` if body.${f.name} and body.${f.name}.length < ${minVal}:`);
1115
+ lines.push(` errors.push("${f.name} must be at least ${minVal} characters")`);
1116
+ }
1117
+ }
1118
+ const maxMod = f.modifiers.find(m => m.startsWith('max('));
1119
+ if (maxMod) {
1120
+ const maxVal = maxMod.match(/\d+/)[0];
1121
+ if (f.type === 'str') {
1122
+ lines.push(` if body.${f.name} and body.${f.name}.length > ${maxVal}:`);
1123
+ lines.push(` errors.push("${f.name} must be at most ${maxVal} characters")`);
1124
+ }
1125
+ }
1126
+ if (ENUM_VALUES[f.name]) {
1127
+ const vals = ENUM_VALUES[f.name].map(v => `"${v}"`).join(', ');
1128
+ lines.push(` if body.${f.name} and not [${vals}].includes(body.${f.name}):`);
1129
+ lines.push(` errors.push("${f.name} must be one of: ${ENUM_VALUES[f.name].join(', ')}")`);
1130
+ }
1131
+ }
1132
+ lines.push(` ret errors`);
1133
+ return lines.join('\n');
1134
+ }
1135
+
1136
+ function validatedCreateRoute(entity) {
1137
+ const lower = entity.name.toLowerCase();
1138
+ const plural = pluralize(lower);
1139
+ const store = entity.name + 'Store';
1140
+ return [
1141
+ `post "/api/${plural}" (req, res):`,
1142
+ ` list errors = validate${entity.name}(req.body)`,
1143
+ ` if errors.length > 0:`,
1144
+ ` ret.status(400) {ok: false, errors}`,
1145
+ ` any created = ${store}.create(req.body)`,
1146
+ ` ret.status(201) ok(created, null)`,
1147
+ ``,
1148
+ `put "/api/${plural}/:id" (req, res):`,
1149
+ ` list errors = validate${entity.name}(req.body)`,
1150
+ ` if errors.length > 0:`,
1151
+ ` ret.status(400) {ok: false, errors}`,
1152
+ ` any updated = ${store}.update(req.params.id, req.body)`,
1153
+ ` ret ok(updated, null)`,
1154
+ ];
1155
+ }
1156
+
1157
+ // ── Swagger UI Route (A5) ──────────────────────────────────
1158
+
1159
+ function swaggerUIRoute() {
1160
+ return [
1161
+ `get "/docs" (req, res):`,
1162
+ ` str html = "<!DOCTYPE html><html><head><title>API Docs</title><link rel=stylesheet href=https://unpkg.com/swagger-ui-dist/swagger-ui.css></head><body><div id=swagger-ui></div><script src=https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js></script><script>SwaggerUIBundle({url:'/api/openapi.json',dom_id:'#swagger-ui'})</script></body></html>"`,
1163
+ ` res.type("html")`,
1164
+ ` ret html`,
1165
+ ];
1166
+ }
1167
+
1168
+ // ── Password Reset Routes (A6) ─────────────────────────────
1169
+
1170
+ function passwordResetRoutes() {
1171
+ return [
1172
+ `post "/api/auth/forgot-password" (req, res):`,
1173
+ ` any user = UserStore.findBy("email", req.body.email)`,
1174
+ ` if not user:`,
1175
+ ` ret ok({message: "If the email exists, a reset link has been sent"}, null)`,
1176
+ ` str resetToken = crypto.randomUUID()`,
1177
+ ` UserStore.update(user.id, {resetToken})`,
1178
+ ` log("Password reset requested for: " + req.body.email)`,
1179
+ ` ret ok({message: "If the email exists, a reset link has been sent"}, null)`,
1180
+ ``,
1181
+ `post "/api/auth/reset-password" (req, res):`,
1182
+ ` if not req.body.token or not req.body.password:`,
1183
+ ` ret.status(400) err("Token and password are required", 400)`,
1184
+ ` any user = UserStore.findBy("resetToken", req.body.token)`,
1185
+ ` if not user:`,
1186
+ ` ret.status(400) err("Invalid or expired token", 400)`,
1187
+ ` str hashed = await hash(req.body.password)`,
1188
+ ` UserStore.update(user.id, {password: hashed, resetToken: null})`,
1189
+ ` ret ok({message: "Password has been reset"}, null)`,
1190
+ ``,
1191
+ `post "/api/auth/change-password" (req, res):`,
1192
+ ` if not req.body.oldPassword or not req.body.newPassword:`,
1193
+ ` ret.status(400) err("Old and new passwords are required", 400)`,
1194
+ ` bool valid = await verify(req.body.oldPassword, req.user.password)`,
1195
+ ` if not valid:`,
1196
+ ` ret.status(401) err("Invalid old password", 401)`,
1197
+ ` str hashed = await hash(req.body.newPassword)`,
1198
+ ` UserStore.update(req.user.id, {password: hashed})`,
1199
+ ` ret ok({message: "Password changed"}, null)`,
1200
+ ];
1201
+ }
1202
+
1203
+ // ── WebSocket Rooms (B11) ──────────────────────────────────
1204
+
1205
+ function wsRoomsBlock() {
1206
+ return [
1207
+ `ws "/ws":`,
1208
+ ` on "connection" (socket):`,
1209
+ ` socket.room = "general"`,
1210
+ ` log("Client connected to room: general")`,
1211
+ ` on "message" (data, socket):`,
1212
+ ` any msg = JSON.parse(data)`,
1213
+ ` if msg.type == "join":`,
1214
+ ` socket.room = msg.room`,
1215
+ ` socket.send(JSON.stringify({type: "joined", room: msg.room}))`,
1216
+ ` if msg.type == "leave":`,
1217
+ ` socket.room = "general"`,
1218
+ ` if msg.type == "message":`,
1219
+ ` broadcast({type: "message", room: socket.room, data: msg.data, sender: msg.sender}, socket.room)`,
1220
+ ` if msg.type == "broadcast":`,
1221
+ ` broadcast(msg.data)`,
1222
+ ` on "close" (socket):`,
1223
+ ` log("Client disconnected")`,
1224
+ ];
1225
+ }
1226
+
1227
+ // ── Rate Limiting (B12) ────────────────────────────────────
1228
+
1229
+ function rateLimitBlock() {
1230
+ return `fn rateLimit(int windowMs, int maxReqs):
1231
+ map hits = {}
1232
+ ret fn(req, res, next):
1233
+ str key = req.ip
1234
+ int now = Date.now()
1235
+ if not hits[key] or now - hits[key].start > windowMs:
1236
+ hits[key] = {count: 1, start: now}
1237
+ else:
1238
+ hits[key].count = hits[key].count + 1
1239
+ if hits[key].count > maxReqs:
1240
+ ret.status(429) {error: "Too many requests"}
1241
+ next()`;
1242
+ }
1243
+
1244
+ // ── Logging Middleware (C17) ───────────────────────────────
1245
+
1246
+ function loggingBlock() {
1247
+ return `fn requestLogger(req, res, next):
1248
+ int startTime = Date.now()
1249
+ log("[" + req.method + "] " + req.path)
1250
+ next()`;
1251
+ }
1252
+
1253
+ // ── Caching Headers (C18) ─────────────────────────────────
1254
+
1255
+ function cachingRoute() {
1256
+ return [
1257
+ `get "/api/cache-headers" (req, res):`,
1258
+ ` res.set("Cache-Control", "public, max-age=60")`,
1259
+ ` ret {cached: true, ttl: 60}`,
1260
+ ];
1261
+ }
1262
+
1263
+ // ── Frontend SPA (S2) ─────────────────────────────────────
1264
+
1265
+ function frontendSPA(entities, intents, port) {
1266
+ const L = [];
1267
+ L.push('<!DOCTYPE html>');
1268
+ L.push('<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">');
1269
+ L.push('<title>App</title>');
1270
+ L.push(`<style>
1271
+ :root{--bg:#f8fafc;--card:#fff;--primary:#2563eb;--danger:#dc2626;--text:#1e293b;--muted:#64748b;--border:#e2e8f0;--radius:8px}
1272
+ *{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--text);line-height:1.6}
1273
+ nav{background:var(--primary);color:#fff;padding:1rem 2rem;display:flex;gap:1rem;align-items:center;box-shadow:0 2px 4px rgba(0,0,0,.1)}
1274
+ nav a{color:#fff;text-decoration:none;padding:.4rem 1rem;border-radius:var(--radius);opacity:.8;cursor:pointer}nav a:hover,nav a.active{opacity:1;background:rgba(255,255,255,.15)}
1275
+ .container{max-width:1200px;margin:2rem auto;padding:0 1rem}
1276
+ .card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:1.5rem;margin-bottom:1rem;box-shadow:0 1px 3px rgba(0,0,0,.05)}
1277
+ .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:1rem}
1278
+ .form-group{margin-bottom:1rem}
1279
+ .form-group label{display:block;font-weight:500;margin-bottom:.25rem;font-size:.875rem}
1280
+ .form-group input,.form-group textarea,.form-group select{width:100%;padding:.5rem .75rem;border:1px solid var(--border);border-radius:var(--radius);font-size:.875rem}
1281
+ .form-group textarea{min-height:80px;resize:vertical}
1282
+ .btn{padding:.5rem 1rem;border:none;border-radius:var(--radius);cursor:pointer;font-size:.875rem;font-weight:500;transition:opacity .2s}
1283
+ .btn-primary{background:var(--primary);color:#fff}.btn-danger{background:var(--danger);color:#fff}
1284
+ .btn:hover{opacity:.85}.btn-sm{padding:.25rem .5rem;font-size:.75rem}
1285
+ table{width:100%;border-collapse:collapse}th,td{padding:.75rem;text-align:left;border-bottom:1px solid var(--border)}
1286
+ th{background:var(--bg);font-weight:600;font-size:.875rem;color:var(--muted)}
1287
+ .search{display:flex;gap:.5rem;margin-bottom:1rem}
1288
+ .search input{flex:1;padding:.5rem .75rem;border:1px solid var(--border);border-radius:var(--radius)}
1289
+ .pagination{display:flex;gap:.5rem;justify-content:center;margin-top:1rem;align-items:center}
1290
+ .toast{position:fixed;top:1rem;right:1rem;background:#22c55e;color:#fff;padding:.75rem 1.5rem;border-radius:var(--radius);opacity:0;transition:opacity .3s;z-index:999}
1291
+ .toast.show{opacity:1}.modal-bg{position:fixed;inset:0;background:rgba(0,0,0,.4);display:flex;align-items:center;justify-content:center;z-index:100}
1292
+ .modal{background:var(--card);border-radius:var(--radius);padding:2rem;width:90%;max-width:500px;box-shadow:0 20px 60px rgba(0,0,0,.2)}
1293
+ .modal h3{margin-bottom:1rem}.modal-actions{display:flex;gap:.5rem;justify-content:flex-end;margin-top:1.5rem}
1294
+ .badge{display:inline-block;padding:.15rem .5rem;border-radius:99px;font-size:.75rem;font-weight:500;background:var(--border)}
1295
+ .hidden{display:none}
1296
+ </style></head><body>`);
1297
+ L.push('<div id="toast" class="toast"></div>');
1298
+ const hasAuth = intents.has('auth');
1299
+ const navLinks = entities.map(e => `<a onclick="showSection('${e.name.toLowerCase()}')" id="nav-${e.name.toLowerCase()}">${pluralize(e.name)}</a>`).join('');
1300
+ const authNav = hasAuth ? `<a onclick="showSection('auth')" id="nav-auth">Login</a><span id="user-info" class="hidden" style="margin-left:auto;font-size:.875rem"></span><a id="logout-btn" class="hidden" onclick="doLogout()">Logout</a>` : '';
1301
+ L.push(`<nav><strong style="font-size:1.25rem">App</strong>${navLinks}${authNav}</nav>`);
1302
+ L.push('<div class="container">');
1303
+ if (hasAuth) {
1304
+ L.push(`<div id="section-auth"><div class="card" style="max-width:400px;margin:2rem auto"><h2 id="auth-title">Login</h2>`);
1305
+ L.push(`<div class="form-group"><label>Email</label><input type="email" id="auth-email" placeholder="email@example.com"></div>`);
1306
+ L.push(`<div class="form-group"><label>Password</label><input type="password" id="auth-password" placeholder="password"></div>`);
1307
+ L.push(`<div id="auth-name-group" class="form-group hidden"><label>Name</label><input type="text" id="auth-name" placeholder="Your name"></div>`);
1308
+ L.push(`<button class="btn btn-primary" style="width:100%" onclick="doAuth()">Login</button>`);
1309
+ L.push(`<p style="text-align:center;margin-top:1rem;font-size:.875rem">Don't have an account? <a onclick="toggleAuthMode()" style="color:var(--primary);cursor:pointer">Register</a></p>`);
1310
+ L.push(`</div></div>`);
1311
+ }
1312
+ for (const entity of entities) {
1313
+ const lower = entity.name.toLowerCase();
1314
+ const plural = pluralize(lower);
1315
+ const fields = entity.fields.filter(f => f.type !== 'auto');
1316
+ L.push(`<div id="section-${lower}" class="hidden">`);
1317
+ L.push(`<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem"><h2>${pluralize(entity.name)}</h2><button class="btn btn-primary" onclick="${lower}ShowModal()">+ New ${entity.name}</button></div>`);
1318
+ L.push(`<div class="search"><input id="${lower}-search" placeholder="Search ${plural}..." oninput="${lower}Load()"><select id="${lower}-sort" onchange="${lower}Load()">`);
1319
+ for (const f of entity.fields) L.push(`<option value="${f.name}">${f.name}</option>`);
1320
+ L.push(`</select><select id="${lower}-order" onchange="${lower}Load()"><option value="desc">Newest</option><option value="asc">Oldest</option></select></div>`);
1321
+ L.push(`<div class="card"><table><thead><tr>${entity.fields.map(f => `<th>${f.name}</th>`).join('')}<th>Actions</th></tr></thead><tbody id="${lower}-tbody"></tbody></table>`);
1322
+ L.push(`<div class="pagination" id="${lower}-pager"></div></div>`);
1323
+ L.push(`<div id="${lower}-modal" class="modal-bg hidden" onclick="if(event.target===this)this.classList.add('hidden')"><div class="modal"><h3 id="${lower}-modal-title">New ${entity.name}</h3>`);
1324
+ L.push(`<input type="hidden" id="${lower}-edit-id">`);
1325
+ for (const f of fields) {
1326
+ if (f.type === 'bool') {
1327
+ L.push(`<div class="form-group"><label><input type="checkbox" id="${lower}-f-${f.name}"> ${f.name}</label></div>`);
1328
+ } else {
1329
+ const inputType = f.type === 'int' ? 'number' : f.name === 'password' ? 'password' : f.name === 'email' ? 'email' : 'text';
1330
+ const tag = f.name === 'body' || f.name === 'content' || f.name === 'description' ? 'textarea' : 'input';
1331
+ L.push(`<div class="form-group"><label>${f.name}${f.modifiers.includes('required') ? ' *' : ''}</label><${tag} ${tag === 'input' ? `type="${inputType}" ` : ''}id="${lower}-f-${f.name}" placeholder="${f.name}"></${tag}></div>`);
1332
+ }
1333
+ }
1334
+ L.push(`<div class="modal-actions"><button class="btn" onclick="document.getElementById('${lower}-modal').classList.add('hidden')">Cancel</button><button class="btn btn-primary" onclick="${lower}Save()">Save</button></div></div></div>`);
1335
+ L.push('</div>');
1336
+ }
1337
+ L.push('</div>');
1338
+ L.push('<script>');
1339
+ L.push(`const API="";let currentPage={};let _token=null;let _isRegister=false;`);
1340
+ L.push(`function authHeaders(){const h={"Content-Type":"application/json"};if(_token)h.Authorization="Bearer "+_token;return h}`);
1341
+ L.push(`function toast(m){const t=document.getElementById("toast");t.textContent=m;t.classList.add("show");setTimeout(()=>t.classList.remove("show"),2500)}`);
1342
+ L.push(`function showSection(name){document.querySelectorAll('[id^="section-"]').forEach(s=>s.classList.add("hidden"));const el=document.getElementById("section-"+name);if(el)el.classList.remove("hidden");document.querySelectorAll("nav a").forEach(a=>a.classList.remove("active"));const n=document.getElementById("nav-"+name);if(n)n.classList.add("active");if(window[name+"Load"])window[name+"Load"]()}`);
1343
+ if (hasAuth) {
1344
+ L.push(`function toggleAuthMode(){_isRegister=!_isRegister;document.getElementById("auth-title").textContent=_isRegister?"Register":"Login";document.getElementById("auth-name-group").classList.toggle("hidden",!_isRegister)}`);
1345
+ L.push(`async function doAuth(){const email=document.getElementById("auth-email").value;const password=document.getElementById("auth-password").value;const url=_isRegister?"/api/auth/register":"/api/auth/login";const body=_isRegister?{name:document.getElementById("auth-name").value,email,password}:{email,password};try{const r=await fetch(API+url,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)});const d=await r.json();if(d.token){_token=d.token;document.getElementById("user-info").textContent="Hi, "+(d.user?.name||email);document.getElementById("user-info").classList.remove("hidden");document.getElementById("logout-btn").classList.remove("hidden");document.getElementById("nav-auth").classList.add("hidden");toast(_isRegister?"Registered!":"Logged in!");showSection("${entities[0].name.toLowerCase()}")}else{toast(d.error||"Auth failed")}}catch(e){toast("Error: "+e.message)}}`);
1346
+ L.push(`function doLogout(){_token=null;document.getElementById("user-info").classList.add("hidden");document.getElementById("logout-btn").classList.add("hidden");document.getElementById("nav-auth").classList.remove("hidden");toast("Logged out");showSection("auth")}`);
1347
+ }
1348
+ for (const entity of entities) {
1349
+ const lower = entity.name.toLowerCase();
1350
+ const plural = pluralize(lower);
1351
+ const fields = entity.fields.filter(f => f.type !== 'auto');
1352
+ L.push(`async function ${lower}Load(page){page=page||1;currentPage["${lower}"]=page;const s=document.getElementById("${lower}-search").value;const sort=document.getElementById("${lower}-sort").value;const ord=document.getElementById("${lower}-order").value;`);
1353
+ L.push(`const r=await fetch(API+"/api/${plural}?page="+page+"&limit=10&search="+encodeURIComponent(s)+"&sort="+sort+"&order="+ord,{headers:authHeaders()});const d=await r.json();const items=d.items||d;`);
1354
+ L.push(`document.getElementById("${lower}-tbody").innerHTML=items.map(i=>"<tr>${entity.fields.map(f => `<td>"+(i.${f.name}!=null?i.${f.name}:"")+"</td>`).join('')}<td><button class='btn btn-sm btn-primary' onclick='${lower}Edit("+i.id+")'>Edit</button> <button class='btn btn-sm btn-danger' onclick='${lower}Del("+i.id+")'>Del</button></td></tr>").join("");`);
1355
+ L.push(`const tp=Math.ceil((d.total||items.length)/10);let pg="";for(let p=1;p<=tp;p++)pg+="<button class='btn btn-sm "+(p===page?"btn-primary":"")+"' onclick='${lower}Load("+p+")'>"+p+"</button>";document.getElementById("${lower}-pager").innerHTML=pg}`);
1356
+ L.push(`function ${lower}ShowModal(id){document.getElementById("${lower}-edit-id").value=id||"";document.getElementById("${lower}-modal-title").textContent=id?"Edit ${entity.name}":"New ${entity.name}";${fields.map(f => f.type === 'bool' ? `document.getElementById("${lower}-f-${f.name}").checked=false` : `document.getElementById("${lower}-f-${f.name}").value=""`).join(';')};document.getElementById("${lower}-modal").classList.remove("hidden")}`);
1357
+ L.push(`async function ${lower}Edit(id){const r=await fetch(API+"/api/${plural}/"+id,{headers:authHeaders()});const d=await r.json();const i=d.data||d;${lower}ShowModal(id);${fields.map(f => f.type === 'bool' ? `document.getElementById("${lower}-f-${f.name}").checked=!!i.${f.name}` : `document.getElementById("${lower}-f-${f.name}").value=i.${f.name}||""`).join(';')}}`);
1358
+ L.push(`async function ${lower}Save(){const id=document.getElementById("${lower}-edit-id").value;const body={${fields.map(f => {
1359
+ if (f.type === 'bool') return `${f.name}:document.getElementById("${lower}-f-${f.name}").checked`;
1360
+ if (f.type === 'int') return `${f.name}:+document.getElementById("${lower}-f-${f.name}").value`;
1361
+ return `${f.name}:document.getElementById("${lower}-f-${f.name}").value`;
1362
+ }).join(',')}};`);
1363
+ L.push(`const method=id?"PUT":"POST";const url=API+"/api/${plural}"+(id?"/"+id:"");await fetch(url,{method,headers:authHeaders(),body:JSON.stringify(body)});document.getElementById("${lower}-modal").classList.add("hidden");toast(id?"Updated!":"Created!");${lower}Load()}`);
1364
+ L.push(`async function ${lower}Del(id){if(!confirm("Delete this ${lower}?"))return;await fetch(API+"/api/${plural}/"+id,{method:"DELETE",headers:authHeaders()});toast("Deleted!");${lower}Load()}`);
1365
+ }
1366
+ const initSection = hasAuth ? 'auth' : entities[0].name.toLowerCase();
1367
+ L.push(`showSection("${initSection}")`);
1368
+ L.push('</script></body></html>');
1369
+ return L.join('\n');
1370
+ }
1371
+
1372
+ // ── Docker Compose (B14) ──────────────────────────────────
1373
+
1374
+ function dockerCompose(projectName, port, intents) {
1375
+ const lines = ['version: "3.8"', '', 'services:'];
1376
+ lines.push(` app:`);
1377
+ lines.push(` build: .`);
1378
+ lines.push(` ports:`);
1379
+ lines.push(` - "${port}:${port}"`);
1380
+ lines.push(` env_file: .env`);
1381
+ lines.push(` volumes:`);
1382
+ lines.push(` - app-data:/app/data`);
1383
+ if (intents.has('database')) {
1384
+ lines.push(` depends_on:`);
1385
+ lines.push(` - db`);
1386
+ lines.push('');
1387
+ lines.push(` db:`);
1388
+ lines.push(` image: postgres:16-alpine`);
1389
+ lines.push(` environment:`);
1390
+ lines.push(` POSTGRES_DB: ${projectName.replace(/-/g, '_')}`);
1391
+ lines.push(` POSTGRES_USER: app`);
1392
+ lines.push(` POSTGRES_PASSWORD: changeme`);
1393
+ lines.push(` ports:`);
1394
+ lines.push(` - "5432:5432"`);
1395
+ lines.push(` volumes:`);
1396
+ lines.push(` - db-data:/var/lib/postgresql/data`);
1397
+ }
1398
+ lines.push('');
1399
+ lines.push('volumes:');
1400
+ lines.push(' app-data:');
1401
+ if (intents.has('database')) lines.push(' db-data:');
1402
+ return lines.join('\n') + '\n';
1403
+ }
1404
+
1405
+ // ── GitHub Actions CI (C16) ────────────────────────────────
1406
+
1407
+ function githubActionsCI(projectName) {
1408
+ return `name: CI
1409
+
1410
+ on:
1411
+ push:
1412
+ branches: [main]
1413
+ pull_request:
1414
+ branches: [main]
1415
+
1416
+ jobs:
1417
+ test:
1418
+ runs-on: ubuntu-latest
1419
+ strategy:
1420
+ matrix:
1421
+ node-version: [20, 22]
1422
+ steps:
1423
+ - uses: actions/checkout@v4
1424
+ - uses: actions/setup-node@v4
1425
+ with:
1426
+ node-version: \${{ matrix.node-version }}
1427
+ cache: npm
1428
+ - run: npm ci
1429
+ - run: npm run build
1430
+ - run: npm test
1431
+
1432
+ lint:
1433
+ runs-on: ubuntu-latest
1434
+ steps:
1435
+ - uses: actions/checkout@v4
1436
+ - uses: actions/setup-node@v4
1437
+ with:
1438
+ node-version: 22
1439
+ cache: npm
1440
+ - run: npm ci
1441
+ - run: npx naide app.naide --check
1442
+ `;
1443
+ }
1444
+
1445
+ // ── Environment Files (C15) ───────────────────────────────
1446
+
1447
+ function envDevelopment(port, intents) {
1448
+ const lines = ['NODE_ENV=development', `PORT=${port}`, 'LOG_LEVEL=debug'];
1449
+ if (intents.has('auth')) lines.push('JWT_SECRET=dev-secret-do-not-use-in-prod');
1450
+ if (intents.has('database')) lines.push('DATABASE_URL=postgres://app:changeme@localhost:5432/app_dev');
1451
+ return lines.join('\n') + '\n';
1452
+ }
1453
+
1454
+ function envProduction(port, intents) {
1455
+ const lines = ['NODE_ENV=production', `PORT=${port}`, 'LOG_LEVEL=warn'];
1456
+ if (intents.has('auth')) lines.push('JWT_SECRET=CHANGE_ME_TO_RANDOM_SECRET');
1457
+ if (intents.has('database')) lines.push('DATABASE_URL=postgres://user:pass@db-host:5432/app_prod');
1458
+ return lines.join('\n') + '\n';
1459
+ }
1460
+
994
1461
  // ── Auth Route Templates ────────────────────────────────────
995
1462
 
996
1463
  function authRoutesBlock(userEntity) {
@@ -1058,6 +1525,19 @@ function compose(intents, params) {
1058
1525
  const utils = utilityFunctions(intents);
1059
1526
  if (utils.length > 0) sections.push(...utils);
1060
1527
 
1528
+ // Rate limiting & logging middleware (B12, C17)
1529
+ if (intents.has('server')) {
1530
+ sections.push(rateLimitBlock());
1531
+ sections.push(loggingBlock());
1532
+ }
1533
+
1534
+ // Input validation functions (A4)
1535
+ if (intents.has('schema') && intents.has('crud')) {
1536
+ for (const entity of entities) {
1537
+ sections.push(validationBlock(entity));
1538
+ }
1539
+ }
1540
+
1061
1541
  // Schema blocks for all entities (add role + deleted fields)
1062
1542
  if (intents.has('schema')) {
1063
1543
  for (const entity of entities) {
@@ -1071,6 +1551,12 @@ function compose(intents, params) {
1071
1551
  sections.push(schemaBlock(entity.name, entity.fields));
1072
1552
  }
1073
1553
  }
1554
+ // M:N junction table schemas (S3)
1555
+ const m2mRels = detectManyToMany(entities.map(e => e.name));
1556
+ for (const rel of m2mRels) {
1557
+ sections.push(junctionSchemaBlock(rel));
1558
+ }
1559
+
1074
1560
  // Audit log schema when auth is present
1075
1561
  if (hasAuth) {
1076
1562
  sections.push(auditSchema());
@@ -1099,6 +1585,8 @@ function compose(intents, params) {
1099
1585
  body.push(' protect "/api/*"');
1100
1586
  body.push(' public "/api/auth/*"');
1101
1587
  body.push(' public "/api/health"');
1588
+ body.push(' public "/docs"');
1589
+ body.push(' public "/api/openapi.json"');
1102
1590
  if (userEntity) {
1103
1591
  body.push('');
1104
1592
  body.push(...authRoutesBlock(userEntity.name));
@@ -1114,6 +1602,12 @@ function compose(intents, params) {
1114
1602
  body.push(' ret {status: "ok"}');
1115
1603
  }
1116
1604
  }
1605
+
1606
+ // Password reset routes (A6)
1607
+ if (userEntity) {
1608
+ body.push('');
1609
+ body.push(...passwordResetRoutes());
1610
+ }
1117
1611
  }
1118
1612
 
1119
1613
  // Paginated list endpoints (override default CRUD list)
@@ -1125,21 +1619,30 @@ function compose(intents, params) {
1125
1619
  }
1126
1620
  }
1127
1621
 
1622
+ // Validated create/update routes (A4)
1623
+ if (intents.has('crud') && intents.has('schema')) {
1624
+ for (const entity of entities) {
1625
+ body.push(...validatedCreateRoute(entity));
1626
+ body.push('');
1627
+ }
1628
+ }
1629
+
1128
1630
  // Nested routes for relationships
1129
1631
  for (const rel of relationships) {
1130
1632
  body.push(...nestedRouteBlock(rel.parent, rel.child, rel.fk));
1131
1633
  body.push('');
1132
1634
  }
1133
1635
 
1134
- // WebSocket
1636
+ // M:N routes (S3)
1637
+ const m2mRels = detectManyToMany(entities.map(e => e.name));
1638
+ for (const rel of m2mRels) {
1639
+ body.push(...manyToManyRoutes(rel));
1640
+ body.push('');
1641
+ }
1642
+
1643
+ // WebSocket with rooms (B11)
1135
1644
  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")');
1645
+ body.push(...wsRoomsBlock());
1143
1646
  body.push('');
1144
1647
  }
1145
1648
 
@@ -1175,6 +1678,14 @@ function compose(intents, params) {
1175
1678
  body.push('');
1176
1679
  }
1177
1680
 
1681
+ // Swagger UI (A5)
1682
+ body.push(...swaggerUIRoute());
1683
+ body.push('');
1684
+
1685
+ // Cache control (C18)
1686
+ body.push(...cachingRoute());
1687
+ body.push('');
1688
+
1178
1689
  // Health check
1179
1690
  body.push('get "/api/health" (req, res):');
1180
1691
  if (intents.has('schema') && intents.has('crud') && entities.length > 0) {
@@ -1416,40 +1927,33 @@ CMD ["node", "dist/app.mjs"]
1416
1927
  if (intents.has('server')) {
1417
1928
  readmeLines.push(`## API Endpoints\n`);
1418
1929
  readmeLines.push(`- \`GET /api/health\` — Health check`);
1930
+ readmeLines.push(`- \`GET /docs\` — Swagger UI`);
1419
1931
  for (const e of entities) {
1420
1932
  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}`);
1933
+ readmeLines.push(`- \`GET /api/${ep}?page=1&limit=20&search=&sort=id&order=desc\` — List (paginated, sortable, filterable)`);
1934
+ readmeLines.push(`- \`POST /api/${ep}\` — Create (validated)`, `- \`PUT /api/${ep}/:id\` — Update (validated)`);
1935
+ readmeLines.push(`- \`GET /api/${ep}/:id\` — Get`, `- \`DELETE /api/${ep}/:id\` — Delete`);
1936
+ readmeLines.push(`- \`DELETE /api/${ep}/:id/soft\` — Soft delete`, `- \`PUT /api/${ep}/:id/restore\` — Restore`);
1937
+ readmeLines.push(`- \`POST /api/${ep}/batch\` — Batch create`, `- \`DELETE /api/${ep}/batch\` — Batch delete`);
1430
1938
  }
1431
1939
  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}`);
1940
+ readmeLines.push(`- \`GET /api/${pluralize(r.parent.toLowerCase())}/:id/${pluralize(r.child.toLowerCase())}\` — ${r.child}s by ${r.parent}`);
1435
1941
  }
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`);
1942
+ readmeLines.push(`- \`POST /api/upload\` — File upload`, `- \`POST /api/webhooks\` — Webhook`);
1439
1943
  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)`);
1944
+ readmeLines.push(`- \`POST /api/auth/register\` — Register`, `- \`POST /api/auth/login\` — Login`, `- \`GET /api/auth/me\` — Current user`);
1945
+ readmeLines.push(`- \`POST /api/auth/forgot-password\` — Forgot password`, `- \`POST /api/auth/reset-password\` — Reset password`);
1946
+ readmeLines.push(`- \`POST /api/auth/change-password\` — Change password`, `- \`GET /api/admin/stats\` — Admin stats`);
1444
1947
  }
1445
1948
  }
1446
1949
  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('```');
1950
+ readmeLines.push(`\n## Docker\n`, '```bash', `docker-compose up -d`, '```');
1452
1951
  }
1952
+ readmeLines.push(`\n## Generated Files\n`);
1953
+ readmeLines.push('| File | Purpose |', '|------|---------|');
1954
+ readmeLines.push('| app.naide | Main application |', '| index.html | Frontend SPA |', '| admin.html | Admin dashboard |');
1955
+ readmeLines.push('| client.mjs | API client library |', '| openapi.json | OpenAPI 3.0 spec |');
1956
+ readmeLines.push('| docker-compose.yml | Docker orchestration |', '| .github/workflows/ci.yml | CI/CD pipeline |');
1453
1957
  files.push({ path: 'README.md', content: readmeLines.join('\n') + '\n' });
1454
1958
 
1455
1959
  // Build full entity objects for generators
@@ -1476,6 +1980,23 @@ CMD ["node", "dist/app.mjs"]
1476
1980
  files.push({ path: 'client.mjs', content: apiClientCode(entityObjs, intents, result.port) });
1477
1981
  }
1478
1982
 
1983
+ // Frontend SPA (S2)
1984
+ if (intents.has('server') && entityObjs.length > 0) {
1985
+ files.push({ path: 'index.html', content: frontendSPA(entityObjs, intents, result.port) });
1986
+ }
1987
+
1988
+ // Docker Compose (B14)
1989
+ if (intents.has('server')) {
1990
+ files.push({ path: 'docker-compose.yml', content: dockerCompose(projectName, result.port, intents) });
1991
+ }
1992
+
1993
+ // Environment files (C15)
1994
+ files.push({ path: '.env.development', content: envDevelopment(result.port, intents) });
1995
+ files.push({ path: '.env.production', content: envProduction(result.port, intents) });
1996
+
1997
+ // GitHub Actions CI (C16)
1998
+ files.push({ path: '.github/workflows/ci.yml', content: githubActionsCI(projectName) });
1999
+
1479
2000
  return { ...result, files };
1480
2001
  }
1481
2002
 
@@ -1487,8 +2008,34 @@ export function updateProject(projectDir, instruction, options = {}) {
1487
2008
 
1488
2009
  const existingState = existing ? parseExistingProject(existing) : { entities: [], intents: [], port: 3000 };
1489
2010
  const newResult = generate(instruction, options);
1490
- const newEntities = (newResult.entities || []).filter(e => !existingState.entities.includes(e));
1491
2011
 
2012
+ // Entity removal detection (B8)
2013
+ const isRemoval = /(?:remove|delete|drop|取り除|削除)\s+/i.test(instruction);
2014
+ let removedEntities = [];
2015
+ if (isRemoval) {
2016
+ const removeTargets = (newResult.entities || []);
2017
+ removedEntities = existingState.entities.filter(e => removeTargets.includes(e));
2018
+ const allEntities = existingState.entities.filter(e => !removeTargets.includes(e));
2019
+ if (allEntities.length === 0) {
2020
+ return { code: '', valid: true, fixed: false, repairs: [], intents: [], entities: [], relationships: [], port: existingState.port,
2021
+ files: [{ path: 'app.naide', content: '' }], analysis: { instruction, entities: [], relationships: [], plan: [`Removed: ${removedEntities.join(', ')}`], added: [] },
2022
+ action: 'removed', removed: removedEntities };
2023
+ }
2024
+ const combinedIntents = existingState.intents.filter(i => i !== 'schema' || allEntities.length > 0);
2025
+ const extras = [];
2026
+ if (combinedIntents.includes('auth')) extras.push('auth');
2027
+ if (combinedIntents.includes('websocket')) extras.push('websocket');
2028
+ if (combinedIntents.includes('database')) extras.push('database');
2029
+ const fullInstruction = 'REST API for ' + allEntities.join(' and ') + (extras.length ? ' with ' + extras.join(' and ') : '');
2030
+ const combined = generate(fullInstruction, options);
2031
+ const result = buildUpdateFiles(projectDir, combined, allEntities, existingState, options);
2032
+ result.action = 'removed';
2033
+ result.removed = removedEntities;
2034
+ result.analysis.plan.unshift(`Removed: ${removedEntities.join(', ')}`);
2035
+ return result;
2036
+ }
2037
+
2038
+ const newEntities = (newResult.entities || []).filter(e => !existingState.entities.includes(e));
1492
2039
  const allEntities = [...existingState.entities, ...newEntities];
1493
2040
  const combinedIntents = [...new Set([...existingState.intents, ...newResult.intents])];
1494
2041
  const extras = [];
@@ -1498,8 +2045,12 @@ export function updateProject(projectDir, instruction, options = {}) {
1498
2045
  const fullInstruction = 'REST API for ' + allEntities.join(' and ') + (extras.length ? ' with ' + extras.join(' and ') : '');
1499
2046
 
1500
2047
  const combined = generate(fullInstruction, options);
1501
- const finalCode = combined.code;
2048
+ return buildUpdateFiles(projectDir, combined, allEntities, existingState, options);
2049
+ }
1502
2050
 
2051
+ function buildUpdateFiles(projectDir, combined, allEntities, existingState, options) {
2052
+ const existing = _existsFS(projectDir + '/app.naide') ? _readFS(projectDir + '/app.naide', 'utf-8') : '';
2053
+ const finalCode = combined.code;
1503
2054
  const files = [];
1504
2055
  files.push({ path: 'app.naide', content: finalCode });
1505
2056
 
@@ -1514,10 +2065,7 @@ export function updateProject(projectDir, instruction, options = {}) {
1514
2065
  const deps = {};
1515
2066
  if (intents.has('server')) deps.express = '^4.21.0';
1516
2067
  if (intents.has('websocket')) deps.ws = '^8.18.0';
1517
- if (intents.has('auth')) {
1518
- deps.jsonwebtoken = '^9.0.2';
1519
- deps.bcryptjs = '^2.4.3';
1520
- }
2068
+ if (intents.has('auth')) { deps.jsonwebtoken = '^9.0.2'; deps.bcryptjs = '^2.4.3'; }
1521
2069
  if (intents.has('mail')) deps.nodemailer = '^6.9.0';
1522
2070
  if (entities.length > 0) deps.naider = '^1.21.0';
1523
2071
 
@@ -1536,8 +2084,7 @@ export function updateProject(projectDir, instruction, options = {}) {
1536
2084
  }
1537
2085
  files.push({ path: 'package.json', content: JSON.stringify(pkg, null, 2) + '\n' });
1538
2086
 
1539
- const envL = [];
1540
- envL.push(`PORT=${port}`);
2087
+ const envL = [`PORT=${port}`];
1541
2088
  if (intents.has('auth')) envL.push('JWT_SECRET=change-me-in-production');
1542
2089
  if (intents.has('bot')) envL.push(`${(options.platform || 'DISCORD').toUpperCase()}_TOKEN=your-bot-token`);
1543
2090
  if (intents.has('mail')) { envL.push('SMTP_HOST=smtp.example.com'); envL.push('SMTP_USER=user@example.com'); envL.push('SMTP_PASS=password'); }
@@ -1547,9 +2094,7 @@ export function updateProject(projectDir, instruction, options = {}) {
1547
2094
  const existingEnv = _readFS(existingEnvPath, 'utf-8');
1548
2095
  const existingKeys = new Set(existingEnv.split('\n').map(l => l.split('=')[0]).filter(Boolean));
1549
2096
  const newEnvLines = envL.filter(l => !existingKeys.has(l.split('=')[0]));
1550
- if (newEnvLines.length > 0) {
1551
- files.push({ path: '.env', content: existingEnv.trimEnd() + '\n' + newEnvLines.join('\n') + '\n' });
1552
- }
2097
+ if (newEnvLines.length > 0) files.push({ path: '.env', content: existingEnv.trimEnd() + '\n' + newEnvLines.join('\n') + '\n' });
1553
2098
  } else {
1554
2099
  files.push({ path: '.env', content: envL.join('\n') + '\n' });
1555
2100
  }
@@ -1559,56 +2104,48 @@ export function updateProject(projectDir, instruction, options = {}) {
1559
2104
  if (intents.has('server')) {
1560
2105
  files.push({ path: 'Dockerfile', content: `FROM node:22-slim\nWORKDIR /app\nCOPY package*.json ./\nRUN npm ci --omit=dev\nRUN npm install -g naider\nCOPY . .\nRUN naide app.naide --emit -o dist/app.mjs\nEXPOSE ${port}\nCMD ["node", "dist/app.mjs"]\n` });
1561
2106
  files.push({ path: '.dockerignore', content: 'node_modules\ndist\ndata\n.env\n.git\n' });
2107
+ files.push({ path: 'docker-compose.yml', content: dockerCompose(projectName, port, intents) });
1562
2108
  }
1563
2109
 
1564
- const readmeLines = [`# ${projectName}\n`];
1565
- readmeLines.push(`Generated by NAIDE Agent Coder (~~)\n`);
1566
- readmeLines.push(`## Entities\n`);
2110
+ files.push({ path: '.env.development', content: envDevelopment(port, intents) });
2111
+ files.push({ path: '.env.production', content: envProduction(port, intents) });
2112
+ files.push({ path: '.github/workflows/ci.yml', content: githubActionsCI(projectName) });
2113
+
2114
+ const readmeLines = [`# ${projectName}\n`, `Generated by NAIDE Agent Coder (~~)\n`, `## Entities\n`];
1567
2115
  for (const e of entities) {
1568
- const preset = SCHEMA_PRESETS[e.toLowerCase()];
1569
- const fc = preset ? preset.length : 3;
2116
+ const fc = SCHEMA_PRESETS[e.toLowerCase()]?.length || 3;
1570
2117
  readmeLines.push(`- **${e}** (${fc} fields)`);
1571
2118
  }
1572
2119
  if (relationships.length > 0) {
1573
2120
  readmeLines.push(`\n## Relationships\n`);
1574
2121
  for (const r of relationships) readmeLines.push(`- ${r.parent} → ${r.child} (${r.fk})`);
1575
2122
  }
1576
- readmeLines.push(`\n## Quick Start\n`);
1577
- readmeLines.push('```bash\nnpm install\nnpm run dev\n```\n');
2123
+ readmeLines.push(`\n## Quick Start\n`, '```bash', 'npm install', 'npm run dev', '```\n');
1578
2124
  if (intents.has('server')) {
1579
- readmeLines.push(`## API Endpoints\n`);
1580
- readmeLines.push(`- \`GET /api/health\` — Health check`);
2125
+ readmeLines.push(`## API Endpoints\n`, `- \`GET /api/health\` — Health check`, `- \`GET /docs\` — Swagger UI`);
1581
2126
  for (const e of entities) {
1582
2127
  const ep = pluralize(e.toLowerCase());
1583
- readmeLines.push(`- \`GET /api/${ep}?page=1&limit=20&search=\` — List ${ep} (paginated)`);
1584
- readmeLines.push(`- \`POST /api/${ep}\` — Create ${e.toLowerCase()}`);
1585
- readmeLines.push(`- \`GET /api/${ep}/:id\` — Get ${e.toLowerCase()}`);
1586
- readmeLines.push(`- \`PUT /api/${ep}/:id\` — Update ${e.toLowerCase()}`);
1587
- readmeLines.push(`- \`DELETE /api/${ep}/:id\` — Delete ${e.toLowerCase()}`);
1588
- readmeLines.push(`- \`DELETE /api/${ep}/:id/soft\` — Soft delete`);
1589
- readmeLines.push(`- \`PUT /api/${ep}/:id/restore\` — Restore`);
1590
- readmeLines.push(`- \`POST /api/${ep}/batch\` — Batch create`);
1591
- readmeLines.push(`- \`DELETE /api/${ep}/batch\` — Batch delete`);
2128
+ readmeLines.push(`- \`GET /api/${ep}?page=1&limit=20&search=&sort=id&order=desc\` — List (paginated, sortable, filterable)`);
2129
+ readmeLines.push(`- \`POST /api/${ep}\` — Create (validated)`, `- \`PUT /api/${ep}/:id\` — Update (validated)`);
2130
+ readmeLines.push(`- \`GET /api/${ep}/:id\` — Get`, `- \`DELETE /api/${ep}/:id\` — Delete`);
2131
+ readmeLines.push(`- \`DELETE /api/${ep}/:id/soft\` — Soft delete`, `- \`PUT /api/${ep}/:id/restore\` — Restore`);
2132
+ readmeLines.push(`- \`POST /api/${ep}/batch\` — Batch create`, `- \`DELETE /api/${ep}/batch\` — Batch delete`);
1592
2133
  }
1593
- for (const r of relationships) {
1594
- readmeLines.push(`- \`GET /api/${pluralize(r.parent.toLowerCase())}/:id/${pluralize(r.child.toLowerCase())}\` — ${r.child}s by ${r.parent}`);
1595
- }
1596
- readmeLines.push(`- \`POST /api/upload\` — File upload`);
1597
- readmeLines.push(`- \`POST /api/webhooks\` — Register webhook`);
2134
+ readmeLines.push(`- \`POST /api/upload\` — File upload`, `- \`POST /api/webhooks\` — Webhook`);
1598
2135
  if (intents.has('auth')) {
1599
- readmeLines.push(`- \`POST /api/auth/register\` — Register`);
1600
- readmeLines.push(`- \`POST /api/auth/login\` — Login`);
1601
- readmeLines.push(`- \`GET /api/auth/me\` — Current user`);
1602
- readmeLines.push(`- \`GET /api/admin/stats\` — Admin stats`);
2136
+ readmeLines.push(`- \`POST /api/auth/register\` — Register`, `- \`POST /api/auth/login\` — Login`, `- \`GET /api/auth/me\` — Current user`);
2137
+ readmeLines.push(`- \`POST /api/auth/forgot-password\` — Forgot password`, `- \`POST /api/auth/reset-password\` — Reset password`);
2138
+ readmeLines.push(`- \`POST /api/auth/change-password\` — Change password`, `- \`GET /api/admin/stats\` — Admin stats`);
1603
2139
  }
1604
2140
  }
1605
2141
  if (intents.has('server')) {
1606
- readmeLines.push(`\n## Docker\n`);
1607
- readmeLines.push('```bash');
1608
- readmeLines.push(`docker build -t ${projectName} .`);
1609
- readmeLines.push(`docker run -p ${port}:${port} --env-file .env ${projectName}`);
1610
- readmeLines.push('```');
2142
+ readmeLines.push(`\n## Docker\n`, '```bash', `docker-compose up -d`, '```');
1611
2143
  }
2144
+ readmeLines.push(`\n## Generated Files\n`);
2145
+ readmeLines.push('| File | Purpose |', '|------|---------|');
2146
+ readmeLines.push('| app.naide | Main application |', '| index.html | Frontend SPA |', '| admin.html | Admin dashboard |');
2147
+ readmeLines.push('| client.mjs | API client library |', '| openapi.json | OpenAPI 3.0 spec |');
2148
+ readmeLines.push('| docker-compose.yml | Docker orchestration |', '| .github/workflows/ci.yml | CI/CD pipeline |');
1612
2149
  files.push({ path: 'README.md', content: readmeLines.join('\n') + '\n' });
1613
2150
 
1614
2151
  const entityObjs = entities.map(name => {
@@ -1621,25 +2158,20 @@ export function updateProject(projectDir, instruction, options = {}) {
1621
2158
  if (intents.has('server') && entityObjs.length > 0) {
1622
2159
  files.push({ path: 'openapi.json', content: JSON.stringify(openApiSpec(entityObjs, intents, { port }, relationships), null, 2) + '\n' });
1623
2160
  files.push({ path: 'admin.html', content: adminPage(entityObjs, intents) });
2161
+ files.push({ path: 'index.html', content: frontendSPA(entityObjs, intents, port) });
1624
2162
  }
1625
2163
  if (intents.has('server')) {
1626
2164
  files.push({ path: 'client.mjs', content: apiClientCode(entityObjs, intents, port) });
1627
2165
  }
1628
2166
 
1629
- const added = existing
1630
- ? entities.filter(e => !existing.includes(`schema ${e}`))
1631
- : entities;
2167
+ const added = existing ? entities.filter(e => !existing.includes(`schema ${e}`)) : entities;
1632
2168
  const analysis = {
1633
- instruction,
1634
- entities: entityObjs.map(e => ({ name: e.name, fieldCount: e.fields.length })),
1635
- relationships,
1636
- plan: [],
1637
- added,
2169
+ instruction: '', entities: entityObjs.map(e => ({ name: e.name, fieldCount: e.fields.length })),
2170
+ relationships, plan: [], added,
1638
2171
  };
1639
2172
  if (added.length > 0) analysis.plan.push(`Added: ${added.join(', ')}`);
1640
2173
  if (existing) analysis.plan.push('Merged with existing app.naide');
1641
- analysis.plan.push(`Total entities: ${entities.length}`);
1642
- analysis.plan.push(`Supporting files regenerated`);
2174
+ analysis.plan.push(`Total entities: ${entities.length}`, `Supporting files regenerated (${files.length} files)`);
1643
2175
 
1644
2176
  return {
1645
2177
  code: finalCode, valid: combined.valid, fixed: combined.fixed, repairs: combined.repairs,
@@ -1793,17 +2325,21 @@ function buildAnalysis(instruction, intents, params) {
1793
2325
  }
1794
2326
 
1795
2327
  if (intents.has('database')) plan.push(`Database: ${params.dbType || 'file-based'}`);
1796
- if (intents.has('server')) plan.push(`Server on port ${params.port}`);
1797
- if (intents.has('crud')) plan.push(`CRUD endpoints for ${params.entities.map(e => pluralize(e.name.toLowerCase())).join(', ')} (paginated)`);
1798
- if (intents.has('auth')) plan.push('Auth: JWT + register/login/me + role-based admin');
2328
+ if (intents.has('server')) plan.push(`Server on port ${params.port} (rate-limited, request logging)`);
2329
+ if (intents.has('crud')) plan.push(`CRUD endpoints for ${params.entities.map(e => pluralize(e.name.toLowerCase())).join(', ')} (paginated, sortable, filterable)`);
2330
+ if (intents.has('crud')) plan.push(`Input validation: ${params.entities.map(e => `validate${e.name}()`).join(', ')}`);
2331
+ if (intents.has('auth')) plan.push('Auth: JWT + register/login/me + forgot/reset/change password + role-based admin');
1799
2332
  if (params.relationships.length > 0) plan.push(`Nested routes: ${params.relationships.map(r => `/${pluralize(r.parent.toLowerCase())}/:id/${pluralize(r.child.toLowerCase())}`).join(', ')}`);
1800
- if (intents.has('websocket'))plan.push('WebSocket: real-time messaging');
2333
+ const m2m = detectManyToMany(params.entities.map(e => e.name));
2334
+ if (m2m.length > 0) plan.push(`M:N relations: ${m2m.map(r => `${r.left}↔${r.right} via ${r.junction}`).join(', ')}`);
2335
+ if (intents.has('websocket'))plan.push('WebSocket: rooms + broadcast');
1801
2336
  if (intents.has('ai')) plan.push('AI endpoint: /api/ask');
1802
2337
  if (intents.has('bot')) plan.push(`Bot: ${params.platform || 'discord'}`);
1803
2338
  if (intents.has('mail')) plan.push('Mail: SMTP configuration');
1804
2339
  if (intents.has('graphql')) plan.push('GraphQL endpoint');
1805
2340
  if (intents.has('page')) plan.push('Page: HTML generation');
1806
2341
  if (intents.has('cli')) plan.push('CLI app with flags');
2342
+ if (intents.has('server')) plan.push('Swagger UI at /docs');
1807
2343
  if (intents.has('database') && intents.has('schema')) plan.push('Seed: sample data function');
1808
2344
  plan.push(`Tests: ${params.entities.length} entity CRUD test suites`);
1809
2345