mycontext-cli 4.2.6 → 4.2.7
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/README.md +531 -144
- package/dist/README.md +531 -144
- package/dist/cli.js +14 -5
- package/dist/cli.js.map +1 -1
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +17 -4
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/init-interactive.d.ts +127 -0
- package/dist/commands/init-interactive.d.ts.map +1 -0
- package/dist/commands/init-interactive.js +754 -0
- package/dist/commands/init-interactive.js.map +1 -0
- package/dist/doctor/rules/index.d.ts +3 -1
- package/dist/doctor/rules/index.d.ts.map +1 -1
- package/dist/doctor/rules/index.js +7 -1
- package/dist/doctor/rules/index.js.map +1 -1
- package/dist/doctor/rules/instantdb-rules.d.ts +3 -0
- package/dist/doctor/rules/instantdb-rules.d.ts.map +1 -0
- package/dist/doctor/rules/instantdb-rules.js +335 -0
- package/dist/doctor/rules/instantdb-rules.js.map +1 -0
- package/dist/doctor/rules/typescript-rules.d.ts +3 -0
- package/dist/doctor/rules/typescript-rules.d.ts.map +1 -0
- package/dist/doctor/rules/typescript-rules.js +177 -0
- package/dist/doctor/rules/typescript-rules.js.map +1 -0
- package/dist/package.json +1 -1
- package/dist/services/InferenceEngine.d.ts +41 -0
- package/dist/services/InferenceEngine.d.ts.map +1 -0
- package/dist/services/InferenceEngine.js +307 -0
- package/dist/services/InferenceEngine.js.map +1 -0
- package/dist/services/Planner.d.ts +77 -0
- package/dist/services/Planner.d.ts.map +1 -0
- package/dist/services/Planner.js +828 -0
- package/dist/services/Planner.js.map +1 -0
- package/dist/types/asl.d.ts +387 -0
- package/dist/types/asl.d.ts.map +1 -0
- package/dist/types/asl.js +139 -0
- package/dist/types/asl.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,828 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Planner Service
|
|
4
|
+
*
|
|
5
|
+
* The "Planner Layer" in the MyContext compiler pipeline.
|
|
6
|
+
* Responsible for:
|
|
7
|
+
* 1. Validating ASL for 100% completeness
|
|
8
|
+
* 2. Generating clarifying questions for gaps
|
|
9
|
+
* 3. Creating diff previews before generation
|
|
10
|
+
* 4. Task decomposition with confidence scoring (NEW)
|
|
11
|
+
* 5. Auto-inference for high-confidence tasks (NEW)
|
|
12
|
+
*
|
|
13
|
+
* The Planner ensures NO code is generated until the specification is perfect.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.Planner = void 0;
|
|
17
|
+
const asl_1 = require("../types/asl");
|
|
18
|
+
const AICore_1 = require("../core/ai/AICore");
|
|
19
|
+
const InferenceEngine_1 = require("./InferenceEngine");
|
|
20
|
+
class Planner {
|
|
21
|
+
constructor() {
|
|
22
|
+
this.inferenceEngine = new InferenceEngine_1.InferenceEngine();
|
|
23
|
+
this.aiCore = AICore_1.AICore.getInstance({
|
|
24
|
+
fallbackEnabled: true,
|
|
25
|
+
workingDirectory: process.cwd(),
|
|
26
|
+
});
|
|
27
|
+
this.state = {
|
|
28
|
+
tasks: [],
|
|
29
|
+
completedTasks: [],
|
|
30
|
+
pendingTasks: [],
|
|
31
|
+
revealedContext: [],
|
|
32
|
+
confidenceScore: 0,
|
|
33
|
+
learningContext: {
|
|
34
|
+
corrections: [],
|
|
35
|
+
preferences: {},
|
|
36
|
+
patterns: [],
|
|
37
|
+
},
|
|
38
|
+
checkpoints: [],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// NEW: TASK DECOMPOSITION & INFERENCE
|
|
43
|
+
// ============================================================================
|
|
44
|
+
/**
|
|
45
|
+
* Decompose initial input into inference tasks with confidence scores
|
|
46
|
+
*/
|
|
47
|
+
async decompose(initialInput) {
|
|
48
|
+
const prompt = this.buildDecompositionPrompt(initialInput);
|
|
49
|
+
const response = await this.aiCore.generateText(prompt, {
|
|
50
|
+
temperature: 0.3,
|
|
51
|
+
});
|
|
52
|
+
let parsedTasks;
|
|
53
|
+
try {
|
|
54
|
+
// Strip markdown code blocks if present
|
|
55
|
+
let cleanedResponse = response.trim();
|
|
56
|
+
if (cleanedResponse.startsWith('```json')) {
|
|
57
|
+
cleanedResponse = cleanedResponse.replace(/^```json\s*/m, '').replace(/\s*```$/m, '');
|
|
58
|
+
}
|
|
59
|
+
else if (cleanedResponse.startsWith('```')) {
|
|
60
|
+
cleanedResponse = cleanedResponse.replace(/^```\s*/m, '').replace(/\s*```$/m, '');
|
|
61
|
+
}
|
|
62
|
+
parsedTasks = JSON.parse(cleanedResponse);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
throw new Error(`Failed to parse task decomposition: ${response}`);
|
|
66
|
+
}
|
|
67
|
+
// Convert to InferenceTask objects
|
|
68
|
+
const tasks = parsedTasks.tasks.map((t, idx) => ({
|
|
69
|
+
id: `task-${idx + 1}`,
|
|
70
|
+
description: t.description,
|
|
71
|
+
category: t.category,
|
|
72
|
+
confidence: t.confidence,
|
|
73
|
+
dependencies: t.dependencies || [],
|
|
74
|
+
autoInfer: t.confidence >= 90,
|
|
75
|
+
needsConfirmation: t.confidence >= 70 && t.confidence < 90,
|
|
76
|
+
needsUserInput: t.confidence < 70,
|
|
77
|
+
completed: false,
|
|
78
|
+
}));
|
|
79
|
+
this.state.tasks = tasks;
|
|
80
|
+
this.state.pendingTasks = tasks.map(t => t.id);
|
|
81
|
+
return tasks;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Select next task to execute based on dependencies and confidence
|
|
85
|
+
*/
|
|
86
|
+
selectNextTask() {
|
|
87
|
+
const availableTasks = this.state.tasks.filter(task => {
|
|
88
|
+
// Must be pending
|
|
89
|
+
if (task.completed)
|
|
90
|
+
return false;
|
|
91
|
+
// All dependencies must be completed
|
|
92
|
+
const dependenciesMet = task.dependencies.every(depId => this.state.completedTasks.includes(depId));
|
|
93
|
+
return dependenciesMet;
|
|
94
|
+
});
|
|
95
|
+
if (availableTasks.length === 0)
|
|
96
|
+
return null;
|
|
97
|
+
// Prioritize by confidence (highest first)
|
|
98
|
+
availableTasks.sort((a, b) => b.confidence - a.confidence);
|
|
99
|
+
return availableTasks[0] || null;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Mark task as completed and update state
|
|
103
|
+
*/
|
|
104
|
+
markTaskComplete(taskId) {
|
|
105
|
+
const task = this.state.tasks.find(t => t.id === taskId);
|
|
106
|
+
if (!task)
|
|
107
|
+
return;
|
|
108
|
+
task.completed = true;
|
|
109
|
+
task.completedAt = new Date();
|
|
110
|
+
this.state.completedTasks.push(taskId);
|
|
111
|
+
this.state.pendingTasks = this.state.pendingTasks.filter(id => id !== taskId);
|
|
112
|
+
// Update overall confidence score
|
|
113
|
+
this.updateConfidenceScore();
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Update dependent tasks after a task completes
|
|
117
|
+
*/
|
|
118
|
+
updateDependentTasks(completedTask) {
|
|
119
|
+
this.state.tasks = this.inferenceEngine.feedToNextTasks(completedTask, this.state.tasks.filter(t => !t.completed));
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Reveal context to user (what was inferred and why)
|
|
123
|
+
*/
|
|
124
|
+
revealContext(task, inference) {
|
|
125
|
+
const revelations = [];
|
|
126
|
+
// Generate human-readable explanations
|
|
127
|
+
if (inference.entities) {
|
|
128
|
+
Object.keys(inference.entities).forEach(entityName => {
|
|
129
|
+
revelations.push({
|
|
130
|
+
taskId: task.id,
|
|
131
|
+
message: `${entityName} entity (${task.reasoning || 'inferred from context'})`,
|
|
132
|
+
confidence: task.confidence,
|
|
133
|
+
timestamp: new Date(),
|
|
134
|
+
category: task.category,
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
if (inference.auth) {
|
|
139
|
+
revelations.push({
|
|
140
|
+
taskId: task.id,
|
|
141
|
+
message: `Authentication: ${inference.auth.provider} (${task.reasoning || 'inferred from context'})`,
|
|
142
|
+
confidence: task.confidence,
|
|
143
|
+
timestamp: new Date(),
|
|
144
|
+
category: task.category,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
if (inference.pages) {
|
|
148
|
+
inference.pages.forEach(page => {
|
|
149
|
+
revelations.push({
|
|
150
|
+
taskId: task.id,
|
|
151
|
+
message: `Page: ${page.path} (${task.reasoning || 'inferred from context'})`,
|
|
152
|
+
confidence: task.confidence,
|
|
153
|
+
timestamp: new Date(),
|
|
154
|
+
category: task.category,
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
this.state.revealedContext.push(...revelations);
|
|
159
|
+
return revelations;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Create checkpoint summary before proceeding
|
|
163
|
+
*/
|
|
164
|
+
createCheckpoint(autoInferredTasks) {
|
|
165
|
+
const entitiesCreated = [];
|
|
166
|
+
const rolesCreated = [];
|
|
167
|
+
const pagesCreated = [];
|
|
168
|
+
let fieldsAdded = 0;
|
|
169
|
+
let permissionsAdded = 0;
|
|
170
|
+
autoInferredTasks.forEach(task => {
|
|
171
|
+
if (task.inference?.entities) {
|
|
172
|
+
Object.keys(task.inference.entities).forEach(entityName => {
|
|
173
|
+
entitiesCreated.push(entityName);
|
|
174
|
+
const entity = task.inference?.entities?.[entityName];
|
|
175
|
+
if (entity) {
|
|
176
|
+
fieldsAdded += entity.fields?.length || 0;
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
if (task.inference?.auth?.roles) {
|
|
181
|
+
rolesCreated.push(...task.inference.auth.roles.map(r => r.name));
|
|
182
|
+
}
|
|
183
|
+
if (task.inference?.permissions) {
|
|
184
|
+
permissionsAdded += task.inference.permissions.length;
|
|
185
|
+
}
|
|
186
|
+
if (task.inference?.pages) {
|
|
187
|
+
pagesCreated.push(...task.inference.pages.map(p => p.path));
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
const totalConfidence = autoInferredTasks.reduce((sum, t) => sum + t.confidence, 0) /
|
|
191
|
+
(autoInferredTasks.length || 1);
|
|
192
|
+
const summary = {
|
|
193
|
+
entitiesCreated,
|
|
194
|
+
fieldsAdded,
|
|
195
|
+
rolesCreated,
|
|
196
|
+
permissionsAdded,
|
|
197
|
+
pagesCreated,
|
|
198
|
+
totalConfidence: Math.round(totalConfidence),
|
|
199
|
+
};
|
|
200
|
+
const checkpoint = {
|
|
201
|
+
id: `checkpoint-${this.state.checkpoints.length + 1}`,
|
|
202
|
+
timestamp: new Date(),
|
|
203
|
+
autoInferredTasks,
|
|
204
|
+
summary,
|
|
205
|
+
approved: false,
|
|
206
|
+
};
|
|
207
|
+
this.state.checkpoints.push(checkpoint);
|
|
208
|
+
return checkpoint;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Get current planner state
|
|
212
|
+
*/
|
|
213
|
+
getState() {
|
|
214
|
+
return this.state;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Update overall confidence score
|
|
218
|
+
*/
|
|
219
|
+
updateConfidenceScore() {
|
|
220
|
+
if (this.state.tasks.length === 0) {
|
|
221
|
+
this.state.confidenceScore = 0;
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const totalConfidence = this.state.tasks
|
|
225
|
+
.filter(t => t.completed)
|
|
226
|
+
.reduce((sum, task) => sum + task.confidence, 0);
|
|
227
|
+
const totalTasks = this.state.tasks.length;
|
|
228
|
+
this.state.confidenceScore = Math.round(totalConfidence / totalTasks);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Build decomposition prompt
|
|
232
|
+
*/
|
|
233
|
+
buildDecompositionPrompt(initialInput) {
|
|
234
|
+
return `You are an expert software architect helping to break down a project description into actionable inference tasks.
|
|
235
|
+
|
|
236
|
+
## User Input
|
|
237
|
+
"${initialInput}"
|
|
238
|
+
|
|
239
|
+
## Your Task
|
|
240
|
+
Break this down into specific inference tasks that can be completed sequentially. Each task should:
|
|
241
|
+
1. Have a clear description
|
|
242
|
+
2. Be assignable to a category (project, entities, auth, permissions, pages, design)
|
|
243
|
+
3. Have a confidence score (0-100) indicating how confidently it can be inferred
|
|
244
|
+
4. List dependencies on other tasks (by task number)
|
|
245
|
+
|
|
246
|
+
Consider:
|
|
247
|
+
- What entities (data models) are needed?
|
|
248
|
+
- What authentication/authorization is needed?
|
|
249
|
+
- What pages/routes are needed?
|
|
250
|
+
- What relationships exist between entities?
|
|
251
|
+
- What can be inferred with high confidence vs. what needs user input?
|
|
252
|
+
|
|
253
|
+
Return JSON:
|
|
254
|
+
\`\`\`json
|
|
255
|
+
{
|
|
256
|
+
"tasks": [
|
|
257
|
+
{
|
|
258
|
+
"description": "Infer core entities from 'blog' context",
|
|
259
|
+
"category": "entities",
|
|
260
|
+
"confidence": 95,
|
|
261
|
+
"dependencies": []
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
"description": "Infer User entity fields",
|
|
265
|
+
"category": "entities",
|
|
266
|
+
"confidence": 90,
|
|
267
|
+
"dependencies": [1]
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
"description": "Infer authentication requirements",
|
|
271
|
+
"category": "auth",
|
|
272
|
+
"confidence": 85,
|
|
273
|
+
"dependencies": [1]
|
|
274
|
+
}
|
|
275
|
+
]
|
|
276
|
+
}
|
|
277
|
+
\`\`\`
|
|
278
|
+
|
|
279
|
+
Confidence guidelines:
|
|
280
|
+
- 95-100%: Extremely certain (e.g., blog → needs Post entity)
|
|
281
|
+
- 85-94%: Very confident (e.g., blog → likely needs Comment entity)
|
|
282
|
+
- 70-84%: Moderately confident (e.g., auth method could be email or OAuth)
|
|
283
|
+
- Below 70%: Too ambiguous, user input needed
|
|
284
|
+
|
|
285
|
+
Only return JSON, no other text.`;
|
|
286
|
+
}
|
|
287
|
+
// ============================================================================
|
|
288
|
+
// EXISTING METHODS (KEEP AS-IS)
|
|
289
|
+
// ============================================================================
|
|
290
|
+
/**
|
|
291
|
+
* Validate ASL for completeness and correctness
|
|
292
|
+
*/
|
|
293
|
+
validate(asl) {
|
|
294
|
+
const errors = [];
|
|
295
|
+
const warnings = [];
|
|
296
|
+
// 1. Check required top-level fields
|
|
297
|
+
if (!asl.version) {
|
|
298
|
+
errors.push({
|
|
299
|
+
path: "version",
|
|
300
|
+
message: "ASL version is required",
|
|
301
|
+
severity: "error",
|
|
302
|
+
fix: 'Set version to "1.0"',
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
if (!asl.project) {
|
|
306
|
+
errors.push({
|
|
307
|
+
path: "project",
|
|
308
|
+
message: "Project specification is required",
|
|
309
|
+
severity: "error",
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
// Validate project fields
|
|
314
|
+
if (!asl.project.name) {
|
|
315
|
+
errors.push({
|
|
316
|
+
path: "project.name",
|
|
317
|
+
message: "Project name is required",
|
|
318
|
+
severity: "error",
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
if (!asl.project.framework) {
|
|
322
|
+
errors.push({
|
|
323
|
+
path: "project.framework",
|
|
324
|
+
message: "Framework must be specified",
|
|
325
|
+
severity: "error",
|
|
326
|
+
fix: 'Set framework to "nextjs"',
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
if (!asl.project.backend) {
|
|
330
|
+
errors.push({
|
|
331
|
+
path: "project.backend",
|
|
332
|
+
message: "Backend must be specified",
|
|
333
|
+
severity: "error",
|
|
334
|
+
fix: 'Set backend to "instantdb"',
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// 2. Check entities
|
|
339
|
+
if (!asl.entities || Object.keys(asl.entities).length === 0) {
|
|
340
|
+
errors.push({
|
|
341
|
+
path: "entities",
|
|
342
|
+
message: "At least one entity must be defined",
|
|
343
|
+
severity: "error",
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
// Validate each entity
|
|
348
|
+
Object.entries(asl.entities).forEach(([entityName, entity], idx) => {
|
|
349
|
+
if (!entity.fields || entity.fields.length === 0) {
|
|
350
|
+
errors.push({
|
|
351
|
+
path: `entities.${entityName}.fields`,
|
|
352
|
+
message: `Entity "${entityName}" must have at least one field`,
|
|
353
|
+
severity: "error",
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
// Check field types
|
|
357
|
+
entity.fields?.forEach((field, fieldIdx) => {
|
|
358
|
+
if (!field.name) {
|
|
359
|
+
errors.push({
|
|
360
|
+
path: `entities.${entityName}.fields[${fieldIdx}].name`,
|
|
361
|
+
message: "Field name is required",
|
|
362
|
+
severity: "error",
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
if (!field.type) {
|
|
366
|
+
errors.push({
|
|
367
|
+
path: `entities.${entityName}.fields[${fieldIdx}].type`,
|
|
368
|
+
message: `Field "${field.name}" must have a type`,
|
|
369
|
+
severity: "error",
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
// Warn about missing descriptions
|
|
373
|
+
if (!field.description) {
|
|
374
|
+
warnings.push({
|
|
375
|
+
path: `entities.${entityName}.fields[${fieldIdx}].description`,
|
|
376
|
+
message: `Field "${field.name}" missing description`,
|
|
377
|
+
severity: "warning",
|
|
378
|
+
suggestion: "Add description for better documentation",
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
// 3. Check pages
|
|
385
|
+
if (!asl.pages || asl.pages.length === 0) {
|
|
386
|
+
errors.push({
|
|
387
|
+
path: "pages",
|
|
388
|
+
message: "At least one page must be defined",
|
|
389
|
+
severity: "error",
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
asl.pages.forEach((page, idx) => {
|
|
394
|
+
if (!page.path) {
|
|
395
|
+
errors.push({
|
|
396
|
+
path: `pages[${idx}].path`,
|
|
397
|
+
message: "Page path is required",
|
|
398
|
+
severity: "error",
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
if (!page.name) {
|
|
402
|
+
errors.push({
|
|
403
|
+
path: `pages[${idx}].name`,
|
|
404
|
+
message: "Page name (component name) is required",
|
|
405
|
+
severity: "error",
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
// Check if page requires auth but no auth is configured
|
|
409
|
+
if (!page.public && !asl.auth) {
|
|
410
|
+
warnings.push({
|
|
411
|
+
path: `pages[${idx}]`,
|
|
412
|
+
message: `Page "${page.path}" requires authentication but no auth is configured`,
|
|
413
|
+
severity: "warning",
|
|
414
|
+
suggestion: "Add auth configuration or mark page as public",
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
// 4. Check auth if provided
|
|
420
|
+
if (asl.auth) {
|
|
421
|
+
if (!asl.auth.provider) {
|
|
422
|
+
errors.push({
|
|
423
|
+
path: "auth.provider",
|
|
424
|
+
message: "Auth provider must be specified",
|
|
425
|
+
severity: "error",
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
if (!asl.auth.roles || asl.auth.roles.length === 0) {
|
|
429
|
+
warnings.push({
|
|
430
|
+
path: "auth.roles",
|
|
431
|
+
message: "No roles defined",
|
|
432
|
+
severity: "warning",
|
|
433
|
+
suggestion: "Define at least one role (e.g., 'user', 'admin')",
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
// 5. Validate entity references in pages
|
|
438
|
+
const refErrors = (0, asl_1.validateEntityReferences)(asl);
|
|
439
|
+
refErrors.forEach(err => {
|
|
440
|
+
const parts = err.split(":");
|
|
441
|
+
errors.push({
|
|
442
|
+
path: parts[0] || "",
|
|
443
|
+
message: parts[1] || err,
|
|
444
|
+
severity: "error",
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
// 6. Validate permission references
|
|
448
|
+
const permErrors = (0, asl_1.validatePermissionReferences)(asl);
|
|
449
|
+
permErrors.forEach(err => {
|
|
450
|
+
const parts = err.split(":");
|
|
451
|
+
errors.push({
|
|
452
|
+
path: parts[0] || "",
|
|
453
|
+
message: parts[1] || err,
|
|
454
|
+
severity: "error",
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
// 7. Calculate completeness
|
|
458
|
+
const completeness = (0, asl_1.calculateCompleteness)(asl);
|
|
459
|
+
return {
|
|
460
|
+
valid: errors.length === 0,
|
|
461
|
+
errors,
|
|
462
|
+
warnings,
|
|
463
|
+
completeness,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Generate clarifying questions for gaps in ASL
|
|
468
|
+
*/
|
|
469
|
+
generateQuestions(asl) {
|
|
470
|
+
const questions = [];
|
|
471
|
+
const missing = (0, asl_1.getMissingSections)(asl);
|
|
472
|
+
// 1. Project questions
|
|
473
|
+
if (!asl.project) {
|
|
474
|
+
questions.push({
|
|
475
|
+
id: "project.name",
|
|
476
|
+
type: "text",
|
|
477
|
+
text: "What is the name of your project?",
|
|
478
|
+
context: "This will be used as the package name and directory name.",
|
|
479
|
+
validation: {
|
|
480
|
+
required: true,
|
|
481
|
+
pattern: "^[a-z][a-z0-9-]*$",
|
|
482
|
+
},
|
|
483
|
+
category: "project",
|
|
484
|
+
});
|
|
485
|
+
questions.push({
|
|
486
|
+
id: "project.description",
|
|
487
|
+
type: "text",
|
|
488
|
+
text: "Describe your project in one sentence:",
|
|
489
|
+
context: "This helps the AI understand your project's purpose.",
|
|
490
|
+
validation: {
|
|
491
|
+
required: true,
|
|
492
|
+
minLength: 10,
|
|
493
|
+
},
|
|
494
|
+
category: "project",
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
// 2. Entity questions
|
|
498
|
+
if (!asl.entities || Object.keys(asl.entities).length === 0) {
|
|
499
|
+
questions.push({
|
|
500
|
+
id: "entities.list",
|
|
501
|
+
type: "text",
|
|
502
|
+
text: "What are the main entities (data models) in your app?",
|
|
503
|
+
context: "Examples: User, Post, Comment, Product, Order. Separate with commas.",
|
|
504
|
+
validation: {
|
|
505
|
+
required: true,
|
|
506
|
+
},
|
|
507
|
+
category: "entities",
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
// For each entity, check if fields are complete
|
|
512
|
+
Object.entries(asl.entities).forEach(([entityName, entity]) => {
|
|
513
|
+
if (!entity.fields || entity.fields.length === 0) {
|
|
514
|
+
questions.push({
|
|
515
|
+
id: `entities.${entityName}.fields`,
|
|
516
|
+
type: "entity-builder",
|
|
517
|
+
text: `What fields should the "${entityName}" entity have?`,
|
|
518
|
+
context: `Define the data structure for ${entityName}. Examples: title (string), price (number), published (boolean).`,
|
|
519
|
+
validation: {
|
|
520
|
+
required: true,
|
|
521
|
+
},
|
|
522
|
+
category: "entities",
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
// 3. Auth questions
|
|
528
|
+
if (!asl.auth) {
|
|
529
|
+
questions.push({
|
|
530
|
+
id: "auth.needed",
|
|
531
|
+
type: "boolean",
|
|
532
|
+
text: "Does your app need user authentication?",
|
|
533
|
+
context: "Most apps with user-specific data need authentication.",
|
|
534
|
+
category: "auth",
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
if (!asl.auth.provider) {
|
|
539
|
+
questions.push({
|
|
540
|
+
id: "auth.provider",
|
|
541
|
+
type: "select",
|
|
542
|
+
text: "Which authentication method do you want to use?",
|
|
543
|
+
options: [
|
|
544
|
+
{
|
|
545
|
+
value: "email",
|
|
546
|
+
label: "Email/Password",
|
|
547
|
+
description: "Traditional email and password authentication",
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
value: "magic-link",
|
|
551
|
+
label: "Magic Link",
|
|
552
|
+
description: "Passwordless login via email link",
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
value: "oauth-github",
|
|
556
|
+
label: "OAuth (GitHub)",
|
|
557
|
+
description: "Login with GitHub account",
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
value: "oauth-google",
|
|
561
|
+
label: "OAuth (Google)",
|
|
562
|
+
description: "Login with Google account",
|
|
563
|
+
},
|
|
564
|
+
],
|
|
565
|
+
validation: {
|
|
566
|
+
required: true,
|
|
567
|
+
},
|
|
568
|
+
category: "auth",
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
if (!asl.auth.roles || asl.auth.roles.length === 0) {
|
|
572
|
+
questions.push({
|
|
573
|
+
id: "auth.roles",
|
|
574
|
+
type: "multi-select",
|
|
575
|
+
text: "What user roles do you need?",
|
|
576
|
+
context: "Roles define different permission levels (e.g., admin can do everything, user can only edit their own content).",
|
|
577
|
+
options: [
|
|
578
|
+
{ value: "admin", label: "Admin", description: "Full system access" },
|
|
579
|
+
{ value: "user", label: "User", description: "Regular user" },
|
|
580
|
+
{ value: "moderator", label: "Moderator", description: "Content moderation" },
|
|
581
|
+
{ value: "guest", label: "Guest", description: "Read-only access" },
|
|
582
|
+
],
|
|
583
|
+
validation: {
|
|
584
|
+
required: true,
|
|
585
|
+
},
|
|
586
|
+
category: "auth",
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
// 4. Permission questions
|
|
591
|
+
if (asl.auth && (!asl.permissions || asl.permissions.length === 0)) {
|
|
592
|
+
questions.push({
|
|
593
|
+
id: "permissions.needed",
|
|
594
|
+
type: "boolean",
|
|
595
|
+
text: "Do you need role-based access control (RBAC)?",
|
|
596
|
+
context: "RBAC controls who can create, read, update, or delete specific resources.",
|
|
597
|
+
dependsOn: ["auth.needed"],
|
|
598
|
+
category: "permissions",
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
// 5. Page questions
|
|
602
|
+
if (!asl.pages || asl.pages.length === 0) {
|
|
603
|
+
questions.push({
|
|
604
|
+
id: "pages.list",
|
|
605
|
+
type: "text",
|
|
606
|
+
text: "What are the main pages/routes in your app?",
|
|
607
|
+
context: "Examples: Home (/), Posts (/posts), Profile (/profile). Separate with commas.",
|
|
608
|
+
validation: {
|
|
609
|
+
required: true,
|
|
610
|
+
},
|
|
611
|
+
category: "pages",
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
// Check if pages need more details
|
|
616
|
+
asl.pages.forEach((page, idx) => {
|
|
617
|
+
if (!page.type) {
|
|
618
|
+
questions.push({
|
|
619
|
+
id: `pages[${idx}].type`,
|
|
620
|
+
type: "select",
|
|
621
|
+
text: `What type of page is "${page.path}"?`,
|
|
622
|
+
options: [
|
|
623
|
+
{ value: "page", label: "Regular Page" },
|
|
624
|
+
{ value: "layout", label: "Layout" },
|
|
625
|
+
{ value: "route-group", label: "Route Group" },
|
|
626
|
+
],
|
|
627
|
+
validation: {
|
|
628
|
+
required: true,
|
|
629
|
+
},
|
|
630
|
+
category: "pages",
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
if (page.public === undefined) {
|
|
634
|
+
questions.push({
|
|
635
|
+
id: `pages[${idx}].public`,
|
|
636
|
+
type: "boolean",
|
|
637
|
+
text: `Should "${page.path}" be publicly accessible (no login required)?`,
|
|
638
|
+
category: "pages",
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
// 6. Design questions
|
|
644
|
+
if (!asl.design) {
|
|
645
|
+
questions.push({
|
|
646
|
+
id: "design.theme",
|
|
647
|
+
type: "select",
|
|
648
|
+
text: "What theme do you prefer?",
|
|
649
|
+
options: [
|
|
650
|
+
{ value: "light", label: "Light", description: "Light color scheme" },
|
|
651
|
+
{ value: "dark", label: "Dark", description: "Dark color scheme" },
|
|
652
|
+
{ value: "system", label: "System", description: "Follow system preference" },
|
|
653
|
+
],
|
|
654
|
+
validation: {
|
|
655
|
+
required: false,
|
|
656
|
+
},
|
|
657
|
+
category: "design",
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
return questions;
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Generate diff preview showing what will be generated
|
|
664
|
+
*/
|
|
665
|
+
generateDiff(asl) {
|
|
666
|
+
const files = [];
|
|
667
|
+
const registries = [];
|
|
668
|
+
const warnings = [];
|
|
669
|
+
// 1. Schema file
|
|
670
|
+
files.push({
|
|
671
|
+
path: "instant.schema.ts",
|
|
672
|
+
action: "create",
|
|
673
|
+
type: "schema",
|
|
674
|
+
preview: this.generateSchemaPreview(asl),
|
|
675
|
+
size: this.estimateSize(asl.entities),
|
|
676
|
+
});
|
|
677
|
+
// 2. Types file
|
|
678
|
+
files.push({
|
|
679
|
+
path: "src/types/schema.ts",
|
|
680
|
+
action: "create",
|
|
681
|
+
type: "type",
|
|
682
|
+
preview: this.generateTypesPreview(asl),
|
|
683
|
+
size: this.estimateSize(asl.entities) * 2,
|
|
684
|
+
});
|
|
685
|
+
// 3. Page files
|
|
686
|
+
asl.pages.forEach(page => {
|
|
687
|
+
const pagePath = this.pageSpecToPath(page);
|
|
688
|
+
files.push({
|
|
689
|
+
path: pagePath,
|
|
690
|
+
action: "create",
|
|
691
|
+
type: "page",
|
|
692
|
+
preview: `export default function ${page.name}() { ... }`,
|
|
693
|
+
size: 500,
|
|
694
|
+
});
|
|
695
|
+
});
|
|
696
|
+
// 4. Component files (estimate based on entities)
|
|
697
|
+
const componentCount = Object.keys(asl.entities).length * 3; // Card, List, Form per entity
|
|
698
|
+
for (let i = 0; i < componentCount; i++) {
|
|
699
|
+
files.push({
|
|
700
|
+
path: `src/components/.../${i}.tsx`,
|
|
701
|
+
action: "create",
|
|
702
|
+
type: "component",
|
|
703
|
+
size: 300,
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
// 5. Action files
|
|
707
|
+
Object.keys(asl.entities).forEach(entityName => {
|
|
708
|
+
files.push({
|
|
709
|
+
path: `src/actions/${entityName.toLowerCase()}.ts`,
|
|
710
|
+
action: "create",
|
|
711
|
+
type: "action",
|
|
712
|
+
preview: `'use server';\n\nexport async function create${entityName}(...) { ... }`,
|
|
713
|
+
size: 800,
|
|
714
|
+
});
|
|
715
|
+
});
|
|
716
|
+
// 6. Auth files (if auth is configured)
|
|
717
|
+
if (asl.auth) {
|
|
718
|
+
files.push({
|
|
719
|
+
path: "src/lib/guards.ts",
|
|
720
|
+
action: "create",
|
|
721
|
+
type: "config",
|
|
722
|
+
preview: "export function withAuthGuard(...) { ... }",
|
|
723
|
+
size: 600,
|
|
724
|
+
}, {
|
|
725
|
+
path: "src/lib/permissions.ts",
|
|
726
|
+
action: "create",
|
|
727
|
+
type: "config",
|
|
728
|
+
preview: "export function hasPermission(...) { ... }",
|
|
729
|
+
size: 400,
|
|
730
|
+
}, {
|
|
731
|
+
path: "middleware.ts",
|
|
732
|
+
action: "create",
|
|
733
|
+
type: "config",
|
|
734
|
+
preview: "export function middleware(request: NextRequest) { ... }",
|
|
735
|
+
size: 500,
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
// 7. Registry previews
|
|
739
|
+
registries.push({
|
|
740
|
+
type: "components",
|
|
741
|
+
added: Object.keys(asl.entities).flatMap(e => [
|
|
742
|
+
`${e}Card`,
|
|
743
|
+
`${e}List`,
|
|
744
|
+
`${e}Form`,
|
|
745
|
+
]),
|
|
746
|
+
modified: [],
|
|
747
|
+
removed: [],
|
|
748
|
+
});
|
|
749
|
+
registries.push({
|
|
750
|
+
type: "types",
|
|
751
|
+
added: Object.keys(asl.entities).flatMap(e => [e, `${e}Insert`, `${e}WithRelations`]),
|
|
752
|
+
modified: [],
|
|
753
|
+
removed: [],
|
|
754
|
+
});
|
|
755
|
+
if (asl.permissions) {
|
|
756
|
+
registries.push({
|
|
757
|
+
type: "permissions",
|
|
758
|
+
added: asl.permissions.map(p => `${p.role}:${p.resource}:${(p.actions || []).join(",")}`),
|
|
759
|
+
modified: [],
|
|
760
|
+
removed: [],
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
// 8. Warnings
|
|
764
|
+
if (!asl.auth && asl.pages.some(p => !p.public)) {
|
|
765
|
+
warnings.push("Some pages require authentication but no auth is configured");
|
|
766
|
+
}
|
|
767
|
+
// 9. Summary
|
|
768
|
+
const summary = {
|
|
769
|
+
totalFiles: files.length,
|
|
770
|
+
newFiles: files.length,
|
|
771
|
+
modifiedFiles: 0,
|
|
772
|
+
deletedFiles: 0,
|
|
773
|
+
linesAdded: files.reduce((sum, f) => sum + (f.size || 0) / 50, 0), // Rough estimate
|
|
774
|
+
linesRemoved: 0,
|
|
775
|
+
};
|
|
776
|
+
return {
|
|
777
|
+
summary,
|
|
778
|
+
files,
|
|
779
|
+
registries,
|
|
780
|
+
warnings,
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Check if ASL is complete enough for generation
|
|
785
|
+
*/
|
|
786
|
+
isComplete(asl) {
|
|
787
|
+
return (0, asl_1.isASLComplete)(asl);
|
|
788
|
+
}
|
|
789
|
+
// ============================================================================
|
|
790
|
+
// HELPER METHODS
|
|
791
|
+
// ============================================================================
|
|
792
|
+
generateSchemaPreview(asl) {
|
|
793
|
+
const entityNames = Object.keys(asl.entities).slice(0, 2).join(", ");
|
|
794
|
+
const entityCount = Object.keys(asl.entities).length;
|
|
795
|
+
const more = entityCount > 2 ? ` + ${entityCount - 2} more` : "";
|
|
796
|
+
return `import { i } from "@instantdb/core";
|
|
797
|
+
|
|
798
|
+
const schema = i.schema({
|
|
799
|
+
entities: {
|
|
800
|
+
${entityNames}${more} ...
|
|
801
|
+
}
|
|
802
|
+
});`;
|
|
803
|
+
}
|
|
804
|
+
generateTypesPreview(asl) {
|
|
805
|
+
const firstEntity = Object.keys(asl.entities)[0];
|
|
806
|
+
return `export interface ${firstEntity} {
|
|
807
|
+
id: string;
|
|
808
|
+
...
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
export type ${firstEntity}Insert = Omit<${firstEntity}, "id">;`;
|
|
812
|
+
}
|
|
813
|
+
pageSpecToPath(page) {
|
|
814
|
+
// Convert page spec to Next.js file path
|
|
815
|
+
const basePath = page.path === "/" ? "" : page.path;
|
|
816
|
+
return `src/app${basePath}/page.tsx`;
|
|
817
|
+
}
|
|
818
|
+
estimateSize(entities) {
|
|
819
|
+
// Rough estimate: 100 bytes per field
|
|
820
|
+
let total = 0;
|
|
821
|
+
Object.values(entities).forEach(entity => {
|
|
822
|
+
total += (entity.fields?.length || 0) * 100;
|
|
823
|
+
});
|
|
824
|
+
return total;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
exports.Planner = Planner;
|
|
828
|
+
//# sourceMappingURL=Planner.js.map
|