@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.
@@ -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
- return value
180
- .map((entry) => {
181
- if (typeof entry === "string")
182
- return entry;
183
- if (entry && typeof entry === "object" && "handle" in entry && typeof entry.handle === "string") {
184
- return entry.handle;
185
- }
186
- return undefined;
187
- })
188
- .filter((entry) => Boolean(entry && DOCDEX_HANDLE.test(entry)));
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
- ensureNonEmpty(description, "Summarize the problem, users, and constraints for this epic."),
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, "Provide 5–10 testable acceptance criteria."),
425
+ formatBullets(acceptance, "Define measurable and testable outcomes for this epic."),
317
426
  "* **Related Documentation / References**",
318
- formatBullets(relatedDocs, "Link relevant docdex entries and sections."),
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
- ensureNonEmpty(userStory, `As a user, I want ${title} so that it delivers value.`),
437
+ userStoryText,
327
438
  "* **Context**",
328
439
  "",
329
- ensureNonEmpty(description, "Context for systems, dependencies, and scope."),
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
- ensureNonEmpty(description, `Deliver ${title} for story ${storyKey}.`),
489
+ objectiveText,
364
490
  "* **Context**",
365
491
  "",
366
492
  `- Epic: ${epicKey}`,
367
493
  `- Story: ${storyKey}`,
368
494
  "* **Inputs**",
369
- formatBullets(relatedDocs, "Docdex excerpts, SDS/PDR/RFP sections, OpenAPI endpoints."),
495
+ formatBullets(relatedDocs, "No explicit external references."),
370
496
  "* **Implementation Plan**",
371
- "- Break this into concrete steps during execution.",
497
+ formatBullets(implementationLines, defaultImplementationPlan.join(" ")),
372
498
  "* **Definition of Done**",
373
- "- Tests passing, docs updated, review/QA complete.",
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, "Enumerate prerequisite tasks by key."),
514
+ formatBullets(dependencies, "None."),
389
515
  "* **Risks & Gotchas**",
390
- "- Highlight edge cases or risky areas.",
516
+ formatBullets(riskLines, defaultRisks.join(" ")),
391
517
  "* **Related Documentation / References**",
392
- formatBullets(relatedDocs, "Docdex handles or file paths to consult."),
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, 24)
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, 300);
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, 2000);
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) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""));
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 = (doc.segments ?? []).slice(0, 5);
1959
- const content = segments.length
1960
- ? segments
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
- ? doc.content.slice(0, 800)
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 creating EPICS ONLY for project ${projectKey}.`,
2007
- "Follow mcoda SDS epic template:",
2008
- "- Context/Problem; Goals & Outcomes; In Scope; Out of Scope; Key Flows; Non-functional Requirements; Dependencies & Constraints; Risks & Open Questions; Acceptance Criteria; Related Documentation.",
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
- "- Prefer dependency-first sequencing: foundational codebase/service setup epics should precede dependent feature epics.",
2016
- "- Keep output technology-agnostic and derived from docs; do not assume specific stacks unless docs state them.",
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: "Summarize requirements",
2051
- type: "chore",
2052
- description: "Summarize key asks from docs and SDS/PDR/RFP inputs.",
2053
- estimatedStoryPoints: 1,
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: "Propose tasks and ordering",
2255
+ title: "Integrate core contracts and dependencies",
2064
2256
  type: "feature",
2065
- description: "Break down the scope into tasks with initial dependencies.",
2066
- estimatedStoryPoints: 2,
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
- "Use the User Story template: User Story; Context; Preconditions; Main Flow; Alternative/Error Flows; UX/UI; Data & Integrations; Acceptance Criteria; NFR; Related Docs.",
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
- "Use the Task template: Objective; Context; Inputs; Implementation Plan; DoD; Testing & QA; Dependencies; Risks; References.",
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: `Fallback planning for ${story.title}`,
2394
- type: "chore",
2609
+ title: `Implement core scope for ${story.title}`,
2610
+ type: "feature",
2395
2611
  description: [
2396
- `Draft a concrete implementation plan for story "${story.title}" using SDS/OpenAPI context.`,
2397
- "List exact files/modules to touch and implementation order.",
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: 2,
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: `Fallback implementation for ${story.title}`,
2629
+ title: `Integrate contracts for ${story.title}`,
2412
2630
  type: "feature",
2413
2631
  description: [
2414
- `Implement story "${story.title}" according to the fallback planning task.`,
2415
- "Ensure done criteria and test requirements are explicitly documented for execution.",
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
- async writePlanArtifacts(projectKey, plan, docSummary) {
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");