@mcoda/core 0.1.35 → 0.1.37

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.
@@ -566,17 +566,17 @@ export class SdsPreflightService {
566
566
  if (technologies.length > 0) {
567
567
  return [
568
568
  "## Technology Stack",
569
- `- Chosen stack baseline: ${technologies.join(", ")}.`,
570
- "- Alternatives considered must be recorded with trade-offs for runtime, complexity, and verification impact.",
571
- "- Keep one explicit baseline per layer so create-tasks can generate deterministic implementation work.",
569
+ `- Observed source-backed technology signals: ${technologies.join(", ")}.`,
570
+ "- Keep the chosen stack explicit in the source docs for runtime, language, persistence, interface, and tooling layers.",
571
+ "- Record alternatives only when the source docs name them; do not invent default stack choices during preflight.",
572
572
  "",
573
573
  ];
574
574
  }
575
575
  return [
576
576
  "## Technology Stack",
577
- "- Chosen stack baseline must be explicit for runtime, language, persistence, and tooling layers.",
578
- "- Alternatives considered should be named with rationale and trade-offs.",
579
- "- Use one baseline per layer to avoid ambiguous backlog generation.",
577
+ "- Source docs do not yet make the technology stack explicit.",
578
+ "- Record runtime, language, persistence, interface, and tooling decisions explicitly in the source docs without assuming defaults.",
579
+ "- Preflight must not invent a chosen stack baseline when the source is silent.",
580
580
  "",
581
581
  ];
582
582
  }
@@ -1,6 +1,19 @@
1
1
  import { WorkspaceRepository } from "@mcoda/db";
2
2
  import { WorkspaceResolution } from "../../workspace/WorkspaceManager.js";
3
3
  import { JobService } from "../jobs/JobService.js";
