@mcoda/core 0.1.24 → 0.1.27
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/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/services/planning/CreateTasksService.d.ts +9 -0
- package/dist/services/planning/CreateTasksService.d.ts.map +1 -1
- package/dist/services/planning/CreateTasksService.js +453 -92
- package/dist/services/planning/RefineTasksService.d.ts.map +1 -1
- package/dist/services/planning/RefineTasksService.js +89 -15
- package/dist/services/planning/TaskSufficiencyService.d.ts +74 -0
- package/dist/services/planning/TaskSufficiencyService.d.ts.map +1 -0
- package/dist/services/planning/TaskSufficiencyService.js +908 -0
- package/package.json +6 -6
|
@@ -12,6 +12,7 @@ import { classifyTask } from "../backlog/TaskOrderingHeuristics.js";
|
|
|
12
12
|
import { TaskOrderingService } from "../backlog/TaskOrderingService.js";
|
|
13
13
|
import { QaTestCommandBuilder } from "../execution/QaTestCommandBuilder.js";
|
|
14
14
|
import { createEpicKeyGenerator, createStoryKeyGenerator, createTaskKeyGenerator, } from "./KeyHelpers.js";
|
|
15
|
+
import { TaskSufficiencyService } from "./TaskSufficiencyService.js";
|
|
15
16
|
const formatBullets = (items, fallback) => {
|
|
16
17
|
if (!items || items.length === 0)
|
|
17
18
|
return `- ${fallback}`;
|
|
@@ -116,6 +117,33 @@ const formatTestList = (items) => {
|
|
|
116
117
|
return items.join("; ");
|
|
117
118
|
};
|
|
118
119
|
const ensureNonEmpty = (value, fallback) => value && value.trim().length > 0 ? value.trim() : fallback;
|
|
120
|
+
const normalizeTaskLine = (line) => line.replace(/^[-*]\s+/, "").trim();
|
|
121
|
+
const looksLikeSectionHeader = (line) => /^\* \*\*.+\*\*$/.test(line.trim());
|
|
122
|
+
const isReferenceOnlyLine = (line) => /^(epic|story|references?|related docs?|inputs?|objective|context|implementation plan|definition of done|testing & qa)\s*:/i.test(line.trim());
|
|
123
|
+
const extractActionableLines = (description, limit) => {
|
|
124
|
+
if (!description)
|
|
125
|
+
return [];
|
|
126
|
+
const lines = description
|
|
127
|
+
.split(/\r?\n/)
|
|
128
|
+
.map((line) => normalizeTaskLine(line))
|
|
129
|
+
.filter(Boolean)
|
|
130
|
+
.filter((line) => !looksLikeSectionHeader(line))
|
|
131
|
+
.filter((line) => !isReferenceOnlyLine(line));
|
|
132
|
+
const actionable = lines.filter((line) => /^(?:\d+[.)]\s+|implement\b|create\b|update\b|add\b|define\b|wire\b|integrate\b|enforce\b|publish\b|configure\b|materialize\b|validate\b|verify\b)/i.test(line));
|
|
133
|
+
const source = actionable.length > 0 ? actionable : lines;
|
|
134
|
+
return uniqueStrings(source.slice(0, limit));
|
|
135
|
+
};
|
|
136
|
+
const extractRiskLines = (description, limit) => {
|
|
137
|
+
if (!description)
|
|
138
|
+
return [];
|
|
139
|
+
const lines = description
|
|
140
|
+
.split(/\r?\n/)
|
|
141
|
+
.map((line) => normalizeTaskLine(line))
|
|
142
|
+
.filter(Boolean)
|
|
143
|
+
.filter((line) => !looksLikeSectionHeader(line));
|
|
144
|
+
const risks = lines.filter((line) => /\b(risk|edge case|gotcha|constraint|failure|flaky|drift|regression)\b/i.test(line));
|
|
145
|
+
return uniqueStrings(risks.slice(0, limit));
|
|
146
|
+
};
|
|
119
147
|
const extractScriptPort = (script) => {
|
|
120
148
|
const matches = [script.match(/(?:--port|-p)\s*(\d{2,5})/), script.match(/PORT\s*=\s*(\d{2,5})/)];
|
|
121
149
|
for (const match of matches) {
|
|
@@ -129,8 +157,18 @@ const extractScriptPort = (script) => {
|
|
|
129
157
|
};
|
|
130
158
|
const estimateTokens = (text) => Math.max(1, Math.ceil(text.length / 4));
|
|
131
159
|
const DOC_CONTEXT_BUDGET = 8000;
|
|
160
|
+
const DOC_CONTEXT_SEGMENTS_PER_DOC = 8;
|
|
161
|
+
const DOC_CONTEXT_FALLBACK_CHUNK_LENGTH = 480;
|
|
162
|
+
const SDS_COVERAGE_HINT_HEADING_LIMIT = 24;
|
|
163
|
+
const SDS_COVERAGE_REPORT_SECTION_LIMIT = 80;
|
|
132
164
|
const OPENAPI_HINT_OPERATIONS_LIMIT = 30;
|
|
133
165
|
const DOCDEX_HANDLE = /^docdex:/i;
|
|
166
|
+
const DOCDEX_LOCAL_HANDLE = /^docdex:local[-:/]/i;
|
|
167
|
+
const RELATED_DOC_PATH_PATTERN = /^(?:~\/|\/|[A-Za-z]:[\\/]|[A-Za-z0-9._-]+\/)[A-Za-z0-9._/-]+(?:\.[A-Za-z0-9._-]+)?(?:#[A-Za-z0-9._:-]+)?$/;
|
|
168
|
+
const RELATIVE_DOC_PATH_PATTERN = /^(?:\.{1,2}\/)+[A-Za-z0-9._/-]+(?:\.[A-Za-z0-9._-]+)?(?:#[A-Za-z0-9._:-]+)?$/;
|
|
169
|
+
const FUZZY_DOC_CANDIDATE_LIMIT = 64;
|
|
170
|
+
const DEPENDENCY_SCAN_LINE_LIMIT = 1400;
|
|
171
|
+
const STARTUP_WAVE_SCAN_LINE_LIMIT = 4000;
|
|
134
172
|
const VALID_AREAS = new Set(["web", "adm", "bck", "ops", "infra", "mobile"]);
|
|
135
173
|
const VALID_TASK_TYPES = new Set(["feature", "bug", "chore", "spike"]);
|
|
136
174
|
const inferDocType = (filePath) => {
|
|
@@ -176,16 +214,85 @@ const normalizeTaskType = (value) => {
|
|
|
176
214
|
const normalizeRelatedDocs = (value) => {
|
|
177
215
|
if (!Array.isArray(value))
|
|
178
216
|
return [];
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
217
|
+
const seen = new Set();
|
|
218
|
+
const normalized = [];
|
|
219
|
+
for (const entry of value) {
|
|
220
|
+
const candidate = typeof entry === "string"
|
|
221
|
+
? entry.trim()
|
|
222
|
+
: entry && typeof entry === "object" && "handle" in entry && typeof entry.handle === "string"
|
|
223
|
+
? entry.handle.trim()
|
|
224
|
+
: "";
|
|
225
|
+
if (!candidate)
|
|
226
|
+
continue;
|
|
227
|
+
if (DOCDEX_LOCAL_HANDLE.test(candidate))
|
|
228
|
+
continue;
|
|
229
|
+
const isDocHandle = DOCDEX_HANDLE.test(candidate);
|
|
230
|
+
const isHttp = /^https?:\/\/\S+$/i.test(candidate);
|
|
231
|
+
const isPath = RELATED_DOC_PATH_PATTERN.test(candidate) || RELATIVE_DOC_PATH_PATTERN.test(candidate);
|
|
232
|
+
if (!isDocHandle && !isHttp && !isPath)
|
|
233
|
+
continue;
|
|
234
|
+
if (seen.has(candidate))
|
|
235
|
+
continue;
|
|
236
|
+
seen.add(candidate);
|
|
237
|
+
normalized.push(candidate);
|
|
238
|
+
}
|
|
239
|
+
return normalized;
|
|
240
|
+
};
|
|
241
|
+
const extractMarkdownHeadings = (value, limit) => {
|
|
242
|
+
if (!value)
|
|
243
|
+
return [];
|
|
244
|
+
const lines = value.split(/\r?\n/);
|
|
245
|
+
const headings = [];
|
|
246
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
247
|
+
const line = lines[index]?.trim() ?? "";
|
|
248
|
+
if (!line)
|
|
249
|
+
continue;
|
|
250
|
+
const hashHeading = line.match(/^#{1,6}\s+(.+)$/);
|
|
251
|
+
if (hashHeading) {
|
|
252
|
+
headings.push(hashHeading[1].trim());
|
|
253
|
+
}
|
|
254
|
+
else if (index + 1 < lines.length &&
|
|
255
|
+
/^[=-]{3,}\s*$/.test((lines[index + 1] ?? "").trim()) &&
|
|
256
|
+
!line.startsWith("-") &&
|
|
257
|
+
!line.startsWith("*")) {
|
|
258
|
+
headings.push(line);
|
|
259
|
+
}
|
|
260
|
+
if (headings.length >= limit)
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
return uniqueStrings(headings
|
|
264
|
+
.map((entry) => entry.replace(/[`*_]/g, "").trim())
|
|
265
|
+
.filter(Boolean));
|
|
266
|
+
};
|
|
267
|
+
const pickDistributedIndices = (length, limit) => {
|
|
268
|
+
if (length <= 0 || limit <= 0)
|
|
269
|
+
return [];
|
|
270
|
+
if (length <= limit)
|
|
271
|
+
return Array.from({ length }, (_, index) => index);
|
|
272
|
+
const selected = new Set();
|
|
273
|
+
for (let index = 0; index < limit; index += 1) {
|
|
274
|
+
const ratio = limit === 1 ? 0 : index / (limit - 1);
|
|
275
|
+
selected.add(Math.round(ratio * (length - 1)));
|
|
276
|
+
}
|
|
277
|
+
return Array.from(selected)
|
|
278
|
+
.sort((a, b) => a - b)
|
|
279
|
+
.slice(0, limit);
|
|
280
|
+
};
|
|
281
|
+
const sampleRawContent = (value, chunkLength) => {
|
|
282
|
+
if (!value)
|
|
283
|
+
return [];
|
|
284
|
+
const content = value.trim();
|
|
285
|
+
if (!content)
|
|
286
|
+
return [];
|
|
287
|
+
if (content.length <= chunkLength)
|
|
288
|
+
return [content];
|
|
289
|
+
const anchors = [
|
|
290
|
+
0,
|
|
291
|
+
Math.max(0, Math.floor(content.length / 2) - Math.floor(chunkLength / 2)),
|
|
292
|
+
Math.max(0, content.length - chunkLength),
|
|
293
|
+
];
|
|
294
|
+
const sampled = anchors.map((anchor) => content.slice(anchor, anchor + chunkLength).trim()).filter(Boolean);
|
|
295
|
+
return uniqueStrings(sampled);
|
|
189
296
|
};
|
|
190
297
|
const isPlainObject = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
191
298
|
const parseStructuredDoc = (raw) => {
|
|
@@ -291,58 +398,50 @@ const extractJsonObjects = (value) => {
|
|
|
291
398
|
}
|
|
292
399
|
return results;
|
|
293
400
|
};
|
|
401
|
+
const compactNarrative = (value, fallback, maxLines = 5) => {
|
|
402
|
+
if (!value || value.trim().length === 0)
|
|
403
|
+
return fallback;
|
|
404
|
+
const lines = value
|
|
405
|
+
.split(/\r?\n/)
|
|
406
|
+
.map((line) => line
|
|
407
|
+
.replace(/^[*-]\s+/, "")
|
|
408
|
+
.replace(/^#+\s+/, "")
|
|
409
|
+
.replace(/^\*+\s*\*\*(.+?)\*\*\s*$/, "$1")
|
|
410
|
+
.trim())
|
|
411
|
+
.filter(Boolean)
|
|
412
|
+
.filter((line) => !/^(in scope|out of scope|key flows?|non-functional requirements|dependencies|risks|acceptance criteria|related docs?)\s*:/i.test(line))
|
|
413
|
+
.slice(0, maxLines);
|
|
414
|
+
return lines.length > 0 ? lines.join("\n") : fallback;
|
|
415
|
+
};
|
|
294
416
|
const buildEpicDescription = (epicKey, title, description, acceptance, relatedDocs) => {
|
|
417
|
+
const context = compactNarrative(description, `Deliver ${title} with implementation-ready scope and sequencing aligned to SDS guidance.`, 6);
|
|
295
418
|
return [
|
|
296
419
|
`* **Epic Key**: ${epicKey}`,
|
|
297
420
|
`* **Epic Title**: ${title}`,
|
|
298
421
|
"* **Context / Problem**",
|
|
299
422
|
"",
|
|
300
|
-
|
|
301
|
-
"* **Goals & Outcomes**",
|
|
302
|
-
formatBullets(acceptance, "List measurable outcomes for this epic."),
|
|
303
|
-
"* **In Scope**",
|
|
304
|
-
"- Clarify during refinement; derived from RFP/PDR/SDS.",
|
|
305
|
-
"* **Out of Scope**",
|
|
306
|
-
"- To be defined; exclude unrelated systems.",
|
|
307
|
-
"* **Key Flows / Scenarios**",
|
|
308
|
-
"- Outline primary user flows for this epic.",
|
|
309
|
-
"* **Non-functional Requirements**",
|
|
310
|
-
"- Performance, security, reliability expectations go here.",
|
|
311
|
-
"* **Dependencies & Constraints**",
|
|
312
|
-
"- Capture upstream/downstream systems and blockers.",
|
|
313
|
-
"* **Risks & Open Questions**",
|
|
314
|
-
"- Identify risks and unknowns to resolve.",
|
|
423
|
+
context,
|
|
315
424
|
"* **Acceptance Criteria**",
|
|
316
|
-
formatBullets(acceptance, "
|
|
425
|
+
formatBullets(acceptance, "Define measurable and testable outcomes for this epic."),
|
|
317
426
|
"* **Related Documentation / References**",
|
|
318
|
-
formatBullets(relatedDocs, "Link
|
|
427
|
+
formatBullets(relatedDocs, "Link SDS/PDR/OpenAPI references used by this epic."),
|
|
319
428
|
].join("\n");
|
|
320
429
|
};
|
|
321
430
|
const buildStoryDescription = (storyKey, title, userStory, description, acceptanceCriteria, relatedDocs) => {
|
|
431
|
+
const userStoryText = compactNarrative(userStory, `As a user, I want ${title} so that it delivers clear product value.`, 3);
|
|
432
|
+
const contextText = compactNarrative(description, `Implement ${title} with concrete scope and dependency context.`, 5);
|
|
322
433
|
return [
|
|
323
434
|
`* **Story Key**: ${storyKey}`,
|
|
324
435
|
"* **User Story**",
|
|
325
436
|
"",
|
|
326
|
-
|
|
437
|
+
userStoryText,
|
|
327
438
|
"* **Context**",
|
|
328
439
|
"",
|
|
329
|
-
|
|
330
|
-
"* **Preconditions / Assumptions**",
|
|
331
|
-
"- Confirm required data, environments, and access.",
|
|
332
|
-
"* **Main Flow**",
|
|
333
|
-
"- Outline the happy path for this story.",
|
|
334
|
-
"* **Alternative / Error Flows**",
|
|
335
|
-
"- Capture error handling and non-happy paths.",
|
|
336
|
-
"* **UX / UI Notes**",
|
|
337
|
-
"- Enumerate screens/states if applicable.",
|
|
338
|
-
"* **Data & Integrations**",
|
|
339
|
-
"- Note key entities, APIs, queues, or third-party dependencies.",
|
|
440
|
+
contextText,
|
|
340
441
|
"* **Acceptance Criteria**",
|
|
341
442
|
formatBullets(acceptanceCriteria, "List testable outcomes for this story."),
|
|
342
|
-
"* **Non-functional Requirements**",
|
|
343
|
-
"- Add story-specific performance/reliability/security expectations.",
|
|
344
443
|
"* **Related Documentation / References**",
|
|
345
|
-
formatBullets(relatedDocs, "Docdex handles, OpenAPI endpoints, code modules."),
|
|
444
|
+
formatBullets(relatedDocs, "Docdex handles, OpenAPI endpoints, and code modules."),
|
|
346
445
|
].join("\n");
|
|
347
446
|
};
|
|
348
447
|
const buildTaskDescription = (taskKey, title, description, storyKey, epicKey, relatedDocs, dependencies, tests, qa) => {
|
|
@@ -356,21 +455,48 @@ const buildTaskDescription = (taskKey, title, description, storyKey, epicKey, re
|
|
|
356
455
|
})
|
|
357
456
|
.join("\n");
|
|
358
457
|
};
|
|
458
|
+
const objectiveText = compactNarrative(description, `Deliver ${title} for story ${storyKey}.`, 3);
|
|
459
|
+
const implementationLines = extractActionableLines(description, 4);
|
|
460
|
+
const riskLines = extractRiskLines(description, 3);
|
|
461
|
+
const testsDefined = (tests.unitTests?.length ?? 0) +
|
|
462
|
+
(tests.componentTests?.length ?? 0) +
|
|
463
|
+
(tests.integrationTests?.length ?? 0) +
|
|
464
|
+
(tests.apiTests?.length ?? 0) >
|
|
465
|
+
0;
|
|
466
|
+
const definitionOfDone = [
|
|
467
|
+
`- Implementation for \`${taskKey}\` is complete and scoped to ${storyKey}.`,
|
|
468
|
+
testsDefined
|
|
469
|
+
? "- Task-specific tests are added/updated and green in the task validation loop."
|
|
470
|
+
: "- Verification evidence is captured in task logs/checklists for this scope.",
|
|
471
|
+
relatedDocs?.length
|
|
472
|
+
? "- Related contracts/docs are consistent with delivered behavior."
|
|
473
|
+
: "- Documentation impact is reviewed and no additional contract docs are required.",
|
|
474
|
+
qa?.blockers?.length ? "- Remaining QA blockers are explicit and actionable." : "- QA blockers are resolved or not present.",
|
|
475
|
+
];
|
|
476
|
+
const defaultImplementationPlan = [
|
|
477
|
+
`Implement ${title} with concrete file/module-level changes aligned to the objective.`,
|
|
478
|
+
dependencies.length
|
|
479
|
+
? `Respect dependency order before completion: ${dependencies.join(", ")}.`
|
|
480
|
+
: "Finalize concrete implementation steps before coding and keep scope bounded.",
|
|
481
|
+
];
|
|
482
|
+
const defaultRisks = dependencies.length
|
|
483
|
+
? [`Delivery depends on upstream tasks: ${dependencies.join(", ")}.`]
|
|
484
|
+
: ["Keep implementation aligned to SDS/OpenAPI contracts to avoid drift."];
|
|
359
485
|
return [
|
|
360
486
|
`* **Task Key**: ${taskKey}`,
|
|
361
487
|
"* **Objective**",
|
|
362
488
|
"",
|
|
363
|
-
|
|
489
|
+
objectiveText,
|
|
364
490
|
"* **Context**",
|
|
365
491
|
"",
|
|
366
492
|
`- Epic: ${epicKey}`,
|
|
367
493
|
`- Story: ${storyKey}`,
|
|
368
494
|
"* **Inputs**",
|
|
369
|
-
formatBullets(relatedDocs, "
|
|
495
|
+
formatBullets(relatedDocs, "No explicit external references."),
|
|
370
496
|
"* **Implementation Plan**",
|
|
371
|
-
|
|
497
|
+
formatBullets(implementationLines, defaultImplementationPlan.join(" ")),
|
|
372
498
|
"* **Definition of Done**",
|
|
373
|
-
|
|
499
|
+
definitionOfDone.join("\n"),
|
|
374
500
|
"* **Testing & QA**",
|
|
375
501
|
`- Unit tests: ${formatTestList(tests.unitTests)}`,
|
|
376
502
|
`- Component tests: ${formatTestList(tests.componentTests)}`,
|
|
@@ -385,11 +511,11 @@ const buildTaskDescription = (taskKey, title, description, storyKey, epicKey, re
|
|
|
385
511
|
"* **QA Blockers**",
|
|
386
512
|
formatBullets(qa?.blockers, "None known."),
|
|
387
513
|
"* **Dependencies**",
|
|
388
|
-
formatBullets(dependencies, "
|
|
514
|
+
formatBullets(dependencies, "None."),
|
|
389
515
|
"* **Risks & Gotchas**",
|
|
390
|
-
|
|
516
|
+
formatBullets(riskLines, defaultRisks.join(" ")),
|
|
391
517
|
"* **Related Documentation / References**",
|
|
392
|
-
formatBullets(relatedDocs, "
|
|
518
|
+
formatBullets(relatedDocs, "None."),
|
|
393
519
|
].join("\n");
|
|
394
520
|
};
|
|
395
521
|
const collectFilesRecursively = async (target) => {
|
|
@@ -594,6 +720,7 @@ export class CreateTasksService {
|
|
|
594
720
|
this.routingService = deps.routingService;
|
|
595
721
|
this.ratingService = deps.ratingService;
|
|
596
722
|
this.taskOrderingFactory = deps.taskOrderingFactory ?? TaskOrderingService.create;
|
|
723
|
+
this.taskSufficiencyFactory = deps.taskSufficiencyFactory ?? TaskSufficiencyService.create;
|
|
597
724
|
}
|
|
598
725
|
static async create(workspace) {
|
|
599
726
|
const repo = await GlobalRepository.create();
|
|
@@ -614,6 +741,7 @@ export class CreateTasksService {
|
|
|
614
741
|
repo,
|
|
615
742
|
workspaceRepo,
|
|
616
743
|
routingService,
|
|
744
|
+
taskSufficiencyFactory: TaskSufficiencyService.create,
|
|
617
745
|
});
|
|
618
746
|
}
|
|
619
747
|
async close() {
|
|
@@ -903,7 +1031,7 @@ export class CreateTasksService {
|
|
|
903
1031
|
}
|
|
904
1032
|
return ranked
|
|
905
1033
|
.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path))
|
|
906
|
-
.slice(0,
|
|
1034
|
+
.slice(0, FUZZY_DOC_CANDIDATE_LIMIT)
|
|
907
1035
|
.map((entry) => entry.path);
|
|
908
1036
|
}
|
|
909
1037
|
normalizeStructurePathToken(value) {
|
|
@@ -1072,7 +1200,7 @@ export class CreateTasksService {
|
|
|
1072
1200
|
.split(/\r?\n/)
|
|
1073
1201
|
.map((line) => line.trim())
|
|
1074
1202
|
.filter(Boolean)
|
|
1075
|
-
.slice(0,
|
|
1203
|
+
.slice(0, DEPENDENCY_SCAN_LINE_LIMIT);
|
|
1076
1204
|
const dependencyPatterns = [
|
|
1077
1205
|
{
|
|
1078
1206
|
regex: /^(.+?)\b(?:depends on|requires|needs|uses|consumes|calls|reads from|writes to|must come after|comes after|built after|runs after|backed by)\b(.+)$/i,
|
|
@@ -1156,7 +1284,7 @@ export class CreateTasksService {
|
|
|
1156
1284
|
.split(/\r?\n/)
|
|
1157
1285
|
.map((line) => line.trim())
|
|
1158
1286
|
.filter(Boolean)
|
|
1159
|
-
.slice(0,
|
|
1287
|
+
.slice(0, STARTUP_WAVE_SCAN_LINE_LIMIT);
|
|
1160
1288
|
for (const line of lines) {
|
|
1161
1289
|
if (!line.startsWith("|"))
|
|
1162
1290
|
continue;
|
|
@@ -1353,6 +1481,21 @@ export class CreateTasksService {
|
|
|
1353
1481
|
"5) Keep task dependencies story-scoped while preserving epic/story/task ordering by this build method.",
|
|
1354
1482
|
].join("\n");
|
|
1355
1483
|
}
|
|
1484
|
+
buildProjectPlanArtifact(projectKey, docs, graph, buildMethod) {
|
|
1485
|
+
const sourceDocs = docs
|
|
1486
|
+
.map((doc) => doc.path ?? (doc.id ? `docdex:${doc.id}` : doc.title ?? "doc"))
|
|
1487
|
+
.filter((value) => Boolean(value))
|
|
1488
|
+
.slice(0, 24);
|
|
1489
|
+
return {
|
|
1490
|
+
projectKey,
|
|
1491
|
+
generatedAt: new Date().toISOString(),
|
|
1492
|
+
sourceDocs,
|
|
1493
|
+
startupWaves: graph.startupWaves.slice(0, 12),
|
|
1494
|
+
services: graph.services.slice(0, 40),
|
|
1495
|
+
foundationalDependencies: graph.foundationalDependencies.slice(0, 16),
|
|
1496
|
+
buildMethod,
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1356
1499
|
orderStoryTasksByDependencies(storyTasks, serviceRank, taskServiceByScope) {
|
|
1357
1500
|
const byLocalId = new Map(storyTasks.map((task) => [task.localId, task]));
|
|
1358
1501
|
const indegree = new Map();
|
|
@@ -1949,23 +2092,59 @@ export class CreateTasksService {
|
|
|
1949
2092
|
}
|
|
1950
2093
|
return lines.join("\n");
|
|
1951
2094
|
}
|
|
2095
|
+
extractSdsSectionCandidates(docs, limit) {
|
|
2096
|
+
const sections = [];
|
|
2097
|
+
for (const doc of docs) {
|
|
2098
|
+
if (!looksLikeSdsDoc(doc))
|
|
2099
|
+
continue;
|
|
2100
|
+
const segmentHeadings = (doc.segments ?? [])
|
|
2101
|
+
.map((segment) => segment.heading?.trim())
|
|
2102
|
+
.filter((heading) => Boolean(heading));
|
|
2103
|
+
const contentHeadings = extractMarkdownHeadings(doc.content ?? "", limit);
|
|
2104
|
+
for (const heading of [...segmentHeadings, ...contentHeadings]) {
|
|
2105
|
+
const normalized = heading.replace(/[`*_]/g, "").trim();
|
|
2106
|
+
if (!normalized)
|
|
2107
|
+
continue;
|
|
2108
|
+
sections.push(normalized);
|
|
2109
|
+
if (sections.length >= limit)
|
|
2110
|
+
break;
|
|
2111
|
+
}
|
|
2112
|
+
if (sections.length >= limit)
|
|
2113
|
+
break;
|
|
2114
|
+
}
|
|
2115
|
+
return uniqueStrings(sections).slice(0, limit);
|
|
2116
|
+
}
|
|
2117
|
+
buildSdsCoverageHints(docs) {
|
|
2118
|
+
const hints = this.extractSdsSectionCandidates(docs, SDS_COVERAGE_HINT_HEADING_LIMIT);
|
|
2119
|
+
if (hints.length === 0)
|
|
2120
|
+
return "";
|
|
2121
|
+
return hints.map((hint) => `- ${hint}`).join("\n");
|
|
2122
|
+
}
|
|
1952
2123
|
buildDocContext(docs) {
|
|
1953
2124
|
const warnings = [];
|
|
1954
2125
|
const blocks = [];
|
|
1955
2126
|
let budget = DOC_CONTEXT_BUDGET;
|
|
1956
|
-
const sorted = [...docs].sort((a, b) =>
|
|
2127
|
+
const sorted = [...docs].sort((a, b) => {
|
|
2128
|
+
const sdsDelta = Number(looksLikeSdsDoc(b)) - Number(looksLikeSdsDoc(a));
|
|
2129
|
+
if (sdsDelta !== 0)
|
|
2130
|
+
return sdsDelta;
|
|
2131
|
+
return (b.updatedAt ?? "").localeCompare(a.updatedAt ?? "");
|
|
2132
|
+
});
|
|
1957
2133
|
for (const [idx, doc] of sorted.entries()) {
|
|
1958
|
-
const segments =
|
|
1959
|
-
const
|
|
1960
|
-
|
|
2134
|
+
const segments = doc.segments ?? [];
|
|
2135
|
+
const sampledSegments = pickDistributedIndices(segments.length, DOC_CONTEXT_SEGMENTS_PER_DOC)
|
|
2136
|
+
.map((index) => segments[index])
|
|
2137
|
+
.filter(Boolean);
|
|
2138
|
+
const content = sampledSegments.length
|
|
2139
|
+
? sampledSegments
|
|
1961
2140
|
.map((seg, i) => {
|
|
1962
2141
|
const trimmed = seg.content.length > 600 ? `${seg.content.slice(0, 600)}...` : seg.content;
|
|
1963
2142
|
return ` - (${i + 1}) ${seg.heading ? `${seg.heading}: ` : ""}${trimmed}`;
|
|
1964
2143
|
})
|
|
1965
2144
|
.join("\n")
|
|
1966
|
-
: doc.content
|
|
1967
|
-
?
|
|
1968
|
-
|
|
2145
|
+
: sampleRawContent(doc.content, DOC_CONTEXT_FALLBACK_CHUNK_LENGTH)
|
|
2146
|
+
.map((chunk, i) => ` - (${i + 1}) ${chunk.length > 600 ? `${chunk.slice(0, 600)}...` : chunk}`)
|
|
2147
|
+
.join("\n");
|
|
1969
2148
|
const entry = [`[${doc.docType}] docdex:${doc.id ?? `doc-${idx + 1}`}`, describeDoc(doc, idx), content]
|
|
1970
2149
|
.filter(Boolean)
|
|
1971
2150
|
.join("\n");
|
|
@@ -1991,6 +2170,18 @@ export class CreateTasksService {
|
|
|
1991
2170
|
warnings.push("Context truncated due to token budget; skipped OpenAPI hint summary.");
|
|
1992
2171
|
}
|
|
1993
2172
|
}
|
|
2173
|
+
const sdsCoverageHints = this.buildSdsCoverageHints(sorted);
|
|
2174
|
+
if (sdsCoverageHints) {
|
|
2175
|
+
const hintBlock = ["[SDS_COVERAGE_HINTS]", sdsCoverageHints].join("\n");
|
|
2176
|
+
const hintCost = estimateTokens(hintBlock);
|
|
2177
|
+
if (budget - hintCost >= 0) {
|
|
2178
|
+
budget -= hintCost;
|
|
2179
|
+
blocks.push(hintBlock);
|
|
2180
|
+
}
|
|
2181
|
+
else {
|
|
2182
|
+
warnings.push("Context truncated due to token budget; skipped SDS coverage hints.");
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
1994
2185
|
return { docSummary: blocks.join("\n\n") || "(no docs)", warnings };
|
|
1995
2186
|
}
|
|
1996
2187
|
buildPrompt(projectKey, docs, projectBuildMethod, options) {
|
|
@@ -2003,17 +2194,18 @@ export class CreateTasksService {
|
|
|
2003
2194
|
.filter(Boolean)
|
|
2004
2195
|
.join(" ");
|
|
2005
2196
|
const prompt = [
|
|
2006
|
-
`You are assisting in
|
|
2007
|
-
"
|
|
2008
|
-
"
|
|
2197
|
+
`You are assisting in phase 1 of 3 for project ${projectKey}: generate epics only.`,
|
|
2198
|
+
"Process is strict and direct: build plan -> epics -> stories -> tasks.",
|
|
2199
|
+
"This step outputs only epics derived from the build plan and docs.",
|
|
2009
2200
|
"Return strictly valid JSON (no prose) matching:",
|
|
2010
2201
|
EPIC_SCHEMA_SNIPPET,
|
|
2011
2202
|
"Rules:",
|
|
2012
2203
|
"- Do NOT include final slugs; the system will assign keys.",
|
|
2013
2204
|
"- Use docdex handles when referencing docs.",
|
|
2014
2205
|
"- acceptanceCriteria must be an array of strings (5-10 items).",
|
|
2015
|
-
"-
|
|
2016
|
-
"-
|
|
2206
|
+
"- Keep epics actionable and implementation-oriented; avoid glossary/admin-only epics.",
|
|
2207
|
+
"- Prefer dependency-first sequencing: foundational setup epics before dependent feature epics.",
|
|
2208
|
+
"- Keep output derived from docs; do not assume stacks unless docs state them.",
|
|
2017
2209
|
"Project construction method to follow:",
|
|
2018
2210
|
projectBuildMethod,
|
|
2019
2211
|
limits || "Use reasonable scope without over-generating epics.",
|
|
@@ -2047,10 +2239,10 @@ export class CreateTasksService {
|
|
|
2047
2239
|
tasks: [
|
|
2048
2240
|
{
|
|
2049
2241
|
localId: "task-1",
|
|
2050
|
-
title: "
|
|
2051
|
-
type: "
|
|
2052
|
-
description: "
|
|
2053
|
-
estimatedStoryPoints:
|
|
2242
|
+
title: "Implement baseline project scaffolding",
|
|
2243
|
+
type: "feature",
|
|
2244
|
+
description: "Create SDS-aligned baseline structure and core implementation entrypoints from the available docs.",
|
|
2245
|
+
estimatedStoryPoints: 3,
|
|
2054
2246
|
priorityHint: 10,
|
|
2055
2247
|
relatedDocs: docRefs,
|
|
2056
2248
|
unitTests: [],
|
|
@@ -2060,10 +2252,10 @@ export class CreateTasksService {
|
|
|
2060
2252
|
},
|
|
2061
2253
|
{
|
|
2062
2254
|
localId: "task-2",
|
|
2063
|
-
title: "
|
|
2255
|
+
title: "Integrate core contracts and dependencies",
|
|
2064
2256
|
type: "feature",
|
|
2065
|
-
description: "
|
|
2066
|
-
estimatedStoryPoints:
|
|
2257
|
+
description: "Wire key contracts/interfaces and dependency paths so core behavior can execute end-to-end.",
|
|
2258
|
+
estimatedStoryPoints: 3,
|
|
2067
2259
|
priorityHint: 20,
|
|
2068
2260
|
dependsOnKeys: ["task-1"],
|
|
2069
2261
|
relatedDocs: docRefs,
|
|
@@ -2072,6 +2264,20 @@ export class CreateTasksService {
|
|
|
2072
2264
|
integrationTests: [],
|
|
2073
2265
|
apiTests: [],
|
|
2074
2266
|
},
|
|
2267
|
+
{
|
|
2268
|
+
localId: "task-3",
|
|
2269
|
+
title: "Validate baseline behavior and regressions",
|
|
2270
|
+
type: "chore",
|
|
2271
|
+
description: "Add targeted validation coverage and readiness evidence for the implemented baseline capabilities.",
|
|
2272
|
+
estimatedStoryPoints: 2,
|
|
2273
|
+
priorityHint: 30,
|
|
2274
|
+
dependsOnKeys: ["task-2"],
|
|
2275
|
+
relatedDocs: docRefs,
|
|
2276
|
+
unitTests: [],
|
|
2277
|
+
componentTests: [],
|
|
2278
|
+
integrationTests: [],
|
|
2279
|
+
apiTests: [],
|
|
2280
|
+
},
|
|
2075
2281
|
],
|
|
2076
2282
|
},
|
|
2077
2283
|
],
|
|
@@ -2255,14 +2461,15 @@ export class CreateTasksService {
|
|
|
2255
2461
|
}
|
|
2256
2462
|
async generateStoriesForEpic(agent, epic, docSummary, projectBuildMethod, stream, jobId, commandRunId) {
|
|
2257
2463
|
const prompt = [
|
|
2258
|
-
`Generate user stories for epic "${epic.title}".`,
|
|
2259
|
-
"
|
|
2464
|
+
`Generate user stories for epic "${epic.title}" (phase 2 of 3).`,
|
|
2465
|
+
"This phase is stories-only. Do not generate tasks yet.",
|
|
2260
2466
|
"Return JSON only matching:",
|
|
2261
2467
|
STORY_SCHEMA_SNIPPET,
|
|
2262
2468
|
"Rules:",
|
|
2263
2469
|
"- No tasks in this step.",
|
|
2264
2470
|
"- acceptanceCriteria must be an array of strings.",
|
|
2265
2471
|
"- Use docdex handles when citing docs.",
|
|
2472
|
+
"- Keep stories direct and implementation-oriented; avoid placeholder-only narrative sections.",
|
|
2266
2473
|
"- Keep story sequencing aligned with the project construction method.",
|
|
2267
2474
|
`Epic context (key=${epic.key ?? epic.localId ?? "TBD"}):`,
|
|
2268
2475
|
epic.description ?? "(no description provided)",
|
|
@@ -2300,23 +2507,29 @@ export class CreateTasksService {
|
|
|
2300
2507
|
.filter(Boolean);
|
|
2301
2508
|
};
|
|
2302
2509
|
const prompt = [
|
|
2303
|
-
`Generate tasks for story "${story.title}" (Epic: ${epic.title}).`,
|
|
2304
|
-
"
|
|
2510
|
+
`Generate tasks for story "${story.title}" (Epic: ${epic.title}, phase 3 of 3).`,
|
|
2511
|
+
"This phase is tasks-only for the given story.",
|
|
2305
2512
|
"Return JSON only matching:",
|
|
2306
2513
|
TASK_SCHEMA_SNIPPET,
|
|
2307
2514
|
"Rules:",
|
|
2308
2515
|
"- Each task must include localId, title, description, type, estimatedStoryPoints, priorityHint.",
|
|
2516
|
+
"- Descriptions must be implementation-concrete and include target modules/files/services where work happens.",
|
|
2517
|
+
"- Prioritize software construction tasks before test-only/docs-only chores unless story scope explicitly requires those first.",
|
|
2309
2518
|
"- Include test arrays: unitTests, componentTests, integrationTests, apiTests. Use [] when not applicable.",
|
|
2310
2519
|
"- Only include tests that are relevant to the task's scope.",
|
|
2311
2520
|
"- Prefer including task-relevant tests when they are concrete and actionable; do not invent generic placeholders.",
|
|
2312
2521
|
"- When known, include qa object with profiles_expected/requires/entrypoints/data_setup to guide QA.",
|
|
2313
2522
|
"- Do not hardcode ports. For QA entrypoints, use http://localhost:<PORT> placeholders or omit base_url when unknown.",
|
|
2314
2523
|
"- dependsOnKeys must reference localIds in this story.",
|
|
2524
|
+
"- If dependsOnKeys is non-empty, include dependency rationale in the task description.",
|
|
2315
2525
|
"- Start from prerequisite codebase setup: add structure/bootstrap tasks before feature tasks when missing.",
|
|
2316
2526
|
"- Keep dependencies strictly inside this story; never reference tasks from other stories/epics.",
|
|
2317
2527
|
"- Order tasks from foundational prerequisites to dependents based on documented dependency direction and startup constraints.",
|
|
2528
|
+
"- Avoid placeholder wording (TBD, TODO, to be defined, generic follow-up phrases).",
|
|
2529
|
+
"- Avoid documentation-only or glossary-only tasks unless story acceptance explicitly requires them.",
|
|
2318
2530
|
"- Use docdex handles when citing docs.",
|
|
2319
2531
|
"- If OPENAPI_HINTS are present in Docs, align tasks with hinted service/capability/stage/test_requirements.",
|
|
2532
|
+
"- If SDS_COVERAGE_HINTS are present in Docs, cover the relevant SDS sections in implementation tasks.",
|
|
2320
2533
|
"- Follow the project construction method and startup-wave order from SDS when available.",
|
|
2321
2534
|
`Story context (key=${story.key ?? story.localId ?? "TBD"}):`,
|
|
2322
2535
|
story.description ?? story.userStory ?? "",
|
|
@@ -2387,17 +2600,22 @@ export class CreateTasksService {
|
|
|
2387
2600
|
.slice(0, 6)
|
|
2388
2601
|
.map((criterion) => `- ${criterion}`)
|
|
2389
2602
|
.join("\n");
|
|
2603
|
+
const objectiveLine = story.description && story.description.trim().length > 0
|
|
2604
|
+
? story.description.trim().split(/\r?\n/)[0]
|
|
2605
|
+
: `Deliver story scope for "${story.title}".`;
|
|
2390
2606
|
return [
|
|
2391
2607
|
{
|
|
2392
2608
|
localId: "t-fallback-1",
|
|
2393
|
-
title: `
|
|
2394
|
-
type: "
|
|
2609
|
+
title: `Implement core scope for ${story.title}`,
|
|
2610
|
+
type: "feature",
|
|
2395
2611
|
description: [
|
|
2396
|
-
`
|
|
2397
|
-
|
|
2612
|
+
`Implement the core product behavior for story "${story.title}".`,
|
|
2613
|
+
`Primary objective: ${objectiveLine}`,
|
|
2614
|
+
"Create or update concrete modules/files and wire baseline runtime paths first.",
|
|
2615
|
+
"Capture exact implementation targets and sequencing in commit-level task notes.",
|
|
2398
2616
|
criteriaLines ? `Acceptance criteria to satisfy:\n${criteriaLines}` : "Acceptance criteria: use story definition.",
|
|
2399
2617
|
].join("\n"),
|
|
2400
|
-
estimatedStoryPoints:
|
|
2618
|
+
estimatedStoryPoints: 3,
|
|
2401
2619
|
priorityHint: 1,
|
|
2402
2620
|
dependsOnKeys: [],
|
|
2403
2621
|
relatedDocs: story.relatedDocs ?? [],
|
|
@@ -2408,11 +2626,12 @@ export class CreateTasksService {
|
|
|
2408
2626
|
},
|
|
2409
2627
|
{
|
|
2410
2628
|
localId: "t-fallback-2",
|
|
2411
|
-
title: `
|
|
2629
|
+
title: `Integrate contracts for ${story.title}`,
|
|
2412
2630
|
type: "feature",
|
|
2413
2631
|
description: [
|
|
2414
|
-
`
|
|
2415
|
-
"
|
|
2632
|
+
`Integrate dependent contracts/interfaces for "${story.title}" after core scope implementation.`,
|
|
2633
|
+
"Align internal/external interfaces, data contracts, and dependency wiring with SDS/OpenAPI context.",
|
|
2634
|
+
"Record dependency rationale and compatibility constraints in the task output.",
|
|
2416
2635
|
].join("\n"),
|
|
2417
2636
|
estimatedStoryPoints: 3,
|
|
2418
2637
|
priorityHint: 2,
|
|
@@ -2423,6 +2642,24 @@ export class CreateTasksService {
|
|
|
2423
2642
|
integrationTests: [],
|
|
2424
2643
|
apiTests: [],
|
|
2425
2644
|
},
|
|
2645
|
+
{
|
|
2646
|
+
localId: "t-fallback-3",
|
|
2647
|
+
title: `Validate ${story.title} regressions and readiness`,
|
|
2648
|
+
type: "chore",
|
|
2649
|
+
description: [
|
|
2650
|
+
`Validate "${story.title}" end-to-end with focused regression coverage and readiness evidence.`,
|
|
2651
|
+
"Add/update targeted tests and verification scripts tied to implemented behavior.",
|
|
2652
|
+
"Document release/code-review/QA evidence and unresolved risks explicitly.",
|
|
2653
|
+
].join("\n"),
|
|
2654
|
+
estimatedStoryPoints: 2,
|
|
2655
|
+
priorityHint: 3,
|
|
2656
|
+
dependsOnKeys: ["t-fallback-2"],
|
|
2657
|
+
relatedDocs: story.relatedDocs ?? [],
|
|
2658
|
+
unitTests: [],
|
|
2659
|
+
componentTests: [],
|
|
2660
|
+
integrationTests: [],
|
|
2661
|
+
apiTests: [],
|
|
2662
|
+
},
|
|
2426
2663
|
];
|
|
2427
2664
|
}
|
|
2428
2665
|
async generatePlanFromAgent(epics, agent, docSummary, options) {
|
|
@@ -2493,17 +2730,65 @@ export class CreateTasksService {
|
|
|
2493
2730
|
}
|
|
2494
2731
|
return { epics: planEpics, stories: planStories, tasks: planTasks };
|
|
2495
2732
|
}
|
|
2496
|
-
|
|
2733
|
+
buildSdsCoverageReport(projectKey, docs, plan) {
|
|
2734
|
+
const sections = this.extractSdsSectionCandidates(docs, SDS_COVERAGE_REPORT_SECTION_LIMIT);
|
|
2735
|
+
const normalize = (value) => value
|
|
2736
|
+
.toLowerCase()
|
|
2737
|
+
.replace(/[`*_]/g, "")
|
|
2738
|
+
.replace(/[^a-z0-9\s/-]+/g, " ")
|
|
2739
|
+
.replace(/\s+/g, " ")
|
|
2740
|
+
.trim();
|
|
2741
|
+
const planCorpus = normalize([
|
|
2742
|
+
...plan.epics.map((epic) => `${epic.title} ${epic.description ?? ""} ${(epic.acceptanceCriteria ?? []).join(" ")}`),
|
|
2743
|
+
...plan.stories.map((story) => `${story.title} ${story.userStory ?? ""} ${story.description ?? ""} ${(story.acceptanceCriteria ?? []).join(" ")}`),
|
|
2744
|
+
...plan.tasks.map((task) => `${task.title} ${task.description ?? ""}`),
|
|
2745
|
+
].join("\n"));
|
|
2746
|
+
const matched = [];
|
|
2747
|
+
const unmatched = [];
|
|
2748
|
+
for (const section of sections) {
|
|
2749
|
+
const normalizedSection = normalize(section);
|
|
2750
|
+
if (!normalizedSection)
|
|
2751
|
+
continue;
|
|
2752
|
+
const keywords = normalizedSection
|
|
2753
|
+
.split(/\s+/)
|
|
2754
|
+
.filter((token) => token.length >= 4)
|
|
2755
|
+
.slice(0, 6);
|
|
2756
|
+
const hasDirectMatch = normalizedSection.length >= 6 && planCorpus.includes(normalizedSection);
|
|
2757
|
+
const hasKeywordMatch = keywords.some((keyword) => planCorpus.includes(keyword));
|
|
2758
|
+
if (hasDirectMatch || hasKeywordMatch) {
|
|
2759
|
+
matched.push(section);
|
|
2760
|
+
}
|
|
2761
|
+
else {
|
|
2762
|
+
unmatched.push(section);
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
const totalSections = matched.length + unmatched.length;
|
|
2766
|
+
const coverageRatio = totalSections === 0 ? 1 : matched.length / totalSections;
|
|
2767
|
+
return {
|
|
2768
|
+
projectKey,
|
|
2769
|
+
generatedAt: new Date().toISOString(),
|
|
2770
|
+
totalSections,
|
|
2771
|
+
matched,
|
|
2772
|
+
unmatched,
|
|
2773
|
+
coverageRatio: Number(coverageRatio.toFixed(4)),
|
|
2774
|
+
notes: totalSections === 0
|
|
2775
|
+
? ["No SDS section headings detected; coverage defaults to 1.0."]
|
|
2776
|
+
: ["Coverage is heading-based heuristic match between SDS sections and generated epic/story/task corpus."],
|
|
2777
|
+
};
|
|
2778
|
+
}
|
|
2779
|
+
async writePlanArtifacts(projectKey, plan, docSummary, docs, buildPlan) {
|
|
2497
2780
|
const baseDir = path.join(this.workspace.mcodaDir, "tasks", projectKey);
|
|
2498
2781
|
await fs.mkdir(baseDir, { recursive: true });
|
|
2499
2782
|
const write = async (file, data) => {
|
|
2500
2783
|
const target = path.join(baseDir, file);
|
|
2501
2784
|
await fs.writeFile(target, JSON.stringify(data, null, 2), "utf8");
|
|
2502
2785
|
};
|
|
2503
|
-
await write("plan.json", { projectKey, generatedAt: new Date().toISOString(), docSummary, ...plan });
|
|
2786
|
+
await write("plan.json", { projectKey, generatedAt: new Date().toISOString(), docSummary, buildPlan, ...plan });
|
|
2787
|
+
await write("build-plan.json", buildPlan);
|
|
2504
2788
|
await write("epics.json", plan.epics);
|
|
2505
2789
|
await write("stories.json", plan.stories);
|
|
2506
2790
|
await write("tasks.json", plan.tasks);
|
|
2791
|
+
await write("coverage-report.json", this.buildSdsCoverageReport(projectKey, docs, plan));
|
|
2507
2792
|
return { folder: baseDir };
|
|
2508
2793
|
}
|
|
2509
2794
|
async persistPlanToDb(projectId, projectKey, plan, jobId, commandRunId, options) {
|
|
@@ -2632,12 +2917,7 @@ export class CreateTasksService {
|
|
|
2632
2917
|
discoveredTestCommands = [];
|
|
2633
2918
|
}
|
|
2634
2919
|
}
|
|
2635
|
-
const qaBlockers = uniqueStrings([
|
|
2636
|
-
...(qaReadiness.blockers ?? []),
|
|
2637
|
-
...(testsRequired && discoveredTestCommands.length === 0
|
|
2638
|
-
? ["No runnable test harness discovered for required tests during planning."]
|
|
2639
|
-
: []),
|
|
2640
|
-
]);
|
|
2920
|
+
const qaBlockers = uniqueStrings(qaReadiness.blockers ?? []);
|
|
2641
2921
|
const qaReadinessWithHarness = {
|
|
2642
2922
|
...qaReadiness,
|
|
2643
2923
|
blockers: qaBlockers.length ? qaBlockers : undefined,
|
|
@@ -2766,6 +3046,7 @@ export class CreateTasksService {
|
|
|
2766
3046
|
const { docSummary, warnings: docWarnings } = this.buildDocContext(docs);
|
|
2767
3047
|
const discoveryGraph = this.buildServiceDependencyGraph({ epics: [], stories: [], tasks: [] }, docs);
|
|
2768
3048
|
const projectBuildMethod = this.buildProjectConstructionMethod(docs, discoveryGraph);
|
|
3049
|
+
const projectBuildPlan = this.buildProjectPlanArtifact(options.projectKey, docs, discoveryGraph, projectBuildMethod);
|
|
2769
3050
|
const { prompt } = this.buildPrompt(options.projectKey, docs, projectBuildMethod, options);
|
|
2770
3051
|
const qaPreflight = await this.buildQaPreflight();
|
|
2771
3052
|
const qaOverrides = this.buildQaOverrides(options);
|
|
@@ -2774,6 +3055,15 @@ export class CreateTasksService {
|
|
|
2774
3055
|
timestamp: new Date().toISOString(),
|
|
2775
3056
|
details: { count: docs.length, warnings: docWarnings, startupWaves: discoveryGraph.startupWaves.slice(0, 8) },
|
|
2776
3057
|
});
|
|
3058
|
+
await this.jobService.writeCheckpoint(job.id, {
|
|
3059
|
+
stage: "build_plan_defined",
|
|
3060
|
+
timestamp: new Date().toISOString(),
|
|
3061
|
+
details: {
|
|
3062
|
+
sourceDocs: projectBuildPlan.sourceDocs.length,
|
|
3063
|
+
services: projectBuildPlan.services.length,
|
|
3064
|
+
startupWaves: projectBuildPlan.startupWaves.length,
|
|
3065
|
+
},
|
|
3066
|
+
});
|
|
2777
3067
|
await this.jobService.writeCheckpoint(job.id, {
|
|
2778
3068
|
stage: "qa_preflight",
|
|
2779
3069
|
timestamp: new Date().toISOString(),
|
|
@@ -2833,7 +3123,7 @@ export class CreateTasksService {
|
|
|
2833
3123
|
timestamp: new Date().toISOString(),
|
|
2834
3124
|
details: { tasks: plan.tasks.length, source: planSource, fallbackReason },
|
|
2835
3125
|
});
|
|
2836
|
-
const { folder } = await this.writePlanArtifacts(options.projectKey, plan, docSummary);
|
|
3126
|
+
const { folder } = await this.writePlanArtifacts(options.projectKey, plan, docSummary, docs, projectBuildPlan);
|
|
2837
3127
|
await this.jobService.writeCheckpoint(job.id, {
|
|
2838
3128
|
stage: "plan_written",
|
|
2839
3129
|
timestamp: new Date().toISOString(),
|
|
@@ -2846,6 +3136,61 @@ export class CreateTasksService {
|
|
|
2846
3136
|
qaOverrides,
|
|
2847
3137
|
});
|
|
2848
3138
|
await this.seedPriorities(options.projectKey);
|
|
3139
|
+
let sufficiencyAudit;
|
|
3140
|
+
let sufficiencyAuditError;
|
|
3141
|
+
if (this.taskSufficiencyFactory) {
|
|
3142
|
+
try {
|
|
3143
|
+
const sufficiencyService = await this.taskSufficiencyFactory(this.workspace);
|
|
3144
|
+
try {
|
|
3145
|
+
try {
|
|
3146
|
+
sufficiencyAudit = await sufficiencyService.runAudit({
|
|
3147
|
+
workspace: options.workspace,
|
|
3148
|
+
projectKey: options.projectKey,
|
|
3149
|
+
sourceCommand: "create-tasks",
|
|
3150
|
+
});
|
|
3151
|
+
}
|
|
3152
|
+
catch (error) {
|
|
3153
|
+
sufficiencyAuditError = error?.message ?? String(error);
|
|
3154
|
+
await this.jobService.appendLog(job.id, `Task sufficiency audit failed; continuing with created backlog: ${sufficiencyAuditError}\n`);
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
finally {
|
|
3158
|
+
try {
|
|
3159
|
+
await sufficiencyService.close();
|
|
3160
|
+
}
|
|
3161
|
+
catch (closeError) {
|
|
3162
|
+
const closeMessage = closeError?.message ?? String(closeError);
|
|
3163
|
+
const details = `Task sufficiency audit close failed; continuing with created backlog: ${closeMessage}`;
|
|
3164
|
+
sufficiencyAuditError = sufficiencyAuditError ? `${sufficiencyAuditError}; ${details}` : details;
|
|
3165
|
+
await this.jobService.appendLog(job.id, `${details}\n`);
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
catch (error) {
|
|
3170
|
+
sufficiencyAuditError = error?.message ?? String(error);
|
|
3171
|
+
await this.jobService.appendLog(job.id, `Task sufficiency audit setup failed; continuing with created backlog: ${sufficiencyAuditError}\n`);
|
|
3172
|
+
}
|
|
3173
|
+
await this.jobService.writeCheckpoint(job.id, {
|
|
3174
|
+
stage: "task_sufficiency_audit",
|
|
3175
|
+
timestamp: new Date().toISOString(),
|
|
3176
|
+
details: {
|
|
3177
|
+
status: sufficiencyAudit ? "succeeded" : "failed",
|
|
3178
|
+
error: sufficiencyAuditError,
|
|
3179
|
+
jobId: sufficiencyAudit?.jobId,
|
|
3180
|
+
commandRunId: sufficiencyAudit?.commandRunId,
|
|
3181
|
+
satisfied: sufficiencyAudit?.satisfied,
|
|
3182
|
+
dryRun: sufficiencyAudit?.dryRun,
|
|
3183
|
+
totalTasksAdded: sufficiencyAudit?.totalTasksAdded,
|
|
3184
|
+
totalTasksUpdated: sufficiencyAudit?.totalTasksUpdated,
|
|
3185
|
+
finalCoverageRatio: sufficiencyAudit?.finalCoverageRatio,
|
|
3186
|
+
reportPath: sufficiencyAudit?.reportPath,
|
|
3187
|
+
remainingSectionCount: sufficiencyAudit?.remainingSectionHeadings.length,
|
|
3188
|
+
remainingFolderCount: sufficiencyAudit?.remainingFolderEntries.length,
|
|
3189
|
+
remainingGapCount: sufficiencyAudit?.remainingGaps.total,
|
|
3190
|
+
warnings: sufficiencyAudit?.warnings,
|
|
3191
|
+
},
|
|
3192
|
+
});
|
|
3193
|
+
}
|
|
2849
3194
|
await this.jobService.updateJobStatus(job.id, "completed", {
|
|
2850
3195
|
payload: {
|
|
2851
3196
|
epicsCreated: epicRows.length,
|
|
@@ -2856,6 +3201,22 @@ export class CreateTasksService {
|
|
|
2856
3201
|
planFolder: folder,
|
|
2857
3202
|
planSource,
|
|
2858
3203
|
fallbackReason,
|
|
3204
|
+
sufficiencyAudit: sufficiencyAudit
|
|
3205
|
+
? {
|
|
3206
|
+
jobId: sufficiencyAudit.jobId,
|
|
3207
|
+
commandRunId: sufficiencyAudit.commandRunId,
|
|
3208
|
+
satisfied: sufficiencyAudit.satisfied,
|
|
3209
|
+
totalTasksAdded: sufficiencyAudit.totalTasksAdded,
|
|
3210
|
+
totalTasksUpdated: sufficiencyAudit.totalTasksUpdated,
|
|
3211
|
+
finalCoverageRatio: sufficiencyAudit.finalCoverageRatio,
|
|
3212
|
+
reportPath: sufficiencyAudit.reportPath,
|
|
3213
|
+
remainingSectionCount: sufficiencyAudit.remainingSectionHeadings.length,
|
|
3214
|
+
remainingFolderCount: sufficiencyAudit.remainingFolderEntries.length,
|
|
3215
|
+
remainingGapCount: sufficiencyAudit.remainingGaps.total,
|
|
3216
|
+
warnings: sufficiencyAudit.warnings,
|
|
3217
|
+
}
|
|
3218
|
+
: undefined,
|
|
3219
|
+
sufficiencyAuditError,
|
|
2859
3220
|
},
|
|
2860
3221
|
});
|
|
2861
3222
|
await this.jobService.finishCommandRun(commandRun.id, "succeeded");
|