@mcoda/core 0.1.23 → 0.1.26

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) => {
@@ -356,11 +463,38 @@ const buildTaskDescription = (taskKey, title, description, storyKey, epicKey, re
356
463
  })
357
464
  .join("\n");
358
465
  };
466
+ const objectiveText = ensureNonEmpty(description, `Deliver ${title} for story ${storyKey}.`);
467
+ const implementationLines = extractActionableLines(description, 4);
468
+ const riskLines = extractRiskLines(description, 3);
469
+ const testsDefined = (tests.unitTests?.length ?? 0) +
470
+ (tests.componentTests?.length ?? 0) +
471
+ (tests.integrationTests?.length ?? 0) +
472
+ (tests.apiTests?.length ?? 0) >
473
+ 0;
474
+ const definitionOfDone = [
475
+ `- Implementation for \`${taskKey}\` is complete and scoped to ${storyKey}.`,
476
+ testsDefined
477
+ ? "- Task-specific tests are added/updated and green in the task validation loop."
478
+ : "- Verification evidence is captured in task logs/checklists for this scope.",
479
+ relatedDocs?.length
480
+ ? "- Related contracts/docs are consistent with delivered behavior."
481
+ : "- Documentation impact is reviewed and no additional contract docs are required.",
482
+ qa?.blockers?.length ? "- Remaining QA blockers are explicit and actionable." : "- QA blockers are resolved or not present.",
483
+ ];
484
+ const defaultImplementationPlan = [
485
+ `- Implement ${title} with file/module-level changes aligned to the objective.`,
486
+ dependencies.length
487
+ ? `- Respect dependency order before completion: ${dependencies.join(", ")}.`
488
+ : "- Validate assumptions and finalize concrete implementation steps before coding.",
489
+ ];
490
+ const defaultRisks = dependencies.length
491
+ ? [`- Delivery depends on upstream tasks: ${dependencies.join(", ")}.`]
492
+ : ["- Keep implementation aligned to SDS/OpenAPI contracts to avoid drift."];
359
493
  return [
360
494
  `* **Task Key**: ${taskKey}`,
361
495
  "* **Objective**",
362
496
  "",
363
- ensureNonEmpty(description, `Deliver ${title} for story ${storyKey}.`),
497
+ objectiveText,
364
498
  "* **Context**",
365
499
  "",
366
500
  `- Epic: ${epicKey}`,
@@ -368,9 +502,9 @@ const buildTaskDescription = (taskKey, title, description, storyKey, epicKey, re
368
502
  "* **Inputs**",
369
503
  formatBullets(relatedDocs, "Docdex excerpts, SDS/PDR/RFP sections, OpenAPI endpoints."),
370
504
  "* **Implementation Plan**",
371
- "- Break this into concrete steps during execution.",
505
+ formatBullets(implementationLines, defaultImplementationPlan.join(" ")),
372
506
  "* **Definition of Done**",
373
- "- Tests passing, docs updated, review/QA complete.",
507
+ definitionOfDone.join("\n"),
374
508
  "* **Testing & QA**",
375
509
  `- Unit tests: ${formatTestList(tests.unitTests)}`,
376
510
  `- Component tests: ${formatTestList(tests.componentTests)}`,
@@ -387,7 +521,7 @@ const buildTaskDescription = (taskKey, title, description, storyKey, epicKey, re
387
521
  "* **Dependencies**",
388
522
  formatBullets(dependencies, "Enumerate prerequisite tasks by key."),
389
523
  "* **Risks & Gotchas**",
390
- "- Highlight edge cases or risky areas.",
524
+ formatBullets(riskLines, defaultRisks.join(" ")),
391
525
  "* **Related Documentation / References**",
392
526
  formatBullets(relatedDocs, "Docdex handles or file paths to consult."),
393
527
  ].join("\n");
@@ -594,6 +728,7 @@ export class CreateTasksService {
594
728
  this.routingService = deps.routingService;
595
729
  this.ratingService = deps.ratingService;
596
730
  this.taskOrderingFactory = deps.taskOrderingFactory ?? TaskOrderingService.create;
731
+ this.taskSufficiencyFactory = deps.taskSufficiencyFactory ?? TaskSufficiencyService.create;
597
732
  }
598
733
  static async create(workspace) {
599
734
  const repo = await GlobalRepository.create();
@@ -614,6 +749,7 @@ export class CreateTasksService {
614
749
  repo,
615
750
  workspaceRepo,
616
751
  routingService,
752
+ taskSufficiencyFactory: TaskSufficiencyService.create,
617
753
  });
618
754
  }
619
755
  async close() {
@@ -903,7 +1039,7 @@ export class CreateTasksService {
903
1039
  }
904
1040
  return ranked
905
1041
  .sort((a, b) => b.score - a.score || a.path.localeCompare(b.path))
906
- .slice(0, 24)
1042
+ .slice(0, FUZZY_DOC_CANDIDATE_LIMIT)
907
1043
  .map((entry) => entry.path);
908
1044
  }
909
1045
  normalizeStructurePathToken(value) {
@@ -1072,7 +1208,7 @@ export class CreateTasksService {
1072
1208
  .split(/\r?\n/)
1073
1209
  .map((line) => line.trim())
1074
1210
  .filter(Boolean)
1075
- .slice(0, 300);
1211
+ .slice(0, DEPENDENCY_SCAN_LINE_LIMIT);
1076
1212
  const dependencyPatterns = [
1077
1213
  {
1078
1214
  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 +1292,7 @@ export class CreateTasksService {
1156
1292
  .split(/\r?\n/)
1157
1293
  .map((line) => line.trim())
1158
1294
  .filter(Boolean)
1159
- .slice(0, 2000);
1295
+ .slice(0, STARTUP_WAVE_SCAN_LINE_LIMIT);
1160
1296
  for (const line of lines) {
1161
1297
  if (!line.startsWith("|"))
1162
1298
  continue;
@@ -1949,23 +2085,59 @@ export class CreateTasksService {
1949
2085
  }
1950
2086
  return lines.join("\n");
1951
2087
  }
2088
+ extractSdsSectionCandidates(docs, limit) {
2089
+ const sections = [];
2090
+ for (const doc of docs) {
2091
+ if (!looksLikeSdsDoc(doc))
2092
+ continue;
2093
+ const segmentHeadings = (doc.segments ?? [])
2094
+ .map((segment) => segment.heading?.trim())
2095
+ .filter((heading) => Boolean(heading));
2096
+ const contentHeadings = extractMarkdownHeadings(doc.content ?? "", limit);
2097
+ for (const heading of [...segmentHeadings, ...contentHeadings]) {
2098
+ const normalized = heading.replace(/[`*_]/g, "").trim();
2099
+ if (!normalized)
2100
+ continue;
2101
+ sections.push(normalized);
2102
+ if (sections.length >= limit)
2103
+ break;
2104
+ }
2105
+ if (sections.length >= limit)
2106
+ break;
2107
+ }
2108
+ return uniqueStrings(sections).slice(0, limit);
2109
+ }
2110
+ buildSdsCoverageHints(docs) {
2111
+ const hints = this.extractSdsSectionCandidates(docs, SDS_COVERAGE_HINT_HEADING_LIMIT);
2112
+ if (hints.length === 0)
2113
+ return "";
2114
+ return hints.map((hint) => `- ${hint}`).join("\n");
2115
+ }
1952
2116
  buildDocContext(docs) {
1953
2117
  const warnings = [];
1954
2118
  const blocks = [];
1955
2119
  let budget = DOC_CONTEXT_BUDGET;
1956
- const sorted = [...docs].sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""));
2120
+ const sorted = [...docs].sort((a, b) => {
2121
+ const sdsDelta = Number(looksLikeSdsDoc(b)) - Number(looksLikeSdsDoc(a));
2122
+ if (sdsDelta !== 0)
2123
+ return sdsDelta;
2124
+ return (b.updatedAt ?? "").localeCompare(a.updatedAt ?? "");
2125
+ });
1957
2126
  for (const [idx, doc] of sorted.entries()) {
1958
- const segments = (doc.segments ?? []).slice(0, 5);
1959
- const content = segments.length
1960
- ? segments
2127
+ const segments = doc.segments ?? [];
2128
+ const sampledSegments = pickDistributedIndices(segments.length, DOC_CONTEXT_SEGMENTS_PER_DOC)
2129
+ .map((index) => segments[index])
2130
+ .filter(Boolean);
2131
+ const content = sampledSegments.length
2132
+ ? sampledSegments
1961
2133
  .map((seg, i) => {
1962
2134
  const trimmed = seg.content.length > 600 ? `${seg.content.slice(0, 600)}...` : seg.content;
1963
2135
  return ` - (${i + 1}) ${seg.heading ? `${seg.heading}: ` : ""}${trimmed}`;
1964
2136
  })
1965
2137
  .join("\n")
1966
- : doc.content
1967
- ? doc.content.slice(0, 800)
1968
- : "";
2138
+ : sampleRawContent(doc.content, DOC_CONTEXT_FALLBACK_CHUNK_LENGTH)
2139
+ .map((chunk, i) => ` - (${i + 1}) ${chunk.length > 600 ? `${chunk.slice(0, 600)}...` : chunk}`)
2140
+ .join("\n");
1969
2141
  const entry = [`[${doc.docType}] docdex:${doc.id ?? `doc-${idx + 1}`}`, describeDoc(doc, idx), content]
1970
2142
  .filter(Boolean)
1971
2143
  .join("\n");
@@ -1991,6 +2163,18 @@ export class CreateTasksService {
1991
2163
  warnings.push("Context truncated due to token budget; skipped OpenAPI hint summary.");
1992
2164
  }
1993
2165
  }
2166
+ const sdsCoverageHints = this.buildSdsCoverageHints(sorted);
2167
+ if (sdsCoverageHints) {
2168
+ const hintBlock = ["[SDS_COVERAGE_HINTS]", sdsCoverageHints].join("\n");
2169
+ const hintCost = estimateTokens(hintBlock);
2170
+ if (budget - hintCost >= 0) {
2171
+ budget -= hintCost;
2172
+ blocks.push(hintBlock);
2173
+ }
2174
+ else {
2175
+ warnings.push("Context truncated due to token budget; skipped SDS coverage hints.");
2176
+ }
2177
+ }
1994
2178
  return { docSummary: blocks.join("\n\n") || "(no docs)", warnings };
1995
2179
  }
1996
2180
  buildPrompt(projectKey, docs, projectBuildMethod, options) {
@@ -2047,10 +2231,10 @@ export class CreateTasksService {
2047
2231
  tasks: [
2048
2232
  {
2049
2233
  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,
2234
+ title: "Implement baseline project scaffolding",
2235
+ type: "feature",
2236
+ description: "Create SDS-aligned baseline structure and core implementation entrypoints from the available docs.",
2237
+ estimatedStoryPoints: 3,
2054
2238
  priorityHint: 10,
2055
2239
  relatedDocs: docRefs,
2056
2240
  unitTests: [],
@@ -2060,10 +2244,10 @@ export class CreateTasksService {
2060
2244
  },
2061
2245
  {
2062
2246
  localId: "task-2",
2063
- title: "Propose tasks and ordering",
2247
+ title: "Integrate core contracts and dependencies",
2064
2248
  type: "feature",
2065
- description: "Break down the scope into tasks with initial dependencies.",
2066
- estimatedStoryPoints: 2,
2249
+ description: "Wire key contracts/interfaces and dependency paths so core behavior can execute end-to-end.",
2250
+ estimatedStoryPoints: 3,
2067
2251
  priorityHint: 20,
2068
2252
  dependsOnKeys: ["task-1"],
2069
2253
  relatedDocs: docRefs,
@@ -2072,6 +2256,20 @@ export class CreateTasksService {
2072
2256
  integrationTests: [],
2073
2257
  apiTests: [],
2074
2258
  },
2259
+ {
2260
+ localId: "task-3",
2261
+ title: "Validate baseline behavior and regressions",
2262
+ type: "chore",
2263
+ description: "Add targeted validation coverage and readiness evidence for the implemented baseline capabilities.",
2264
+ estimatedStoryPoints: 2,
2265
+ priorityHint: 30,
2266
+ dependsOnKeys: ["task-2"],
2267
+ relatedDocs: docRefs,
2268
+ unitTests: [],
2269
+ componentTests: [],
2270
+ integrationTests: [],
2271
+ apiTests: [],
2272
+ },
2075
2273
  ],
2076
2274
  },
2077
2275
  ],
@@ -2306,17 +2504,22 @@ export class CreateTasksService {
2306
2504
  TASK_SCHEMA_SNIPPET,
2307
2505
  "Rules:",
2308
2506
  "- Each task must include localId, title, description, type, estimatedStoryPoints, priorityHint.",
2507
+ "- Descriptions must be implementation-concrete and include target modules/files/services where work happens.",
2508
+ "- Prioritize software construction tasks before test-only/docs-only chores unless story scope explicitly requires those first.",
2309
2509
  "- Include test arrays: unitTests, componentTests, integrationTests, apiTests. Use [] when not applicable.",
2310
2510
  "- Only include tests that are relevant to the task's scope.",
2311
2511
  "- Prefer including task-relevant tests when they are concrete and actionable; do not invent generic placeholders.",
2312
2512
  "- When known, include qa object with profiles_expected/requires/entrypoints/data_setup to guide QA.",
2313
2513
  "- Do not hardcode ports. For QA entrypoints, use http://localhost:<PORT> placeholders or omit base_url when unknown.",
2314
2514
  "- dependsOnKeys must reference localIds in this story.",
2515
+ "- If dependsOnKeys is non-empty, include dependency rationale in the task description.",
2315
2516
  "- Start from prerequisite codebase setup: add structure/bootstrap tasks before feature tasks when missing.",
2316
2517
  "- Keep dependencies strictly inside this story; never reference tasks from other stories/epics.",
2317
2518
  "- Order tasks from foundational prerequisites to dependents based on documented dependency direction and startup constraints.",
2519
+ "- Avoid placeholder wording (TBD, TODO, to be defined, generic follow-up phrases).",
2318
2520
  "- Use docdex handles when citing docs.",
2319
2521
  "- If OPENAPI_HINTS are present in Docs, align tasks with hinted service/capability/stage/test_requirements.",
2522
+ "- If SDS_COVERAGE_HINTS are present in Docs, cover the relevant SDS sections in implementation tasks.",
2320
2523
  "- Follow the project construction method and startup-wave order from SDS when available.",
2321
2524
  `Story context (key=${story.key ?? story.localId ?? "TBD"}):`,
2322
2525
  story.description ?? story.userStory ?? "",
@@ -2387,17 +2590,22 @@ export class CreateTasksService {
2387
2590
  .slice(0, 6)
2388
2591
  .map((criterion) => `- ${criterion}`)
2389
2592
  .join("\n");
2593
+ const objectiveLine = story.description && story.description.trim().length > 0
2594
+ ? story.description.trim().split(/\r?\n/)[0]
2595
+ : `Deliver story scope for "${story.title}".`;
2390
2596
  return [
2391
2597
  {
2392
2598
  localId: "t-fallback-1",
2393
- title: `Fallback planning for ${story.title}`,
2394
- type: "chore",
2599
+ title: `Implement core scope for ${story.title}`,
2600
+ type: "feature",
2395
2601
  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.",
2602
+ `Implement the core product behavior for story "${story.title}".`,
2603
+ `Primary objective: ${objectiveLine}`,
2604
+ "Create or update concrete modules/files and wire baseline runtime paths first.",
2605
+ "Capture exact implementation targets and sequencing in commit-level task notes.",
2398
2606
  criteriaLines ? `Acceptance criteria to satisfy:\n${criteriaLines}` : "Acceptance criteria: use story definition.",
2399
2607
  ].join("\n"),
2400
- estimatedStoryPoints: 2,
2608
+ estimatedStoryPoints: 3,
2401
2609
  priorityHint: 1,
2402
2610
  dependsOnKeys: [],
2403
2611
  relatedDocs: story.relatedDocs ?? [],
@@ -2408,11 +2616,12 @@ export class CreateTasksService {
2408
2616
  },
2409
2617
  {
2410
2618
  localId: "t-fallback-2",
2411
- title: `Fallback implementation for ${story.title}`,
2619
+ title: `Integrate contracts for ${story.title}`,
2412
2620
  type: "feature",
2413
2621
  description: [
2414
- `Implement story "${story.title}" according to the fallback planning task.`,
2415
- "Ensure done criteria and test requirements are explicitly documented for execution.",
2622
+ `Integrate dependent contracts/interfaces for "${story.title}" after core scope implementation.`,
2623
+ "Align internal/external interfaces, data contracts, and dependency wiring with SDS/OpenAPI context.",
2624
+ "Record dependency rationale and compatibility constraints in the task output.",
2416
2625
  ].join("\n"),
2417
2626
  estimatedStoryPoints: 3,
2418
2627
  priorityHint: 2,
@@ -2423,6 +2632,24 @@ export class CreateTasksService {
2423
2632
  integrationTests: [],
2424
2633
  apiTests: [],
2425
2634
  },
2635
+ {
2636
+ localId: "t-fallback-3",
2637
+ title: `Validate ${story.title} regressions and readiness`,
2638
+ type: "chore",
2639
+ description: [
2640
+ `Validate "${story.title}" end-to-end with focused regression coverage and readiness evidence.`,
2641
+ "Add/update targeted tests and verification scripts tied to implemented behavior.",
2642
+ "Document release/code-review/QA evidence and unresolved risks explicitly.",
2643
+ ].join("\n"),
2644
+ estimatedStoryPoints: 2,
2645
+ priorityHint: 3,
2646
+ dependsOnKeys: ["t-fallback-2"],
2647
+ relatedDocs: story.relatedDocs ?? [],
2648
+ unitTests: [],
2649
+ componentTests: [],
2650
+ integrationTests: [],
2651
+ apiTests: [],
2652
+ },
2426
2653
  ];
2427
2654
  }
2428
2655
  async generatePlanFromAgent(epics, agent, docSummary, options) {
@@ -2493,7 +2720,53 @@ export class CreateTasksService {
2493
2720
  }
2494
2721
  return { epics: planEpics, stories: planStories, tasks: planTasks };
2495
2722
  }
2496
- async writePlanArtifacts(projectKey, plan, docSummary) {
2723
+ buildSdsCoverageReport(projectKey, docs, plan) {
2724
+ const sections = this.extractSdsSectionCandidates(docs, SDS_COVERAGE_REPORT_SECTION_LIMIT);
2725
+ const normalize = (value) => value
2726
+ .toLowerCase()
2727
+ .replace(/[`*_]/g, "")
2728
+ .replace(/[^a-z0-9\s/-]+/g, " ")
2729
+ .replace(/\s+/g, " ")
2730
+ .trim();
2731
+ const planCorpus = normalize([
2732
+ ...plan.epics.map((epic) => `${epic.title} ${epic.description ?? ""} ${(epic.acceptanceCriteria ?? []).join(" ")}`),
2733
+ ...plan.stories.map((story) => `${story.title} ${story.userStory ?? ""} ${story.description ?? ""} ${(story.acceptanceCriteria ?? []).join(" ")}`),
2734
+ ...plan.tasks.map((task) => `${task.title} ${task.description ?? ""}`),
2735
+ ].join("\n"));
2736
+ const matched = [];
2737
+ const unmatched = [];
2738
+ for (const section of sections) {
2739
+ const normalizedSection = normalize(section);
2740
+ if (!normalizedSection)
2741
+ continue;
2742
+ const keywords = normalizedSection
2743
+ .split(/\s+/)
2744
+ .filter((token) => token.length >= 4)
2745
+ .slice(0, 6);
2746
+ const hasDirectMatch = normalizedSection.length >= 6 && planCorpus.includes(normalizedSection);
2747
+ const hasKeywordMatch = keywords.some((keyword) => planCorpus.includes(keyword));
2748
+ if (hasDirectMatch || hasKeywordMatch) {
2749
+ matched.push(section);
2750
+ }
2751
+ else {
2752
+ unmatched.push(section);
2753
+ }
2754
+ }
2755
+ const totalSections = matched.length + unmatched.length;
2756
+ const coverageRatio = totalSections === 0 ? 1 : matched.length / totalSections;
2757
+ return {
2758
+ projectKey,
2759
+ generatedAt: new Date().toISOString(),
2760
+ totalSections,
2761
+ matched,
2762
+ unmatched,
2763
+ coverageRatio: Number(coverageRatio.toFixed(4)),
2764
+ notes: totalSections === 0
2765
+ ? ["No SDS section headings detected; coverage defaults to 1.0."]
2766
+ : ["Coverage is heading-based heuristic match between SDS sections and generated epic/story/task corpus."],
2767
+ };
2768
+ }
2769
+ async writePlanArtifacts(projectKey, plan, docSummary, docs) {
2497
2770
  const baseDir = path.join(this.workspace.mcodaDir, "tasks", projectKey);
2498
2771
  await fs.mkdir(baseDir, { recursive: true });
2499
2772
  const write = async (file, data) => {
@@ -2504,6 +2777,7 @@ export class CreateTasksService {
2504
2777
  await write("epics.json", plan.epics);
2505
2778
  await write("stories.json", plan.stories);
2506
2779
  await write("tasks.json", plan.tasks);
2780
+ await write("coverage-report.json", this.buildSdsCoverageReport(projectKey, docs, plan));
2507
2781
  return { folder: baseDir };
2508
2782
  }
2509
2783
  async persistPlanToDb(projectId, projectKey, plan, jobId, commandRunId, options) {
@@ -2632,12 +2906,7 @@ export class CreateTasksService {
2632
2906
  discoveredTestCommands = [];
2633
2907
  }
2634
2908
  }
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
- ]);
2909
+ const qaBlockers = uniqueStrings(qaReadiness.blockers ?? []);
2641
2910
  const qaReadinessWithHarness = {
2642
2911
  ...qaReadiness,
2643
2912
  blockers: qaBlockers.length ? qaBlockers : undefined,
@@ -2833,7 +3102,7 @@ export class CreateTasksService {
2833
3102
  timestamp: new Date().toISOString(),
2834
3103
  details: { tasks: plan.tasks.length, source: planSource, fallbackReason },
2835
3104
  });
2836
- const { folder } = await this.writePlanArtifacts(options.projectKey, plan, docSummary);
3105
+ const { folder } = await this.writePlanArtifacts(options.projectKey, plan, docSummary, docs);
2837
3106
  await this.jobService.writeCheckpoint(job.id, {
2838
3107
  stage: "plan_written",
2839
3108
  timestamp: new Date().toISOString(),
@@ -2846,6 +3115,61 @@ export class CreateTasksService {
2846
3115
  qaOverrides,
2847
3116
  });
2848
3117
  await this.seedPriorities(options.projectKey);
3118
+ let sufficiencyAudit;
3119
+ let sufficiencyAuditError;
3120
+ if (this.taskSufficiencyFactory) {
3121
+ try {
3122
+ const sufficiencyService = await this.taskSufficiencyFactory(this.workspace);
3123
+ try {
3124
+ try {
3125
+ sufficiencyAudit = await sufficiencyService.runAudit({
3126
+ workspace: options.workspace,
3127
+ projectKey: options.projectKey,
3128
+ sourceCommand: "create-tasks",
3129
+ });
3130
+ }
3131
+ catch (error) {
3132
+ sufficiencyAuditError = error?.message ?? String(error);
3133
+ await this.jobService.appendLog(job.id, `Task sufficiency audit failed; continuing with created backlog: ${sufficiencyAuditError}\n`);
3134
+ }
3135
+ }
3136
+ finally {
3137
+ try {
3138
+ await sufficiencyService.close();
3139
+ }
3140
+ catch (closeError) {
3141
+ const closeMessage = closeError?.message ?? String(closeError);
3142
+ const details = `Task sufficiency audit close failed; continuing with created backlog: ${closeMessage}`;
3143
+ sufficiencyAuditError = sufficiencyAuditError ? `${sufficiencyAuditError}; ${details}` : details;
3144
+ await this.jobService.appendLog(job.id, `${details}\n`);
3145
+ }
3146
+ }
3147
+ }
3148
+ catch (error) {
3149
+ sufficiencyAuditError = error?.message ?? String(error);
3150
+ await this.jobService.appendLog(job.id, `Task sufficiency audit setup failed; continuing with created backlog: ${sufficiencyAuditError}\n`);
3151
+ }
3152
+ await this.jobService.writeCheckpoint(job.id, {
3153
+ stage: "task_sufficiency_audit",
3154
+ timestamp: new Date().toISOString(),
3155
+ details: {
3156
+ status: sufficiencyAudit ? "succeeded" : "failed",
3157
+ error: sufficiencyAuditError,
3158
+ jobId: sufficiencyAudit?.jobId,
3159
+ commandRunId: sufficiencyAudit?.commandRunId,
3160
+ satisfied: sufficiencyAudit?.satisfied,
3161
+ dryRun: sufficiencyAudit?.dryRun,
3162
+ totalTasksAdded: sufficiencyAudit?.totalTasksAdded,
3163
+ totalTasksUpdated: sufficiencyAudit?.totalTasksUpdated,
3164
+ finalCoverageRatio: sufficiencyAudit?.finalCoverageRatio,
3165
+ reportPath: sufficiencyAudit?.reportPath,
3166
+ remainingSectionCount: sufficiencyAudit?.remainingSectionHeadings.length,
3167
+ remainingFolderCount: sufficiencyAudit?.remainingFolderEntries.length,
3168
+ remainingGapCount: sufficiencyAudit?.remainingGaps.total,
3169
+ warnings: sufficiencyAudit?.warnings,
3170
+ },
3171
+ });
3172
+ }
2849
3173
  await this.jobService.updateJobStatus(job.id, "completed", {
2850
3174
  payload: {
2851
3175
  epicsCreated: epicRows.length,
@@ -2856,6 +3180,22 @@ export class CreateTasksService {
2856
3180
  planFolder: folder,
2857
3181
  planSource,
2858
3182
  fallbackReason,
3183
+ sufficiencyAudit: sufficiencyAudit
3184
+ ? {
3185
+ jobId: sufficiencyAudit.jobId,
3186
+ commandRunId: sufficiencyAudit.commandRunId,
3187
+ satisfied: sufficiencyAudit.satisfied,
3188
+ totalTasksAdded: sufficiencyAudit.totalTasksAdded,
3189
+ totalTasksUpdated: sufficiencyAudit.totalTasksUpdated,
3190
+ finalCoverageRatio: sufficiencyAudit.finalCoverageRatio,
3191
+ reportPath: sufficiencyAudit.reportPath,
3192
+ remainingSectionCount: sufficiencyAudit.remainingSectionHeadings.length,
3193
+ remainingFolderCount: sufficiencyAudit.remainingFolderEntries.length,
3194
+ remainingGapCount: sufficiencyAudit.remainingGaps.total,
3195
+ warnings: sufficiencyAudit.warnings,
3196
+ }
3197
+ : undefined,
3198
+ sufficiencyAuditError,
2859
3199
  },
2860
3200
  });
2861
3201
  await this.jobService.finishCommandRun(commandRun.id, "succeeded");
@@ -1 +1 @@
1
- {"version":3,"file":"RefineTasksService.d.ts","sourceRoot":"","sources":["../../../src/services/planning/RefineTasksService.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAEnD,OAAO,EAAE,gBAAgB,EAA6C,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAC7G,OAAO,EAIL,kBAAkB,EAClB,iBAAiB,EAKlB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAKrE,UAAU,kBAAmB,SAAQ,kBAAkB;IACrD,SAAS,EAAE,mBAAmB,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AA6MD,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,IAAI,CAAmB;IAC/B,OAAO,CAAC,aAAa,CAAsB;IAC3C,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,SAAS,CAAsB;IACvC,OAAO,CAAC,aAAa,CAAC,CAAqB;gBAGzC,SAAS,EAAE,mBAAmB,EAC9B,IAAI,EAAE;QACJ,MAAM,EAAE,YAAY,CAAC;QACrB,UAAU,EAAE,UAAU,CAAC;QACvB,YAAY,EAAE,YAAY,CAAC;QAC3B,IAAI,EAAE,gBAAgB,CAAC;QACvB,aAAa,EAAE,mBAAmB,CAAC;QACnC,cAAc,EAAE,cAAc,CAAC;QAC/B,aAAa,CAAC,EAAE,kBAAkB,CAAC;KACpC;WAYU,MAAM,CAAC,SAAS,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAuB1E,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAkBd,cAAc;YAYd,YAAY;YAwBZ,mBAAmB;IA4CjC,OAAO,CAAC,mBAAmB;YAYb,WAAW;IA4KzB,OAAO,CAAC,iBAAiB;YASX,gBAAgB;IAmK9B,OAAO,CAAC,gBAAgB;IA+BxB,OAAO,CAAC,uBAAuB;YA+CjB,aAAa;YA4Eb,gBAAgB;YA+BhB,kBAAkB;IAkChC,OAAO,CAAC,aAAa;IAKrB,OAAO,CAAC,kBAAkB;IAkB1B,OAAO,CAAC,iBAAiB;IA8EzB,OAAO,CAAC,WAAW;IA0BnB,OAAO,CAAC,iBAAiB;YAkBX,eAAe;YAqYf,WAAW;IAmGnB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC;CAma3E"}
1
+ {"version":3,"file":"RefineTasksService.d.ts","sourceRoot":"","sources":["../../../src/services/planning/RefineTasksService.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAEnD,OAAO,EAAE,gBAAgB,EAA6C,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAC7G,OAAO,EAIL,kBAAkB,EAClB,iBAAiB,EAKlB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAKrE,UAAU,kBAAmB,SAAQ,kBAAkB;IACrD,SAAS,EAAE,mBAAmB,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAgOD,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,IAAI,CAAmB;IAC/B,OAAO,CAAC,aAAa,CAAsB;IAC3C,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,SAAS,CAAsB;IACvC,OAAO,CAAC,aAAa,CAAC,CAAqB;gBAGzC,SAAS,EAAE,mBAAmB,EAC9B,IAAI,EAAE;QACJ,MAAM,EAAE,YAAY,CAAC;QACrB,UAAU,EAAE,UAAU,CAAC;QACvB,YAAY,EAAE,YAAY,CAAC;QAC3B,IAAI,EAAE,gBAAgB,CAAC;QACvB,aAAa,EAAE,mBAAmB,CAAC;QACnC,cAAc,EAAE,cAAc,CAAC;QAC/B,aAAa,CAAC,EAAE,kBAAkB,CAAC;KACpC;WAYU,MAAM,CAAC,SAAS,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAuB1E,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAkBd,cAAc;YAYd,YAAY;YAwBZ,mBAAmB;IA4CjC,OAAO,CAAC,mBAAmB;YAYb,WAAW;IA4KzB,OAAO,CAAC,iBAAiB;YASX,gBAAgB;IAmK9B,OAAO,CAAC,gBAAgB;IA+BxB,OAAO,CAAC,uBAAuB;YA+CjB,aAAa;YA4Eb,gBAAgB;YA+BhB,kBAAkB;IAkChC,OAAO,CAAC,aAAa;IAKrB,OAAO,CAAC,kBAAkB;IAkB1B,OAAO,CAAC,iBAAiB;IA2GzB,OAAO,CAAC,WAAW;IA0BnB,OAAO,CAAC,iBAAiB;YAkBX,eAAe;YAmaf,WAAW;IAmGnB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC;CAma3E"}