@mcoda/core 0.1.34 → 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.
Files changed (27) hide show
  1. package/dist/api/AgentsApi.d.ts +4 -1
  2. package/dist/api/AgentsApi.d.ts.map +1 -1
  3. package/dist/api/AgentsApi.js +4 -1
  4. package/dist/prompts/PdrPrompts.js +1 -1
  5. package/dist/services/docs/DocsService.d.ts +37 -0
  6. package/dist/services/docs/DocsService.d.ts.map +1 -1
  7. package/dist/services/docs/DocsService.js +537 -2
  8. package/dist/services/docs/review/gates/OpenQuestionsGate.d.ts.map +1 -1
  9. package/dist/services/docs/review/gates/OpenQuestionsGate.js +13 -2
  10. package/dist/services/docs/review/gates/SdsNoUnresolvedItemsGate.d.ts.map +1 -1
  11. package/dist/services/docs/review/gates/SdsNoUnresolvedItemsGate.js +12 -1
  12. package/dist/services/planning/CreateTasksService.d.ts +57 -0
  13. package/dist/services/planning/CreateTasksService.d.ts.map +1 -1
  14. package/dist/services/planning/CreateTasksService.js +2491 -291
  15. package/dist/services/planning/SdsCoverageModel.d.ts +27 -0
  16. package/dist/services/planning/SdsCoverageModel.d.ts.map +1 -0
  17. package/dist/services/planning/SdsCoverageModel.js +138 -0
  18. package/dist/services/planning/SdsPreflightService.d.ts +2 -0
  19. package/dist/services/planning/SdsPreflightService.d.ts.map +1 -1
  20. package/dist/services/planning/SdsPreflightService.js +131 -37
  21. package/dist/services/planning/SdsStructureSignals.d.ts +24 -0
  22. package/dist/services/planning/SdsStructureSignals.d.ts.map +1 -0
  23. package/dist/services/planning/SdsStructureSignals.js +402 -0
  24. package/dist/services/planning/TaskSufficiencyService.d.ts +17 -0
  25. package/dist/services/planning/TaskSufficiencyService.d.ts.map +1 -1
  26. package/dist/services/planning/TaskSufficiencyService.js +409 -278
  27. package/package.json +6 -6
@@ -4,6 +4,8 @@ import { WorkspaceRepository } from "@mcoda/db";
4
4
  import { PathHelper } from "@mcoda/shared";
5
5
  import { JobService } from "../jobs/JobService.js";
6
6
  import { createEpicKeyGenerator, createStoryKeyGenerator, createTaskKeyGenerator } from "./KeyHelpers.js";
7
+ import { collectSdsCoverageSignalsFromDocs, evaluateSdsCoverage, normalizeCoverageAnchor, normalizeCoverageText, } from "./SdsCoverageModel.js";
8
+ import { collectSdsImplementationSignals, isStructuredFilePath, normalizeFolderEntry, normalizeHeadingCandidate, stripManagedSdsPreflightBlock, } from "./SdsStructureSignals.js";
7
9
  const DEFAULT_MAX_ITERATIONS = 5;
8
10
  const DEFAULT_MAX_TASKS_PER_ITERATION = 24;
9
11
  const DEFAULT_MIN_COVERAGE_RATIO = 1;
@@ -16,115 +18,118 @@ const REPORT_FILE_NAME = "task-sufficiency-report.json";
16
18
  const ignoredDirs = new Set([".git", "node_modules", "dist", "build", ".mcoda", ".docdex"]);
17
19
  const sdsFilenamePattern = /(sds|software[-_ ]design|system[-_ ]design|design[-_ ]spec)/i;