4
+ export interface TaskSufficiencyPlannedGapBundle {
5
+ kind: "section" | "folder" | "mixed";
6
+ domain: string;
7
+ values: string[];
8
+ anchors: string[];
9
+ implementationTargets: string[];
10
+ }
11
+ export interface TaskSufficiencyUnresolvedBundle {
12
+ kind: "section" | "folder" | "mixed";
13
+ domain: string;
14
+ values: string[];
15
+ anchors: string[];
16
+ }
4
17
  export interface TaskSufficiencyAuditRequest {
5
18
  workspace: WorkspaceResolution;
6
19
  projectKey: string;
@@ -16,6 +29,7 @@ export interface TaskSufficiencyAuditIteration {
16
29
  totalSignals: number;
17
30
  missingSectionCount: number;
18
31
  missingFolderCount: number;
32
+ unresolvedBundleCount: number;
19
33
  createdTaskKeys: string[];
20
34
  }
21
35
  export interface TaskSufficiencyAuditResult {
@@ -38,6 +52,8 @@ export interface TaskSufficiencyAuditResult {
38
52
  folders: number;
39
53
  total: number;
40
54
  };
55
+ plannedGapBundles: TaskSufficiencyPlannedGapBundle[];
56
+ unresolvedBundles: TaskSufficiencyUnresolvedBundle[];
41
57
  iterations: TaskSufficiencyAuditIteration[];
42
58
  reportPath: string;
43
59
  reportHistoryPath?: string;
@@ -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;AAoEnD,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,iBAAiB,EAAE,MAAM,CAAC;IAC1B,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;AA+KF,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;YAgBd,mBAAmB;IAwEjC,OAAO,CAAC,gBAAgB;IASxB,OAAO,CAAC,aAAa;IA+BrB,OAAO,CAAC,cAAc;YA2BR,iBAAiB;YAyGjB,cAAc;YAoId,oBAAoB;IAkB5B,QAAQ,CAAC,OAAO,EAAE,2BAA2B,GAAG,OAAO,CAAC,0BAA0B,CAAC;CA6X1F"}
1
+ {"version":3,"file":"TaskSufficiencyService.d.ts","sourceRoot":"","sources":["../../../src/services/planning/TaskSufficiencyService.ts"],"names":[],"mappings":"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"}
@@ -20,8 +20,170 @@ const sdsFilenamePattern = /(sds|software[-_ ]design|system[-_ ]design|design[-_
20
20
  const sdsContentPattern = /(software design specification|system design specification|^#\s*sds\b)/im;
21
21
  const supportRootSegments = new Set(["docs", "fixtures", "policies", "policy", "runbooks", "pdr", "rfp", "sds"]);
22
22
  const headingNoiseTokens = new Set(["and", "for", "from", "into", "the", "with"]);
23
+ const runtimePathSegments = new Set([
24
+ "api",
25
+ "app",
26
+ "apps",
27
+ "bin",
28
+ "cli",
29
+ "client",
30
+ "clients",
31
+ "cmd",
32
+ "command",
33
+ "commands",
34
+ "engine",
35
+ "engines",
36
+ "feature",
37
+ "features",
38
+ "gateway",
39
+ "gateways",
40
+ "handler",
41
+ "handlers",
42
+ "module",
43
+ "modules",
44
+ "pipeline",
45
+ "pipelines",
46
+ "processor",
47
+ "processors",
48
+ "route",
49
+ "routes",
50
+ "server",
51
+ "servers",
52
+ "service",
53
+ "services",
54
+ "src",
55
+ "ui",
56
+ "web",
57
+ "worker",
58
+ "workers",
59
+ ]);
60
+ const interfacePathSegments = new Set([
61
+ "contract",
62
+ "contracts",
63
+ "dto",
64
+ "dtos",
65
+ "interface",
66
+ "interfaces",
67
+ "policy",
68
+ "policies",
69
+ "proto",
70
+ "protocol",
71
+ "protocols",
72
+ "schema",
73
+ "schemas",
74
+ "spec",
75
+ "specs",
76
+ "type",
77
+ "types",
78
+ ]);
79
+ const dataPathSegments = new Set([
80
+ "cache",
81
+ "caches",
82
+ "data",
83
+ "db",
84
+ "ledger",
85
+ "migration",
86
+ "migrations",
87
+ "model",
88
+ "models",
89
+ "persistence",
90
+ "repository",
91
+ "repositories",
92
+ "storage",
93
+ ]);
94
+ const testPathSegments = new Set(["acceptance", "e2e", "integration", "spec", "specs", "test", "tests"]);
95
+ const opsPathSegments = new Set([
96
+ "automation",
97
+ "deploy",
98
+ "deployment",
99
+ "deployments",
100
+ "infra",
101
+ "ops",
102
+ "operation",
103
+ "operations",
104
+ "orchestration",
105
+ "orchestrations",
106
+ "provision",
107
+ "provisioning",
108
+ "runbook",
109
+ "runbooks",
110
+ "script",
111
+ "scripts",
112
+ ]);
113
+ const manifestSignalTokens = new Set([
114
+ "build",
115
+ "config",
116
+ "configuration",
117
+ "dependency",
118
+ "dependencies",
119
+ "environment",
120
+ "lock",
121
+ "lockfile",
122
+ "manifest",
123
+ "package",
124
+ "project",
125
+ "settings",
126
+ "tooling",
127
+ "workspace",
128
+ "worktree",
129
+ ]);
130
+ const manifestMachineFilePattern = /\.(?:cfg|cnf|conf|gradle|ini|json|kts|lock|mod|properties|sum|toml|txt|xml|ya?ml)$/i;
131
+ const nonManifestTokens = new Set([
132
+ "acceptance",
133
+ "archive",
134
+ "changelog",
135
+ "contract",
136
+ "contracts",
137
+ "example",
138
+ "examples",
139
+ "fixture",
140
+ "fixtures",
141
+ "guide",
142
+ "guides",
143
+ "license",
144
+ "manual",
145
+ "manuals",
146
+ "notice",
147
+ "readme",
148
+ "reference",
149
+ "references",
150
+ "sample",
151
+ "samples",
152
+ "schema",
153
+ "schemas",
154
+ "spec",
155
+ "specs",
156
+ "test",
157
+ "tests",
158
+ ]);
159
+ const serviceArtifactSignalPattern = /(?:^|[._-])(compose|daemon|orchestrator|scheduler|service|socket|timer|worker)(?:[._-]|$)|\.(?:service|socket|timer)$/i;
160
+ const tokenizeBasename = (value) => value
161
+ .toLowerCase()
162
+ .split(/[^a-z0-9]+/)
163
+ .map((segment) => segment.trim())
164
+ .filter(Boolean);
165
+ const isManifestLikeBasename = (basename, segments = []) => {
166
+ const normalized = basename.toLowerCase();
167
+ const tokens = tokenizeBasename(normalized);
168
+ if (tokens.some((token) => manifestSignalTokens.has(token)))
169
+ return true;
170
+ if (!manifestMachineFilePattern.test(normalized))
171
+ return false;
172
+ if (segments.length <= 2 && tokens.length <= 3 && !tokens.some((token) => nonManifestTokens.has(token))) {
173
+ return true;
174
+ }
175
+ return false;
176
+ };
177
+ const isServiceArtifactBasename = (basename) => serviceArtifactSignalPattern.test(basename.toLowerCase());
23
178
  const normalizeText = (value) => normalizeCoverageText(value);
24
179
  const normalizeAnchor = normalizeCoverageAnchor;
180
+ const toPlannedGapBundle = (entry) => ({
181
+ kind: entry.bundle.kind,
182
+ domain: entry.bundle.domain,
183
+ values: entry.bundle.values,
184
+ anchors: entry.bundle.normalizedAnchors,
185
+ implementationTargets: entry.implementationTargets,
186
+ });
25
187
  const unique = (items) => Array.from(new Set(items.filter(Boolean)));
26
188
  const tokenizeCoverageSignal = (value) => unique(normalizeCoverageText(value)
27
189
  .split(/\s+/)
@@ -70,6 +232,44 @@ const implementationRootWeight = (target) => {
70
232
  }
71
233
  return score;
72
234
  };
235
+ const classifyImplementationTarget = (target) => {
236
+ const normalized = normalizeFolderEntry(target)?.toLowerCase() ?? normalizeText(target);
237
+ const segments = normalized.split("/").filter(Boolean);
238
+ const basename = segments[segments.length - 1] ?? normalized;
239
+ const isServiceArtifact = isServiceArtifactBasename(basename);
240
+ if (segments.some((segment) => supportRootSegments.has(segment))) {
241
+ return { normalized, basename, segments, kind: "doc", isServiceArtifact };
242
+ }
243
+ if (isManifestLikeBasename(basename, segments) || isServiceArtifact) {
244
+ return { normalized, basename, segments, kind: "manifest", isServiceArtifact };
245
+ }
246
+ if (segments.some((segment) => testPathSegments.has(segment))) {
247
+ return { normalized, basename, segments, kind: "test", isServiceArtifact };
248
+ }
249
+ if (segments.some((segment) => opsPathSegments.has(segment))) {
250
+ return { normalized, basename, segments, kind: "ops", isServiceArtifact };
251
+ }
252
+ if (segments.some((segment) => interfacePathSegments.has(segment))) {
253
+ return { normalized, basename, segments, kind: "interface", isServiceArtifact };
254
+ }
255
+ if (segments.some((segment) => dataPathSegments.has(segment))) {
256
+ return { normalized, basename, segments, kind: "data", isServiceArtifact };
257
+ }
258
+ if (segments.some((segment) => runtimePathSegments.has(segment))) {
259
+ return { normalized, basename, segments, kind: "runtime", isServiceArtifact };
260
+ }
261
+ return { normalized, basename, segments, kind: "unknown", isServiceArtifact };
262
+ };
263
+ const deriveSemanticTargetNeeds = (bundle) => {
264
+ const corpus = normalizeText([bundle.domain, ...bundle.values].join(" "));
265
+ return {
266
+ wantsVerification: /\b(verify|verification|acceptance|scenario|suite|test|tests|quality|gate|matrix)\b/.test(corpus),
267
+ wantsOps: /\b(rollback|recovery|replay|restart|rotation|drill|runbook|failover|release|startup|deploy|deployment|operations?|incident|compromise)\b/.test(corpus),
268
+ wantsInterface: /\b(contract|interface|schema|policy|policies|oracle|gateway|api|protocol)\b/.test(corpus),
269
+ wantsData: /\b(data|storage|cache|db|database|ledger|pipeline|metering|pricing)\b/.test(corpus),
270
+ wantsProvider: /\b(provider|providers|gateway|gateways|rpc|adapter|adapters|sanctions|moderation|kyt)\b/.test(corpus),
271
+ };
272
+ };
73
273
  const inferImplementationTargets = (bundle, availablePaths, limit = 3) => {
74
274
  const explicitTargets = bundle.values
75
275
  .map((value) => normalizeFolderEntry(value))
@@ -79,24 +279,135 @@ const inferImplementationTargets = (bundle, availablePaths, limit = 3) => {
79
279
  }
80
280
  const anchorTokens = tokenizeCoverageSignal(normalizeText([bundle.domain, ...bundle.values].join(" ")).replace(/[-/]+/g, " "));
81
281
  const domainNeedle = bundle.domain.replace(/[-_]+/g, " ").trim();
282
+ const targetNeeds = deriveSemanticTargetNeeds(bundle);
82
283
  const scored = availablePaths
83
284
  .map((candidate) => normalizeFolderEntry(candidate))
84
285
  .filter((candidate) => Boolean(candidate))
85
286
  .filter((candidate) => bundle.kind === "folder" || !candidate.startsWith("docs/"))
86
287
  .map((candidate) => {
87
- const normalizedCandidate = normalizeText(candidate.replace(/\//g, " "));
288
+ const classification = classifyImplementationTarget(candidate);
289
+ const normalizedCandidate = classification.normalized.replace(/\//g, " ");
88
290
  const overlap = anchorTokens.filter((token) => normalizedCandidate.includes(token)).length;
89
291
  const hasDomainMatch = domainNeedle.length > 0 && normalizedCandidate.includes(domainNeedle);
90
- const hasEvidence = overlap > 0 || hasDomainMatch;
292
+ const semanticEvidence = (targetNeeds.wantsVerification && classification.kind === "test") ||
293
+ (targetNeeds.wantsOps && classification.kind === "ops") ||
294
+ (targetNeeds.wantsInterface && classification.kind === "interface") ||
295
+ (targetNeeds.wantsData && classification.kind === "data") ||
296
+ (targetNeeds.wantsProvider &&
297
+ (classification.kind === "interface" ||
298
+ classification.kind === "runtime" ||
299
+ classification.kind === "ops"));
300
+ const hasEvidence = overlap > 0 || hasDomainMatch || semanticEvidence;
91
301
  const score = implementationRootWeight(candidate) +
92
302
  overlap * 20 +
93
303
  (hasDomainMatch ? 15 : 0) -
94
- (candidate.startsWith("docs/") ? 25 : 0);
95
- return { candidate, score, hasEvidence };
304
+ (candidate.startsWith("docs/") ? 25 : 0) +
305
+ (targetNeeds.wantsVerification && classification.kind === "test" ? 45 : 0) +
306
+ (targetNeeds.wantsOps && classification.kind === "ops" ? 60 : 0) +
307
+ (targetNeeds.wantsInterface && classification.kind === "interface" ? 55 : 0) +
308
+ (targetNeeds.wantsData && classification.kind === "data" ? 55 : 0) +
309
+ (targetNeeds.wantsProvider &&
310
+ (classification.kind === "interface" || classification.kind === "runtime")
311
+ ? 35
312
+ : 0) -
313
+ (classification.kind === "manifest" ? 120 : 0) -
314
+ (classification.kind === "doc" ? 120 : 0);
315
+ return { candidate, classification, score, hasEvidence };
96
316
  })
97
317
  .filter((entry) => entry.hasEvidence)
98
318
  .sort((left, right) => right.score - left.score || left.candidate.localeCompare(right.candidate));
99
- return unique(scored.filter((entry) => entry.score > 0).map((entry) => entry.candidate)).slice(0, limit);
319
+ const hasStrongCandidates = scored.some((entry) => entry.score > 0 &&
320
+ (entry.classification.kind === "runtime" ||
321
+ entry.classification.kind === "interface" ||
322
+ entry.classification.kind === "data" ||
323
+ entry.classification.kind === "test" ||
324
+ entry.classification.kind === "ops"));
325
+ const filtered = scored.filter((entry) => {
326
+ if (entry.score <= 0)
327
+ return false;
328
+ if (!hasStrongCandidates)
329
+ return true;
330
+ if (entry.classification.kind === "manifest")
331
+ return false;
332
+ if (entry.classification.kind === "doc")
333
+ return false;
334
+ return true;
335
+ });
336
+ const prioritized = (filtered.length > 0 ? filtered : scored.filter((entry) => entry.score > 0)).filter((entry, _, entries) => {
337
+ if (isStructuredFilePath(entry.classification.basename))
338
+ return true;
339
+ return !entries.some((other) => {
340
+ if (other === entry)
341
+ return false;
342
+ if (other.score + 5 < entry.score)
343
+ return false;
344
+ if (!other.candidate.startsWith(`${entry.candidate}/`))
345
+ return false;
346
+ return (other.classification.kind === entry.classification.kind ||
347
+ isStructuredFilePath(other.classification.basename));
348
+ });
349
+ });
350
+ const prioritizedFiles = prioritized.filter((entry) => isStructuredFilePath(entry.classification.basename));
351
+ const fileLifted = prioritized.map((entry) => {
352
+ if (isStructuredFilePath(entry.classification.basename))
353
+ return entry;
354
+ const replacement = prioritizedFiles.find((other) => {
355
+ if (!other.candidate.startsWith(`${entry.candidate}/`))
356
+ return false;
357
+ if (other.score + 10 < entry.score)
358
+ return false;
359
+ return (other.classification.kind === entry.classification.kind ||
360
+ other.classification.kind === "runtime" ||
361
+ entry.classification.kind === "unknown");
362
+ });
363
+ return replacement ?? entry;
364
+ });
365
+ const collapsed = fileLifted.filter((entry, _, entries) => {
366
+ if (isStructuredFilePath(entry.classification.basename))
367
+ return true;
368
+ return !entries.some((other) => {
369
+ if (other === entry)
370
+ return false;
371
+ if (!isStructuredFilePath(other.classification.basename))
372
+ return false;
373
+ return other.candidate.startsWith(`${entry.candidate}/`);
374
+ });
375
+ });
376
+ const descendantFiles = unique(availablePaths
377
+ .map((candidate) => normalizeFolderEntry(candidate))
378
+ .filter((candidate) => Boolean(candidate))
379
+ .filter((candidate) => isStructuredFilePath(path.basename(candidate)))
380
+ .map((candidate) => {
381
+ const classification = classifyImplementationTarget(candidate);
382
+ const normalizedCandidate = classification.normalized.replace(/\//g, " ");
383
+ const overlap = anchorTokens.filter((token) => normalizedCandidate.includes(token)).length;
384
+ const hasDomainMatch = domainNeedle.length > 0 && normalizedCandidate.includes(domainNeedle);
385
+ const semanticBonus = (targetNeeds.wantsVerification && classification.kind === "test" ? 40 : 0) +
386
+ (targetNeeds.wantsOps && classification.kind === "ops" ? 50 : 0) +
387
+ (targetNeeds.wantsInterface && classification.kind === "interface" ? 45 : 0) +
388
+ (targetNeeds.wantsData && classification.kind === "data" ? 45 : 0) +
389
+ (targetNeeds.wantsProvider &&
390
+ (classification.kind === "runtime" || classification.kind === "interface")
391
+ ? 25
392
+ : 0);
393
+ return {
394
+ candidate,
395
+ score: implementationRootWeight(candidate) + overlap * 20 + (hasDomainMatch ? 20 : 0) + semanticBonus,
396
+ };
397
+ })
398
+ .sort((left, right) => right.score - left.score || left.candidate.localeCompare(right.candidate))
399
+ .map((entry) => entry.candidate));
400
+ const promoted = unique(collapsed.map((entry) => {
401
+ if (isStructuredFilePath(entry.classification.basename))
402
+ return entry.candidate;
403
+ const replacement = descendantFiles.find((candidate) => candidate.startsWith(`${entry.candidate}/`) || candidate.startsWith(`${entry.candidate.replace(/\/+$/g, "")}/`));
404
+ return replacement ?? entry.candidate;
405
+ })).filter((candidate, _, entries) => {
406
+ if (isStructuredFilePath(path.basename(candidate)))
407
+ return true;
408
+ return !entries.some((other) => other !== candidate && isStructuredFilePath(path.basename(other)) && other.startsWith(`${candidate}/`));
409
+ });
410
+ return promoted.slice(0, limit);
100
411
  };
101
412
  const summarizeImplementationTargets = (targets) => {
102
413
  if (targets.length === 0)
@@ -137,6 +448,12 @@ const summarizeAnchorBundle = (bundle) => {
137
448
  }
138
449
  return `${bundle.normalizedAnchors[0]} (+${bundle.normalizedAnchors.length - 1} more)`;
139
450
  };
451
+ const toUnresolvedBundle = (bundle) => ({
452
+ kind: bundle.kind,
453
+ domain: bundle.domain,
454
+ values: [...bundle.values],
455
+ anchors: [...bundle.normalizedAnchors],
456
+ });
140
457
  const buildTargetedTestGuidance = (implementationTargets) => {
141
458
  const guidance = new Set();
142
459
  for (const target of implementationTargets) {
@@ -666,6 +983,7 @@ export class TaskSufficiencyService {
666
983
  let totalTasksAdded = 0;
667
984
  const totalTasksUpdated = 0;
668
985
  let satisfied = false;
986
+ let latestUnresolvedBundles = [];
669
987
  for (let iteration = 1; iteration <= maxIterations; iteration += 1) {
670
988
  const snapshot = await this.loadProjectSnapshot(request.projectKey);
671
989
  const coverage = this.evaluateCoverage(snapshot.corpus, sectionHeadings, folderEntries, snapshot.existingAnchors);
@@ -679,6 +997,7 @@ export class TaskSufficiencyService {
679
997
  totalSignals: coverage.totalSignals,
680
998
  missingSectionCount: coverage.missingSectionHeadings.length,
681
999
  missingFolderCount: coverage.missingFolderEntries.length,
1000
+ unresolvedBundleCount: 0,
682
1001
  createdTaskKeys: [],
683
1002
  });
684
1003
  await this.jobService.writeCheckpoint(job.id, {
@@ -690,6 +1009,7 @@ export class TaskSufficiencyService {
690
1009
  totalSignals: coverage.totalSignals,
691
1010
  missingSectionCount: coverage.missingSectionHeadings.length,
692
1011
  missingFolderCount: coverage.missingFolderEntries.length,
1012
+ unresolvedBundleCount: 0,
693
1013
  action: "complete",
694
1014
  },
695
1015
  });
@@ -698,6 +1018,7 @@ export class TaskSufficiencyService {
698
1018
  const gapItems = this.buildGapItems(coverage, snapshot.existingAnchors, maxTasksPerIteration);
699
1019
  const gapBundles = this.bundleGapItems(gapItems, maxTasksPerIteration);
700
1020
  if (gapBundles.length === 0) {
1021
+ latestUnresolvedBundles = [];
701
1022
  warnings.push(`Iteration ${iteration}: unresolved SDS gaps remain but no insertable gap items were identified.`);
702
1023
  iterations.push({
703
1024
  iteration,
@@ -705,6 +1026,7 @@ export class TaskSufficiencyService {
705
1026
  totalSignals: coverage.totalSignals,
706
1027
  missingSectionCount: coverage.missingSectionHeadings.length,
707
1028
  missingFolderCount: coverage.missingFolderEntries.length,
1029
+ unresolvedBundleCount: 0,
708
1030
  createdTaskKeys: [],
709
1031
  });
710
1032
  break;
@@ -715,6 +1037,7 @@ export class TaskSufficiencyService {
715
1037
  }));
716
1038
  const actionableGapBundles = plannedGapBundles.filter((entry) => entry.implementationTargets.length > 0);
717
1039
  const unresolvedGapBundles = plannedGapBundles.filter((entry) => entry.implementationTargets.length === 0);
1040
+ latestUnresolvedBundles = unresolvedGapBundles.map((entry) => toUnresolvedBundle(entry.bundle));
718
1041
  if (unresolvedGapBundles.length > 0) {
719
1042
  warnings.push(`Iteration ${iteration}: ${unresolvedGapBundles.length} SDS gap bundle(s) remain unresolved because no concrete implementation targets were inferred (${unresolvedGapBundles
720
1043
  .map((entry) => summarizeAnchorBundle(entry.bundle))
@@ -728,6 +1051,7 @@ export class TaskSufficiencyService {
728
1051
  totalSignals: coverage.totalSignals,
729
1052
  missingSectionCount: coverage.missingSectionHeadings.length,
730
1053
  missingFolderCount: coverage.missingFolderEntries.length,
1054
+ unresolvedBundleCount: latestUnresolvedBundles.length,
731
1055
  createdTaskKeys: [],
732
1056
  });
733
1057
  await this.jobService.writeCheckpoint(job.id, {
@@ -739,13 +1063,9 @@ export class TaskSufficiencyService {
739
1063
  totalSignals: coverage.totalSignals,
740
1064
  missingSectionCount: coverage.missingSectionHeadings.length,
741
1065
  missingFolderCount: coverage.missingFolderEntries.length,
1066
+ unresolvedBundleCount: latestUnresolvedBundles.length,
742
1067
  action: "unresolved",
743
- unresolvedGapItems: unresolvedGapBundles.map((entry) => ({
744
- kind: entry.bundle.kind,
745
- domain: entry.bundle.domain,
746
- values: entry.bundle.values,
747
- anchors: entry.bundle.normalizedAnchors,
748
- })),
1068
+ unresolvedGapItems: latestUnresolvedBundles,
749
1069
  },
750
1070
  });
751
1071
  break;
@@ -757,6 +1077,7 @@ export class TaskSufficiencyService {
757
1077
  totalSignals: coverage.totalSignals,
758
1078
  missingSectionCount: coverage.missingSectionHeadings.length,
759
1079
  missingFolderCount: coverage.missingFolderEntries.length,
1080
+ unresolvedBundleCount: latestUnresolvedBundles.length,
760
1081
  createdTaskKeys: [],
761
1082
  });
762
1083
  await this.jobService.writeCheckpoint(job.id, {
@@ -768,6 +1089,7 @@ export class TaskSufficiencyService {
768
1089
  totalSignals: coverage.totalSignals,
769
1090
  missingSectionCount: coverage.missingSectionHeadings.length,
770
1091
  missingFolderCount: coverage.missingFolderEntries.length,
1092
+ unresolvedBundleCount: latestUnresolvedBundles.length,
771
1093
  action: "dry_run",
772
1094
  proposedGapItems: actionableGapBundles.map((entry) => ({
773
1095
  kind: entry.bundle.kind,
@@ -775,12 +1097,7 @@ export class TaskSufficiencyService {
775
1097
  values: entry.bundle.values,
776
1098
  implementationTargets: entry.implementationTargets,
777
1099
  })),
778
- unresolvedGapItems: unresolvedGapBundles.map((entry) => ({
779
- kind: entry.bundle.kind,
780
- domain: entry.bundle.domain,
781
- values: entry.bundle.values,
782
- anchors: entry.bundle.normalizedAnchors,
783
- })),
1100
+ unresolvedGapItems: latestUnresolvedBundles,
784
1101
  },
785
1102
  });
786
1103
  break;
@@ -805,6 +1122,7 @@ export class TaskSufficiencyService {
805
1122
  totalSignals: coverage.totalSignals,
806
1123
  missingSectionCount: coverage.missingSectionHeadings.length,
807
1124
  missingFolderCount: coverage.missingFolderEntries.length,
1125
+ unresolvedBundleCount: latestUnresolvedBundles.length,
808
1126
  createdTaskKeys,
809
1127
  });
810
1128
  await this.jobService.writeCheckpoint(job.id, {
@@ -816,6 +1134,7 @@ export class TaskSufficiencyService {
816
1134
  totalSignals: coverage.totalSignals,
817
1135
  missingSectionCount: coverage.missingSectionHeadings.length,
818
1136
  missingFolderCount: coverage.missingFolderEntries.length,
1137
+ unresolvedBundleCount: latestUnresolvedBundles.length,
819
1138
  createdTaskKeys,
820
1139
  addedCount: createdTaskKeys.length,
821
1140
  },
@@ -831,6 +1150,17 @@ export class TaskSufficiencyService {
831
1150
  if (!satisfied) {
832
1151
  warnings.push(`Sufficiency target not reached (coverage=${finalCoverage.coverageRatio}, threshold=${minCoverageRatio}) after ${iterations.length} iteration(s).`);
833
1152
  }
1153
+ const finalGapItemLimit = Math.max(1, finalCoverage.missingSectionHeadings.length + finalCoverage.missingFolderEntries.length);
1154
+ const finalGapItems = this.buildGapItems(finalCoverage, finalSnapshot.existingAnchors, finalGapItemLimit);
1155
+ const finalGapBundles = this.bundleGapItems(finalGapItems, finalGapItemLimit);
1156
+ const finalPlannedGapBundles = finalGapBundles.map((bundle) => ({
1157
+ bundle,
1158
+ implementationTargets: inferImplementationTargets(bundle, folderEntries, 3),
1159
+ }));
1160
+ const plannedGapBundles = finalPlannedGapBundles.map(toPlannedGapBundle);
1161
+ const unresolvedBundles = finalPlannedGapBundles
1162
+ .filter((entry) => entry.implementationTargets.length === 0)
1163
+ .map((entry) => toUnresolvedBundle(entry.bundle));
834
1164
  const report = {
835
1165
  projectKey: request.projectKey,
836
1166
  sourceCommand,
@@ -862,6 +1192,8 @@ export class TaskSufficiencyService {
862
1192
  missingSectionHeadings: finalCoverage.missingSectionHeadings,
863
1193
  missingFolderEntries: finalCoverage.missingFolderEntries,
864
1194
  },
1195
+ plannedGapBundles,
1196
+ unresolvedBundles,
865
1197
  iterations,
866
1198
  warnings,
867
1199
  };
@@ -877,6 +1209,7 @@ export class TaskSufficiencyService {
877
1209
  totalTasksUpdated,
878
1210
  finalTotalSignals: finalCoverage.totalSignals,
879
1211
  finalCoverageRatio: finalCoverage.coverageRatio,
1212
+ unresolvedBundleCount: unresolvedBundles.length,
880
1213
  },
881
1214
  });
882
1215
  const result = {
@@ -899,6 +1232,8 @@ export class TaskSufficiencyService {
899
1232
  folders: finalCoverage.missingFolderEntries.length,
900
1233
  total: finalCoverage.missingSectionHeadings.length + finalCoverage.missingFolderEntries.length,
901
1234
  },
1235
+ plannedGapBundles,
1236
+ unresolvedBundles,
902
1237
  iterations,
903
1238
  reportPath,
904
1239
  reportHistoryPath: historyPath,
@@ -917,6 +1252,7 @@ export class TaskSufficiencyService {
917
1252
  finalCoverageRatio: finalCoverage.coverageRatio,
918
1253
  remainingSectionCount: finalCoverage.missingSectionHeadings.length,
919
1254
  remainingFolderCount: finalCoverage.missingFolderEntries.length,
1255
+ unresolvedBundleCount: unresolvedBundles.length,
920
1256
  reportPath,
921
1257
  reportHistoryPath: historyPath,
922
1258
  warnings,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcoda/core",
3
- "version": "0.1.35",
3
+ "version": "0.1.37",
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.35",
36
- "@mcoda/db": "0.1.35",
37
- "@mcoda/agents": "0.1.35",
38
- "@mcoda/generators": "0.1.35",
39
- "@mcoda/integrations": "0.1.35"
35
+ "@mcoda/db": "0.1.37",
36
+ "@mcoda/shared": "0.1.37",
37
+ "@mcoda/generators": "0.1.37",
38
+ "@mcoda/agents": "0.1.37",
39
+ "@mcoda/integrations": "0.1.37"
40
40
  },
41
41
  "scripts": {
42
42
  "build": "tsc -p tsconfig.json",