@mcoda/core 0.1.33 → 0.1.35
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.
- package/dist/api/AgentsApi.d.ts +4 -1
- package/dist/api/AgentsApi.d.ts.map +1 -1
- package/dist/api/AgentsApi.js +4 -1
- package/dist/services/docs/DocsService.d.ts +37 -0
- package/dist/services/docs/DocsService.d.ts.map +1 -1
- package/dist/services/docs/DocsService.js +537 -2
- package/dist/services/docs/review/gates/OpenQuestionsGate.d.ts.map +1 -1
- package/dist/services/docs/review/gates/OpenQuestionsGate.js +13 -2
- package/dist/services/docs/review/gates/SdsNoUnresolvedItemsGate.d.ts.map +1 -1
- package/dist/services/docs/review/gates/SdsNoUnresolvedItemsGate.js +12 -1
- package/dist/services/planning/CreateTasksService.d.ts +30 -0
- package/dist/services/planning/CreateTasksService.d.ts.map +1 -1
- package/dist/services/planning/CreateTasksService.js +1269 -178
- package/dist/services/planning/SdsCoverageModel.d.ts +27 -0
- package/dist/services/planning/SdsCoverageModel.d.ts.map +1 -0
- package/dist/services/planning/SdsCoverageModel.js +138 -0
- package/dist/services/planning/SdsPreflightService.d.ts +2 -0
- package/dist/services/planning/SdsPreflightService.d.ts.map +1 -1
- package/dist/services/planning/SdsPreflightService.js +125 -31
- package/dist/services/planning/SdsStructureSignals.d.ts +24 -0
- package/dist/services/planning/SdsStructureSignals.d.ts.map +1 -0
- package/dist/services/planning/SdsStructureSignals.js +402 -0
- package/dist/services/planning/TaskSufficiencyService.d.ts +1 -0
- package/dist/services/planning/TaskSufficiencyService.d.ts.map +1 -1
- package/dist/services/planning/TaskSufficiencyService.js +218 -285
- 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,20 @@ 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
|
|
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",
|
|
23
|
-
"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",
|
|
37
|
-
"service",
|
|
38
|
-
"services",
|
|
39
|
-
"shared",
|
|
40
|
-
"src",
|
|
41
|
-
"test",
|
|
42
|
-
"tests",
|
|
43
|
-
"ui",
|
|
44
|
-
"web",
|
|
45
|
-
]);
|
|
21
|
+
const supportRootSegments = new Set(["docs", "fixtures", "policies", "policy", "runbooks", "pdr", "rfp", "sds"]);
|
|
46
22
|
const headingNoiseTokens = new Set(["and", "for", "from", "into", "the", "with"]);
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
"across",
|
|
50
|
-
"after",
|
|
51
|
-
"before",
|
|
52
|
-
"between",
|
|
53
|
-
"from",
|
|
54
|
-
"into",
|
|
55
|
-
"over",
|
|
56
|
-
"under",
|
|
57
|
-
"using",
|
|
58
|
-
"with",
|
|
59
|
-
"without",
|
|
60
|
-
"into",
|
|
61
|
-
"onto",
|
|
62
|
-
"the",
|
|
63
|
-
"and",
|
|
64
|
-
"for",
|
|
65
|
-
"of",
|
|
66
|
-
"to",
|
|
67
|
-
]);
|
|
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()}`;
|
|
23
|
+
const normalizeText = (value) => normalizeCoverageText(value);
|
|
24
|
+
const normalizeAnchor = normalizeCoverageAnchor;
|
|
75
25
|
const unique = (items) => Array.from(new Set(items.filter(Boolean)));
|
|
26
|
+
const tokenizeCoverageSignal = (value) => unique(normalizeCoverageText(value)
|
|
27
|
+
.split(/\s+/)
|
|
28
|
+
.map((token) => token.replace(/[^a-z0-9._-]+/g, ""))
|
|
29
|
+
.filter((token) => token.length >= 3));
|
|
76
30
|
const stripDecorators = (value) => value
|
|
77
31
|
.replace(/[`*_]/g, " ")
|
|
78
32
|
.replace(/^[\s>:\-[\]().]+/, "")
|
|
79
33
|
.replace(/\s+/g, " ")
|
|
80
34
|
.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
35
|
const deriveSectionDomain = (heading) => {
|
|
129
36
|
const normalized = normalizeHeadingCandidate(heading).toLowerCase();
|
|
130
37
|
const tokens = normalized
|
|
@@ -142,130 +49,125 @@ const deriveFolderDomain = (entry) => {
|
|
|
142
49
|
return "structure";
|
|
143
50
|
return segments.length === 1 ? segments[0] : `${segments[0]}-${segments[1]}`;
|
|
144
51
|
};
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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;
|
|
52
|
+
const implementationRootWeight = (target) => {
|
|
53
|
+
const normalized = normalizeFolderEntry(target)?.toLowerCase() ?? normalizeText(target);
|
|
54
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
55
|
+
const root = segments[0] ?? normalized;
|
|
56
|
+
let score = 55 + Math.min(segments.length, 4) * 6 + (isStructuredFilePath(normalized) ? 10 : 0);
|
|
57
|
+
if (supportRootSegments.has(root))
|
|
58
|
+
score -= 30;
|
|
59
|
+
if (segments.some((segment) => ["src", "app", "apps", "lib", "libs", "server", "api", "worker", "workers"].includes(segment))) {
|
|
60
|
+
score += 20;
|
|
61
|
+
}
|
|
62
|
+
if (segments.some((segment) => ["test", "tests", "spec", "specs", "integration", "acceptance", "e2e"].includes(segment))) {
|
|
63
|
+
score += 10;
|
|
64
|
+
}
|
|
65
|
+
if (segments.some((segment) => ["db", "data", "schema", "schemas", "migration", "migrations"].includes(segment))) {
|
|
66
|
+
score += 12;
|
|
176
67
|
}
|
|
177
|
-
|
|
68
|
+
if (segments.some((segment) => ["ops", "infra", "deploy", "deployment", "deployments", "script", "scripts"].includes(segment))) {
|
|
69
|
+
score += 12;
|
|
70
|
+
}
|
|
71
|
+
return score;
|
|
178
72
|
};
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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;
|
|
73
|
+
const inferImplementationTargets = (bundle, availablePaths, limit = 3) => {
|
|
74
|
+
const explicitTargets = bundle.values
|
|
75
|
+
.map((value) => normalizeFolderEntry(value))
|
|
76
|
+
.filter((value) => Boolean(value));
|
|
77
|
+
if (explicitTargets.length > 0) {
|
|
78
|
+
return unique(explicitTargets).slice(0, limit);
|
|
199
79
|
}
|
|
200
|
-
|
|
80
|
+
const anchorTokens = tokenizeCoverageSignal(normalizeText([bundle.domain, ...bundle.values].join(" ")).replace(/[-/]+/g, " "));
|
|
81
|
+
const domainNeedle = bundle.domain.replace(/[-_]+/g, " ").trim();
|
|
82
|
+
const scored = availablePaths
|
|
83
|
+
.map((candidate) => normalizeFolderEntry(candidate))
|
|
84
|
+
.filter((candidate) => Boolean(candidate))
|
|
85
|
+
.filter((candidate) => bundle.kind === "folder" || !candidate.startsWith("docs/"))
|
|
86
|
+
.map((candidate) => {
|
|
87
|
+
const normalizedCandidate = normalizeText(candidate.replace(/\//g, " "));
|
|
88
|
+
const overlap = anchorTokens.filter((token) => normalizedCandidate.includes(token)).length;
|
|
89
|
+
const hasDomainMatch = domainNeedle.length > 0 && normalizedCandidate.includes(domainNeedle);
|
|
90
|
+
const hasEvidence = overlap > 0 || hasDomainMatch;
|
|
91
|
+
const score = implementationRootWeight(candidate) +
|
|
92
|
+
overlap * 20 +
|
|
93
|
+
(hasDomainMatch ? 15 : 0) -
|
|
94
|
+
(candidate.startsWith("docs/") ? 25 : 0);
|
|
95
|
+
return { candidate, score, hasEvidence };
|
|
96
|
+
})
|
|
97
|
+
.filter((entry) => entry.hasEvidence)
|
|
98
|
+
.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);
|
|
201
100
|
};
|
|
202
|
-
const
|
|
203
|
-
.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
101
|
+
const summarizeImplementationTargets = (targets) => {
|
|
102
|
+
if (targets.length === 0)
|
|
103
|
+
return "target implementation surfaces";
|
|
104
|
+
if (targets.length === 1)
|
|
105
|
+
return targets[0];
|
|
106
|
+
if (targets.length === 2)
|
|
107
|
+
return `${targets[0]}, ${targets[1]}`;
|
|
108
|
+
return `${targets[0]} (+${targets.length - 1} more)`;
|
|
109
|
+
};
|
|
110
|
+
const summarizeBundleScope = (bundle) => {
|
|
111
|
+
const normalizedValues = unique(bundle.values
|
|
112
|
+
.map((value) => bundle.kind === "folder" ? normalizeFolderEntry(value) ?? stripDecorators(value) : normalizeHeadingCandidate(value))
|
|
113
|
+
.filter(Boolean));
|
|
114
|
+
if (normalizedValues.length === 0)
|
|
115
|
+
return bundle.domain.replace(/[-_]+/g, " ").trim() || "coverage";
|
|
116
|
+
if (normalizedValues.length === 1)
|
|
117
|
+
return normalizedValues[0];
|
|
118
|
+
return `${normalizedValues[0]} (+${normalizedValues.length - 1} more)`;
|
|
119
|
+
};
|
|
120
|
+
const buildRemediationTitle = (bundle, implementationTargets) => {
|
|
121
|
+
const domainLabel = bundle.domain.replace(/[-_]+/g, " ").trim() || "coverage";
|
|
122
|
+
const scopeLabel = summarizeBundleScope(bundle);
|
|
123
|
+
if (implementationTargets.length === 0) {
|
|
124
|
+
return `Implement ${scopeLabel || domainLabel}`.slice(0, 180);
|
|
214
125
|
}
|
|
215
|
-
|
|
126
|
+
if (bundle.kind === "folder") {
|
|
127
|
+
return `Implement ${summarizeImplementationTargets(implementationTargets)}`.slice(0, 180);
|
|
128
|
+
}
|
|
129
|
+
return `Implement ${scopeLabel} in ${summarizeImplementationTargets(implementationTargets)}`.slice(0, 180);
|
|
216
130
|
};
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
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))
|
|
237
|
-
return false;
|
|
131
|
+
const summarizeAnchorBundle = (bundle) => {
|
|
132
|
+
if (bundle.normalizedAnchors.length === 0) {
|
|
133
|
+
return `${bundle.kind}:${summarizeBundleScope(bundle)}`;
|
|
238
134
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
return false;
|
|
135
|
+
if (bundle.normalizedAnchors.length === 1) {
|
|
136
|
+
return bundle.normalizedAnchors[0];
|
|
242
137
|
}
|
|
243
|
-
return
|
|
138
|
+
return `${bundle.normalizedAnchors[0]} (+${bundle.normalizedAnchors.length - 1} more)`;
|
|
244
139
|
};
|
|
245
|
-
const
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
.
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if (
|
|
266
|
-
|
|
140
|
+
const buildTargetedTestGuidance = (implementationTargets) => {
|
|
141
|
+
const guidance = new Set();
|
|
142
|
+
for (const target of implementationTargets) {
|
|
143
|
+
const segments = target
|
|
144
|
+
.toLowerCase()
|
|
145
|
+
.split("/")
|
|
146
|
+
.map((segment) => segment.trim())
|
|
147
|
+
.filter(Boolean);
|
|
148
|
+
if (segments.some((segment) => ["ui", "web", "frontend", "page", "pages", "screen", "screens", "component", "components"].includes(segment))) {
|
|
149
|
+
guidance.add("- Add/update unit, component, and flow coverage for the affected user-facing path.");
|
|
150
|
+
}
|
|
151
|
+
else if (segments.some((segment) => ["api", "server", "backend", "route", "routes", "controller", "controllers", "handler", "handlers"].includes(segment))) {
|
|
152
|
+
guidance.add("- Add/update unit and integration coverage for the affected service/API surface.");
|
|
153
|
+
}
|
|
154
|
+
else if (segments.some((segment) => ["cli", "cmd", "bin", "command", "commands"].includes(segment))) {
|
|
155
|
+
guidance.add("- Add/update command-level tests and end-to-end invocation coverage for the affected runtime flow.");
|
|
156
|
+
}
|
|
157
|
+
else if (segments.some((segment) => ["ops", "infra", "deploy", "deployment", "deployments", "script", "scripts"].includes(segment))) {
|
|
158
|
+
guidance.add("- Add/update operational smoke coverage or script validation for the affected deployment/runbook path.");
|
|
159
|
+
}
|
|
160
|
+
else if (segments.some((segment) => ["db", "data", "schema", "schemas", "migration", "migrations", "model", "models"].includes(segment))) {
|
|
161
|
+
guidance.add("- Add/update persistence, schema, and integration coverage for the affected implementation path.");
|
|
162
|
+
}
|
|
163
|
+
else if (segments.some((segment) => ["worker", "workers", "job", "jobs", "queue", "task", "tasks", "service", "services", "module", "modules"].includes(segment))) {
|
|
164
|
+
guidance.add("- Add/update unit and integration coverage for the affected implementation path.");
|
|
165
|
+
}
|
|
267
166
|
}
|
|
268
|
-
|
|
167
|
+
if (guidance.size === 0) {
|
|
168
|
+
guidance.add("- Add/update the smallest deterministic test scope that proves the affected implementation targets.");
|
|
169
|
+
}
|
|
170
|
+
return Array.from(guidance);
|
|
269
171
|
};
|
|
270
172
|
const readJsonSafe = (raw, fallback) => {
|
|
271
173
|
if (typeof raw !== "string" || raw.trim().length === 0)
|
|
@@ -371,7 +273,10 @@ export class TaskSufficiencyService {
|
|
|
371
273
|
const docs = [];
|
|
372
274
|
for (const filePath of paths) {
|
|
373
275
|
try {
|
|
374
|
-
const
|
|
276
|
+
const rawContent = await fs.readFile(filePath, "utf8");
|
|
277
|
+
const content = stripManagedSdsPreflightBlock(rawContent) ?? "";
|
|
278
|
+
if (!content)
|
|
279
|
+
continue;
|
|
375
280
|
if (!sdsContentPattern.test(content) && !sdsFilenamePattern.test(path.basename(filePath)))
|
|
376
281
|
continue;
|
|
377
282
|
docs.push({ path: filePath, content });
|
|
@@ -433,33 +338,13 @@ export class TaskSufficiencyService {
|
|
|
433
338
|
epicCount: epics.length,
|
|
434
339
|
storyCount: stories.length,
|
|
435
340
|
taskCount: tasks.length,
|
|
436
|
-
corpus:
|
|
341
|
+
corpus: normalizeCoverageText(corpusChunks.join("\n")),
|
|
437
342
|
existingAnchors,
|
|
438
343
|
maxPriority: Number(maxPriorityRow?.max_priority ?? 0),
|
|
439
344
|
};
|
|
440
345
|
}
|
|
441
346
|
evaluateCoverage(corpus, sectionHeadings, folderEntries, existingAnchors) {
|
|
442
|
-
|
|
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
|
-
};
|
|
347
|
+
return evaluateSdsCoverage(corpus, { sectionHeadings, folderEntries }, existingAnchors);
|
|
463
348
|
}
|
|
464
349
|
buildGapItems(coverage, existingAnchors, limit) {
|
|
465
350
|
const items = [];
|
|
@@ -604,29 +489,26 @@ export class TaskSufficiencyService {
|
|
|
604
489
|
const existingTaskKeys = await this.workspaceRepo.listTaskKeys(params.storyId);
|
|
605
490
|
const taskKeyGen = createTaskKeyGenerator(params.storyKey, existingTaskKeys);
|
|
606
491
|
const now = new Date().toISOString();
|
|
607
|
-
const taskInserts = params.gapBundles.map((bundle, index) => {
|
|
608
|
-
|
|
492
|
+
const taskInserts = params.gapBundles.map(({ bundle, implementationTargets }, index) => {
|
|
493
|
+
if (implementationTargets.length === 0) {
|
|
494
|
+
throw new Error(`task-sufficiency-audit attempted to insert a remediation task without implementation targets (${summarizeAnchorBundle(bundle)}).`);
|
|
495
|
+
}
|
|
609
496
|
const scopeCount = bundle.values.length;
|
|
610
497
|
const domainLabel = bundle.domain.replace(/[-_]+/g, " ").trim() || "coverage";
|
|
611
|
-
const
|
|
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);
|
|
498
|
+
const title = buildRemediationTitle(bundle, implementationTargets);
|
|
619
499
|
const objective = bundle.kind === "folder"
|
|
620
500
|
? scopeCount <= 1
|
|
621
|
-
? `Create or update production code under the SDS
|
|
501
|
+
? `Create or update production code under the SDS path \`${bundle.values[0] ?? implementationTargets[0] ?? domainLabel}\`.`
|
|
622
502
|
: `Create or update production code for ${scopeCount} related SDS folder-tree paths in the ${domainLabel} domain.`
|
|
623
503
|
: bundle.kind === "mixed"
|
|
624
504
|
? `Implement a cohesive capability slice covering both SDS sections and folder targets in the ${domainLabel} domain.`
|
|
625
505
|
: scopeCount <= 1
|
|
626
|
-
? `Implement the missing production functionality described by SDS section \`${
|
|
506
|
+
? `Implement the missing production functionality described by SDS section \`${bundle.values[0] ?? domainLabel}\`.`
|
|
627
507
|
: `Implement ${scopeCount} related SDS section requirements in the ${domainLabel} domain.`;
|
|
628
508
|
const scopeLines = bundle.values.map((value) => `- ${value}`);
|
|
629
509
|
const anchorLines = bundle.normalizedAnchors.map((anchor) => `- ${anchor}`);
|
|
510
|
+
const targetLines = implementationTargets.map((target) => `- ${target}`);
|
|
511
|
+
const testingLines = buildTargetedTestGuidance(implementationTargets);
|
|
630
512
|
const description = [
|
|
631
513
|
"## Objective",
|
|
632
514
|
objective,
|
|
@@ -639,21 +521,25 @@ export class TaskSufficiencyService {
|
|
|
639
521
|
"## Anchor Scope",
|
|
640
522
|
...scopeLines,
|
|
641
523
|
"",
|
|
524
|
+
"## Concrete Implementation Targets",
|
|
525
|
+
...targetLines,
|
|
526
|
+
"",
|
|
642
527
|
"## Anchor Keys",
|
|
643
528
|
...anchorLines,
|
|
644
529
|
"",
|
|
645
530
|
"## Implementation Plan",
|
|
646
|
-
"- Phase 1: add
|
|
647
|
-
"- Phase 2: implement production logic for each anchor item
|
|
648
|
-
"- Phase 3: wire dependencies
|
|
531
|
+
"- Phase 1: update the listed implementation targets first; add missing files/scripts/tests only where the SDS requires them.",
|
|
532
|
+
"- Phase 2: implement production logic for each anchor item and keep the work mapped to the concrete targets above.",
|
|
533
|
+
"- Phase 3: wire dependencies, startup/runtime sequencing, and artifact generation affected by this scope.",
|
|
649
534
|
"- Keep implementation traceable to anchor keys in commit and test evidence.",
|
|
650
535
|
"",
|
|
651
536
|
"## Testing",
|
|
652
|
-
|
|
653
|
-
"- Execute the smallest deterministic test scope that proves the behavior.",
|
|
537
|
+
...testingLines,
|
|
538
|
+
"- Execute the smallest deterministic test scope that proves the behavior for the listed targets.",
|
|
654
539
|
"",
|
|
655
540
|
"## Definition of Done",
|
|
656
541
|
"- All anchor scope items in this bundle are represented in working code.",
|
|
542
|
+
"- All concrete implementation targets above are updated or explicitly confirmed unchanged with evidence.",
|
|
657
543
|
"- Validation evidence exists and maps back to each anchor key.",
|
|
658
544
|
].join("\n");
|
|
659
545
|
const storyPointsBase = bundle.kind === "folder" ? 1 : 2;
|
|
@@ -676,6 +562,7 @@ export class TaskSufficiencyService {
|
|
|
676
562
|
domain: bundle.domain,
|
|
677
563
|
scopeCount,
|
|
678
564
|
values: bundle.values,
|
|
565
|
+
implementationTargets,
|
|
679
566
|
anchor: bundle.normalizedAnchors[0],
|
|
680
567
|
anchors: bundle.normalizedAnchors,
|
|
681
568
|
iteration: params.iteration,
|
|
@@ -746,17 +633,11 @@ export class TaskSufficiencyService {
|
|
|
746
633
|
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
634
|
}
|
|
748
635
|
const warnings = [];
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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);
|
|
636
|
+
const coverageSignals = collectSdsCoverageSignalsFromDocs(sdsDocs, {
|
|
637
|
+
headingLimit: SDS_HEADING_LIMIT,
|
|
638
|
+
folderLimit: SDS_FOLDER_LIMIT,
|
|
639
|
+
});
|
|
640
|
+
const { rawSectionHeadings, rawFolderEntries, sectionHeadings, folderEntries, skippedHeadingSignals, skippedFolderSignals, } = coverageSignals;
|
|
760
641
|
if (skippedHeadingSignals > 0 || skippedFolderSignals > 0) {
|
|
761
642
|
warnings.push(`Filtered non-actionable SDS signals (headings=${skippedHeadingSignals}, folders=${skippedFolderSignals}) before remediation.`);
|
|
762
643
|
}
|
|
@@ -828,6 +709,47 @@ export class TaskSufficiencyService {
|
|
|
828
709
|
});
|
|
829
710
|
break;
|
|
830
711
|
}
|
|
712
|
+
const plannedGapBundles = gapBundles.map((bundle) => ({
|
|
713
|
+
bundle,
|
|
714
|
+
implementationTargets: inferImplementationTargets(bundle, folderEntries, 3),
|
|
715
|
+
}));
|
|
716
|
+
const actionableGapBundles = plannedGapBundles.filter((entry) => entry.implementationTargets.length > 0);
|
|
717
|
+
const unresolvedGapBundles = plannedGapBundles.filter((entry) => entry.implementationTargets.length === 0);
|
|
718
|
+
if (unresolvedGapBundles.length > 0) {
|
|
719
|
+
warnings.push(`Iteration ${iteration}: ${unresolvedGapBundles.length} SDS gap bundle(s) remain unresolved because no concrete implementation targets were inferred (${unresolvedGapBundles
|
|
720
|
+
.map((entry) => summarizeAnchorBundle(entry.bundle))
|
|
721
|
+
.join("; ")}).`);
|
|
722
|
+
}
|
|
723
|
+
if (actionableGapBundles.length === 0) {
|
|
724
|
+
warnings.push(`Iteration ${iteration}: unresolved SDS gaps remain but no executable remediation tasks could be generated.`);
|
|
725
|
+
iterations.push({
|
|
726
|
+
iteration,
|
|
727
|
+
coverageRatio: coverage.coverageRatio,
|
|
728
|
+
totalSignals: coverage.totalSignals,
|
|
729
|
+
missingSectionCount: coverage.missingSectionHeadings.length,
|
|
730
|
+
missingFolderCount: coverage.missingFolderEntries.length,
|
|
731
|
+
createdTaskKeys: [],
|
|
732
|
+
});
|
|
733
|
+
await this.jobService.writeCheckpoint(job.id, {
|
|
734
|
+
stage: "iteration",
|
|
735
|
+
timestamp: new Date().toISOString(),
|
|
736
|
+
details: {
|
|
737
|
+
iteration,
|
|
738
|
+
coverageRatio: coverage.coverageRatio,
|
|
739
|
+
totalSignals: coverage.totalSignals,
|
|
740
|
+
missingSectionCount: coverage.missingSectionHeadings.length,
|
|
741
|
+
missingFolderCount: coverage.missingFolderEntries.length,
|
|
742
|
+
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
|
+
})),
|
|
749
|
+
},
|
|
750
|
+
});
|
|
751
|
+
break;
|
|
752
|
+
}
|
|
831
753
|
if (dryRun) {
|
|
832
754
|
iterations.push({
|
|
833
755
|
iteration,
|
|
@@ -847,10 +769,17 @@ export class TaskSufficiencyService {
|
|
|
847
769
|
missingSectionCount: coverage.missingSectionHeadings.length,
|
|
848
770
|
missingFolderCount: coverage.missingFolderEntries.length,
|
|
849
771
|
action: "dry_run",
|
|
850
|
-
proposedGapItems:
|
|
851
|
-
kind: bundle.kind,
|
|
852
|
-
domain: bundle.domain,
|
|
853
|
-
values: bundle.values,
|
|
772
|
+
proposedGapItems: actionableGapBundles.map((entry) => ({
|
|
773
|
+
kind: entry.bundle.kind,
|
|
774
|
+
domain: entry.bundle.domain,
|
|
775
|
+
values: entry.bundle.values,
|
|
776
|
+
implementationTargets: entry.implementationTargets,
|
|
777
|
+
})),
|
|
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,
|
|
854
783
|
})),
|
|
855
784
|
},
|
|
856
785
|
});
|
|
@@ -863,7 +792,7 @@ export class TaskSufficiencyService {
|
|
|
863
792
|
storyKey: target.storyKey,
|
|
864
793
|
epicId: target.epicId,
|
|
865
794
|
maxPriority: snapshot.maxPriority,
|
|
866
|
-
gapBundles,
|
|
795
|
+
gapBundles: actionableGapBundles,
|
|
867
796
|
iteration,
|
|
868
797
|
jobId: job.id,
|
|
869
798
|
commandRunId: commandRun.id,
|
|
@@ -891,7 +820,7 @@ export class TaskSufficiencyService {
|
|
|
891
820
|
addedCount: createdTaskKeys.length,
|
|
892
821
|
},
|
|
893
822
|
});
|
|
894
|
-
await this.jobService.appendLog(job.id, `Iteration ${iteration}: added ${createdTaskKeys.length} remediation task(s) from ${
|
|
823
|
+
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
824
|
}
|
|
896
825
|
const finalSnapshot = await this.loadProjectSnapshot(request.projectKey);
|
|
897
826
|
const finalCoverage = this.evaluateCoverage(finalSnapshot.corpus, sectionHeadings, folderEntries, finalSnapshot.existingAnchors);
|
|
@@ -914,18 +843,19 @@ export class TaskSufficiencyService {
|
|
|
914
843
|
satisfied,
|
|
915
844
|
totalTasksAdded,
|
|
916
845
|
totalTasksUpdated,
|
|
917
|
-
docs: sdsDocs.map((doc) =>
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
.
|
|
924
|
-
.
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
846
|
+
docs: sdsDocs.map((doc) => {
|
|
847
|
+
const signals = collectSdsImplementationSignals(doc.content, {
|
|
848
|
+
headingLimit: SDS_HEADING_LIMIT,
|
|
849
|
+
folderLimit: SDS_FOLDER_LIMIT,
|
|
850
|
+
});
|
|
851
|
+
return {
|
|
852
|
+
path: path.relative(request.workspace.workspaceRoot, doc.path),
|
|
853
|
+
headingSignals: signals.sectionHeadings.length,
|
|
854
|
+
folderSignals: signals.folderEntries.length,
|
|
855
|
+
rawHeadingSignals: signals.rawSectionHeadings.length,
|
|
856
|
+
rawFolderSignals: signals.rawFolderEntries.length,
|
|
857
|
+
};
|
|
858
|
+
}),
|
|
929
859
|
finalCoverage: {
|
|
930
860
|
coverageRatio: finalCoverage.coverageRatio,
|
|
931
861
|
totalSignals: finalCoverage.totalSignals,
|
|
@@ -945,6 +875,7 @@ export class TaskSufficiencyService {
|
|
|
945
875
|
satisfied,
|
|
946
876
|
totalTasksAdded,
|
|
947
877
|
totalTasksUpdated,
|
|
878
|
+
finalTotalSignals: finalCoverage.totalSignals,
|
|
948
879
|
finalCoverageRatio: finalCoverage.coverageRatio,
|
|
949
880
|
},
|
|
950
881
|
});
|
|
@@ -959,6 +890,7 @@ export class TaskSufficiencyService {
|
|
|
959
890
|
totalTasksUpdated,
|
|
960
891
|
maxIterations,
|
|
961
892
|
minCoverageRatio,
|
|
893
|
+
finalTotalSignals: finalCoverage.totalSignals,
|
|
962
894
|
finalCoverageRatio: finalCoverage.coverageRatio,
|
|
963
895
|
remainingSectionHeadings: finalCoverage.missingSectionHeadings,
|
|
964
896
|
remainingFolderEntries: finalCoverage.missingFolderEntries,
|
|
@@ -981,6 +913,7 @@ export class TaskSufficiencyService {
|
|
|
981
913
|
totalTasksUpdated,
|
|
982
914
|
maxIterations,
|
|
983
915
|
minCoverageRatio,
|
|
916
|
+
finalTotalSignals: finalCoverage.totalSignals,
|
|
984
917
|
finalCoverageRatio: finalCoverage.coverageRatio,
|
|
985
918
|
remainingSectionCount: finalCoverage.missingSectionHeadings.length,
|
|
986
919
|
remainingFolderCount: finalCoverage.missingFolderEntries.length,
|