18
20
  const sdsContentPattern = /(software design specification|system design specification|^#\s*sds\b)/im;
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;
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;
21
- const repoRootSegments = new Set([
22
- "apps",
21
+ const supportRootSegments = new Set(["docs", "fixtures", "policies", "policy", "runbooks", "pdr", "rfp", "sds"]);
22
+ const headingNoiseTokens = new Set(["and", "for", "from", "into", "the", "with"]);
23
+ const runtimePathSegments = new Set([
23
24
  "api",
24
- "backend",
25
- "config",
26
- "configs",
27
- "db",
28
- "deployment",
29
- "deployments",
30
- "docs",
31
- "frontend",
32
- "implementation",
33
- "infra",
34
- "internal",
35
- "packages",
36
- "scripts",
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",
37
52
  "service",
38
53
  "services",
39
- "shared",
40
54
  "src",
41
- "test",
42
- "tests",
43
55
  "ui",
44
56
  "web",
57
+ "worker",
58
+ "workers",
45
59
  ]);
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",
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",
67
78
  ]);
68
- const normalizeText = (value) => value
69
- .toLowerCase()
70
- .replace(/[`*_]/g, " ")
71
- .replace(/[^a-z0-9/\s.-]+/g, " ")
72
- .replace(/\s+/g, " ")
73
- .trim();
74
- const normalizeAnchor = (kind, value) => `${kind}:${normalizeText(value).replace(/\s+/g, " ").trim()}`;
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;
114
+ const normalizeText = (value) => normalizeCoverageText(value);
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
+ });
75
123
  const unique = (items) => Array.from(new Set(items.filter(Boolean)));
124
+ const tokenizeCoverageSignal = (value) => unique(normalizeCoverageText(value)
125
+ .split(/\s+/)
126
+ .map((token) => token.replace(/[^a-z0-9._-]+/g, ""))
127
+ .filter((token) => token.length >= 3));
76
128
  const stripDecorators = (value) => value
77
129
  .replace(/[`*_]/g, " ")
78
130
  .replace(/^[\s>:\-[\]().]+/, "")
79
131
  .replace(/\s+/g, " ")
80
132
  .trim();
81
- const normalizeHeadingCandidate = (value) => {
82
- const cleaned = stripDecorators(value).replace(/^\d+(?:\.\d+)*\s+/, "").trim();
83
- return cleaned.length > 0 ? cleaned : stripDecorators(value);
84
- };
85
- const headingLooksImplementationRelevant = (heading) => {
86
- const normalized = normalizeHeadingCandidate(heading).toLowerCase();
87
- if (!normalized || normalized.length < 3)
88
- return false;
89
- if (nonImplementationHeadingPattern.test(normalized))
90
- return false;
91
- if (likelyImplementationHeadingPattern.test(normalized))
92
- return true;
93
- const sectionMatch = heading.trim().match(/^(\d+)(?:\.\d+)*(?:\s+|$)/);
94
- if (sectionMatch) {
95
- const major = Number.parseInt(sectionMatch[1] ?? "", 10);
96
- if (Number.isFinite(major) && major >= 3)
97
- return true;
98
- }
99
- const tokens = normalized
100
- .split(/\s+/)
101
- .map((token) => token.replace(/[^a-z0-9.-]+/g, ""))
102
- .filter((token) => token.length >= 4 && !headingNoiseTokens.has(token));
103
- return tokens.length >= 2;
104
- };
105
- const normalizeFolderEntry = (entry) => {
106
- const trimmed = stripDecorators(entry)
107
- .replace(/^\.?\//, "")
108
- .replace(/\/+$/, "")
109
- .replace(/\s+/g, "");
110
- if (!trimmed.includes("/"))
111
- return undefined;
112
- if (trimmed.includes("...") || trimmed.includes("*"))
113
- return undefined;
114
- return trimmed;
115
- };
116
- const folderEntryLooksRepoRelevant = (entry) => {
117
- const normalized = normalizeFolderEntry(entry);
118
- if (!normalized)
119
- return false;
120
- if (normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized))
121
- return false;
122
- const segments = normalized.split("/").filter(Boolean);
123
- if (segments.length < 2)
124
- return false;
125
- const root = segments[0].toLowerCase();
126
- return repoRootSegments.has(root);
127
- };
128
133
  const deriveSectionDomain = (heading) => {
129
134
  const normalized = normalizeHeadingCandidate(heading).toLowerCase();
130
135
  const tokens = normalized
@@ -142,130 +147,206 @@ const deriveFolderDomain = (entry) => {
142
147
  return "structure";
143
148
  return segments.length === 1 ? segments[0] : `${segments[0]}-${segments[1]}`;
144
149
  };
145
- const extractMarkdownHeadings = (content, limit) => {
146
- if (!content)
147
- return [];
148
- const lines = content.split(/\r?\n/);
149
- const headings = [];
150
- for (let index = 0; index < lines.length; index += 1) {
151
- const line = lines[index]?.trim() ?? "";
152
- if (!line)
153
- continue;
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
- }
174
- if (headings.length >= limit)
175
- break;
150
+ const implementationRootWeight = (target) => {
151
+ const normalized = normalizeFolderEntry(target)?.toLowerCase() ?? normalizeText(target);
152
+ const segments = normalized.split("/").filter(Boolean);
153
+ const root = segments[0] ?? normalized;
154
+ let score = 55 + Math.min(segments.length, 4) * 6 + (isStructuredFilePath(normalized) ? 10 : 0);
155
+ if (supportRootSegments.has(root))
156
+ score -= 30;
157
+ if (segments.some((segment) => ["src", "app", "apps", "lib", "libs", "server", "api", "worker", "workers"].includes(segment))) {
158
+ score += 20;
159
+ }
160
+ if (segments.some((segment) => ["test", "tests", "spec", "specs", "integration", "acceptance", "e2e"].includes(segment))) {
161
+ score += 10;
162
+ }
163
+ if (segments.some((segment) => ["db", "data", "schema", "schemas", "migration", "migrations"].includes(segment))) {
164
+ score += 12;
165
+ }
166
+ if (segments.some((segment) => ["ops", "infra", "deploy", "deployment", "deployments", "script", "scripts"].includes(segment))) {
167
+ score += 12;
176
168
  }
177
- return unique(headings).slice(0, limit);
169
+ return score;
178
170
  };
179
- const extractFolderEntries = (content, limit) => {
180
- if (!content)
181
- return [];
182
- const candidates = [];
183
- const lines = content.split(/\r?\n/);
184
- for (const line of lines) {
185
- const trimmed = line.trim();
186
- if (!trimmed)
187
- continue;
188
- const matches = [...trimmed.matchAll(/[`'"]?([a-zA-Z0-9._-]+(?:\/[a-zA-Z0-9._-]+)+(?:\/[a-zA-Z0-9._-]+)*)[`'"]?/g)];
189
- for (const match of matches) {
190
- const raw = (match[1] ?? "").replace(/^\.?\//, "").replace(/\/+$/, "").trim();
191
- if (!raw || !raw.includes("/"))
192
- continue;
193
- candidates.push(raw);
194
- if (candidates.length >= limit)
195
- break;
196
- }
197
- if (candidates.length >= limit)
198
- break;
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 };
199
187
  }
200
- return unique(candidates).slice(0, limit);
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 };
201
198
  };
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);
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
+ };
216
208
  };
217
- const headingCovered = (corpus, heading) => {
218
- const normalized = normalizeText(normalizeHeadingCandidate(heading));
219
- if (!normalized)
220
- return true;
221
- if (corpus.includes(normalized))
222
- return true;
223
- const tokens = tokenizeCoverageSignal(normalized).slice(0, 10);
224
- if (tokens.length === 0)
225
- return true;
226
- const hitCount = tokens.filter((token) => corpus.includes(token)).length;
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))
209
+ const inferImplementationTargets = (bundle, availablePaths, limit = 3) => {
210
+ const explicitTargets = bundle.values
211
+ .map((value) => normalizeFolderEntry(value))
212
+ .filter((value) => Boolean(value));
213
+ if (explicitTargets.length > 0) {
214
+ return unique(explicitTargets).slice(0, limit);
215
+ }
216
+ const anchorTokens = tokenizeCoverageSignal(normalizeText([bundle.domain, ...bundle.values].join(" ")).replace(/[-/]+/g, " "));
217
+ const domainNeedle = bundle.domain.replace(/[-_]+/g, " ").trim();
218
+ const targetNeeds = deriveSemanticTargetNeeds(bundle);
219
+ const scored = availablePaths
220
+ .map((candidate) => normalizeFolderEntry(candidate))
221
+ .filter((candidate) => Boolean(candidate))
222
+ .filter((candidate) => bundle.kind === "folder" || !candidate.startsWith("docs/"))
223
+ .map((candidate) => {
224
+ const classification = classifyImplementationTarget(candidate);
225
+ const normalizedCandidate = classification.normalized.replace(/\//g, " ");
226
+ const overlap = anchorTokens.filter((token) => normalizedCandidate.includes(token)).length;
227
+ const hasDomainMatch = domainNeedle.length > 0 && normalizedCandidate.includes(domainNeedle);
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;
237
+ const score = implementationRootWeight(candidate) +
238
+ overlap * 20 +
239
+ (hasDomainMatch ? 15 : 0) -
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 };
252
+ })
253
+ .filter((entry) => entry.hasEvidence)
254
+ .sort((left, right) => right.score - left.score || left.candidate.localeCompare(right.candidate));
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")
237
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);
273
+ };
274
+ const summarizeImplementationTargets = (targets) => {
275
+ if (targets.length === 0)
276
+ return "target implementation surfaces";
277
+ if (targets.length === 1)
278
+ return targets[0];
279
+ if (targets.length === 2)
280
+ return `${targets[0]}, ${targets[1]}`;
281
+ return `${targets[0]} (+${targets.length - 1} more)`;
282
+ };
283
+ const summarizeBundleScope = (bundle) => {
284
+ const normalizedValues = unique(bundle.values
285
+ .map((value) => bundle.kind === "folder" ? normalizeFolderEntry(value) ?? stripDecorators(value) : normalizeHeadingCandidate(value))
286
+ .filter(Boolean));
287
+ if (normalizedValues.length === 0)
288
+ return bundle.domain.replace(/[-_]+/g, " ").trim() || "coverage";
289
+ if (normalizedValues.length === 1)
290
+ return normalizedValues[0];
291
+ return `${normalizedValues[0]} (+${normalizedValues.length - 1} more)`;
292
+ };
293
+ const buildRemediationTitle = (bundle, implementationTargets) => {
294
+ const domainLabel = bundle.domain.replace(/[-_]+/g, " ").trim() || "coverage";
295
+ const scopeLabel = summarizeBundleScope(bundle);
296
+ if (implementationTargets.length === 0) {
297
+ return `Implement ${scopeLabel || domainLabel}`.slice(0, 180);
238
298
  }
239
- const bigrams = buildBigrams(tokens);
240
- if (tokens.length >= 3 && bigrams.length > 0 && !bigrams.some((bigram) => corpus.includes(bigram))) {
241
- return false;
299
+ if (bundle.kind === "folder") {
300
+ return `Implement ${summarizeImplementationTargets(implementationTargets)}`.slice(0, 180);
242
301
  }
243
- return true;
302
+ return `Implement ${scopeLabel} in ${summarizeImplementationTargets(implementationTargets)}`.slice(0, 180);
244
303
  };
245
- const folderEntryCovered = (corpus, entry) => {
246
- const normalizedEntry = normalizeFolderEntry(entry)?.toLowerCase().replace(/\/+/g, "/");
247
- if (!normalizedEntry)
248
- return true;
249
- const corpusTight = corpus.replace(/\s+/g, "");
250
- if (corpusTight.includes(normalizedEntry.replace(/\s+/g, "")))
251
- return true;
252
- const segments = normalizedEntry
253
- .split("/")
254
- .map((segment) => segment.trim().replace(/[^a-z0-9._-]+/g, ""))
255
- .filter(Boolean);
256
- if (segments.length === 0)
257
- return true;
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;
304
+ const summarizeAnchorBundle = (bundle) => {
305
+ if (bundle.normalizedAnchors.length === 0) {
306
+ return `${bundle.kind}:${summarizeBundleScope(bundle)}`;
267
307
  }
268
- return true;
308
+ if (bundle.normalizedAnchors.length === 1) {
309
+ return bundle.normalizedAnchors[0];
310
+ }
311
+ return `${bundle.normalizedAnchors[0]} (+${bundle.normalizedAnchors.length - 1} more)`;
312
+ };
313
+ const toUnresolvedBundle = (bundle) => ({
314
+ kind: bundle.kind,
315
+ domain: bundle.domain,
316
+ values: [...bundle.values],
317
+ anchors: [...bundle.normalizedAnchors],
318
+ });
319
+ const buildTargetedTestGuidance = (implementationTargets) => {
320
+ const guidance = new Set();
321
+ for (const target of implementationTargets) {
322
+ const segments = target
323
+ .toLowerCase()
324
+ .split("/")
325
+ .map((segment) => segment.trim())
326
+ .filter(Boolean);
327
+ if (segments.some((segment) => ["ui", "web", "frontend", "page", "pages", "screen", "screens", "component", "components"].includes(segment))) {
328
+ guidance.add("- Add/update unit, component, and flow coverage for the affected user-facing path.");
329
+ }
330
+ else if (segments.some((segment) => ["api", "server", "backend", "route", "routes", "controller", "controllers", "handler", "handlers"].includes(segment))) {
331
+ guidance.add("- Add/update unit and integration coverage for the affected service/API surface.");
332
+ }
333
+ else if (segments.some((segment) => ["cli", "cmd", "bin", "command", "commands"].includes(segment))) {
334
+ guidance.add("- Add/update command-level tests and end-to-end invocation coverage for the affected runtime flow.");
335
+ }
336
+ else if (segments.some((segment) => ["ops", "infra", "deploy", "deployment", "deployments", "script", "scripts"].includes(segment))) {
337
+ guidance.add("- Add/update operational smoke coverage or script validation for the affected deployment/runbook path.");
338
+ }
339
+ else if (segments.some((segment) => ["db", "data", "schema", "schemas", "migration", "migrations", "model", "models"].includes(segment))) {
340
+ guidance.add("- Add/update persistence, schema, and integration coverage for the affected implementation path.");
341
+ }
342
+ else if (segments.some((segment) => ["worker", "workers", "job", "jobs", "queue", "task", "tasks", "service", "services", "module", "modules"].includes(segment))) {
343
+ guidance.add("- Add/update unit and integration coverage for the affected implementation path.");
344
+ }
345
+ }
346
+ if (guidance.size === 0) {
347
+ guidance.add("- Add/update the smallest deterministic test scope that proves the affected implementation targets.");
348
+ }
349
+ return Array.from(guidance);
269
350
  };
270
351
  const readJsonSafe = (raw, fallback) => {
271
352
  if (typeof raw !== "string" || raw.trim().length === 0)
@@ -371,7 +452,10 @@ export class TaskSufficiencyService {
371
452
  const docs = [];
372
453
  for (const filePath of paths) {
373
454
  try {
374
- const content = await fs.readFile(filePath, "utf8");
455
+ const rawContent = await fs.readFile(filePath, "utf8");
456
+ const content = stripManagedSdsPreflightBlock(rawContent) ?? "";
457
+ if (!content)
458
+ continue;
375
459
  if (!sdsContentPattern.test(content) && !sdsFilenamePattern.test(path.basename(filePath)))
376
460
  continue;
377
461
  docs.push({ path: filePath, content });
@@ -433,33 +517,13 @@ export class TaskSufficiencyService {
433
517
  epicCount: epics.length,
434
518
  storyCount: stories.length,
435
519
  taskCount: tasks.length,
436
- corpus: normalizeText(corpusChunks.join("\n")).replace(/\s+/g, " ").trim(),
520
+ corpus: normalizeCoverageText(corpusChunks.join("\n")),
437
521
  existingAnchors,
438
522
  maxPriority: Number(maxPriorityRow?.max_priority ?? 0),
439
523
  };
440
524
  }
441
525
  evaluateCoverage(corpus, sectionHeadings, folderEntries, existingAnchors) {
442
- const missingSectionHeadings = sectionHeadings.filter((heading) => {
443
- const anchor = normalizeAnchor("section", heading);
444
- if (existingAnchors.has(anchor))
445
- return false;
446
- return !headingCovered(corpus, heading);
447
- });
448
- const missingFolderEntries = folderEntries.filter((entry) => {
449
- const anchor = normalizeAnchor("folder", entry);
450
- if (existingAnchors.has(anchor))
451
- return false;
452
- return !folderEntryCovered(corpus, entry);
453
- });
454
- const totalSignals = sectionHeadings.length + folderEntries.length;
455
- const coveredSignals = totalSignals - missingSectionHeadings.length - missingFolderEntries.length;
456
- const coverageRatio = totalSignals === 0 ? 1 : coveredSignals / totalSignals;
457
- return {
458
- coverageRatio: Number(coverageRatio.toFixed(4)),
459
- totalSignals,
460
- missingSectionHeadings,
461
- missingFolderEntries,
462
- };
526
+ return evaluateSdsCoverage(corpus, { sectionHeadings, folderEntries }, existingAnchors);
463
527
  }
464
528
  buildGapItems(coverage, existingAnchors, limit) {
465
529
  const items = [];
@@ -604,29 +668,26 @@ export class TaskSufficiencyService {
604
668
  const existingTaskKeys = await this.workspaceRepo.listTaskKeys(params.storyId);
605
669
  const taskKeyGen = createTaskKeyGenerator(params.storyKey, existingTaskKeys);
606
670
  const now = new Date().toISOString();
607
- const taskInserts = params.gapBundles.map((bundle, index) => {
608
- const target = bundle.values[0] ?? "SDS coverage gap";
671
+ const taskInserts = params.gapBundles.map(({ bundle, implementationTargets }, index) => {
672
+ if (implementationTargets.length === 0) {
673
+ throw new Error(`task-sufficiency-audit attempted to insert a remediation task without implementation targets (${summarizeAnchorBundle(bundle)}).`);
674
+ }
609
675
  const scopeCount = bundle.values.length;
610
676
  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);
677
+ const title = buildRemediationTitle(bundle, implementationTargets);
619
678
  const objective = bundle.kind === "folder"
620
679
  ? scopeCount <= 1
621
- ? `Create or update production code under the SDS folder-tree path \`${target}\`.`
680
+ ? `Create or update production code under the SDS path \`${bundle.values[0] ?? implementationTargets[0] ?? domainLabel}\`.`
622
681
  : `Create or update production code for ${scopeCount} related SDS folder-tree paths in the ${domainLabel} domain.`
623
682
  : bundle.kind === "mixed"
624
683
  ? `Implement a cohesive capability slice covering both SDS sections and folder targets in the ${domainLabel} domain.`
625
684
  : scopeCount <= 1
626
- ? `Implement the missing production functionality described by SDS section \`${target}\`.`
685
+ ? `Implement the missing production functionality described by SDS section \`${bundle.values[0] ?? domainLabel}\`.`
627
686
  : `Implement ${scopeCount} related SDS section requirements in the ${domainLabel} domain.`;
628
687
  const scopeLines = bundle.values.map((value) => `- ${value}`);
629
688
  const anchorLines = bundle.normalizedAnchors.map((anchor) => `- ${anchor}`);
689
+ const targetLines = implementationTargets.map((target) => `- ${target}`);
690
+ const testingLines = buildTargetedTestGuidance(implementationTargets);
630
691
  const description = [
631
692
  "## Objective",
632
693
  objective,
@@ -639,21 +700,25 @@ export class TaskSufficiencyService {
639
700
  "## Anchor Scope",
640
701
  ...scopeLines,
641
702
  "",
703
+ "## Concrete Implementation Targets",
704
+ ...targetLines,
705
+ "",
642
706
  "## Anchor Keys",
643
707
  ...anchorLines,
644
708
  "",
645
709
  "## 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.",
710
+ "- Phase 1: update the listed implementation targets first; add missing files/scripts/tests only where the SDS requires them.",
711
+ "- Phase 2: implement production logic for each anchor item and keep the work mapped to the concrete targets above.",
712
+ "- Phase 3: wire dependencies, startup/runtime sequencing, and artifact generation affected by this scope.",
649
713
  "- Keep implementation traceable to anchor keys in commit and test evidence.",
650
714
  "",
651
715
  "## 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.",
716
+ ...testingLines,
717
+ "- Execute the smallest deterministic test scope that proves the behavior for the listed targets.",
654
718
  "",
655
719
  "## Definition of Done",
656
720
  "- All anchor scope items in this bundle are represented in working code.",
721
+ "- All concrete implementation targets above are updated or explicitly confirmed unchanged with evidence.",
657
722
  "- Validation evidence exists and maps back to each anchor key.",
658
723
  ].join("\n");
659
724
  const storyPointsBase = bundle.kind === "folder" ? 1 : 2;
@@ -676,6 +741,7 @@ export class TaskSufficiencyService {
676
741
  domain: bundle.domain,
677
742
  scopeCount,
678
743
  values: bundle.values,
744
+ implementationTargets,
679
745
  anchor: bundle.normalizedAnchors[0],
680
746
  anchors: bundle.normalizedAnchors,
681
747
  iteration: params.iteration,
@@ -746,17 +812,11 @@ export class TaskSufficiencyService {
746
812
  throw new Error("task-sufficiency-audit requires an SDS document but none was found. Add docs/sds.md (or a fuzzy-match SDS doc) and retry.");
747
813
  }
748
814
  const warnings = [];
749
- const rawSectionHeadings = unique(sdsDocs.flatMap((doc) => extractMarkdownHeadings(doc.content, SDS_HEADING_LIMIT))).slice(0, SDS_HEADING_LIMIT);
750
- const rawFolderEntries = unique(sdsDocs.flatMap((doc) => extractFolderEntries(doc.content, SDS_FOLDER_LIMIT))).slice(0, SDS_FOLDER_LIMIT);
751
- const sectionHeadings = unique(rawSectionHeadings
752
- .map((heading) => normalizeHeadingCandidate(heading))
753
- .filter((heading) => headingLooksImplementationRelevant(heading))).slice(0, SDS_HEADING_LIMIT);
754
- const folderEntries = unique(rawFolderEntries
755
- .map((entry) => normalizeFolderEntry(entry))
756
- .filter((entry) => Boolean(entry))
757
- .filter((entry) => folderEntryLooksRepoRelevant(entry))).slice(0, SDS_FOLDER_LIMIT);
758
- const skippedHeadingSignals = Math.max(0, rawSectionHeadings.length - sectionHeadings.length);
759
- const skippedFolderSignals = Math.max(0, rawFolderEntries.length - folderEntries.length);
815
+ const coverageSignals = collectSdsCoverageSignalsFromDocs(sdsDocs, {
816
+ headingLimit: SDS_HEADING_LIMIT,
817
+ folderLimit: SDS_FOLDER_LIMIT,
818
+ });
819
+ const { rawSectionHeadings, rawFolderEntries, sectionHeadings, folderEntries, skippedHeadingSignals, skippedFolderSignals, } = coverageSignals;
760
820
  if (skippedHeadingSignals > 0 || skippedFolderSignals > 0) {
761
821
  warnings.push(`Filtered non-actionable SDS signals (headings=${skippedHeadingSignals}, folders=${skippedFolderSignals}) before remediation.`);
762
822
  }
@@ -785,6 +845,7 @@ export class TaskSufficiencyService {
785
845
  let totalTasksAdded = 0;
786
846
  const totalTasksUpdated = 0;
787
847
  let satisfied = false;
848
+ let latestUnresolvedBundles = [];
788
849
  for (let iteration = 1; iteration <= maxIterations; iteration += 1) {
789
850
  const snapshot = await this.loadProjectSnapshot(request.projectKey);
790
851
  const coverage = this.evaluateCoverage(snapshot.corpus, sectionHeadings, folderEntries, snapshot.existingAnchors);
@@ -798,6 +859,7 @@ export class TaskSufficiencyService {
798
859
  totalSignals: coverage.totalSignals,
799
860
  missingSectionCount: coverage.missingSectionHeadings.length,
800
861
  missingFolderCount: coverage.missingFolderEntries.length,
862
+ unresolvedBundleCount: 0,
801
863
  createdTaskKeys: [],
802
864
  });
803
865
  await this.jobService.writeCheckpoint(job.id, {
@@ -809,6 +871,7 @@ export class TaskSufficiencyService {
809
871
  totalSignals: coverage.totalSignals,
810
872
  missingSectionCount: coverage.missingSectionHeadings.length,
811
873
  missingFolderCount: coverage.missingFolderEntries.length,
874
+ unresolvedBundleCount: 0,
812
875
  action: "complete",
813
876
  },
814
877
  });
@@ -817,6 +880,7 @@ export class TaskSufficiencyService {
817
880
  const gapItems = this.buildGapItems(coverage, snapshot.existingAnchors, maxTasksPerIteration);
818
881
  const gapBundles = this.bundleGapItems(gapItems, maxTasksPerIteration);
819
882
  if (gapBundles.length === 0) {
883
+ latestUnresolvedBundles = [];
820
884
  warnings.push(`Iteration ${iteration}: unresolved SDS gaps remain but no insertable gap items were identified.`);
821
885
  iterations.push({
822
886
  iteration,
@@ -824,8 +888,48 @@ export class TaskSufficiencyService {
824
888
  totalSignals: coverage.totalSignals,
825
889
  missingSectionCount: coverage.missingSectionHeadings.length,
826
890
  missingFolderCount: coverage.missingFolderEntries.length,
891
+ unresolvedBundleCount: 0,
892
+ createdTaskKeys: [],
893
+ });
894
+ break;
895
+ }
896
+ const plannedGapBundles = gapBundles.map((bundle) => ({
897
+ bundle,
898
+ implementationTargets: inferImplementationTargets(bundle, folderEntries, 3),
899
+ }));
900
+ const actionableGapBundles = plannedGapBundles.filter((entry) => entry.implementationTargets.length > 0);
901
+ const unresolvedGapBundles = plannedGapBundles.filter((entry) => entry.implementationTargets.length === 0);
902
+ latestUnresolvedBundles = unresolvedGapBundles.map((entry) => toUnresolvedBundle(entry.bundle));
903
+ if (unresolvedGapBundles.length > 0) {
904
+ warnings.push(`Iteration ${iteration}: ${unresolvedGapBundles.length} SDS gap bundle(s) remain unresolved because no concrete implementation targets were inferred (${unresolvedGapBundles
905
+ .map((entry) => summarizeAnchorBundle(entry.bundle))
906
+ .join("; ")}).`);
907
+ }
908
+ if (actionableGapBundles.length === 0) {
909
+ warnings.push(`Iteration ${iteration}: unresolved SDS gaps remain but no executable remediation tasks could be generated.`);
910
+ iterations.push({
911
+ iteration,
912
+ coverageRatio: coverage.coverageRatio,
913
+ totalSignals: coverage.totalSignals,
914
+ missingSectionCount: coverage.missingSectionHeadings.length,
915
+ missingFolderCount: coverage.missingFolderEntries.length,
916
+ unresolvedBundleCount: latestUnresolvedBundles.length,
827
917
  createdTaskKeys: [],
828
918
  });
919
+ await this.jobService.writeCheckpoint(job.id, {
920
+ stage: "iteration",
921
+ timestamp: new Date().toISOString(),
922
+ details: {
923
+ iteration,
924
+ coverageRatio: coverage.coverageRatio,
925
+ totalSignals: coverage.totalSignals,
926
+ missingSectionCount: coverage.missingSectionHeadings.length,
927
+ missingFolderCount: coverage.missingFolderEntries.length,
928
+ unresolvedBundleCount: latestUnresolvedBundles.length,
929
+ action: "unresolved",
930
+ unresolvedGapItems: latestUnresolvedBundles,
931
+ },
932
+ });
829
933
  break;
830
934
  }
831
935
  if (dryRun) {
@@ -835,6 +939,7 @@ export class TaskSufficiencyService {
835
939
  totalSignals: coverage.totalSignals,
836
940
  missingSectionCount: coverage.missingSectionHeadings.length,
837
941
  missingFolderCount: coverage.missingFolderEntries.length,
942
+ unresolvedBundleCount: latestUnresolvedBundles.length,
838
943
  createdTaskKeys: [],
839
944
  });
840
945
  await this.jobService.writeCheckpoint(job.id, {
@@ -846,12 +951,15 @@ export class TaskSufficiencyService {
846
951
  totalSignals: coverage.totalSignals,
847
952
  missingSectionCount: coverage.missingSectionHeadings.length,
848
953
  missingFolderCount: coverage.missingFolderEntries.length,
954
+ unresolvedBundleCount: latestUnresolvedBundles.length,
849
955
  action: "dry_run",
850
- proposedGapItems: gapBundles.map((bundle) => ({
851
- kind: bundle.kind,
852
- domain: bundle.domain,
853
- values: bundle.values,
956
+ proposedGapItems: actionableGapBundles.map((entry) => ({
957
+ kind: entry.bundle.kind,
958
+ domain: entry.bundle.domain,
959
+ values: entry.bundle.values,
960
+ implementationTargets: entry.implementationTargets,
854
961
  })),
962
+ unresolvedGapItems: latestUnresolvedBundles,
855
963
  },
856
964
  });
857
965
  break;
@@ -863,7 +971,7 @@ export class TaskSufficiencyService {
863
971
  storyKey: target.storyKey,
864
972
  epicId: target.epicId,
865
973
  maxPriority: snapshot.maxPriority,
866
- gapBundles,
974
+ gapBundles: actionableGapBundles,
867
975
  iteration,
868
976
  jobId: job.id,
869
977
  commandRunId: commandRun.id,
@@ -876,6 +984,7 @@ export class TaskSufficiencyService {
876
984
  totalSignals: coverage.totalSignals,
877
985
  missingSectionCount: coverage.missingSectionHeadings.length,
878
986
  missingFolderCount: coverage.missingFolderEntries.length,
987
+ unresolvedBundleCount: latestUnresolvedBundles.length,
879
988
  createdTaskKeys,
880
989
  });
881
990
  await this.jobService.writeCheckpoint(job.id, {
@@ -887,11 +996,12 @@ export class TaskSufficiencyService {
887
996
  totalSignals: coverage.totalSignals,
888
997
  missingSectionCount: coverage.missingSectionHeadings.length,
889
998
  missingFolderCount: coverage.missingFolderEntries.length,
999
+ unresolvedBundleCount: latestUnresolvedBundles.length,
890
1000
  createdTaskKeys,
891
1001
  addedCount: createdTaskKeys.length,
892
1002
  },
893
1003
  });
894
- await this.jobService.appendLog(job.id, `Iteration ${iteration}: added ${createdTaskKeys.length} remediation task(s) from ${gapBundles.length} gap bundle(s): ${createdTaskKeys.join(", ")}\n`);
1004
+ await this.jobService.appendLog(job.id, `Iteration ${iteration}: added ${createdTaskKeys.length} remediation task(s) from ${actionableGapBundles.length} actionable gap bundle(s): ${createdTaskKeys.join(", ")}\n`);
895
1005
  }
896
1006
  const finalSnapshot = await this.loadProjectSnapshot(request.projectKey);
897
1007
  const finalCoverage = this.evaluateCoverage(finalSnapshot.corpus, sectionHeadings, folderEntries, finalSnapshot.existingAnchors);
@@ -902,6 +1012,17 @@ export class TaskSufficiencyService {
902
1012
  if (!satisfied) {
903
1013
  warnings.push(`Sufficiency target not reached (coverage=${finalCoverage.coverageRatio}, threshold=${minCoverageRatio}) after ${iterations.length} iteration(s).`);
904
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));
905
1026
  const report = {
906
1027
  projectKey: request.projectKey,
907
1028
  sourceCommand,
@@ -914,24 +1035,27 @@ export class TaskSufficiencyService {
914
1035
  satisfied,
915
1036
  totalTasksAdded,
916
1037
  totalTasksUpdated,
917
- docs: sdsDocs.map((doc) => ({
918
- path: path.relative(request.workspace.workspaceRoot, doc.path),
919
- headingSignals: extractMarkdownHeadings(doc.content, SDS_HEADING_LIMIT)
920
- .map((heading) => normalizeHeadingCandidate(heading))
921
- .filter((heading) => headingLooksImplementationRelevant(heading)).length,
922
- folderSignals: extractFolderEntries(doc.content, SDS_FOLDER_LIMIT)
923
- .map((entry) => normalizeFolderEntry(entry))
924
- .filter((entry) => Boolean(entry))
925
- .filter((entry) => folderEntryLooksRepoRelevant(entry)).length,
926
- rawHeadingSignals: extractMarkdownHeadings(doc.content, SDS_HEADING_LIMIT).length,
927
- rawFolderSignals: extractFolderEntries(doc.content, SDS_FOLDER_LIMIT).length,
928
- })),
1038
+ docs: sdsDocs.map((doc) => {
1039
+ const signals = collectSdsImplementationSignals(doc.content, {
1040
+ headingLimit: SDS_HEADING_LIMIT,
1041
+ folderLimit: SDS_FOLDER_LIMIT,
1042
+ });
1043
+ return {
1044
+ path: path.relative(request.workspace.workspaceRoot, doc.path),
1045
+ headingSignals: signals.sectionHeadings.length,
1046
+ folderSignals: signals.folderEntries.length,
1047
+ rawHeadingSignals: signals.rawSectionHeadings.length,
1048
+ rawFolderSignals: signals.rawFolderEntries.length,
1049
+ };
1050
+ }),
929
1051
  finalCoverage: {
930
1052
  coverageRatio: finalCoverage.coverageRatio,
931
1053
  totalSignals: finalCoverage.totalSignals,
932
1054
  missingSectionHeadings: finalCoverage.missingSectionHeadings,
933
1055
  missingFolderEntries: finalCoverage.missingFolderEntries,
934
1056
  },
1057
+ plannedGapBundles,
1058
+ unresolvedBundles,
935
1059
  iterations,
936
1060
  warnings,
937
1061
  };
@@ -945,7 +1069,9 @@ export class TaskSufficiencyService {
945
1069
  satisfied,
946
1070
  totalTasksAdded,
947
1071
  totalTasksUpdated,
1072
+ finalTotalSignals: finalCoverage.totalSignals,
948
1073
  finalCoverageRatio: finalCoverage.coverageRatio,
1074
+ unresolvedBundleCount: unresolvedBundles.length,
949
1075
  },
950
1076
  });
951
1077
  const result = {
@@ -959,6 +1085,7 @@ export class TaskSufficiencyService {
959
1085
  totalTasksUpdated,
960
1086
  maxIterations,
961
1087
  minCoverageRatio,
1088
+ finalTotalSignals: finalCoverage.totalSignals,
962
1089
  finalCoverageRatio: finalCoverage.coverageRatio,
963
1090
  remainingSectionHeadings: finalCoverage.missingSectionHeadings,
964
1091
  remainingFolderEntries: finalCoverage.missingFolderEntries,
@@ -967,6 +1094,8 @@ export class TaskSufficiencyService {
967
1094
  folders: finalCoverage.missingFolderEntries.length,
968
1095
  total: finalCoverage.missingSectionHeadings.length + finalCoverage.missingFolderEntries.length,
969
1096
  },
1097
+ plannedGapBundles,
1098
+ unresolvedBundles,
970
1099
  iterations,
971
1100
  reportPath,
972
1101
  reportHistoryPath: historyPath,
@@ -981,9 +1110,11 @@ export class TaskSufficiencyService {
981
1110
  totalTasksUpdated,
982
1111
  maxIterations,
983
1112
  minCoverageRatio,
1113
+ finalTotalSignals: finalCoverage.totalSignals,
984
1114
  finalCoverageRatio: finalCoverage.coverageRatio,
985
1115
  remainingSectionCount: finalCoverage.missingSectionHeadings.length,
986
1116
  remainingFolderCount: finalCoverage.missingFolderEntries.length,
1117
+ unresolvedBundleCount: unresolvedBundles.length,
987
1118
  reportPath,
988
1119
  reportHistoryPath: historyPath,
989
1120
  warnings,