@mcoda/core 0.1.35 → 0.1.36

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,106 @@ 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
+ "deploy",
97
+ "deployment",
98
+ "deployments",
99
+ "helm",
100
+ "infra",
101
+ "k8s",
102
+ "ops",
103
+ "operation",
104
+ "operations",
105
+ "runbook",
106
+ "runbooks",
107
+ "script",
108
+ "scripts",
109
+ "systemd",
110
+ "terraform",
111
+ ]);
112
+ const manifestBasenamePattern = /^(package\.json|pnpm-workspace\.yaml|pnpm-lock\.yaml|turbo\.json|tsconfig(?:\.[^.]+)?\.json|cargo\.toml|pyproject\.toml|go\.mod|go\.sum|pom\.xml|build\.gradle(?:\.kts)?|settings\.gradle(?:\.kts)?|requirements\.txt|poetry\.lock|foundry\.toml|hardhat\.config\.[^.]+)$/i;
113
+ const serviceArtifactBasenamePattern = /(?:\.service|\.socket|\.timer|(?:^|[.-])compose\.(?:ya?ml|json)$|docker-compose\.(?:ya?ml|json)$)$/i;
23
114
  const normalizeText = (value) => normalizeCoverageText(value);
24
115
  const normalizeAnchor = normalizeCoverageAnchor;
116
+ const toPlannedGapBundle = (entry) => ({
117
+ kind: entry.bundle.kind,
118
+ domain: entry.bundle.domain,
119
+ values: entry.bundle.values,
120
+ anchors: entry.bundle.normalizedAnchors,
121
+ implementationTargets: entry.implementationTargets,
122
+ });
25
123
  const unique = (items) => Array.from(new Set(items.filter(Boolean)));
26
124
  const tokenizeCoverageSignal = (value) => unique(normalizeCoverageText(value)
27
125
  .split(/\s+/)
@@ -70,6 +168,44 @@ const implementationRootWeight = (target) => {
70
168
  }
71
169
  return score;
72
170
  };
