@mcoda/core 0.1.27 → 0.1.28

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.
@@ -1 +1 @@
1
- {"version":3,"file":"TaskSufficiencyService.d.ts","sourceRoot":"","sources":["../../../src/services/planning/TaskSufficiencyService.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAiC,MAAM,WAAW,CAAC;AAE/E,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AA6EnD,MAAM,WAAW,2BAA2B;IAC1C,SAAS,EAAE,mBAAmB,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,6BAA6B;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,eAAe,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,0BAA0B;IACzC,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,wBAAwB,EAAE,MAAM,EAAE,CAAC;IACnC,sBAAsB,EAAE,MAAM,EAAE,CAAC;IACjC,aAAa,EAAE;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;KACf,CAAC;IACF,UAAU,EAAE,6BAA6B,EAAE,CAAC;IAC5C,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,KAAK,mBAAmB,GAAG;IACzB,aAAa,EAAE,mBAAmB,CAAC;IACnC,UAAU,EAAE,UAAU,CAAC;CACxB,CAAC;AAuJF,qBAAa,sBAAsB;IACjC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAsB;IACpD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAa;IACxC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAU;IAC5C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAU;IACzC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAsB;gBAG9C,SAAS,EAAE,mBAAmB,EAC9B,IAAI,EAAE,mBAAmB,EACzB,SAAS,GAAE;QAAE,iBAAiB,CAAC,EAAE,OAAO,CAAC;QAAC,cAAc,CAAC,EAAE,OAAO,CAAA;KAAO;WAS9D,MAAM,CAAC,SAAS,EAAE,mBAAmB,GAAG,OAAO,CAAC,sBAAsB,CAAC;IAU9E,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YASd,gBAAgB;YA4BhB,iBAAiB;YAmCjB,cAAc;YAcd,mBAAmB;IAwEjC,OAAO,CAAC,gBAAgB;IA2BxB,OAAO,CAAC,aAAa;IA+BrB,OAAO,CAAC,cAAc;YA8BR,iBAAiB;YAyGjB,cAAc;YAkHd,oBAAoB;IAkB5B,QAAQ,CAAC,OAAO,EAAE,2BAA2B,GAAG,OAAO,CAAC,0BAA0B,CAAC;CAmU1F"}
1
+ {"version":3,"file":"TaskSufficiencyService.d.ts","sourceRoot":"","sources":["../../../src/services/planning/TaskSufficiencyService.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAiC,MAAM,WAAW,CAAC;AAE/E,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAmGnD,MAAM,WAAW,2BAA2B;IAC1C,SAAS,EAAE,mBAAmB,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,6BAA6B;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,eAAe,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,0BAA0B;IACzC,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,wBAAwB,EAAE,MAAM,EAAE,CAAC;IACnC,sBAAsB,EAAE,MAAM,EAAE,CAAC;IACjC,aAAa,EAAE;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;KACf,CAAC;IACF,UAAU,EAAE,6BAA6B,EAAE,CAAC;IAC5C,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,KAAK,mBAAmB,GAAG;IACzB,aAAa,EAAE,mBAAmB,CAAC;IACnC,UAAU,EAAE,UAAU,CAAC;CACxB,CAAC;AAsNF,qBAAa,sBAAsB;IACjC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAsB;IACpD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAa;IACxC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAU;IAC5C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAU;IACzC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAsB;gBAG9C,SAAS,EAAE,mBAAmB,EAC9B,IAAI,EAAE,mBAAmB,EACzB,SAAS,GAAE;QAAE,iBAAiB,CAAC,EAAE,OAAO,CAAC;QAAC,cAAc,CAAC,EAAE,OAAO,CAAA;KAAO;WAS9D,MAAM,CAAC,SAAS,EAAE,mBAAmB,GAAG,OAAO,CAAC,sBAAsB,CAAC;IAU9E,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YASd,gBAAgB;YA4BhB,iBAAiB;YAmCjB,cAAc;YAcd,mBAAmB;IAwEjC,OAAO,CAAC,gBAAgB;IA2BxB,OAAO,CAAC,aAAa;IA+BrB,OAAO,CAAC,cAAc;YA2BR,iBAAiB;YAyGjB,cAAc;YAkId,oBAAoB;IAkB5B,QAAQ,CAAC,OAAO,EAAE,2BAA2B,GAAG,OAAO,CAAC,0BAA0B,CAAC;CA2U1F"}
@@ -6,16 +6,17 @@ import { JobService } from "../jobs/JobService.js";
6
6
  import { createEpicKeyGenerator, createStoryKeyGenerator, createTaskKeyGenerator } from "./KeyHelpers.js";
7
7
  const DEFAULT_MAX_ITERATIONS = 5;
8
8
  const DEFAULT_MAX_TASKS_PER_ITERATION = 24;
9
- const DEFAULT_MIN_COVERAGE_RATIO = 0.96;
9
+ const DEFAULT_MIN_COVERAGE_RATIO = 1;
10
10
  const SDS_SCAN_MAX_FILES = 120;
11
11
  const SDS_HEADING_LIMIT = 200;
12
12
  const SDS_FOLDER_LIMIT = 240;
13
- const GAP_BUNDLE_SIZE = 4;
13
+ const GAP_BUNDLE_MAX_ANCHORS = 3;
14
+ const COVERAGE_HEURISTICS_VERSION = 2;
14
15
  const REPORT_FILE_NAME = "task-sufficiency-report.json";
15
16
  const ignoredDirs = new Set([".git", "node_modules", "dist", "build", ".mcoda", ".docdex"]);
16
17
  const sdsFilenamePattern = /(sds|software[-_ ]design|system[-_ ]design|design[-_ ]spec)/i;
17
18
  const sdsContentPattern = /(software design specification|system design specification|^#\s*sds\b)/im;
18
- const nonImplementationHeadingPattern = /\b(revision history|table of contents|purpose|scope|definitions?|abbreviations?|glossary|references?|appendix|document control|authors?)\b/i;
19
+ const nonImplementationHeadingPattern = /\b(software design specification|system design specification|\bsds\b|revision history|table of contents|purpose|scope|definitions?|abbreviations?|glossary|references?|appendix|document control|authors?)\b/i;
19
20
  const likelyImplementationHeadingPattern = /\b(architecture|entity|entities|service|services|module|modules|component|components|pipeline|workflow|api|endpoint|schema|model|feature|store|database|ingestion|training|inference|ui|frontend|backend|ops|observability|security|deployment|solver|integration|testing|validation|contract|index|mapping|registry|cache|queue|event|job|task|migration|controller|router|policy)\b/i;
20
21
  const repoRootSegments = new Set([
21
22
  "apps",
@@ -43,6 +44,27 @@ const repoRootSegments = new Set([
43
44
  "web",
44
45
  ]);
45
46
  const headingNoiseTokens = new Set(["and", "for", "from", "into", "the", "with"]);
47
+ const coverageStopTokens = new Set([
48
+ "about",
49
+ "across",
50
+ "after",
51
+ "before",
52
+ "between",
53
+ "from",
54
+ "into",
55
+ "over",
56
+ "under",
57
+ "using",
58
+ "with",
59
+ "without",
60
+ "into",
61
+ "onto",
62
+ "the",
63
+ "and",
64
+ "for",
65
+ "of",
66
+ "to",
67
+ ]);
46
68
  const normalizeText = (value) => value
47
69
  .toLowerCase()
48
70
  .replace(/[`*_]/g, " ")
@@ -123,13 +145,32 @@ const deriveFolderDomain = (entry) => {
123
145
  const extractMarkdownHeadings = (content, limit) => {
124
146
  if (!content)
125
147
  return [];
126
- const matches = [...content.matchAll(/^\s{0,3}#{1,6}\s+(.+?)\s*$/gm)];
148
+ const lines = content.split(/\r?\n/);
127
149
  const headings = [];
128
- for (const match of matches) {
129
- const heading = match[1]?.replace(/#+$/, "").trim();
130
- if (!heading)
150
+ for (let index = 0; index < lines.length; index += 1) {
151
+ const line = lines[index]?.trim() ?? "";
152
+ if (!line)
131
153
  continue;
132
- headings.push(heading);
154
+ const hashHeading = line.match(/^#{1,6}\s+(.+)$/);
155
+ if (hashHeading) {
156
+ const heading = hashHeading[1]?.replace(/#+$/, "").trim();
157
+ if (heading)
158
+ headings.push(heading);
159
+ }
160
+ else if (index + 1 < lines.length &&
161
+ /^[=-]{3,}\s*$/.test((lines[index + 1] ?? "").trim()) &&
162
+ !line.startsWith("-") &&
163
+ !line.startsWith("*")) {
164
+ headings.push(line);
165
+ }
166
+ else {
167
+ const numberedHeading = line.match(/^(\d+(?:\.\d+)+)\s+(.+)$/);
168
+ if (numberedHeading) {
169
+ const heading = `${numberedHeading[1]} ${numberedHeading[2]}`.trim();
170
+ if (/[a-z]/i.test(heading))
171
+ headings.push(heading);
172
+ }
173
+ }
133
174
  if (headings.length >= limit)
134
175
  break;
135
176
  }
@@ -158,39 +199,73 @@ const extractFolderEntries = (content, limit) => {
158
199
  }
159
200
  return unique(candidates).slice(0, limit);
160
201
  };
202
+ const tokenizeCoverageSignal = (value) => unique(value
203
+ .split(/\s+/)
204
+ .map((token) => token.replace(/[^a-z0-9._-]+/g, ""))
205
+ .filter((token) => token.length >= 3 && !coverageStopTokens.has(token)));
206
+ const buildBigrams = (tokens) => {
207
+ const bigrams = [];
208
+ for (let index = 0; index < tokens.length - 1; index += 1) {
209
+ const left = tokens[index];
210
+ const right = tokens[index + 1];
211
+ if (!left || !right)
212
+ continue;
213
+ bigrams.push(`${left} ${right}`);
214
+ }
215
+ return unique(bigrams);
216
+ };
161
217
  const headingCovered = (corpus, heading) => {
162
- const normalized = normalizeText(heading);
218
+ const normalized = normalizeText(normalizeHeadingCandidate(heading));
163
219
  if (!normalized)
164
220
  return true;
165
221
  if (corpus.includes(normalized))
166
222
  return true;
167
- const tokens = normalized
168
- .split(/\s+/)
169
- .filter((token) => token.length >= 4)
170
- .slice(0, 8);
223
+ const tokens = tokenizeCoverageSignal(normalized).slice(0, 10);
171
224
  if (tokens.length === 0)
172
225
  return true;
173
226
  const hitCount = tokens.filter((token) => corpus.includes(token)).length;
174
- const minHits = Math.min(2, tokens.length);
175
- return hitCount >= minHits;
227
+ const requiredHits = tokens.length <= 2
228
+ ? tokens.length
229
+ : tokens.length <= 4
230
+ ? 2
231
+ : Math.min(4, Math.ceil(tokens.length * 0.6));
232
+ if (hitCount < requiredHits)
233
+ return false;
234
+ if (tokens.length >= 3) {
235
+ const longestToken = tokens.reduce((longest, token) => (token.length > longest.length ? token : longest), "");
236
+ if (longestToken.length >= 6 && !corpus.includes(longestToken))
237
+ return false;
238
+ }
239
+ const bigrams = buildBigrams(tokens);
240
+ if (tokens.length >= 3 && bigrams.length > 0 && !bigrams.some((bigram) => corpus.includes(bigram))) {
241
+ return false;
242
+ }
243
+ return true;
176
244
  };
177
245
  const folderEntryCovered = (corpus, entry) => {
178
- const normalized = normalizeText(entry).replace(/\s+/g, "");
179
- if (!normalized)
246
+ const normalizedEntry = normalizeFolderEntry(entry)?.toLowerCase().replace(/\/+/g, "/");
247
+ if (!normalizedEntry)
180
248
  return true;
181
- if (corpus.includes(normalized))
249
+ const corpusTight = corpus.replace(/\s+/g, "");
250
+ if (corpusTight.includes(normalizedEntry.replace(/\s+/g, "")))
182
251
  return true;
183
- const segments = normalized.split("/").filter(Boolean);
252
+ const segments = normalizedEntry
253
+ .split("/")
254
+ .map((segment) => segment.trim().replace(/[^a-z0-9._-]+/g, ""))
255
+ .filter(Boolean);
184
256
  if (segments.length === 0)
185
257
  return true;
186
- const leaf = segments[segments.length - 1];
187
- const parent = segments.length > 1 ? segments[segments.length - 2] : undefined;
188
- if (leaf && corpus.includes(leaf)) {
189
- if (!parent)
190
- return true;
191
- return corpus.includes(parent);
258
+ const tailSegments = unique(segments.slice(Math.max(0, segments.length - 3)));
259
+ const hitCount = tailSegments.filter((segment) => corpus.includes(segment)).length;
260
+ const requiredHits = tailSegments.length <= 1 ? 1 : Math.min(2, tailSegments.length);
261
+ if (hitCount < requiredHits)
262
+ return false;
263
+ if (tailSegments.length >= 2) {
264
+ const hasStrongTokenMatch = tailSegments.some((segment) => segment.length >= 5 && corpus.includes(segment));
265
+ if (!hasStrongTokenMatch)
266
+ return false;
192
267
  }
193
- return false;
268
+ return true;
194
269
  };
195
270
  const readJsonSafe = (raw, fallback) => {
196
271
  if (typeof raw !== "string" || raw.trim().length === 0)
@@ -417,33 +492,33 @@ export class TaskSufficiencyService {
417
492
  return items;
418
493
  }
419
494
  bundleGapItems(gapItems, limit) {
420
- const groups = new Map();
421
- const orderedKeys = [];
422
- for (const item of gapItems) {
423
- const key = `${item.domain}:${item.kind}`;
424
- if (!groups.has(key)) {
425
- groups.set(key, []);
426
- orderedKeys.push(key);
427
- }
428
- groups.get(key)?.push(item);
429
- }
430
495
  const bundles = [];
431
- for (const key of orderedKeys) {
432
- const group = groups.get(key) ?? [];
433
- for (let index = 0; index < group.length; index += GAP_BUNDLE_SIZE) {
434
- if (bundles.length >= limit)
435
- return bundles;
436
- const chunk = group.slice(index, index + GAP_BUNDLE_SIZE);
437
- const kinds = new Set(chunk.map((item) => item.kind));
438
- bundles.push({
439
- kind: kinds.size > 1 ? "mixed" : chunk[0]?.kind ?? "section",
440
- domain: chunk[0]?.domain ?? "coverage",
441
- values: chunk.map((item) => item.value),
442
- normalizedAnchors: chunk.map((item) => item.normalizedAnchor),
443
- });
496
+ const activeByDomain = new Map();
497
+ for (const item of gapItems.slice(0, limit)) {
498
+ const domainKey = item.domain;
499
+ let bundle = activeByDomain.get(domainKey);
500
+ if (!bundle || bundle.values.length >= GAP_BUNDLE_MAX_ANCHORS) {
501
+ bundle = {
502
+ kind: item.kind,
503
+ domain: item.domain,
504
+ values: [],
505
+ normalizedAnchors: [],
506
+ };
507
+ bundles.push(bundle);
508
+ activeByDomain.set(domainKey, bundle);
509
+ }
510
+ else if (bundle.kind !== item.kind) {
511
+ bundle.kind = "mixed";
444
512
  }
513
+ if (!bundle.values.includes(item.value))
514
+ bundle.values.push(item.value);
515
+ if (!bundle.normalizedAnchors.includes(item.normalizedAnchor)) {
516
+ bundle.normalizedAnchors.push(item.normalizedAnchor);
517
+ }
518
+ if (bundles.length >= limit)
519
+ break;
445
520
  }
446
- return bundles;
521
+ return bundles.slice(0, limit);
447
522
  }
448
523
  async ensureTargetStory(project) {
449
524
  const db = this.workspaceRepo.getDb();
@@ -530,45 +605,59 @@ export class TaskSufficiencyService {
530
605
  const taskKeyGen = createTaskKeyGenerator(params.storyKey, existingTaskKeys);
531
606
  const now = new Date().toISOString();
532
607
  const taskInserts = params.gapBundles.map((bundle, index) => {
533
- const domainLabel = bundle.domain.replace(/[-_]+/g, " ").trim();
534
- const titlePrefix = bundle.kind === "section"
535
- ? "Close SDS section coverage"
536
- : bundle.kind === "folder"
537
- ? "Materialize SDS structure coverage"
538
- : "Close SDS coverage bundle";
539
- const title = `${titlePrefix}: ${domainLabel || "implementation scope"}`.slice(0, 180);
608
+ const target = bundle.values[0] ?? "SDS coverage gap";
609
+ const scopeCount = bundle.values.length;
610
+ const domainLabel = bundle.domain.replace(/[-_]+/g, " ").trim() || "coverage";
611
+ const titlePrefix = bundle.kind === "folder"
612
+ ? "Implement SDS path"
613
+ : bundle.kind === "mixed"
614
+ ? "Implement SDS bundle"
615
+ : "Implement SDS section";
616
+ const title = scopeCount <= 1
617
+ ? `${titlePrefix}: ${target}`.slice(0, 180)
618
+ : `Implement SDS ${domainLabel} coverage bundle (${scopeCount} anchors)`.slice(0, 180);
540
619
  const objective = bundle.kind === "folder"
541
- ? `Create or update implementation artifacts for ${bundle.values.length} SDS folder-tree requirement(s).`
542
- : `Implement missing functionality for ${bundle.values.length} SDS section requirement(s).`;
620
+ ? scopeCount <= 1
621
+ ? `Create or update production code under the SDS folder-tree path \`${target}\`.`
622
+ : `Create or update production code for ${scopeCount} related SDS folder-tree paths in the ${domainLabel} domain.`
623
+ : bundle.kind === "mixed"
624
+ ? `Implement a cohesive capability slice covering both SDS sections and folder targets in the ${domainLabel} domain.`
625
+ : scopeCount <= 1
626
+ ? `Implement the missing production functionality described by SDS section \`${target}\`.`
627
+ : `Implement ${scopeCount} related SDS section requirements in the ${domainLabel} domain.`;
543
628
  const scopeLines = bundle.values.map((value) => `- ${value}`);
544
629
  const anchorLines = bundle.normalizedAnchors.map((anchor) => `- ${anchor}`);
545
630
  const description = [
546
- `## Objective`,
631
+ "## Objective",
547
632
  objective,
548
- ``,
549
- `## Context`,
633
+ "",
634
+ "## Context",
550
635
  `- Generated by task-sufficiency-audit iteration ${params.iteration}.`,
551
636
  `- Coverage domain: ${bundle.domain}`,
552
- ``,
553
- `## Anchor Scope`,
637
+ `- Scope kind: ${bundle.kind}`,
638
+ "",
639
+ "## Anchor Scope",
554
640
  ...scopeLines,
555
- ``,
556
- `## Anchor Keys`,
641
+ "",
642
+ "## Anchor Keys",
557
643
  ...anchorLines,
558
- ``,
559
- `## Implementation Plan`,
560
- `- Implement production code for this bundle before adding follow-up docs-only changes.`,
561
- `- Update module wiring/contracts touched by these anchors.`,
562
- `- Ensure each anchor has deterministic evidence (tests or checks).`,
563
- ``,
564
- `## Testing`,
565
- `- Add or update tests that validate each listed anchor scope.`,
566
- `- Keep regression suites green after applying this bundle.`,
567
- ``,
568
- `## Definition of Done`,
569
- `- All anchor scope items in this bundle are represented in implementation code.`,
570
- `- Validation evidence exists for every anchor key listed above.`,
644
+ "",
645
+ "## Implementation Plan",
646
+ "- Phase 1: add or update contracts/interfaces used by this scope.",
647
+ "- Phase 2: implement production logic for each anchor item (no docs-only closure).",
648
+ "- Phase 3: wire dependencies and startup/runtime integration points affected by this scope.",
649
+ "- Keep implementation traceable to anchor keys in commit and test evidence.",
650
+ "",
651
+ "## Testing",
652
+ "- Add or update targeted unit/component/integration/API coverage for all listed anchors.",
653
+ "- Execute the smallest deterministic test scope that proves the behavior.",
654
+ "",
655
+ "## Definition of Done",
656
+ "- All anchor scope items in this bundle are represented in working code.",
657
+ "- Validation evidence exists and maps back to each anchor key.",
571
658
  ].join("\n");
659
+ const storyPointsBase = bundle.kind === "folder" ? 1 : 2;
660
+ const storyPoints = Math.min(8, Math.max(2, storyPointsBase + scopeCount + (bundle.kind === "mixed" ? 1 : 0)));
572
661
  return {
573
662
  projectId: params.project.id,
574
663
  epicId: params.epicId,
@@ -578,13 +667,14 @@ export class TaskSufficiencyService {
578
667
  description,
579
668
  type: "feature",
580
669
  status: "not_started",
581
- storyPoints: Math.min(5, Math.max(2, bundle.normalizedAnchors.length)),
670
+ storyPoints,
582
671
  priority: params.maxPriority + index + 1,
583
672
  metadata: {
584
673
  sufficiencyAudit: {
585
674
  source: "task-sufficiency-audit",
586
675
  kind: bundle.kind,
587
676
  domain: bundle.domain,
677
+ scopeCount,
588
678
  values: bundle.values,
589
679
  anchor: bundle.normalizedAnchors[0],
590
680
  anchors: bundle.normalizedAnchors,
@@ -670,8 +760,9 @@ export class TaskSufficiencyService {
670
760
  if (skippedHeadingSignals > 0 || skippedFolderSignals > 0) {
671
761
  warnings.push(`Filtered non-actionable SDS signals (headings=${skippedHeadingSignals}, folders=${skippedFolderSignals}) before remediation.`);
672
762
  }
673
- if (sectionHeadings.length === 0 && folderEntries.length === 0) {
674
- warnings.push("No actionable implementation signals detected from SDS headings/folder tree after filtering; audit will report coverage only.");
763
+ const noActionableSignals = sectionHeadings.length === 0 && folderEntries.length === 0;
764
+ if (noActionableSignals) {
765
+ warnings.push("No actionable implementation signals detected from SDS headings/folder tree after filtering.");
675
766
  }
676
767
  await this.jobService.writeCheckpoint(job.id, {
677
768
  stage: "sds_loaded",
@@ -687,6 +778,9 @@ export class TaskSufficiencyService {
687
778
  docs: sdsDocs.map((doc) => path.relative(request.workspace.workspaceRoot, doc.path)),
688
779
  },
689
780
  });
781
+ if (noActionableSignals) {
782
+ throw new Error("task-sufficiency-audit could not derive actionable SDS implementation signals after filtering. Expand SDS implementation headings/folder tree and retry.");
783
+ }
690
784
  const iterations = [];
691
785
  let totalTasksAdded = 0;
692
786
  const totalTasksUpdated = 0;
@@ -720,7 +814,7 @@ export class TaskSufficiencyService {
720
814
  });
721
815
  break;
722
816
  }
723
- const gapItems = this.buildGapItems(coverage, snapshot.existingAnchors, maxTasksPerIteration * GAP_BUNDLE_SIZE);
817
+ const gapItems = this.buildGapItems(coverage, snapshot.existingAnchors, maxTasksPerIteration);
724
818
  const gapBundles = this.bundleGapItems(gapItems, maxTasksPerIteration);
725
819
  if (gapBundles.length === 0) {
726
820
  warnings.push(`Iteration ${iteration}: unresolved SDS gaps remain but no insertable gap items were identified.`);
@@ -812,6 +906,7 @@ export class TaskSufficiencyService {
812
906
  projectKey: request.projectKey,
813
907
  sourceCommand,
814
908
  generatedAt: new Date().toISOString(),
909
+ coverageHeuristicsVersion: COVERAGE_HEURISTICS_VERSION,
815
910
  dryRun,
816
911
  maxIterations,
817
912
  maxTasksPerIteration,
@@ -1 +1 @@
1
- {"version":3,"file":"CodeReviewService.d.ts","sourceRoot":"","sources":["../../../src/services/review/CodeReviewService.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"CodeReviewService.d.ts","sourceRoot":"","sources":["../../../src/services/review/CodeReviewService.ts"],"names":[],"mappings":"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"}
@@ -207,6 +207,90 @@ const normalizeSlugList = (input) => {
207
207
  }
208
208
  return Array.from(cleaned);
209
209
  };
210
+ const isRecord = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
211
+ const normalizeFailoverEvents = (value) => {
212
+ if (!Array.isArray(value))
213
+ return [];
214
+ const events = [];
215
+ for (const entry of value) {
216
+ if (!isRecord(entry))
217
+ continue;
218
+ if (typeof entry.type !== "string" || entry.type.trim().length === 0)
219
+ continue;
220
+ events.push({ ...entry });
221
+ }
222
+ return events;
223
+ };
224
+ const mergeFailoverEvents = (left, right) => {
225
+ if (!left.length)
226
+ return right;
227
+ if (!right.length)
228
+ return left;
229
+ const seen = new Set();
230
+ const merged = [];
231
+ const signature = (event) => [
232
+ event.type ?? "",
233
+ event.fromAgentId ?? "",
234
+ event.toAgentId ?? "",
235
+ event.at ?? "",
236
+ event.until ?? "",
237
+ event.durationMs ?? "",
238
+ ].join("|");
239
+ for (const event of [...left, ...right]) {
240
+ const key = signature(event);
241
+ if (seen.has(key))
242
+ continue;
243
+ seen.add(key);
244
+ merged.push(event);
245
+ }
246
+ return merged;
247
+ };
248
+ const mergeInvocationMetadata = (current, incoming) => {
249
+ if (!current && !incoming)
250
+ return undefined;
251
+ if (!incoming)
252
+ return current;
253
+ if (!current)
254
+ return { ...incoming };
255
+ const merged = { ...current, ...incoming };
256
+ const currentEvents = normalizeFailoverEvents(current.failoverEvents);
257
+ const incomingEvents = normalizeFailoverEvents(incoming.failoverEvents);
258
+ if (currentEvents.length > 0 || incomingEvents.length > 0) {
259
+ merged.failoverEvents = mergeFailoverEvents(currentEvents, incomingEvents);
260
+ }
261
+ return merged;
262
+ };
263
+ const summarizeFailoverEvent = (event) => {
264
+ const type = String(event.type ?? "unknown");
265
+ if (type === "switch_agent") {
266
+ const from = typeof event.fromAgentId === "string" ? event.fromAgentId : "unknown";
267
+ const to = typeof event.toAgentId === "string" ? event.toAgentId : "unknown";
268
+ return `switch_agent ${from} -> ${to}`;
269
+ }
270
+ if (type === "sleep_until_reset") {
271
+ const duration = typeof event.durationMs === "number" && Number.isFinite(event.durationMs)
272
+ ? `${Math.round(event.durationMs / 1000)}s`
273
+ : "unknown duration";
274
+ const until = typeof event.until === "string" ? event.until : "unknown";
275
+ return `sleep_until_reset ${duration} (until ${until})`;
276
+ }
277
+ if (type === "stream_restart_after_limit") {
278
+ const from = typeof event.fromAgentId === "string" ? event.fromAgentId : "unknown";
279
+ return `stream_restart_after_limit from ${from}`;
280
+ }
281
+ return type;
282
+ };
283
+ const resolveFailoverAgentId = (events, fallbackAgentId) => {
284
+ for (let index = events.length - 1; index >= 0; index -= 1) {
285
+ const event = events[index];
286
+ if (event?.type !== "switch_agent")
287
+ continue;
288
+ if (typeof event.toAgentId === "string" && event.toAgentId.trim().length > 0) {
289
+ return event.toAgentId;
290
+ }
291
+ }
292
+ return fallbackAgentId;
293
+ };
210
294
  const normalizePath = (value) => value
211
295
  .replace(/\\/g, "/")
212
296
  .replace(/^\.\//, "")
@@ -1990,6 +2074,45 @@ export class CodeReviewService {
1990
2074
  metadata: { commandName: "code-review", phase, action: phase, attempt },
1991
2075
  });
1992
2076
  };
2077
+ const logFailoverEvents = async (events) => {
2078
+ if (!events.length)
2079
+ return;
2080
+ for (const event of events) {
2081
+ await this.deps.workspaceRepo.insertTaskLog({
2082
+ taskRunId: taskRun.id,
2083
+ sequence: this.sequenceForTask(taskRun.id),
2084
+ timestamp: new Date().toISOString(),
2085
+ source: "agent_failover",
2086
+ message: `Agent failover: ${summarizeFailoverEvent(event)}`,
2087
+ details: event,
2088
+ });
2089
+ }
2090
+ };
2091
+ const resolveUsageAgent = async (fallback, events) => {
2092
+ const usageAgentId = resolveFailoverAgentId(events, fallback.id);
2093
+ if (usageAgentId === fallback.id)
2094
+ return fallback;
2095
+ const resolver = this.deps.agentService?.resolveAgent;
2096
+ if (typeof resolver !== "function")
2097
+ return fallback;
2098
+ try {
2099
+ const resolved = await resolver.call(this.deps.agentService, usageAgentId);
2100
+ return {
2101
+ id: resolved.id,
2102
+ defaultModel: typeof resolved.defaultModel === "string" ? resolved.defaultModel : fallback.defaultModel,
2103
+ };
2104
+ }
2105
+ catch (error) {
2106
+ await this.deps.workspaceRepo.insertTaskLog({
2107
+ taskRunId: taskRun.id,
2108
+ sequence: this.sequenceForTask(taskRun.id),
2109
+ timestamp: new Date().toISOString(),
2110
+ source: "agent_failover",
2111
+ message: `Unable to resolve failover agent (${usageAgentId}) for usage accounting: ${error instanceof Error ? error.message : String(error)}`,
2112
+ });
2113
+ return fallback;
2114
+ }
2115
+ };
1993
2116
  agentOutput = "";
1994
2117
  let durationSeconds = 0;
1995
2118
  let lastStreamMeta;
@@ -2002,7 +2125,7 @@ export class CodeReviewService {
2002
2125
  if (useStream && this.deps.agentService.invokeStream) {
2003
2126
  const stream = await withAbort(this.deps.agentService.invokeStream(agentToUse.id, {
2004
2127
  input: prompt,
2005
- metadata: { taskKey: task.key, retry: logSource === "agent_retry" },
2128
+ metadata: { command: "code-review", taskKey: task.key, retry: logSource === "agent_retry" },
2006
2129
  }));
2007
2130
  while (true) {
2008
2131
  abortIfSignaled();
@@ -2011,7 +2134,7 @@ export class CodeReviewService {
2011
2134
  break;
2012
2135
  const chunk = value;
2013
2136
  output += chunk.output ?? "";
2014
- metadata = chunk.metadata ?? metadata;
2137
+ metadata = mergeInvocationMetadata(metadata, chunk.metadata);
2015
2138
  await this.deps.workspaceRepo.insertTaskLog({
2016
2139
  taskRunId: taskRun.id,
2017
2140
  sequence: this.sequenceForTask(taskRun.id),
@@ -2024,10 +2147,10 @@ export class CodeReviewService {
2024
2147
  else {
2025
2148
  const response = await withAbort(this.deps.agentService.invoke(agentToUse.id, {
2026
2149
  input: prompt,
2027
- metadata: { taskKey: task.key, retry: logSource === "agent_retry" },
2150
+ metadata: { command: "code-review", taskKey: task.key, retry: logSource === "agent_retry" },
2028
2151
  }));
2029
2152
  output = response.output ?? "";
2030
- metadata = response.metadata;
2153
+ metadata = mergeInvocationMetadata(metadata, response.metadata);
2031
2154
  await this.deps.workspaceRepo.insertTaskLog({
2032
2155
  taskRunId: taskRun.id,
2033
2156
  sequence: this.sequenceForTask(taskRun.id),
@@ -2074,7 +2197,15 @@ export class CodeReviewService {
2074
2197
  model: (lastStreamMeta.model ?? lastStreamMeta.model_name ?? null),
2075
2198
  }
2076
2199
  : undefined;
2077
- await recordUsage("review_main", prompt, agentOutput, durationSeconds, tokenMetaMain, agentUsedForOutput, outputAttempt);
2200
+ const mainFailoverEvents = normalizeFailoverEvents(lastStreamMeta?.failoverEvents);
2201
+ await logFailoverEvents(mainFailoverEvents);
2202
+ const usageAgentForOutput = await resolveUsageAgent({
2203
+ id: agentUsedForOutput.id,
2204
+ defaultModel: typeof agentUsedForOutput?.defaultModel === "string"
2205
+ ? agentUsedForOutput.defaultModel
2206
+ : undefined,
2207
+ }, mainFailoverEvents);
2208
+ await recordUsage("review_main", prompt, agentOutput, durationSeconds, tokenMetaMain, usageAgentForOutput, outputAttempt);
2078
2209
  const primaryOutput = agentOutput;
2079
2210
  let retryOutput;
2080
2211
  let retryAgentUsed;
@@ -2126,7 +2257,10 @@ export class CodeReviewService {
2126
2257
  message: `Retrying with JSON-only agent override: ${retryAgentUsed.slug ?? retryAgentUsed.id}`,
2127
2258
  });
2128
2259
  }
2129
- const retryResp = await withAbort(this.deps.agentService.invoke(retryAgentUsed.id, { input: retryPrompt, metadata: { taskKey: task.key, retry: true } }));
2260
+ const retryResp = await withAbort(this.deps.agentService.invoke(retryAgentUsed.id, {
2261
+ input: retryPrompt,
2262
+ metadata: { command: "code-review", taskKey: task.key, retry: true },
2263
+ }));
2130
2264
  retryOutput = retryResp.output ?? "";
2131
2265
  const retryDuration = Math.round(((Date.now() - retryStarted) / 1000) * 1000) / 1000;
2132
2266
  await this.deps.workspaceRepo.insertTaskLog({
@@ -2150,7 +2284,15 @@ export class CodeReviewService {
2150
2284
  model: (retryResp.metadata.model ?? retryResp.metadata.model_name ?? null),
2151
2285
  }
2152
2286
  : undefined;
2153
- await recordUsage("review_retry", retryPrompt, retryOutput, retryDuration, retryTokenMeta, retryAgentUsed, 2);
2287
+ const retryFailoverEvents = normalizeFailoverEvents(retryResp.metadata?.failoverEvents);
2288
+ await logFailoverEvents(retryFailoverEvents);
2289
+ const retryUsageAgent = await resolveUsageAgent({
2290
+ id: retryAgentUsed.id,
2291
+ defaultModel: typeof retryAgentUsed?.defaultModel === "string"
2292
+ ? retryAgentUsed.defaultModel
2293
+ : undefined,
2294
+ }, retryFailoverEvents);
2295
+ await recordUsage("review_retry", retryPrompt, retryOutput, retryDuration, retryTokenMeta, retryUsageAgent, 2);
2154
2296
  normalization = normalizeReviewOutput(retryOutput);
2155
2297
  parsed = normalization.result;
2156
2298
  validationError = validateReviewOutput(parsed, { requireCommentSlugs });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcoda/core",
3
- "version": "0.1.27",
3
+ "version": "0.1.28",
4
4
  "description": "Core services and APIs for the mcoda CLI.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -32,11 +32,11 @@
32
32
  "dependencies": {
33
33
  "@apidevtools/swagger-parser": "^10.1.0",
34
34
  "yaml": "^2.4.2",
35
- "@mcoda/shared": "0.1.27",
36
- "@mcoda/db": "0.1.27",
37
- "@mcoda/generators": "0.1.27",
38
- "@mcoda/integrations": "0.1.27",
39
- "@mcoda/agents": "0.1.27"
35
+ "@mcoda/shared": "0.1.28",
36
+ "@mcoda/generators": "0.1.28",
37
+ "@mcoda/db": "0.1.28",
38
+ "@mcoda/agents": "0.1.28",
39
+ "@mcoda/integrations": "0.1.28"
40
40
  },
41
41
  "scripts": {
42
42
  "build": "tsc -p tsconfig.json",