@saltcorn/copilot 0.8.1 → 0.8.3

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.
@@ -46,12 +46,19 @@ const task_tool = {
46
46
  type: "array",
47
47
  items: {
48
48
  type: "object",
49
- required: ["requirement", "priority"],
49
+ required: [
50
+ "name",
51
+ "description",
52
+ "priority",
53
+ "depends_on",
54
+ "task_type",
55
+ ],
50
56
  additionalProperties: false,
51
57
  properties: {
52
58
  name: {
53
59
  type: "string",
54
- description: "A short name for the task",
60
+ description:
61
+ "A short unique name for the task (snake_case). Every other task that depends on this task must use exactly this name in their depends_on array.",
55
62
  },
56
63
  description: {
57
64
  type: "string",
@@ -65,11 +72,17 @@ const task_tool = {
65
72
  depends_on: {
66
73
  type: "array",
67
74
  description:
68
- "The names of the tasks that must be completed before this tasks can be started",
75
+ "Names of tasks in THIS plan that must complete before this task starts. Every name listed here MUST exactly match the name of another task in this same plan_tasks call. Never reference a task name that is not present in the tasks array.",
69
76
  items: {
70
77
  type: "string",
71
78
  },
72
79
  },
80
+ task_type: {
81
+ type: "string",
82
+ enum: ["plugin", "data_model", "feature"],
83
+ description:
84
+ "plugin: specialized — installs a plugin from the Saltcorn plugin store. data_model: specialized — creates or modifies database tables/fields only. feature: broad catch-all — creates views, pages, triggers, workflows, or anything else not covered by the specialized types. Order: plugin tasks first, then data_model, then feature.",
85
+ },
73
86
  },
74
87
  },
75
88
  },
@@ -78,4 +91,4 @@ const task_tool = {
78
91
  },
79
92
  };
80
93
 
81
- module.exports = { requirements_tool,task_tool };
94
+ module.exports = { requirements_tool, task_tool };
@@ -36,14 +36,20 @@ const {
36
36
  const { getState } = require("@saltcorn/data/db/state");
37
37
  const renderLayout = require("@saltcorn/markup/layout");
38
38
  const { viewname } = require("./common");
39
- const { requirementsList, req_routes } = require("./requirements");
40
- const { showSchema, schema_routes } = require("./schema");
41
- const { makeTaskList, task_routes } = require("./tasks");
42
- const { errorList, error_routes } = require("./errors");
39
+ const { showSchema, schema_routes, schemaStaticScript } = require("./schema");
40
+ const { task_routes } = require("./tasks");
41
+ const {
42
+ errorList,
43
+ doCreateErrorFixTask,
44
+ error_routes,
45
+ errTableStaticHtml,
46
+ } = require("./errors");
47
+ const { req_routes, requirementsStaticScript } = require("./requirements");
43
48
  const { feedbackList, feedback_routes } = require("./feedback");
44
- const { progressList, progress_routes } = require("./progress");
49
+ const { progress_routes } = require("./progress");
45
50
  const { runNextTask } = require("./run_task");
46
- const { makeTaskChart } = require("./taskchart");
51
+ const { researchPanel, research_routes } = require("./research");
52
+ const { phasesPanel, phasesStaticScript, phase_routes } = require("./phases");
47
53
 
48
54
  const get_state_fields = () => [];
49
55
 
@@ -56,37 +62,14 @@ const makeSpecForm = async (req) => {
56
62
  });
57
63
 
58
64
  return new Form({
59
- blurb: "Provide a high-level description of the application",
65
+ blurb: "Describe the application you want to build",
60
66
  fields: [
61
67
  {
62
- name: "description",
63
- label: "Description",
64
- type: "String",
65
- fieldview: "textarea",
66
- },
67
- {
68
- name: "audience",
69
- label: "Audience",
70
- type: "String",
71
- fieldview: "textarea",
72
- },
73
- {
74
- name: "core_features",
75
- label: "Core features",
76
- type: "String",
77
- fieldview: "textarea",
78
- },
79
- {
80
- name: "out_of_scope",
81
- label: "Out of scope",
82
- type: "String",
83
- fieldview: "textarea",
84
- },
85
- {
86
- name: "visual_style",
87
- label: "Visual style",
68
+ name: "specification",
69
+ label: "Specification",
88
70
  type: "String",
89
71
  fieldview: "textarea",
72
+ attributes: { rows: 10 },
90
73
  },
91
74
  ],
92
75
  xhrSubmit: true,
@@ -95,14 +78,95 @@ const makeSpecForm = async (req) => {
95
78
  });
96
79
  };
97
80
 
81
+ const specDepsModal = `
82
+ <div class="modal fade" id="specDepsModal" tabindex="-1" aria-hidden="true">
83
+ <div class="modal-dialog">
84
+ <div class="modal-content">
85
+ <div class="modal-header">
86
+ <h5 class="modal-title">Specification changed</h5>
87
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
88
+ </div>
89
+ <div class="modal-body">
90
+ <p>The following items were generated from the previous specification
91
+ and may now be outdated. Select any you want to clear:</p>
92
+ <div id="spec-deps-checks"></div>
93
+ </div>
94
+ <div class="modal-footer">
95
+ <button type="button" class="btn btn-secondary" id="specDepsKeepBtn">Save (keep all)</button>
96
+ <button type="button" class="btn btn-primary" id="specDepsSaveBtn">Clear selected &amp; save</button>
97
+ </div>
98
+ </div>
99
+ </div>
100
+ </div>`;
101
+
102
+ const specDepsScript = (vn) =>
103
+ script(
104
+ domReady(`
105
+ const _specVn = ${JSON.stringify(vn)};
106
+ const form = document.querySelector('form[action*="submit_specs"]');
107
+ if (!form) return;
108
+ const btn = form.querySelector('button[onclick*="ajaxSubmitForm"]');
109
+ if (!btn) return;
110
+
111
+ const doSaveSpec = () => {
112
+ const data = {};
113
+ for (const [k, v] of new FormData(form)) data[k] = v;
114
+ view_post(_specVn, 'submit_specs', data);
115
+ };
116
+
117
+ // Remove Saltcorn's inline onclick and replace with our own handler
118
+ btn.removeAttribute('onclick');
119
+ btn.addEventListener('click', (e) => {
120
+ e.preventDefault();
121
+ e.stopPropagation();
122
+ view_post(_specVn, 'check_spec_dependencies', {}, (deps) => {
123
+ const items = [];
124
+ if (deps.hasResearch) items.push({ key: 'clearResearch', label: 'Research questions & answers' });
125
+ if (deps.hasRequirements) items.push({ key: 'clearRequirements', label: 'Requirements' });
126
+ if (deps.hasSchema) items.push({ key: 'clearSchema', label: 'Schema' });
127
+ if (deps.hasPhases) items.push({ key: 'clearPhases', label: 'Phases' });
128
+ if (deps.hasTasks) items.push({ key: 'clearTasks', label: 'Tasks' });
129
+ if (!items.length) { doSaveSpec(); return; }
130
+ document.getElementById('spec-deps-checks').innerHTML = items.map((item) =>
131
+ '<div class="form-check">' +
132
+ '<input class="form-check-input" type="checkbox" id="dep_' + item.key +
133
+ '" value="' + item.key + '" checked>' +
134
+ '<label class="form-check-label" for="dep_' + item.key + '">' + item.label + '</label>' +
135
+ '</div>'
136
+ ).join('');
137
+ new bootstrap.Modal(document.getElementById('specDepsModal')).show();
138
+ });
139
+ });
140
+
141
+ document.getElementById('specDepsSaveBtn').addEventListener('click', () => {
142
+ const modal = bootstrap.Modal.getInstance(document.getElementById('specDepsModal'));
143
+ const clearData = {};
144
+ document.querySelectorAll('#spec-deps-checks input:checked').forEach((cb) => {
145
+ clearData[cb.value] = '1';
146
+ });
147
+ modal.hide();
148
+ if (Object.keys(clearData).length) {
149
+ const formData = {};
150
+ for (const [k, v] of new FormData(form)) formData[k] = v;
151
+ view_post(_specVn, 'clear_and_save_spec', Object.assign(formData, clearData));
152
+ } else {
153
+ doSaveSpec();
154
+ }
155
+ });
156
+
157
+ document.getElementById('specDepsKeepBtn').addEventListener('click', () => {
158
+ bootstrap.Modal.getInstance(document.getElementById('specDepsModal')).hide();
159
+ doSaveSpec();
160
+ });
161
+ `)
162
+ );
163
+
98
164
  const run = async (table_id, viewname, cfg, state, { req, res }) => {
99
165
  const specForm = await makeSpecForm(req);
100
- const reqList = await requirementsList(req);
101
- const taskList = await makeTaskList(req);
166
+ const research = await researchPanel(req);
167
+ const phases = await phasesPanel(req);
102
168
  const errList = await errorList(req);
103
169
  const feedbacks = await feedbackList(req);
104
- const progress = await progressList(req);
105
- const taskChart = await makeTaskChart(req);
106
170
  const schema = await showSchema(req);
107
171
  const layout = {
108
172
  type: "tabs",
@@ -111,26 +175,40 @@ const run = async (table_id, viewname, cfg, state, { req, res }) => {
111
175
  lazyLoadViews: true,
112
176
  titles: [
113
177
  "Specification",
114
- "Requirements",
178
+ "Research",
179
+ "Phases",
115
180
  "Schema",
116
- "Tasks",
117
- "Task chart",
118
- "Progress",
119
181
  "Feedback",
120
182
  "Errors",
121
183
  ],
122
184
  contents: [
123
185
  {
124
186
  type: "blank",
125
- contents: div({ class: "mt-2" }, renderForm(specForm, req.csrfToken())),
187
+ contents: div(
188
+ { class: "mt-2" },
189
+ renderForm(specForm, req.csrfToken()),
190
+ specDepsModal,
191
+ specDepsScript(viewname)
192
+ ),
193
+ },
194
+ { type: "blank", contents: research },
195
+ { type: "blank", contents: div(phasesStaticScript, phases) },
196
+ {
197
+ type: "blank",
198
+ contents: div(
199
+ schemaStaticScript,
200
+ div({ id: "schema-list-area" }, schema)
201
+ ),
126
202
  },
127
- { type: "blank", contents: reqList },
128
- { type: "blank", contents: schema },
129
- { type: "blank", contents: taskList },
130
- { type: "blank", contents: taskChart },
131
- { type: "blank", contents: progress },
132
203
  { type: "blank", contents: feedbacks },
133
- { type: "blank", contents: errList },
204
+ {
205
+ type: "blank",
206
+ contents: div(
207
+ requirementsStaticScript,
208
+ errTableStaticHtml,
209
+ div({ id: "err-list-area" }, errList)
210
+ ),
211
+ },
134
212
  ],
135
213
  deeplink: true,
136
214
  tabsStyle: "Tabs",
@@ -144,6 +222,119 @@ const run = async (table_id, viewname, cfg, state, { req, res }) => {
144
222
  });
145
223
  };
146
224
 
225
+ const check_spec_dependencies = async (
226
+ table_id,
227
+ viewname,
228
+ config,
229
+ body,
230
+ { req, res }
231
+ ) => {
232
+ const hasResearch =
233
+ !!(await MetaData.findOne({
234
+ type: "CopilotConstructMgr",
235
+ name: "research_questions",
236
+ })) ||
237
+ !!(await MetaData.findOne({
238
+ type: "CopilotConstructMgr",
239
+ name: "research_answers",
240
+ }));
241
+ const hasRequirements =
242
+ (
243
+ await MetaData.find({
244
+ type: "CopilotConstructMgr",
245
+ name: "requirement",
246
+ })
247
+ ).length > 0;
248
+ const hasSchema = !!(await MetaData.findOne({
249
+ type: "CopilotConstructMgr",
250
+ name: "schema",
251
+ }));
252
+ const hasPhases =
253
+ (
254
+ await MetaData.find({
255
+ type: "CopilotConstructMgr",
256
+ name: "phase",
257
+ })
258
+ ).length > 0;
259
+ const hasTasks =
260
+ (
261
+ await MetaData.find({
262
+ type: "CopilotConstructMgr",
263
+ name: "task",
264
+ })
265
+ ).length > 0;
266
+ return {
267
+ json: { hasResearch, hasRequirements, hasPhases, hasSchema, hasTasks },
268
+ };
269
+ };
270
+
271
+ // Clears selected dependencies, saves the spec, and reloads — all in one round trip
272
+ const clear_and_save_spec = async (
273
+ table_id,
274
+ viewname,
275
+ config,
276
+ body,
277
+ { req, res }
278
+ ) => {
279
+ const {
280
+ _csrf,
281
+ clearResearch,
282
+ clearRequirements,
283
+ clearSchema,
284
+ clearPhases,
285
+ clearTasks,
286
+ ...specBody
287
+ } = body;
288
+ if (clearResearch) {
289
+ for (const name of ["research_questions", "research_answers"]) {
290
+ const md = await MetaData.findOne({ type: "CopilotConstructMgr", name });
291
+ if (md) await md.delete();
292
+ }
293
+ }
294
+ if (clearRequirements) {
295
+ const rs = await MetaData.find({
296
+ type: "CopilotConstructMgr",
297
+ name: "requirement",
298
+ });
299
+ for (const r of rs) await r.delete();
300
+ }
301
+ if (clearSchema) {
302
+ const md = await MetaData.findOne({
303
+ type: "CopilotConstructMgr",
304
+ name: "schema",
305
+ });
306
+ if (md) await md.delete();
307
+ }
308
+ if (clearTasks) {
309
+ const ts = await MetaData.find({
310
+ type: "CopilotConstructMgr",
311
+ name: "task",
312
+ });
313
+ for (const t of ts) await t.delete();
314
+ }
315
+ if (clearPhases) {
316
+ const ps = await MetaData.find({
317
+ type: "CopilotConstructMgr",
318
+ name: "phase",
319
+ });
320
+ for (const ph of ps) await ph.delete();
321
+ }
322
+ const existing = await MetaData.findOne({
323
+ type: "CopilotConstructMgr",
324
+ name: "spec",
325
+ });
326
+ if (existing)
327
+ await db.update("_sc_metadata", { body: specBody }, existing.id);
328
+ else
329
+ await MetaData.create({
330
+ type: "CopilotConstructMgr",
331
+ name: "spec",
332
+ user_id: req.user?.id || undefined,
333
+ body: specBody,
334
+ });
335
+ return { json: { reload_page: true } };
336
+ };
337
+
147
338
  const submit_specs = async (table_id, viewname, config, body, { req, res }) => {
148
339
  const { _csrf, ...spec } = body;
149
340
  const existing = await MetaData.findOne({
@@ -159,6 +350,42 @@ const submit_specs = async (table_id, viewname, config, body, { req, res }) => {
159
350
  user_id: req.user?.id || undefined,
160
351
  body: spec,
161
352
  });
353
+ return {
354
+ json: {
355
+ success: "ok",
356
+ notify: "Specification saved",
357
+ notify_type: "success",
358
+ },
359
+ };
360
+ };
361
+
362
+ // Creates fix tasks for application errors recorded while self-healing was off.
363
+ const healPendingErrors = async () => {
364
+ const settings = await MetaData.findOne({
365
+ type: "CopilotConstructMgr",
366
+ name: "settings",
367
+ });
368
+ if (!settings?.body?.error_heal) return;
369
+ const spec = await MetaData.findOne({
370
+ type: "CopilotConstructMgr",
371
+ name: "spec",
372
+ });
373
+ if (!spec) return;
374
+ const errors = await MetaData.find({
375
+ type: "CopilotConstructMgr",
376
+ name: "error",
377
+ });
378
+ for (const err of errors) {
379
+ if (
380
+ err.body.source !== "application" ||
381
+ err.body.fix_task_created ||
382
+ err.body.cannot_fix_reason
383
+ )
384
+ continue;
385
+ doCreateErrorFixTask(err, null).catch((e) =>
386
+ console.error("healPendingErrors failed", e)
387
+ );
388
+ }
162
389
  };
163
390
 
164
391
  const virtual_triggers = () => {
@@ -170,20 +397,49 @@ const virtual_triggers = () => {
170
397
  type: "CopilotConstructMgr",
171
398
  name: "error",
172
399
  });
173
- const messages = new Set(existing.map((m) => m.body?.error?.stack));
174
- if (!messages.has(row.stack))
175
- await MetaData.create({
400
+ // Allow a new record once a fix task exists — same error recurring after a fix gets another task.
401
+ const unfixedDuplicate =
402
+ row.stack &&
403
+ existing.find(
404
+ (m) =>
405
+ m.body?.error?.stack === row.stack && !m.body?.fix_task_created
406
+ );
407
+ if (unfixedDuplicate) return;
408
+
409
+ const source = (row.stack || "").includes("/app-constructor/")
410
+ ? "constructor"
411
+ : "application";
412
+
413
+ const errorMd = await MetaData.create({
414
+ type: "CopilotConstructMgr",
415
+ name: "error",
416
+ body: { status: "New", error: row, source },
417
+ user_id: null,
418
+ });
419
+
420
+ if (source === "application") {
421
+ const settings = await MetaData.findOne({
176
422
  type: "CopilotConstructMgr",
177
- name: "error",
178
- body: { status: "New", error: row },
179
- user_id: null,
423
+ name: "settings",
180
424
  });
425
+ if (settings?.body?.error_heal) {
426
+ const spec = await MetaData.findOne({
427
+ type: "CopilotConstructMgr",
428
+ name: "spec",
429
+ });
430
+ if (spec)
431
+ doCreateErrorFixTask(errorMd, null).catch((e) =>
432
+ console.error("auto error fix task failed", e)
433
+ );
434
+ }
435
+ }
181
436
  },
182
437
  },
183
438
  {
184
439
  when_trigger: "Often",
185
440
  run: async () => {
186
441
  await runNextTask();
442
+ await healPendingErrors();
187
443
  },
188
444
  },
189
445
  ];
@@ -198,12 +454,16 @@ module.exports = {
198
454
  run,
199
455
  routes: {
200
456
  submit_specs,
457
+ check_spec_dependencies,
458
+ clear_and_save_spec,
201
459
  ...req_routes,
460
+ ...research_routes,
202
461
  ...task_routes,
203
462
  ...error_routes,
204
463
  ...feedback_routes,
205
464
  ...progress_routes,
206
465
  ...schema_routes,
466
+ ...phase_routes,
207
467
  },
208
468
  virtual_triggers,
209
469
  };