171
+ const classifyImplementationTarget = (target) => {
172
+ const normalized = normalizeFolderEntry(target)?.toLowerCase() ?? normalizeText(target);
173
+ const segments = normalized.split("/").filter(Boolean);
174
+ const basename = segments[segments.length - 1] ?? normalized;
175
+ const isServiceArtifact = serviceArtifactBasenamePattern.test(basename);
176
+ if (segments.some((segment) => supportRootSegments.has(segment))) {
177
+ return { normalized, basename, segments, kind: "doc", isServiceArtifact };
178
+ }
179
+ if (manifestBasenamePattern.test(basename) || isServiceArtifact) {
180
+ return { normalized, basename, segments, kind: "manifest", isServiceArtifact };
181
+ }
182
+ if (segments.some((segment) => testPathSegments.has(segment))) {
183
+ return { normalized, basename, segments, kind: "test", isServiceArtifact };
184
+ }
185
+ if (segments.some((segment) => opsPathSegments.has(segment))) {
186
+ return { normalized, basename, segments, kind: "ops", isServiceArtifact };
187
+ }
188
+ if (segments.some((segment) => interfacePathSegments.has(segment))) {
189
+ return { normalized, basename, segments, kind: "interface", isServiceArtifact };
190
+ }
191
+ if (segments.some((segment) => dataPathSegments.has(segment))) {
192
+ return { normalized, basename, segments, kind: "data", isServiceArtifact };
193
+ }
194
+ if (segments.some((segment) => runtimePathSegments.has(segment))) {
195
+ return { normalized, basename, segments, kind: "runtime", isServiceArtifact };
196
+ }
197
+ return { normalized, basename, segments, kind: "unknown", isServiceArtifact };
198
+ };
199
+ const deriveSemanticTargetNeeds = (bundle) => {
200
+ const corpus = normalizeText([bundle.domain, ...bundle.values].join(" "));
201
+ return {
202
+ wantsVerification: /\b(verify|verification|acceptance|scenario|suite|test|tests|quality|gate|matrix)\b/.test(corpus),
203
+ wantsOps: /\b(rollback|recovery|replay|restart|rotation|drill|runbook|failover|release|startup|deploy|deployment|operations?|incident|compromise)\b/.test(corpus),
204
+ wantsInterface: /\b(contract|interface|schema|policy|policies|oracle|gateway|api|protocol)\b/.test(corpus),
205
+ wantsData: /\b(data|storage|cache|db|database|ledger|pipeline|metering|pricing)\b/.test(corpus),
206
+ wantsProvider: /\b(provider|providers|gateway|gateways|rpc|adapter|adapters|sanctions|moderation|kyt)\b/.test(corpus),
207
+ };
208
+ };
73
209
  const inferImplementationTargets = (bundle, availablePaths, limit = 3) => {
74
210
  const explicitTargets = bundle.values
75
211
  .map((value) => normalizeFolderEntry(value))
@@ -79,24 +215,61 @@ const inferImplementationTargets = (bundle, availablePaths, limit = 3) => {
79
215
  }
80
216
  const anchorTokens = tokenizeCoverageSignal(normalizeText([bundle.domain, ...bundle.values].join(" ")).replace(/[-/]+/g, " "));
81
217
  const domainNeedle = bundle.domain.replace(/[-_]+/g, " ").trim();
218
+ const targetNeeds = deriveSemanticTargetNeeds(bundle);
82
219
  const scored = availablePaths
83
220
  .map((candidate) => normalizeFolderEntry(candidate))
84
221
  .filter((candidate) => Boolean(candidate))
85
222
  .filter((candidate) => bundle.kind === "folder" || !candidate.startsWith("docs/"))
86
223
  .map((candidate) => {
87
- const normalizedCandidate = normalizeText(candidate.replace(/\//g, " "));
224
+ const classification = classifyImplementationTarget(candidate);
225
+ const normalizedCandidate = classification.normalized.replace(/\//g, " ");
88
226
  const overlap = anchorTokens.filter((token) => normalizedCandidate.includes(token)).length;
89
227
  const hasDomainMatch = domainNeedle.length > 0 && normalizedCandidate.includes(domainNeedle);
90
- const hasEvidence = overlap > 0 || hasDomainMatch;
228
+ const semanticEvidence = (targetNeeds.wantsVerification && classification.kind === "test") ||
229
+ (targetNeeds.wantsOps && classification.kind === "ops") ||
230
+ (targetNeeds.wantsInterface && classification.kind === "interface") ||
231
+ (targetNeeds.wantsData && classification.kind === "data") ||
232
+ (targetNeeds.wantsProvider &&
233
+ (classification.kind === "interface" ||
234
+ classification.kind === "runtime" ||
235
+ classification.kind === "ops"));
236
+ const hasEvidence = overlap > 0 || hasDomainMatch || semanticEvidence;
91
237
  const score = implementationRootWeight(candidate) +
92
238
  overlap * 20 +
93
239
  (hasDomainMatch ? 15 : 0) -
94
- (candidate.startsWith("docs/") ? 25 : 0);
95
- return { candidate, score, hasEvidence };
240
+ (candidate.startsWith("docs/") ? 25 : 0) +
241
+ (targetNeeds.wantsVerification && classification.kind === "test" ? 45 : 0) +
242
+ (targetNeeds.wantsOps && classification.kind === "ops" ? 60 : 0) +
243
+ (targetNeeds.wantsInterface && classification.kind === "interface" ? 55 : 0) +
244
+ (targetNeeds.wantsData && classification.kind === "data" ? 55 : 0) +
245
+ (targetNeeds.wantsProvider &&
246
+ (classification.kind === "interface" || classification.kind === "runtime")
247
+ ? 35
248
+ : 0) -
249
+ (classification.kind === "manifest" ? 120 : 0) -
250
+ (classification.kind === "doc" ? 120 : 0);
251
+ return { candidate, classification, score, hasEvidence };
96
252
  })
97
253
  .filter((entry) => entry.hasEvidence)
98
254
  .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);
255
+ const hasStrongCandidates = scored.some((entry) => entry.score > 0 &&
256
+ (entry.classification.kind === "runtime" ||
257
+ entry.classification.kind === "interface" ||
258
+ entry.classification.kind === "data" ||
259
+ entry.classification.kind === "test" ||
260
+ entry.classification.kind === "ops"));
261
+ const filtered = scored.filter((entry) => {
262
+ if (entry.score <= 0)
263
+ return false;
264
+ if (!hasStrongCandidates)
265
+ return true;
266
+ if (entry.classification.kind === "manifest")
267
+ return false;
268
+ if (entry.classification.kind === "doc")
269
+ return false;
270
+ return true;
271
+ });
272
+ return unique((filtered.length > 0 ? filtered : scored.filter((entry) => entry.score > 0)).map((entry) => entry.candidate)).slice(0, limit);
100
273
  };
101
274
  const summarizeImplementationTargets = (targets) => {
102
275
  if (targets.length === 0)
@@ -137,6 +310,12 @@ const summarizeAnchorBundle = (bundle) => {
137
310
  }
138
311
  return `${bundle.normalizedAnchors[0]} (+${bundle.normalizedAnchors.length - 1} more)`;
139
312
  };
313
+ const toUnresolvedBundle = (bundle) => ({
314
+ kind: bundle.kind,
315
+ domain: bundle.domain,
316
+ values: [...bundle.values],
317
+ anchors: [...bundle.normalizedAnchors],
318
+ });
140
319
  const buildTargetedTestGuidance = (implementationTargets) => {
141
320
  const guidance = new Set();
142
321
  for (const target of implementationTargets) {
@@ -666,6 +845,7 @@ export class TaskSufficiencyService {
666
845
  let totalTasksAdded = 0;
667
846
  const totalTasksUpdated = 0;
668
847
  let satisfied = false;
848
+ let latestUnresolvedBundles = [];
669
849
  for (let iteration = 1; iteration <= maxIterations; iteration += 1) {
670
850
  const snapshot = await this.loadProjectSnapshot(request.projectKey);
671
851
  const coverage = this.evaluateCoverage(snapshot.corpus, sectionHeadings, folderEntries, snapshot.existingAnchors);
@@ -679,6 +859,7 @@ export class TaskSufficiencyService {
679
859
  totalSignals: coverage.totalSignals,
680
860
  missingSectionCount: coverage.missingSectionHeadings.length,
681
861
  missingFolderCount: coverage.missingFolderEntries.length,
862
+ unresolvedBundleCount: 0,
682
863
  createdTaskKeys: [],
683
864
  });
684
865
  await this.jobService.writeCheckpoint(job.id, {
@@ -690,6 +871,7 @@ export class TaskSufficiencyService {
690
871
  totalSignals: coverage.totalSignals,
691
872
  missingSectionCount: coverage.missingSectionHeadings.length,
692
873
  missingFolderCount: coverage.missingFolderEntries.length,
874
+ unresolvedBundleCount: 0,
693
875
  action: "complete",
694
876
  },
695
877
  });
@@ -698,6 +880,7 @@ export class TaskSufficiencyService {
698
880
  const gapItems = this.buildGapItems(coverage, snapshot.existingAnchors, maxTasksPerIteration);
699
881
  const gapBundles = this.bundleGapItems(gapItems, maxTasksPerIteration);
700
882
  if (gapBundles.length === 0) {
883
+ latestUnresolvedBundles = [];
701
884
  warnings.push(`Iteration ${iteration}: unresolved SDS gaps remain but no insertable gap items were identified.`);
702
885
  iterations.push({
703
886
  iteration,
@@ -705,6 +888,7 @@ export class TaskSufficiencyService {
705
888
  totalSignals: coverage.totalSignals,
706
889
  missingSectionCount: coverage.missingSectionHeadings.length,
707
890
  missingFolderCount: coverage.missingFolderEntries.length,
891
+ unresolvedBundleCount: 0,
708
892
  createdTaskKeys: [],
709
893
  });
710
894
  break;
@@ -715,6 +899,7 @@ export class TaskSufficiencyService {
715
899
  }));
716
900
  const actionableGapBundles = plannedGapBundles.filter((entry) => entry.implementationTargets.length > 0);
717
901
  const unresolvedGapBundles = plannedGapBundles.filter((entry) => entry.implementationTargets.length === 0);
902
+ latestUnresolvedBundles = unresolvedGapBundles.map((entry) => toUnresolvedBundle(entry.bundle));
718
903
  if (unresolvedGapBundles.length > 0) {
719
904
  warnings.push(`Iteration ${iteration}: ${unresolvedGapBundles.length} SDS gap bundle(s) remain unresolved because no concrete implementation targets were inferred (${unresolvedGapBundles
720
905
  .map((entry) => summarizeAnchorBundle(entry.bundle))
@@ -728,6 +913,7 @@ export class TaskSufficiencyService {
728
913
  totalSignals: coverage.totalSignals,
729
914
  missingSectionCount: coverage.missingSectionHeadings.length,
730
915
  missingFolderCount: coverage.missingFolderEntries.length,
916
+ unresolvedBundleCount: latestUnresolvedBundles.length,
731
917
  createdTaskKeys: [],
732
918
  });
733
919
  await this.jobService.writeCheckpoint(job.id, {
@@ -739,13 +925,9 @@ export class TaskSufficiencyService {
739
925
  totalSignals: coverage.totalSignals,
740
926
  missingSectionCount: coverage.missingSectionHeadings.length,
741
927
  missingFolderCount: coverage.missingFolderEntries.length,
928
+ unresolvedBundleCount: latestUnresolvedBundles.length,
742
929
  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
- })),
930
+ unresolvedGapItems: latestUnresolvedBundles,
749
931
  },
750
932
  });
751
933
  break;
@@ -757,6 +939,7 @@ export class TaskSufficiencyService {
757
939
  totalSignals: coverage.totalSignals,
758
940
  missingSectionCount: coverage.missingSectionHeadings.length,
759
941
  missingFolderCount: coverage.missingFolderEntries.length,
942
+ unresolvedBundleCount: latestUnresolvedBundles.length,
760
943
  createdTaskKeys: [],
761
944
  });
762
945
  await this.jobService.writeCheckpoint(job.id, {
@@ -768,6 +951,7 @@ export class TaskSufficiencyService {
768
951
  totalSignals: coverage.totalSignals,
769
952
  missingSectionCount: coverage.missingSectionHeadings.length,
770
953
  missingFolderCount: coverage.missingFolderEntries.length,
954
+ unresolvedBundleCount: latestUnresolvedBundles.length,
771
955
  action: "dry_run",
772
956
  proposedGapItems: actionableGapBundles.map((entry) => ({
773
957
  kind: entry.bundle.kind,
@@ -775,12 +959,7 @@ export class TaskSufficiencyService {
775
959
  values: entry.bundle.values,
776
960
  implementationTargets: entry.implementationTargets,
777
961
  })),
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
- })),
962
+ unresolvedGapItems: latestUnresolvedBundles,
784
963
  },
785
964
  });
786
965
  break;
@@ -805,6 +984,7 @@ export class TaskSufficiencyService {
805
984
  totalSignals: coverage.totalSignals,
806
985
  missingSectionCount: coverage.missingSectionHeadings.length,
807
986
  missingFolderCount: coverage.missingFolderEntries.length,
987
+ unresolvedBundleCount: latestUnresolvedBundles.length,
808
988
  createdTaskKeys,
809
989
  });
810
990
  await this.jobService.writeCheckpoint(job.id, {
@@ -816,6 +996,7 @@ export class TaskSufficiencyService {
816
996
  totalSignals: coverage.totalSignals,
817
997
  missingSectionCount: coverage.missingSectionHeadings.length,
818
998
  missingFolderCount: coverage.missingFolderEntries.length,
999
+ unresolvedBundleCount: latestUnresolvedBundles.length,
819
1000
  createdTaskKeys,
820
1001
  addedCount: createdTaskKeys.length,
821
1002
  },
@@ -831,6 +1012,17 @@ export class TaskSufficiencyService {
831
1012
  if (!satisfied) {
832
1013
  warnings.push(`Sufficiency target not reached (coverage=${finalCoverage.coverageRatio}, threshold=${minCoverageRatio}) after ${iterations.length} iteration(s).`);
833
1014
  }
1015
+ const finalGapItemLimit = Math.max(1, finalCoverage.missingSectionHeadings.length + finalCoverage.missingFolderEntries.length);
1016
+ const finalGapItems = this.buildGapItems(finalCoverage, finalSnapshot.existingAnchors, finalGapItemLimit);
1017
+ const finalGapBundles = this.bundleGapItems(finalGapItems, finalGapItemLimit);
1018
+ const finalPlannedGapBundles = finalGapBundles.map((bundle) => ({
1019
+ bundle,
1020
+ implementationTargets: inferImplementationTargets(bundle, folderEntries, 3),
1021
+ }));
1022
+ const plannedGapBundles = finalPlannedGapBundles.map(toPlannedGapBundle);
1023
+ const unresolvedBundles = finalPlannedGapBundles
1024
+ .filter((entry) => entry.implementationTargets.length === 0)
1025
+ .map((entry) => toUnresolvedBundle(entry.bundle));
834
1026
  const report = {
835
1027
  projectKey: request.projectKey,
836
1028
  sourceCommand,
@@ -862,6 +1054,8 @@ export class TaskSufficiencyService {
862
1054
  missingSectionHeadings: finalCoverage.missingSectionHeadings,
863
1055
  missingFolderEntries: finalCoverage.missingFolderEntries,
864
1056
  },
1057
+ plannedGapBundles,
1058
+ unresolvedBundles,
865
1059
  iterations,
866
1060
  warnings,
867
1061
  };
@@ -877,6 +1071,7 @@ export class TaskSufficiencyService {
877
1071
  totalTasksUpdated,
878
1072
  finalTotalSignals: finalCoverage.totalSignals,
879
1073
  finalCoverageRatio: finalCoverage.coverageRatio,
1074
+ unresolvedBundleCount: unresolvedBundles.length,
880
1075
  },
881
1076
  });
882
1077
  const result = {
@@ -899,6 +1094,8 @@ export class TaskSufficiencyService {
899
1094
  folders: finalCoverage.missingFolderEntries.length,
900
1095
  total: finalCoverage.missingSectionHeadings.length + finalCoverage.missingFolderEntries.length,
901
1096
  },
1097
+ plannedGapBundles,
1098
+ unresolvedBundles,
902
1099
  iterations,
903
1100
  reportPath,
904
1101
  reportHistoryPath: historyPath,
@@ -917,6 +1114,7 @@ export class TaskSufficiencyService {
917
1114
  finalCoverageRatio: finalCoverage.coverageRatio,
918
1115
  remainingSectionCount: finalCoverage.missingSectionHeadings.length,
919
1116
  remainingFolderCount: finalCoverage.missingFolderEntries.length,
1117
+ unresolvedBundleCount: unresolvedBundles.length,
920
1118
  reportPath,
921
1119
  reportHistoryPath: historyPath,
922
1120
  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.36",
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.36",
36
+ "@mcoda/generators": "0.1.36",
37
+ "@mcoda/agents": "0.1.36",
38
+ "@mcoda/integrations": "0.1.36",
39
+ "@mcoda/shared": "0.1.36"
40
40
  },
41
41
  "scripts": {
42
42
  "build": "tsc -p tsconfig.json",