@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.
- 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/prompts/PdrPrompts.js +1 -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 +57 -0
- package/dist/services/planning/CreateTasksService.d.ts.map +1 -1
- package/dist/services/planning/CreateTasksService.js +2491 -291
- 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 +131 -37
- 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 +17 -0
- package/dist/services/planning/TaskSufficiencyService.d.ts.map +1 -1
- package/dist/services/planning/TaskSufficiencyService.js +409 -278
- 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
|
|
20
|
-
const
|
|
21
|
-
const
|
|
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
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
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
|
|
47
|
-
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
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
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
169
|
+
return score;
|
|
178
170
|
};
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
|
203
|
-
.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (
|
|
222
|
-
return
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
return false;
|
|
299
|
+
if (bundle.kind === "folder") {
|
|
300
|
+
return `Implement ${summarizeImplementationTargets(implementationTargets)}`.slice(0, 180);
|
|
242
301
|
}
|
|
243
|
-
return
|
|
302
|
+
return `Implement ${scopeLabel} in ${summarizeImplementationTargets(implementationTargets)}`.slice(0, 180);
|
|
244
303
|
};
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 \`${
|
|
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
|
|
647
|
-
"- Phase 2: implement production logic for each anchor item
|
|
648
|
-
"- Phase 3: wire dependencies
|
|
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
|
-
|
|
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
|
|
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);
|
|
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:
|
|
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 ${
|
|
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
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
.
|
|
924
|
-
.
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
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,
|