@mcoda/core 0.1.35 → 0.1.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/prompts/PdrPrompts.js +1 -1
- package/dist/services/planning/CreateTasksService.d.ts +37 -0
- package/dist/services/planning/CreateTasksService.d.ts.map +1 -1
- package/dist/services/planning/CreateTasksService.js +1739 -148
- package/dist/services/planning/SdsPreflightService.js +6 -6
- package/dist/services/planning/TaskSufficiencyService.d.ts +16 -0
- package/dist/services/planning/TaskSufficiencyService.d.ts.map +1 -1
- package/dist/services/planning/TaskSufficiencyService.js +215 -17
- package/package.json +6 -6
|
@@ -14,7 +14,7 @@ import { QaTestCommandBuilder } from "../execution/QaTestCommandBuilder.js";
|
|
|
14
14
|
import { createEpicKeyGenerator, createStoryKeyGenerator, createTaskKeyGenerator, } from "./KeyHelpers.js";
|
|
15
15
|
import { collectSdsCoverageSignalsFromDocs, evaluateSdsCoverage, normalizeCoverageText, } from "./SdsCoverageModel.js";
|
|
16
16
|
import { collectSdsImplementationSignals, extractStructuredPaths, filterImplementationStructuredPaths, headingLooksImplementationRelevant, isStructuredFilePath, normalizeHeadingCandidate, normalizeStructuredPathToken, stripManagedSdsPreflightBlock, } from "./SdsStructureSignals.js";
|
|
17
|
-
import { TaskSufficiencyService } from "./TaskSufficiencyService.js";
|
|
17
|
+
import { TaskSufficiencyService, } from "./TaskSufficiencyService.js";
|
|
18
18
|
import { SdsPreflightService } from "./SdsPreflightService.js";
|
|
19
19
|
const formatBullets = (items, fallback) => {
|
|
20
20
|
if (!items || items.length === 0)
|
|
@@ -832,6 +832,111 @@ const SERVICE_ARROW_PATTERN = /([A-Za-z][A-Za-z0-9 _/-]{1,80})\s*(?:->|=>|→)\s
|
|
|
832
832
|
const SERVICE_HANDLE_PATTERN = /\b((?:svc|ui|worker)-[a-z0-9-*]+)\b/gi;
|
|
833
833
|
const WAVE_LABEL_PATTERN = /\bwave\s*([0-9]{1,2})\b/i;
|
|
834
834
|
const TOPOLOGY_HEADING_PATTERN = /\b(service|services|component|components|module|modules|interface|interfaces|runtime|runtimes|worker|workers|client|clients|gateway|gateways|server|servers|engine|engines|pipeline|pipelines|registry|registries|adapter|adapters|processor|processors|daemon|daemons|ops|operations|deployment|deployments|topology)\b/i;
|
|
835
|
+
const MARKDOWN_HEADING_PATTERN = /^(#{1,6})\s+(.+?)\s*$/;
|
|
836
|
+
const RUNTIME_COMPONENTS_HEADING_PATTERN = /\b(runtime components?|runtime topology|system components?|component topology|services?)\b/i;
|
|
837
|
+
const VERIFICATION_MATRIX_HEADING_PATTERN = /\b(verification matrix|validation matrix|test matrix|verification suites?)\b/i;
|
|
838
|
+
const ACCEPTANCE_SCENARIOS_HEADING_PATTERN = /\b(required )?acceptance scenarios?\b/i;
|
|
839
|
+
const BUILD_TARGET_RUNTIME_SEGMENTS = new Set([
|
|
840
|
+
"api",
|
|
841
|
+
"app",
|
|
842
|
+
"apps",
|
|
843
|
+
"bin",
|
|
844
|
+
"cli",
|
|
845
|
+
"client",
|
|
846
|
+
"clients",
|
|
847
|
+
"cmd",
|
|
848
|
+
"command",
|
|
849
|
+
"commands",
|
|
850
|
+
"engine",
|
|
851
|
+
"engines",
|
|
852
|
+
"feature",
|
|
853
|
+
"features",
|
|
854
|
+
"gateway",
|
|
855
|
+
"gateways",
|
|
856
|
+
"handler",
|
|
857
|
+
"handlers",
|
|
858
|
+
"module",
|
|
859
|
+
"modules",
|
|
860
|
+
"page",
|
|
861
|
+
"pages",
|
|
862
|
+
"processor",
|
|
863
|
+
"processors",
|
|
864
|
+
"route",
|
|
865
|
+
"routes",
|
|
866
|
+
"screen",
|
|
867
|
+
"screens",
|
|
868
|
+
"server",
|
|
869
|
+
"servers",
|
|
870
|
+
"service",
|
|
871
|
+
"services",
|
|
872
|
+
"src",
|
|
873
|
+
"ui",
|
|
874
|
+
"web",
|
|
875
|
+
"worker",
|
|
876
|
+
"workers",
|
|
877
|
+
]);
|
|
878
|
+
const BUILD_TARGET_INTERFACE_SEGMENTS = new Set([
|
|
879
|
+
"contract",
|
|
880
|
+
"contracts",
|
|
881
|
+
"dto",
|
|
882
|
+
"dtos",
|
|
883
|
+
"interface",
|
|
884
|
+
"interfaces",
|
|
885
|
+
"proto",
|
|
886
|
+
"protocol",
|
|
887
|
+
"protocols",
|
|
888
|
+
"schema",
|
|
889
|
+
"schemas",
|
|
890
|
+
"spec",
|
|
891
|
+
"specs",
|
|
892
|
+
"type",
|
|
893
|
+
"types",
|
|
894
|
+
]);
|
|
895
|
+
const BUILD_TARGET_DATA_SEGMENTS = new Set([
|
|
896
|
+
"cache",
|
|
897
|
+
"caches",
|
|
898
|
+
"data",
|
|
899
|
+
"db",
|
|
900
|
+
"ledger",
|
|
901
|
+
"migration",
|
|
902
|
+
"migrations",
|
|
903
|
+
"model",
|
|
904
|
+
"models",
|
|
905
|
+
"persistence",
|
|
906
|
+
"repository",
|
|
907
|
+
"repositories",
|
|
908
|
+
"storage",
|
|
909
|
+
]);
|
|
910
|
+
const BUILD_TARGET_TEST_SEGMENTS = new Set([
|
|
911
|
+
"acceptance",
|
|
912
|
+
"e2e",
|
|
913
|
+
"integration",
|
|
914
|
+
"spec",
|
|
915
|
+
"specs",
|
|
916
|
+
"test",
|
|
917
|
+
"tests",
|
|
918
|
+
]);
|
|
919
|
+
const BUILD_TARGET_OPS_SEGMENTS = new Set([
|
|
920
|
+
"deploy",
|
|
921
|
+
"deployment",
|
|
922
|
+
"deployments",
|
|
923
|
+
"helm",
|
|
924
|
+
"infra",
|
|
925
|
+
"k8s",
|
|
926
|
+
"ops",
|
|
927
|
+
"operation",
|
|
928
|
+
"operations",
|
|
929
|
+
"runbook",
|
|
930
|
+
"runbooks",
|
|
931
|
+
"script",
|
|
932
|
+
"scripts",
|
|
933
|
+
"systemd",
|
|
934
|
+
"terraform",
|
|
935
|
+
]);
|
|
936
|
+
const BUILD_TARGET_DOC_SEGMENTS = new Set(["docs", "policy", "policies", "rfp", "pdr", "sds"]);
|
|
937
|
+
const MANIFEST_TARGET_BASENAME_PATTERN = /^(package\.json|pnpm-workspace\.yaml|pnpm-lock\.yaml|turbo\.json|tsconfig(?:\.[^.]+)?\.json|eslint(?:\.[^.]+)?\.(?:js|cjs|mjs|json)|prettier(?:\.[^.]+)?\.(?:js|cjs|mjs|json)|vite\.config\.[^.]+|webpack\.config\.[^.]+|rollup\.config\.[^.]+|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;
|
|
938
|
+
const SERVICE_ARTIFACT_BASENAME_PATTERN = /(?:\.service|\.socket|\.timer|(?:^|[.-])compose\.(?:ya?ml|json)$|docker-compose\.(?:ya?ml|json)$)$/i;
|
|
939
|
+
const GENERIC_IMPLEMENTATION_TASK_PATTERN = /update the concrete .* modules surfaced by the sds/i;
|
|
835
940
|
const nextUniqueLocalId = (prefix, existing) => {
|
|
836
941
|
let index = 1;
|
|
837
942
|
let candidate = `${prefix}-${index}`;
|
|
@@ -917,6 +1022,48 @@ const TASK_SCHEMA_SNIPPET = `{
|
|
|
917
1022
|
}
|
|
918
1023
|
]
|
|
919
1024
|
}`;
|
|
1025
|
+
const FULL_PLAN_SCHEMA_SNIPPET = `{
|
|
1026
|
+
"epics": [
|
|
1027
|
+
{
|
|
1028
|
+
"localId": "e1",
|
|
1029
|
+
"area": "documented-area-label",
|
|
1030
|
+
"title": "Epic title",
|
|
1031
|
+
"description": "Epic description using the epic template",
|
|
1032
|
+
"acceptanceCriteria": ["criterion"],
|
|
1033
|
+
"relatedDocs": ["docdex:..."],
|
|
1034
|
+
"priorityHint": 50,
|
|
1035
|
+
"serviceIds": ["backend-api"],
|
|
1036
|
+
"tags": ["cross_service"],
|
|
1037
|
+
"stories": [
|
|
1038
|
+
{
|
|
1039
|
+
"localId": "us1",
|
|
1040
|
+
"title": "Story title",
|
|
1041
|
+
"userStory": "As a ...",
|
|
1042
|
+
"description": "Story description using the template",
|
|
1043
|
+
"acceptanceCriteria": ["criterion"],
|
|
1044
|
+
"relatedDocs": ["docdex:..."],
|
|
1045
|
+
"priorityHint": 50,
|
|
1046
|
+
"tasks": [
|
|
1047
|
+
{
|
|
1048
|
+
"localId": "t1",
|
|
1049
|
+
"title": "Task title",
|
|
1050
|
+
"type": "feature|bug|chore|spike",
|
|
1051
|
+
"description": "Task description using the template",
|
|
1052
|
+
"estimatedStoryPoints": 3,
|
|
1053
|
+
"priorityHint": 50,
|
|
1054
|
+
"dependsOnKeys": ["t0"],
|
|
1055
|
+
"relatedDocs": ["docdex:..."],
|
|
1056
|
+
"unitTests": ["unit test description"],
|
|
1057
|
+
"componentTests": ["component test description"],
|
|
1058
|
+
"integrationTests": ["integration test description"],
|
|
1059
|
+
"apiTests": ["api test description"]
|
|
1060
|
+
}
|
|
1061
|
+
]
|
|
1062
|
+
}
|
|
1063
|
+
]
|
|
1064
|
+
}
|
|
1065
|
+
]
|
|
1066
|
+
}`;
|
|
920
1067
|
export class CreateTasksService {
|
|
921
1068
|
constructor(workspace, deps) {
|
|
922
1069
|
this.workspace = workspace;
|
|
@@ -1779,11 +1926,18 @@ export class CreateTasksService {
|
|
|
1779
1926
|
buildServiceDependencyGraph(plan, docs) {
|
|
1780
1927
|
const aliases = new Map();
|
|
1781
1928
|
const dependencies = new Map();
|
|
1929
|
+
const sourceBackedServices = new Set();
|
|
1782
1930
|
const register = (value) => {
|
|
1783
1931
|
if (!value)
|
|
1784
1932
|
return undefined;
|
|
1785
1933
|
return this.addServiceAlias(aliases, value);
|
|
1786
1934
|
};
|
|
1935
|
+
const registerSourceBacked = (value) => {
|
|
1936
|
+
const canonical = register(value);
|
|
1937
|
+
if (canonical)
|
|
1938
|
+
sourceBackedServices.add(canonical);
|
|
1939
|
+
return canonical;
|
|
1940
|
+
};
|
|
1787
1941
|
const docsText = docs
|
|
1788
1942
|
.map((doc) => [doc.title, doc.path, doc.content, ...(doc.segments ?? []).map((segment) => segment.content)].filter(Boolean).join("\n"))
|
|
1789
1943
|
.join("\n");
|
|
@@ -1805,14 +1959,25 @@ export class CreateTasksService {
|
|
|
1805
1959
|
structureTokens.some((candidate) => candidate !== token && candidate.startsWith(`${token}/`))) {
|
|
1806
1960
|
continue;
|
|
1807
1961
|
}
|
|
1808
|
-
|
|
1962
|
+
registerSourceBacked(this.deriveServiceFromPathToken(token));
|
|
1963
|
+
}
|
|
1964
|
+
for (const component of this.extractRuntimeComponentNames(docs)) {
|
|
1965
|
+
registerSourceBacked(component);
|
|
1809
1966
|
}
|
|
1810
1967
|
for (const match of docsText.matchAll(SERVICE_HANDLE_PATTERN))
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1968
|
+
registerSourceBacked(match[1]);
|
|
1969
|
+
const docsHaveRuntimeTopologySignals = sourceBackedServices.size > 0 &&
|
|
1970
|
+
(structureTargets.directories.length > 0 ||
|
|
1971
|
+
structureTargets.files.length > 0 ||
|
|
1972
|
+
this.collectDependencyStatements(docsText).length > 0 ||
|
|
1973
|
+
WAVE_LABEL_PATTERN.test(docsText) ||
|
|
1974
|
+
TOPOLOGY_HEADING_PATTERN.test(docsText));
|
|
1975
|
+
if (!docsHaveRuntimeTopologySignals) {
|
|
1976
|
+
for (const match of planText.matchAll(SERVICE_HANDLE_PATTERN))
|
|
1977
|
+
register(match[1]);
|
|
1978
|
+
for (const mention of this.extractServiceMentionsFromText(planText))
|
|
1979
|
+
register(mention);
|
|
1980
|
+
}
|
|
1816
1981
|
const corpus = [docsText, planText].filter(Boolean);
|
|
1817
1982
|
for (const text of corpus) {
|
|
1818
1983
|
const statements = this.collectDependencyStatements(text);
|
|
@@ -1841,7 +2006,8 @@ export class CreateTasksService {
|
|
|
1841
2006
|
const structureTargets = this.extractStructureTargets(docs);
|
|
1842
2007
|
const structureServices = uniqueStrings([...structureTargets.directories, ...structureTargets.files]
|
|
1843
2008
|
.map((token) => this.deriveServiceFromPathToken(token))
|
|
1844
|
-
.filter((value) => Boolean(value))
|
|
2009
|
+
.filter((value) => Boolean(value))
|
|
2010
|
+
.concat(this.extractRuntimeComponentNames(docs))).slice(0, 24);
|
|
1845
2011
|
const topologyHeadings = this.extractSdsSectionCandidates(docs, 64)
|
|
1846
2012
|
.filter((heading) => TOPOLOGY_HEADING_PATTERN.test(heading))
|
|
1847
2013
|
.slice(0, 24);
|
|
@@ -1862,29 +2028,231 @@ export class CreateTasksService {
|
|
|
1862
2028
|
waveMentions,
|
|
1863
2029
|
};
|
|
1864
2030
|
}
|
|
1865
|
-
|
|
1866
|
-
const
|
|
1867
|
-
const
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
}
|
|
1880
|
-
|
|
2031
|
+
buildSourceTopologyExpectation(docs) {
|
|
2032
|
+
const signalSummary = this.summarizeTopologySignals(docs);
|
|
2033
|
+
const docsOnlyGraph = this.buildServiceDependencyGraph({ epics: [], stories: [], tasks: [] }, docs);
|
|
2034
|
+
return {
|
|
2035
|
+
runtimeBearing: docsOnlyGraph.services.length > 0 ||
|
|
2036
|
+
docsOnlyGraph.startupWaves.length > 0 ||
|
|
2037
|
+
signalSummary.dependencyPairs.length > 0,
|
|
2038
|
+
services: docsOnlyGraph.services,
|
|
2039
|
+
startupWaves: docsOnlyGraph.startupWaves.map((wave) => ({
|
|
2040
|
+
wave: wave.wave,
|
|
2041
|
+
services: [...wave.services],
|
|
2042
|
+
})),
|
|
2043
|
+
dependencyPairs: signalSummary.dependencyPairs,
|
|
2044
|
+
signalSummary,
|
|
2045
|
+
};
|
|
2046
|
+
}
|
|
2047
|
+
buildCanonicalNameInventory(docs) {
|
|
2048
|
+
const structureTargets = this.extractStructureTargets(docs);
|
|
2049
|
+
const paths = uniqueStrings([...structureTargets.directories, ...structureTargets.files]
|
|
2050
|
+
.map((token) => this.normalizeStructurePathToken(token))
|
|
2051
|
+
.filter((value) => Boolean(value))).sort((a, b) => a.length - b.length || a.localeCompare(b));
|
|
2052
|
+
const graph = this.buildServiceDependencyGraph({ epics: [], stories: [], tasks: [] }, docs);
|
|
2053
|
+
const serviceAliases = new Map();
|
|
2054
|
+
for (const [service, aliases] of graph.aliases.entries()) {
|
|
2055
|
+
serviceAliases.set(service, new Set(aliases));
|
|
2056
|
+
}
|
|
2057
|
+
for (const service of graph.services) {
|
|
2058
|
+
const existing = serviceAliases.get(service) ?? new Set();
|
|
2059
|
+
existing.add(service);
|
|
2060
|
+
serviceAliases.set(service, existing);
|
|
2061
|
+
}
|
|
2062
|
+
return {
|
|
2063
|
+
paths,
|
|
2064
|
+
pathSet: new Set(paths),
|
|
2065
|
+
services: [...graph.services],
|
|
2066
|
+
serviceAliases,
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
countCommonPrefixSegments(left, right) {
|
|
2070
|
+
let count = 0;
|
|
2071
|
+
while (count < left.length && count < right.length && left[count] === right[count]) {
|
|
2072
|
+
count += 1;
|
|
2073
|
+
}
|
|
2074
|
+
return count;
|
|
2075
|
+
}
|
|
2076
|
+
countCommonSuffixSegments(left, right, prefixFloor = 0) {
|
|
2077
|
+
let count = 0;
|
|
2078
|
+
while (count < left.length - prefixFloor &&
|
|
2079
|
+
count < right.length - prefixFloor &&
|
|
2080
|
+
left[left.length - 1 - count] === right[right.length - 1 - count]) {
|
|
2081
|
+
count += 1;
|
|
2082
|
+
}
|
|
2083
|
+
return count;
|
|
2084
|
+
}
|
|
2085
|
+
tokenizeCanonicalName(value) {
|
|
2086
|
+
return this.normalizeServiceLookupKey(value)
|
|
2087
|
+
.split(" ")
|
|
2088
|
+
.map((token) => token.trim())
|
|
2089
|
+
.filter(Boolean);
|
|
2090
|
+
}
|
|
2091
|
+
namesSemanticallyCollide(candidate, canonical) {
|
|
2092
|
+
const candidateTokens = this.tokenizeCanonicalName(candidate);
|
|
2093
|
+
const canonicalTokens = this.tokenizeCanonicalName(canonical);
|
|
2094
|
+
if (candidateTokens.length === 0 || canonicalTokens.length === 0)
|
|
2095
|
+
return false;
|
|
2096
|
+
const canonicalSet = new Set(canonicalTokens);
|
|
2097
|
+
const shared = candidateTokens.filter((token) => canonicalSet.has(token));
|
|
2098
|
+
if (shared.length === 0)
|
|
2099
|
+
return false;
|
|
2100
|
+
if (shared.length === Math.min(candidateTokens.length, canonicalTokens.length))
|
|
2101
|
+
return true;
|
|
2102
|
+
const candidateNormalized = candidateTokens.join(" ");
|
|
2103
|
+
const canonicalNormalized = canonicalTokens.join(" ");
|
|
2104
|
+
return (candidateNormalized.includes(canonicalNormalized) || canonicalNormalized.includes(candidateNormalized));
|
|
2105
|
+
}
|
|
2106
|
+
findCanonicalPathConflict(candidatePath, inventory) {
|
|
2107
|
+
if (!candidatePath || inventory.pathSet.has(candidatePath))
|
|
2108
|
+
return undefined;
|
|
2109
|
+
const candidateParts = candidatePath.split("/").filter(Boolean);
|
|
2110
|
+
let bestMatch;
|
|
2111
|
+
for (const canonicalPath of inventory.paths) {
|
|
2112
|
+
if (candidatePath === canonicalPath)
|
|
2113
|
+
continue;
|
|
2114
|
+
const canonicalParts = canonicalPath.split("/").filter(Boolean);
|
|
2115
|
+
const sharedPrefix = this.countCommonPrefixSegments(candidateParts, canonicalParts);
|
|
2116
|
+
if (sharedPrefix === 0)
|
|
2117
|
+
continue;
|
|
2118
|
+
const sharedSuffix = this.countCommonSuffixSegments(candidateParts, canonicalParts, sharedPrefix);
|
|
2119
|
+
if (sharedSuffix === 0)
|
|
2120
|
+
continue;
|
|
2121
|
+
const candidateCore = candidateParts.slice(sharedPrefix, candidateParts.length - sharedSuffix);
|
|
2122
|
+
const canonicalCore = canonicalParts.slice(sharedPrefix, canonicalParts.length - sharedSuffix);
|
|
2123
|
+
if (candidateCore.length !== 1 || canonicalCore.length !== 1)
|
|
2124
|
+
continue;
|
|
2125
|
+
const candidateSegment = candidateCore[0] ?? "";
|
|
2126
|
+
const canonicalSegment = canonicalCore[0] ?? "";
|
|
2127
|
+
if (!this.namesSemanticallyCollide(candidateSegment, canonicalSegment))
|
|
2128
|
+
continue;
|
|
2129
|
+
const candidateService = this.deriveServiceFromPathToken(candidatePath);
|
|
2130
|
+
const canonicalService = this.deriveServiceFromPathToken(canonicalPath);
|
|
2131
|
+
if (candidateService &&
|
|
2132
|
+
canonicalService &&
|
|
2133
|
+
candidateService !== canonicalService &&
|
|
2134
|
+
!this.namesSemanticallyCollide(candidateService, canonicalService)) {
|
|
2135
|
+
continue;
|
|
2136
|
+
}
|
|
2137
|
+
const score = sharedPrefix + sharedSuffix;
|
|
2138
|
+
if (!bestMatch || score > bestMatch.score || (score === bestMatch.score && canonicalPath.length > bestMatch.canonicalPath.length)) {
|
|
2139
|
+
bestMatch = { canonicalPath, canonicalService, score };
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
return bestMatch ? { canonicalPath: bestMatch.canonicalPath, canonicalService: bestMatch.canonicalService } : undefined;
|
|
2143
|
+
}
|
|
2144
|
+
collectCanonicalPlanSources(plan) {
|
|
2145
|
+
const sources = [];
|
|
2146
|
+
for (const epic of plan.epics) {
|
|
2147
|
+
sources.push({
|
|
2148
|
+
location: `epic:${epic.localId}`,
|
|
2149
|
+
text: [epic.title, epic.description, ...(epic.acceptanceCriteria ?? []), ...(epic.serviceIds ?? []), ...(epic.tags ?? [])]
|
|
2150
|
+
.filter(Boolean)
|
|
2151
|
+
.join("\n"),
|
|
2152
|
+
});
|
|
2153
|
+
}
|
|
2154
|
+
for (const story of plan.stories) {
|
|
2155
|
+
sources.push({
|
|
2156
|
+
location: `story:${story.epicLocalId}/${story.localId}`,
|
|
2157
|
+
text: [story.title, story.userStory, story.description, ...(story.acceptanceCriteria ?? [])]
|
|
2158
|
+
.filter(Boolean)
|
|
2159
|
+
.join("\n"),
|
|
2160
|
+
});
|
|
2161
|
+
}
|
|
2162
|
+
for (const task of plan.tasks) {
|
|
2163
|
+
sources.push({
|
|
2164
|
+
location: `task:${task.epicLocalId}/${task.storyLocalId}/${task.localId}`,
|
|
2165
|
+
text: [
|
|
2166
|
+
task.title,
|
|
2167
|
+
task.description,
|
|
2168
|
+
...(task.relatedDocs ?? []),
|
|
2169
|
+
...(task.unitTests ?? []),
|
|
2170
|
+
...(task.componentTests ?? []),
|
|
2171
|
+
...(task.integrationTests ?? []),
|
|
2172
|
+
...(task.apiTests ?? []),
|
|
2173
|
+
]
|
|
2174
|
+
.filter(Boolean)
|
|
2175
|
+
.join("\n"),
|
|
2176
|
+
});
|
|
2177
|
+
}
|
|
2178
|
+
return sources.filter((source) => source.text.trim().length > 0);
|
|
2179
|
+
}
|
|
2180
|
+
assertCanonicalNameConsistency(projectKey, docs, plan) {
|
|
2181
|
+
const inventory = this.buildCanonicalNameInventory(docs);
|
|
2182
|
+
if (inventory.paths.length === 0 && inventory.services.length === 0)
|
|
2183
|
+
return;
|
|
2184
|
+
const conflicts = new Map();
|
|
2185
|
+
for (const source of this.collectCanonicalPlanSources(plan)) {
|
|
2186
|
+
const candidatePaths = uniqueStrings(filterImplementationStructuredPaths(extractStructuredPaths(source.text, 256))
|
|
2187
|
+
.map((token) => this.normalizeStructurePathToken(token))
|
|
2188
|
+
.filter((value) => Boolean(value)));
|
|
2189
|
+
for (const candidatePath of candidatePaths) {
|
|
2190
|
+
if (inventory.pathSet.has(candidatePath))
|
|
2191
|
+
continue;
|
|
2192
|
+
const conflict = this.findCanonicalPathConflict(candidatePath, inventory);
|
|
2193
|
+
if (!conflict)
|
|
2194
|
+
continue;
|
|
2195
|
+
const key = `${source.location}|${candidatePath}|${conflict.canonicalPath}`;
|
|
2196
|
+
conflicts.set(key, {
|
|
2197
|
+
location: source.location,
|
|
2198
|
+
candidate: candidatePath,
|
|
2199
|
+
canonical: conflict.canonicalPath,
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
if (conflicts.size === 0)
|
|
2204
|
+
return;
|
|
2205
|
+
const summary = Array.from(conflicts.values())
|
|
2206
|
+
.slice(0, 8)
|
|
2207
|
+
.map((conflict) => `${conflict.location}: ${conflict.candidate} -> ${conflict.canonical}`)
|
|
2208
|
+
.join("; ");
|
|
2209
|
+
throw new Error(`create-tasks failed canonical name validation for project "${projectKey}". Undocumented alternate implementation paths conflict with source-backed canonical names: ${summary}`);
|
|
2210
|
+
}
|
|
2211
|
+
formatTopologySignalSummary(signalSummary) {
|
|
2212
|
+
return uniqueStrings([
|
|
2213
|
+
...signalSummary.structureServices.map((service) => `structure:${service}`),
|
|
2214
|
+
...signalSummary.topologyHeadings.map((heading) => `heading:${heading}`),
|
|
2215
|
+
...signalSummary.dependencyPairs.map((pair) => `dependency:${pair}`),
|
|
2216
|
+
...signalSummary.waveMentions.map((wave) => `wave:${wave}`),
|
|
2217
|
+
])
|
|
2218
|
+
.slice(0, 10)
|
|
2219
|
+
.join("; ");
|
|
2220
|
+
}
|
|
2221
|
+
validateTopologyExtraction(projectKey, expectation, graph) {
|
|
2222
|
+
const topologySignals = expectation.signalSummary;
|
|
2223
|
+
if (!expectation.runtimeBearing)
|
|
2224
|
+
return topologySignals;
|
|
2225
|
+
if (graph.services.length === 0) {
|
|
2226
|
+
throw new Error(`create-tasks failed internal topology extraction for project "${projectKey}". SDS includes runtime topology signals but no services were resolved. Signals: ${this.formatTopologySignalSummary(topologySignals) || "unavailable"}`);
|
|
2227
|
+
}
|
|
2228
|
+
const missingServices = expectation.services.filter((service) => !graph.services.includes(service));
|
|
2229
|
+
if (missingServices.length > 0) {
|
|
2230
|
+
throw new Error(`create-tasks failed internal topology extraction for project "${projectKey}". Final planning artifacts lost source-backed services: ${missingServices.slice(0, 8).join(", ")}.`);
|
|
2231
|
+
}
|
|
2232
|
+
if (expectation.startupWaves.length > 0 && graph.startupWaves.length === 0) {
|
|
1881
2233
|
throw new Error(`create-tasks failed internal topology extraction for project "${projectKey}". SDS includes startup wave signals but no startup waves were resolved. Signals: ${topologySignals.waveMentions.slice(0, 6).join("; ")}`);
|
|
1882
2234
|
}
|
|
2235
|
+
const graphServicesByWave = new Map(graph.startupWaves.map((wave) => [wave.wave, new Set(wave.services)]));
|
|
2236
|
+
const missingWaves = expectation.startupWaves
|
|
2237
|
+
.map((wave) => wave.wave)
|
|
2238
|
+
.filter((wave) => !graphServicesByWave.has(wave));
|
|
2239
|
+
if (missingWaves.length > 0) {
|
|
2240
|
+
throw new Error(`create-tasks failed internal topology extraction for project "${projectKey}". Final planning artifacts lost source-backed startup waves: ${missingWaves.slice(0, 8).join(", ")}.`);
|
|
2241
|
+
}
|
|
2242
|
+
const missingWaveServices = expectation.startupWaves.flatMap((wave) => {
|
|
2243
|
+
const actualServices = graphServicesByWave.get(wave.wave);
|
|
2244
|
+
return wave.services
|
|
2245
|
+
.filter((service) => !(actualServices?.has(service) ?? false))
|
|
2246
|
+
.map((service) => `wave ${wave.wave}:${service}`);
|
|
2247
|
+
});
|
|
2248
|
+
if (missingWaveServices.length > 0) {
|
|
2249
|
+
throw new Error(`create-tasks failed internal topology extraction for project "${projectKey}". Final planning artifacts lost source-backed startup wave services: ${missingWaveServices.slice(0, 8).join(", ")}.`);
|
|
2250
|
+
}
|
|
1883
2251
|
return topologySignals;
|
|
1884
2252
|
}
|
|
1885
|
-
derivePlanningArtifacts(projectKey, docs, plan) {
|
|
2253
|
+
derivePlanningArtifacts(projectKey, docs, plan, expectation = this.buildSourceTopologyExpectation(docs)) {
|
|
1886
2254
|
const discoveryGraph = this.buildServiceDependencyGraph(plan, docs);
|
|
1887
|
-
const topologySignals = this.validateTopologyExtraction(projectKey,
|
|
2255
|
+
const topologySignals = this.validateTopologyExtraction(projectKey, expectation, discoveryGraph);
|
|
1888
2256
|
const serviceCatalog = this.buildServiceCatalogArtifact(projectKey, docs, discoveryGraph);
|
|
1889
2257
|
const projectBuildMethod = this.buildProjectConstructionMethod(docs, discoveryGraph);
|
|
1890
2258
|
const projectBuildPlan = this.buildProjectPlanArtifact(projectKey, docs, discoveryGraph, projectBuildMethod);
|
|
@@ -2279,6 +2647,570 @@ export class CreateTasksService {
|
|
|
2279
2647
|
buildMethod,
|
|
2280
2648
|
};
|
|
2281
2649
|
}
|
|
2650
|
+
scoreServiceUnitForText(unit, text, graph) {
|
|
2651
|
+
const normalizedText = this.normalizeServiceLookupKey(text);
|
|
2652
|
+
if (!normalizedText)
|
|
2653
|
+
return 0;
|
|
2654
|
+
const tokens = normalizedText
|
|
2655
|
+
.split(" ")
|
|
2656
|
+
.map((token) => token.trim())
|
|
2657
|
+
.filter((token) => token.length >= 3);
|
|
2658
|
+
if (tokens.length === 0)
|
|
2659
|
+
return 0;
|
|
2660
|
+
const unitCorpus = this.normalizeServiceLookupKey([
|
|
2661
|
+
unit.serviceName,
|
|
2662
|
+
...unit.aliases,
|
|
2663
|
+
...unit.directories,
|
|
2664
|
+
...unit.files,
|
|
2665
|
+
...unit.headings,
|
|
2666
|
+
...unit.dependsOnServiceIds,
|
|
2667
|
+
].join("\n"));
|
|
2668
|
+
const overlap = tokens.filter((token) => unitCorpus.includes(token)).length;
|
|
2669
|
+
let score = overlap * 10;
|
|
2670
|
+
const direct = this.resolveServiceMentionFromPhrase(text, graph.aliases);
|
|
2671
|
+
if (direct && unit.serviceName === direct)
|
|
2672
|
+
score += 100;
|
|
2673
|
+
if (normalizedText.includes(this.normalizeServiceLookupKey(unit.serviceName)))
|
|
2674
|
+
score += 25;
|
|
2675
|
+
if (unit.isFoundational)
|
|
2676
|
+
score += 4;
|
|
2677
|
+
return score;
|
|
2678
|
+
}
|
|
2679
|
+
buildSdsServiceUnits(docs, catalog, graph) {
|
|
2680
|
+
const units = new Map();
|
|
2681
|
+
const serviceById = new Map(catalog.services.map((service) => [service.id, service]));
|
|
2682
|
+
const serviceByName = new Map(catalog.services.map((service) => [service.name, service]));
|
|
2683
|
+
const orderedServiceIds = catalog.services.map((service) => service.id);
|
|
2684
|
+
const primaryFoundationalServiceId = catalog.services.find((service) => service.isFoundational)?.id ?? catalog.services[0]?.id;
|
|
2685
|
+
const opsServiceId = catalog.services.find((service) => /\b(ops|operation|deploy|infra|runtime|platform)\b/.test(this.normalizeServiceLookupKey([service.name, ...service.aliases].join(" "))))?.id ?? primaryFoundationalServiceId;
|
|
2686
|
+
const coverageSignals = collectSdsCoverageSignalsFromDocs(docs.map((doc) => ({ content: doc.content })), { headingLimit: 200, folderLimit: 240 });
|
|
2687
|
+
const structureTargets = this.extractStructureTargets(docs);
|
|
2688
|
+
const runtimeComponents = this.extractRuntimeComponentNames(docs);
|
|
2689
|
+
const ensureUnit = (serviceId) => {
|
|
2690
|
+
if (!serviceId)
|
|
2691
|
+
return undefined;
|
|
2692
|
+
const service = serviceById.get(serviceId);
|
|
2693
|
+
if (!service)
|
|
2694
|
+
return undefined;
|
|
2695
|
+
const existing = units.get(serviceId);
|
|
2696
|
+
if (existing)
|
|
2697
|
+
return existing;
|
|
2698
|
+
const unit = {
|
|
2699
|
+
serviceId: service.id,
|
|
2700
|
+
serviceName: service.name,
|
|
2701
|
+
aliases: uniqueStrings([service.name, ...service.aliases]),
|
|
2702
|
+
startupWave: service.startupWave,
|
|
2703
|
+
dependsOnServiceIds: [...service.dependsOnServiceIds],
|
|
2704
|
+
directories: [],
|
|
2705
|
+
files: [],
|
|
2706
|
+
headings: [],
|
|
2707
|
+
isFoundational: service.isFoundational,
|
|
2708
|
+
};
|
|
2709
|
+
units.set(serviceId, unit);
|
|
2710
|
+
return unit;
|
|
2711
|
+
};
|
|
2712
|
+
for (const serviceId of orderedServiceIds)
|
|
2713
|
+
ensureUnit(serviceId);
|
|
2714
|
+
const selectUnitForText = (text, options) => {
|
|
2715
|
+
const directName = this.resolveServiceMentionFromPhrase(text, graph.aliases);
|
|
2716
|
+
if (directName) {
|
|
2717
|
+
const direct = serviceByName.get(directName);
|
|
2718
|
+
if (direct)
|
|
2719
|
+
return ensureUnit(direct.id);
|
|
2720
|
+
}
|
|
2721
|
+
const pathMatch = options?.pathLike ? this.deriveServiceFromPathToken(text) : undefined;
|
|
2722
|
+
if (pathMatch) {
|
|
2723
|
+
const fromPath = serviceByName.get(pathMatch);
|
|
2724
|
+
if (fromPath)
|
|
2725
|
+
return ensureUnit(fromPath.id);
|
|
2726
|
+
}
|
|
2727
|
+
let bestUnit;
|
|
2728
|
+
let bestScore = 0;
|
|
2729
|
+
for (const unit of units.values()) {
|
|
2730
|
+
const score = this.scoreServiceUnitForText(unit, text, graph);
|
|
2731
|
+
if (score > bestScore) {
|
|
2732
|
+
bestScore = score;
|
|
2733
|
+
bestUnit = unit;
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
if (bestUnit && bestScore > 0)
|
|
2737
|
+
return bestUnit;
|
|
2738
|
+
const normalizedText = this.normalizeServiceLookupKey(text);
|
|
2739
|
+
if (/\b(deploy|deployment|startup|rollback|recovery|observability|quality|release|failover|runbook|environment|secret|compute|operations?)\b/.test(normalizedText)) {
|
|
2740
|
+
return ensureUnit(opsServiceId);
|
|
2741
|
+
}
|
|
2742
|
+
return ensureUnit(primaryFoundationalServiceId);
|
|
2743
|
+
};
|
|
2744
|
+
for (const directory of structureTargets.directories) {
|
|
2745
|
+
const unit = selectUnitForText(directory, { pathLike: true });
|
|
2746
|
+
if (!unit)
|
|
2747
|
+
continue;
|
|
2748
|
+
unit.directories.push(directory);
|
|
2749
|
+
}
|
|
2750
|
+
for (const file of structureTargets.files) {
|
|
2751
|
+
const unit = selectUnitForText(file, { pathLike: true });
|
|
2752
|
+
if (!unit)
|
|
2753
|
+
continue;
|
|
2754
|
+
unit.files.push(file);
|
|
2755
|
+
const parent = path.dirname(file).replace(/\\/g, "/");
|
|
2756
|
+
if (parent && parent !== ".")
|
|
2757
|
+
unit.directories.push(parent);
|
|
2758
|
+
}
|
|
2759
|
+
for (const heading of coverageSignals.sectionHeadings) {
|
|
2760
|
+
const unit = selectUnitForText(heading);
|
|
2761
|
+
if (!unit)
|
|
2762
|
+
continue;
|
|
2763
|
+
unit.headings.push(normalizeHeadingCandidate(heading));
|
|
2764
|
+
}
|
|
2765
|
+
for (const component of runtimeComponents) {
|
|
2766
|
+
const unit = selectUnitForText(component);
|
|
2767
|
+
if (!unit)
|
|
2768
|
+
continue;
|
|
2769
|
+
unit.headings.push(normalizeHeadingCandidate(component));
|
|
2770
|
+
}
|
|
2771
|
+
return catalog.services
|
|
2772
|
+
.map((service) => units.get(service.id))
|
|
2773
|
+
.filter((unit) => Boolean(unit))
|
|
2774
|
+
.map((unit) => ({
|
|
2775
|
+
...unit,
|
|
2776
|
+
aliases: uniqueStrings(unit.aliases).sort((a, b) => a.localeCompare(b)),
|
|
2777
|
+
directories: uniqueStrings(unit.directories).sort((a, b) => a.length - b.length || a.localeCompare(b)),
|
|
2778
|
+
files: uniqueStrings(unit.files).sort((a, b) => a.length - b.length || a.localeCompare(b)),
|
|
2779
|
+
headings: uniqueStrings(unit.headings),
|
|
2780
|
+
}));
|
|
2781
|
+
}
|
|
2782
|
+
buildSdsDrivenPlan(projectKey, docs, catalog, graph) {
|
|
2783
|
+
const units = this.buildSdsServiceUnits(docs, catalog, graph);
|
|
2784
|
+
const verificationSuites = this.extractVerificationSuites(docs);
|
|
2785
|
+
const acceptanceScenarios = this.extractAcceptanceScenarios(docs);
|
|
2786
|
+
const localIds = new Set();
|
|
2787
|
+
const epics = [];
|
|
2788
|
+
const stories = [];
|
|
2789
|
+
const tasks = [];
|
|
2790
|
+
const rootPreview = (items, fallback) => items.length > 0 ? items.slice(0, 8).map((item) => `- ${item}`).join("\n") : `- ${fallback}`;
|
|
2791
|
+
const chunk = (items, size) => {
|
|
2792
|
+
const chunks = [];
|
|
2793
|
+
for (let index = 0; index < items.length; index += size) {
|
|
2794
|
+
chunks.push(items.slice(index, index + size));
|
|
2795
|
+
}
|
|
2796
|
+
return chunks;
|
|
2797
|
+
};
|
|
2798
|
+
const toDisplayName = (value) => value
|
|
2799
|
+
.split(/\s+/)
|
|
2800
|
+
.map((token) => (token ? token[0].toUpperCase() + token.slice(1) : token))
|
|
2801
|
+
.join(" ");
|
|
2802
|
+
const defaultArea = normalizeArea(projectKey) ?? "core";
|
|
2803
|
+
for (const unit of units) {
|
|
2804
|
+
const epicLocalId = nextUniqueLocalId(`svc-${unit.serviceId}`, localIds);
|
|
2805
|
+
const epicTitle = `Build ${toDisplayName(unit.serviceName)}`;
|
|
2806
|
+
epics.push({
|
|
2807
|
+
localId: epicLocalId,
|
|
2808
|
+
area: defaultArea,
|
|
2809
|
+
title: epicTitle,
|
|
2810
|
+
description: [
|
|
2811
|
+
`Implement the ${unit.serviceName} build slice from the SDS.`,
|
|
2812
|
+
unit.startupWave !== undefined ? `Startup wave: ${unit.startupWave}.` : undefined,
|
|
2813
|
+
unit.dependsOnServiceIds.length > 0
|
|
2814
|
+
? `Dependencies: ${unit.dependsOnServiceIds.join(", ")}.`
|
|
2815
|
+
: "Dependencies: foundational or none.",
|
|
2816
|
+
unit.headings.length > 0
|
|
2817
|
+
? `Covered SDS sections: ${unit.headings.slice(0, 5).join("; ")}.`
|
|
2818
|
+
: undefined,
|
|
2819
|
+
]
|
|
2820
|
+
.filter(Boolean)
|
|
2821
|
+
.join("\n"),
|
|
2822
|
+
acceptanceCriteria: [
|
|
2823
|
+
`${unit.serviceName} structure and implementation surfaces are represented in the backlog.`,
|
|
2824
|
+
`${unit.serviceName} sequencing stays aligned to SDS dependency and startup ordering.`,
|
|
2825
|
+
`${unit.serviceName} validation work exists for the implemented scope.`,
|
|
2826
|
+
],
|
|
2827
|
+
relatedDocs: docs
|
|
2828
|
+
.map((doc) => (doc.id ? `docdex:${doc.id}` : undefined))
|
|
2829
|
+
.filter((value) => Boolean(value))
|
|
2830
|
+
.slice(0, 12),
|
|
2831
|
+
priorityHint: epics.length + 1,
|
|
2832
|
+
serviceIds: [unit.serviceId],
|
|
2833
|
+
tags: unit.isFoundational ? ["foundational"] : [],
|
|
2834
|
+
stories: [],
|
|
2835
|
+
});
|
|
2836
|
+
if (unit.directories.length > 0 || unit.files.length > 0) {
|
|
2837
|
+
const storyLocalId = nextUniqueLocalId(`svc-${unit.serviceId}-structure`, localIds);
|
|
2838
|
+
stories.push({
|
|
2839
|
+
localId: storyLocalId,
|
|
2840
|
+
epicLocalId,
|
|
2841
|
+
title: `Establish ${toDisplayName(unit.serviceName)} structure`,
|
|
2842
|
+
userStory: `As an engineer, I need the ${unit.serviceName} scaffold in place before implementation continues.`,
|
|
2843
|
+
description: [
|
|
2844
|
+
`Create the SDS-defined folder and file scaffold for ${unit.serviceName}.`,
|
|
2845
|
+
"Use the documented target tree as the implementation baseline.",
|
|
2846
|
+
].join("\n"),
|
|
2847
|
+
acceptanceCriteria: [
|
|
2848
|
+
`Required ${unit.serviceName} directories exist.`,
|
|
2849
|
+
`Required ${unit.serviceName} file entrypoints are present or explicitly queued in dependent tasks.`,
|
|
2850
|
+
`Follow-up tasks can reference real ${unit.serviceName} implementation paths.`,
|
|
2851
|
+
],
|
|
2852
|
+
relatedDocs: [],
|
|
2853
|
+
priorityHint: 1,
|
|
2854
|
+
tasks: [],
|
|
2855
|
+
});
|
|
2856
|
+
const directoryChunks = chunk(unit.directories, 6);
|
|
2857
|
+
const fileChunks = chunk(unit.files, 5);
|
|
2858
|
+
if (directoryChunks.length > 0) {
|
|
2859
|
+
tasks.push({
|
|
2860
|
+
localId: nextUniqueLocalId(`svc-${unit.serviceId}-structure-task`, localIds),
|
|
2861
|
+
storyLocalId,
|
|
2862
|
+
epicLocalId,
|
|
2863
|
+
title: `Create ${unit.serviceName} directory scaffold`,
|
|
2864
|
+
type: "chore",
|
|
2865
|
+
description: [
|
|
2866
|
+
`Create the initial ${unit.serviceName} repository structure required by the SDS.`,
|
|
2867
|
+
"Target directories:",
|
|
2868
|
+
rootPreview(directoryChunks[0] ?? [], "Infer the concrete runtime directories from the SDS build tree."),
|
|
2869
|
+
].join("\n"),
|
|
2870
|
+
estimatedStoryPoints: 2,
|
|
2871
|
+
priorityHint: 1,
|
|
2872
|
+
dependsOnKeys: [],
|
|
2873
|
+
relatedDocs: [],
|
|
2874
|
+
unitTests: [],
|
|
2875
|
+
componentTests: [],
|
|
2876
|
+
integrationTests: [],
|
|
2877
|
+
apiTests: [],
|
|
2878
|
+
});
|
|
2879
|
+
}
|
|
2880
|
+
if (fileChunks.length > 0) {
|
|
2881
|
+
const structureFileTargets = this.selectBuildTargets(unit, [`${unit.serviceName} structure entrypoints`], "structure", 5);
|
|
2882
|
+
tasks.push({
|
|
2883
|
+
localId: nextUniqueLocalId(`svc-${unit.serviceId}-structure-task`, localIds),
|
|
2884
|
+
storyLocalId,
|
|
2885
|
+
epicLocalId,
|
|
2886
|
+
title: `Create ${unit.serviceName} runtime entrypoints`,
|
|
2887
|
+
type: "feature",
|
|
2888
|
+
description: [
|
|
2889
|
+
`Create or stub the first concrete ${unit.serviceName} implementation files from the SDS folder tree.`,
|
|
2890
|
+
"Target files:",
|
|
2891
|
+
rootPreview(structureFileTargets.length > 0 ? structureFileTargets : fileChunks[0] ?? [], "Create the primary runtime entrypoints and implementation surfaces."),
|
|
2892
|
+
].join("\n"),
|
|
2893
|
+
estimatedStoryPoints: 3,
|
|
2894
|
+
priorityHint: 2,
|
|
2895
|
+
dependsOnKeys: tasks.filter((task) => task.storyLocalId === storyLocalId).map((task) => task.localId),
|
|
2896
|
+
relatedDocs: [],
|
|
2897
|
+
unitTests: [],
|
|
2898
|
+
componentTests: [],
|
|
2899
|
+
integrationTests: [],
|
|
2900
|
+
apiTests: [],
|
|
2901
|
+
});
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
if (unit.headings.length > 0 || unit.files.length > 0) {
|
|
2905
|
+
const storyLocalId = nextUniqueLocalId(`svc-${unit.serviceId}-implementation`, localIds);
|
|
2906
|
+
stories.push({
|
|
2907
|
+
localId: storyLocalId,
|
|
2908
|
+
epicLocalId,
|
|
2909
|
+
title: `Implement ${toDisplayName(unit.serviceName)} capabilities`,
|
|
2910
|
+
userStory: `As a delivery team, we need the ${unit.serviceName} capability set implemented from the SDS.`,
|
|
2911
|
+
description: [
|
|
2912
|
+
`Implement the ${unit.serviceName} behavior defined across the SDS sections assigned to this build slice.`,
|
|
2913
|
+
unit.headings.length > 0 ? `Primary SDS sections: ${unit.headings.slice(0, 6).join("; ")}.` : undefined,
|
|
2914
|
+
]
|
|
2915
|
+
.filter(Boolean)
|
|
2916
|
+
.join("\n"),
|
|
2917
|
+
acceptanceCriteria: [
|
|
2918
|
+
`${unit.serviceName} covers its assigned SDS capability sections.`,
|
|
2919
|
+
`${unit.serviceName} implementation targets are concrete and source-backed.`,
|
|
2920
|
+
`${unit.serviceName} leaves no generic placeholder work inside this story.`,
|
|
2921
|
+
],
|
|
2922
|
+
relatedDocs: [],
|
|
2923
|
+
priorityHint: 2,
|
|
2924
|
+
tasks: [],
|
|
2925
|
+
});
|
|
2926
|
+
const headingChunks = chunk(unit.headings.length > 0 ? unit.headings : [`${unit.serviceName} runtime implementation`], 2);
|
|
2927
|
+
headingChunks.slice(0, 6).forEach((group, index) => {
|
|
2928
|
+
const targetSlice = this.selectBuildTargets(unit, group, "implementation", 3);
|
|
2929
|
+
tasks.push({
|
|
2930
|
+
localId: nextUniqueLocalId(`svc-${unit.serviceId}-implementation-task`, localIds),
|
|
2931
|
+
storyLocalId,
|
|
2932
|
+
epicLocalId,
|
|
2933
|
+
title: `Implement ${group[0] ?? unit.serviceName}`,
|
|
2934
|
+
type: "feature",
|
|
2935
|
+
description: [
|
|
2936
|
+
`Implement the ${unit.serviceName} scope required by these SDS sections: ${group.join("; ")}.`,
|
|
2937
|
+
"Primary implementation targets:",
|
|
2938
|
+
rootPreview(targetSlice, `Extend the SDS-defined ${unit.serviceName} runtime surfaces captured in this build slice.`),
|
|
2939
|
+
unit.dependsOnServiceIds.length > 0
|
|
2940
|
+
? `Keep dependency direction aligned to: ${unit.dependsOnServiceIds.join(", ")}.`
|
|
2941
|
+
: "Keep this slice buildable without introducing undocumented dependencies.",
|
|
2942
|
+
].join("\n"),
|
|
2943
|
+
estimatedStoryPoints: Math.min(8, 3 + Math.max(0, Math.max(1, targetSlice.length) - 1)),
|
|
2944
|
+
priorityHint: index + 1,
|
|
2945
|
+
dependsOnKeys: [],
|
|
2946
|
+
relatedDocs: [],
|
|
2947
|
+
unitTests: targetSlice.length > 0 ? [`Cover ${unit.serviceName} implementation behavior for ${targetSlice[0]}.`] : [],
|
|
2948
|
+
componentTests: [],
|
|
2949
|
+
integrationTests: [],
|
|
2950
|
+
apiTests: [],
|
|
2951
|
+
});
|
|
2952
|
+
});
|
|
2953
|
+
}
|
|
2954
|
+
if (unit.dependsOnServiceIds.length > 0) {
|
|
2955
|
+
const storyLocalId = nextUniqueLocalId(`svc-${unit.serviceId}-integration`, localIds);
|
|
2956
|
+
stories.push({
|
|
2957
|
+
localId: storyLocalId,
|
|
2958
|
+
epicLocalId,
|
|
2959
|
+
title: `Integrate ${toDisplayName(unit.serviceName)} dependencies`,
|
|
2960
|
+
userStory: `As an engineer, I need ${unit.serviceName} wired to its SDS-defined dependencies and interfaces.`,
|
|
2961
|
+
description: [
|
|
2962
|
+
`Implement dependency and interface wiring for ${unit.serviceName}.`,
|
|
2963
|
+
`SDS dependency chain: ${unit.dependsOnServiceIds.join(", ")}.`,
|
|
2964
|
+
].join("\n"),
|
|
2965
|
+
acceptanceCriteria: [
|
|
2966
|
+
`${unit.serviceName} dependency integration is explicit and ordered.`,
|
|
2967
|
+
`${unit.serviceName} uses only documented runtime dependencies.`,
|
|
2968
|
+
],
|
|
2969
|
+
relatedDocs: [],
|
|
2970
|
+
priorityHint: 3,
|
|
2971
|
+
tasks: [],
|
|
2972
|
+
});
|
|
2973
|
+
unit.dependsOnServiceIds.slice(0, 4).forEach((dependencyId, index) => {
|
|
2974
|
+
tasks.push({
|
|
2975
|
+
localId: nextUniqueLocalId(`svc-${unit.serviceId}-integration-task`, localIds),
|
|
2976
|
+
storyLocalId,
|
|
2977
|
+
epicLocalId,
|
|
2978
|
+
title: `Wire ${unit.serviceName} to ${dependencyId}`,
|
|
2979
|
+
type: "feature",
|
|
2980
|
+
description: [
|
|
2981
|
+
`Implement the SDS-defined dependency direction from ${unit.serviceName} to ${dependencyId}.`,
|
|
2982
|
+
"Update runtime interfaces, configuration reads, or orchestration flow as required.",
|
|
2983
|
+
].join("\n"),
|
|
2984
|
+
estimatedStoryPoints: 2,
|
|
2985
|
+
priorityHint: index + 1,
|
|
2986
|
+
dependsOnKeys: [],
|
|
2987
|
+
relatedDocs: [],
|
|
2988
|
+
unitTests: [],
|
|
2989
|
+
componentTests: [],
|
|
2990
|
+
integrationTests: [`Validate ${unit.serviceName} integration with ${dependencyId}.`],
|
|
2991
|
+
apiTests: [],
|
|
2992
|
+
});
|
|
2993
|
+
});
|
|
2994
|
+
}
|
|
2995
|
+
const verificationStoryLocalId = nextUniqueLocalId(`svc-${unit.serviceId}-verification`, localIds);
|
|
2996
|
+
stories.push({
|
|
2997
|
+
localId: verificationStoryLocalId,
|
|
2998
|
+
epicLocalId,
|
|
2999
|
+
title: `Verify ${toDisplayName(unit.serviceName)} readiness`,
|
|
3000
|
+
userStory: `As a reviewer, I need concrete verification evidence for ${unit.serviceName}.`,
|
|
3001
|
+
description: [
|
|
3002
|
+
`Add the validation work needed to prove ${unit.serviceName} against the SDS.`,
|
|
3003
|
+
unit.headings.length > 0 ? `Verification should cover: ${unit.headings.slice(0, 4).join("; ")}.` : undefined,
|
|
3004
|
+
]
|
|
3005
|
+
.filter(Boolean)
|
|
3006
|
+
.join("\n"),
|
|
3007
|
+
acceptanceCriteria: [
|
|
3008
|
+
`${unit.serviceName} has deterministic validation coverage.`,
|
|
3009
|
+
`${unit.serviceName} evidence maps back to SDS responsibilities.`,
|
|
3010
|
+
],
|
|
3011
|
+
relatedDocs: [],
|
|
3012
|
+
priorityHint: 4,
|
|
3013
|
+
tasks: [],
|
|
3014
|
+
});
|
|
3015
|
+
tasks.push({
|
|
3016
|
+
localId: nextUniqueLocalId(`svc-${unit.serviceId}-verification-task`, localIds),
|
|
3017
|
+
storyLocalId: verificationStoryLocalId,
|
|
3018
|
+
epicLocalId,
|
|
3019
|
+
title: `Add ${unit.serviceName} focused test coverage`,
|
|
3020
|
+
type: "chore",
|
|
3021
|
+
description: [
|
|
3022
|
+
`Add or update the smallest deterministic validation surface that proves ${unit.serviceName}.`,
|
|
3023
|
+
"Cover the implemented runtime paths, dependency behavior, and SDS acceptance expectations for this build slice.",
|
|
3024
|
+
"Primary verification targets:",
|
|
3025
|
+
rootPreview(this.selectBuildTargets(unit, [...unit.headings, `${unit.serviceName} verification`], "verification", 3), `Capture service-local validation against ${unit.serviceName} runtime surfaces.`),
|
|
3026
|
+
].join("\n"),
|
|
3027
|
+
estimatedStoryPoints: 2,
|
|
3028
|
+
priorityHint: 1,
|
|
3029
|
+
dependsOnKeys: [],
|
|
3030
|
+
relatedDocs: [],
|
|
3031
|
+
unitTests: [`Validate ${unit.serviceName} internal behavior.`],
|
|
3032
|
+
componentTests: unit.files.some((file) => /\b(ui|web|component|page|screen)\b/i.test(file))
|
|
3033
|
+
? [`Validate ${unit.serviceName} component-level behavior.`]
|
|
3034
|
+
: [],
|
|
3035
|
+
integrationTests: [`Validate ${unit.serviceName} end-to-end dependency behavior.`],
|
|
3036
|
+
apiTests: [],
|
|
3037
|
+
});
|
|
3038
|
+
}
|
|
3039
|
+
if (verificationSuites.length > 0 || acceptanceScenarios.length > 0) {
|
|
3040
|
+
const releaseEpicLocalId = nextUniqueLocalId("release-verification", localIds);
|
|
3041
|
+
epics.push({
|
|
3042
|
+
localId: releaseEpicLocalId,
|
|
3043
|
+
area: defaultArea,
|
|
3044
|
+
title: "Verify Release Readiness",
|
|
3045
|
+
description: [
|
|
3046
|
+
"Execute the named verification suites and acceptance scenarios documented in the SDS release gate.",
|
|
3047
|
+
verificationSuites.length > 0
|
|
3048
|
+
? `Named suites: ${verificationSuites.map((suite) => suite.name).slice(0, 8).join("; ")}.`
|
|
3049
|
+
: undefined,
|
|
3050
|
+
acceptanceScenarios.length > 0
|
|
3051
|
+
? `Acceptance scenarios: ${acceptanceScenarios.length} documented launch scenarios must be green.`
|
|
3052
|
+
: undefined,
|
|
3053
|
+
]
|
|
3054
|
+
.filter(Boolean)
|
|
3055
|
+
.join("\n"),
|
|
3056
|
+
acceptanceCriteria: [
|
|
3057
|
+
"Every named verification suite from the SDS is represented as executable backlog work.",
|
|
3058
|
+
"Every required acceptance scenario from the SDS is represented as executable backlog work.",
|
|
3059
|
+
"Release verification evidence maps back to the SDS release gate.",
|
|
3060
|
+
],
|
|
3061
|
+
relatedDocs: docs
|
|
3062
|
+
.map((doc) => (doc.id ? `docdex:${doc.id}` : undefined))
|
|
3063
|
+
.filter((value) => Boolean(value))
|
|
3064
|
+
.slice(0, 12),
|
|
3065
|
+
priorityHint: epics.length + 1,
|
|
3066
|
+
serviceIds: uniqueStrings(units.map((unit) => unit.serviceId)).slice(0, 12),
|
|
3067
|
+
tags: [CROSS_SERVICE_TAG],
|
|
3068
|
+
stories: [],
|
|
3069
|
+
});
|
|
3070
|
+
if (verificationSuites.length > 0) {
|
|
3071
|
+
const verificationStoryLocalId = nextUniqueLocalId("release-verification-suites", localIds);
|
|
3072
|
+
stories.push({
|
|
3073
|
+
localId: verificationStoryLocalId,
|
|
3074
|
+
epicLocalId: releaseEpicLocalId,
|
|
3075
|
+
title: "Execute named verification suites",
|
|
3076
|
+
userStory: "As a release reviewer, I need every named SDS verification suite to exist as executable backlog work.",
|
|
3077
|
+
description: "Create the exact named verification suites from the SDS verification matrix and map them to deterministic evidence.",
|
|
3078
|
+
acceptanceCriteria: [
|
|
3079
|
+
"All named SDS verification suites have backlog tasks with explicit scope.",
|
|
3080
|
+
"Suite tasks reference the exact SDS suite names instead of generic test placeholders.",
|
|
3081
|
+
],
|
|
3082
|
+
relatedDocs: [],
|
|
3083
|
+
priorityHint: 1,
|
|
3084
|
+
tasks: [],
|
|
3085
|
+
});
|
|
3086
|
+
verificationSuites.forEach((suite, index) => {
|
|
3087
|
+
const draft = this.buildVerificationSuiteTaskDraft(suite, "the release slice");
|
|
3088
|
+
tasks.push({
|
|
3089
|
+
localId: nextUniqueLocalId("release-verification-suite-task", localIds),
|
|
3090
|
+
storyLocalId: verificationStoryLocalId,
|
|
3091
|
+
epicLocalId: releaseEpicLocalId,
|
|
3092
|
+
title: `Execute suite: ${suite.name}`,
|
|
3093
|
+
type: "chore",
|
|
3094
|
+
description: draft.description,
|
|
3095
|
+
estimatedStoryPoints: /\b(end-to-end|acceptance|drill|integration)\b/i.test(suite.name) ? 3 : 2,
|
|
3096
|
+
priorityHint: index + 1,
|
|
3097
|
+
dependsOnKeys: [],
|
|
3098
|
+
relatedDocs: [],
|
|
3099
|
+
unitTests: draft.unitTests,
|
|
3100
|
+
componentTests: draft.componentTests,
|
|
3101
|
+
integrationTests: draft.integrationTests,
|
|
3102
|
+
apiTests: draft.apiTests,
|
|
3103
|
+
});
|
|
3104
|
+
});
|
|
3105
|
+
}
|
|
3106
|
+
if (acceptanceScenarios.length > 0) {
|
|
3107
|
+
const scenarioStoryLocalId = nextUniqueLocalId("release-acceptance-scenarios", localIds);
|
|
3108
|
+
stories.push({
|
|
3109
|
+
localId: scenarioStoryLocalId,
|
|
3110
|
+
epicLocalId: releaseEpicLocalId,
|
|
3111
|
+
title: "Run required acceptance scenarios",
|
|
3112
|
+
userStory: "As a launch approver, I need every required acceptance scenario executed before release.",
|
|
3113
|
+
description: "Create one executable backlog task per SDS acceptance scenario so launch approval is tied to concrete scenario evidence.",
|
|
3114
|
+
acceptanceCriteria: [
|
|
3115
|
+
"All numbered SDS acceptance scenarios exist as backlog tasks.",
|
|
3116
|
+
"Scenario tasks preserve the SDS launch wording closely enough for release review.",
|
|
3117
|
+
],
|
|
3118
|
+
relatedDocs: [],
|
|
3119
|
+
priorityHint: 2,
|
|
3120
|
+
tasks: [],
|
|
3121
|
+
});
|
|
3122
|
+
acceptanceScenarios.forEach((scenario) => {
|
|
3123
|
+
tasks.push({
|
|
3124
|
+
localId: nextUniqueLocalId("release-acceptance-scenario-task", localIds),
|
|
3125
|
+
storyLocalId: scenarioStoryLocalId,
|
|
3126
|
+
epicLocalId: releaseEpicLocalId,
|
|
3127
|
+
title: `Validate acceptance scenario ${scenario.index}: ${scenario.title}`,
|
|
3128
|
+
type: "chore",
|
|
3129
|
+
description: [
|
|
3130
|
+
`Execute SDS acceptance scenario ${scenario.index}.`,
|
|
3131
|
+
scenario.details,
|
|
3132
|
+
"Capture deterministic pass/fail evidence and link it to the release gate.",
|
|
3133
|
+
].join("\n"),
|
|
3134
|
+
estimatedStoryPoints: 2,
|
|
3135
|
+
priorityHint: scenario.index,
|
|
3136
|
+
dependsOnKeys: [],
|
|
3137
|
+
relatedDocs: [],
|
|
3138
|
+
unitTests: [],
|
|
3139
|
+
componentTests: [],
|
|
3140
|
+
integrationTests: [`Execute acceptance scenario ${scenario.index} end to end.`],
|
|
3141
|
+
apiTests: [],
|
|
3142
|
+
});
|
|
3143
|
+
});
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
return { epics, stories, tasks };
|
|
3147
|
+
}
|
|
3148
|
+
hasStrongSdsPlanningEvidence(docs, catalog, expectation) {
|
|
3149
|
+
const sdsDocs = docs.filter((doc) => looksLikeSdsDoc(doc));
|
|
3150
|
+
if (sdsDocs.length === 0)
|
|
3151
|
+
return false;
|
|
3152
|
+
const coverageSignals = collectSdsCoverageSignalsFromDocs(docs.map((doc) => ({ content: doc.content })), { headingLimit: 200, folderLimit: 240 });
|
|
3153
|
+
const structureTargets = this.extractStructureTargets(docs);
|
|
3154
|
+
const structureSignalCount = structureTargets.directories.length + structureTargets.files.length;
|
|
3155
|
+
const headingSignalCount = coverageSignals.sectionHeadings.length;
|
|
3156
|
+
const topologySignalCount = expectation.services.length +
|
|
3157
|
+
expectation.startupWaves.length +
|
|
3158
|
+
expectation.dependencyPairs.length +
|
|
3159
|
+
expectation.signalSummary.topologyHeadings.length +
|
|
3160
|
+
expectation.signalSummary.waveMentions.length;
|
|
3161
|
+
return (structureSignalCount >= 4 ||
|
|
3162
|
+
headingSignalCount >= 8 ||
|
|
3163
|
+
catalog.services.length >= 2 ||
|
|
3164
|
+
topologySignalCount >= 4);
|
|
3165
|
+
}
|
|
3166
|
+
taskUsesOnlyWeakImplementationTargets(task, docs) {
|
|
3167
|
+
if (!/\bimplement\b/i.test(`${task.title} ${task.description ?? ""}`))
|
|
3168
|
+
return false;
|
|
3169
|
+
const inventory = this.buildCanonicalNameInventory(docs);
|
|
3170
|
+
if (!inventory.paths.some((candidate) => this.isStrongImplementationTarget(candidate)))
|
|
3171
|
+
return false;
|
|
3172
|
+
const candidateTargets = uniqueStrings(filterImplementationStructuredPaths(extractStructuredPaths(`${task.title}\n${task.description ?? ""}`, 64))
|
|
3173
|
+
.map((token) => this.normalizeStructurePathToken(token))
|
|
3174
|
+
.filter((value) => Boolean(value)));
|
|
3175
|
+
if (candidateTargets.length === 0)
|
|
3176
|
+
return false;
|
|
3177
|
+
return candidateTargets.every((target) => !this.isStrongImplementationTarget(target));
|
|
3178
|
+
}
|
|
3179
|
+
planLooksTooWeakForSds(plan, docs, catalog, expectation) {
|
|
3180
|
+
if (!this.hasStrongSdsPlanningEvidence(docs, catalog, expectation))
|
|
3181
|
+
return false;
|
|
3182
|
+
const genericTitles = plan.tasks.filter((task) => /initial planning|draft backlog|review inputs|baseline project scaffolding|integrate core dependencies|validate baseline behavior/i.test(`${task.title} ${task.description ?? ""}`)).length;
|
|
3183
|
+
const genericImplementationTasks = plan.tasks.filter((task) => GENERIC_IMPLEMENTATION_TASK_PATTERN.test(`${task.title} ${task.description ?? ""}`)).length;
|
|
3184
|
+
const weakImplementationTargetTasks = plan.tasks.filter((task) => this.taskUsesOnlyWeakImplementationTargets(task, docs)).length;
|
|
3185
|
+
const verificationSuites = this.extractVerificationSuites(docs);
|
|
3186
|
+
const acceptanceScenarios = this.extractAcceptanceScenarios(docs);
|
|
3187
|
+
const planCorpus = this.normalizeServiceLookupKey([
|
|
3188
|
+
...plan.epics.map((epic) => `${epic.title}\n${epic.description ?? ""}`),
|
|
3189
|
+
...plan.stories.map((story) => `${story.title}\n${story.description ?? ""}\n${story.userStory ?? ""}`),
|
|
3190
|
+
...plan.tasks.map((task) => `${task.title}\n${task.description ?? ""}`),
|
|
3191
|
+
].join("\n"));
|
|
3192
|
+
const coveredVerificationSuites = verificationSuites.filter((suite) => planCorpus.includes(this.normalizeServiceLookupKey(suite.name))).length;
|
|
3193
|
+
const coveredAcceptanceScenarios = acceptanceScenarios.filter((scenario) => planCorpus.includes(`scenario ${scenario.index}`) ||
|
|
3194
|
+
planCorpus.includes(this.normalizeServiceLookupKey(scenario.title))).length;
|
|
3195
|
+
const coveredServiceIds = new Set(plan.epics.flatMap((epic) => normalizeStringArray(epic.serviceIds)));
|
|
3196
|
+
return (plan.epics.length === 0 ||
|
|
3197
|
+
plan.stories.length === 0 ||
|
|
3198
|
+
plan.tasks.length === 0 ||
|
|
3199
|
+
genericTitles >= Math.min(2, plan.tasks.length) ||
|
|
3200
|
+
genericImplementationTasks > 0 ||
|
|
3201
|
+
weakImplementationTargetTasks > 0 ||
|
|
3202
|
+
(verificationSuites.length > 0 && coveredVerificationSuites === 0) ||
|
|
3203
|
+
(acceptanceScenarios.length > 0 && coveredAcceptanceScenarios < Math.min(3, acceptanceScenarios.length)) ||
|
|
3204
|
+
(genericTitles > 0 &&
|
|
3205
|
+
catalog.services.length > 1 &&
|
|
3206
|
+
coveredServiceIds.size > 0 &&
|
|
3207
|
+
coveredServiceIds.size < Math.min(catalog.services.length, 2)));
|
|
3208
|
+
}
|
|
3209
|
+
backlogMostlyAlignedEnough(audit) {
|
|
3210
|
+
if (!audit)
|
|
3211
|
+
return true;
|
|
3212
|
+
return audit.satisfied;
|
|
3213
|
+
}
|
|
2282
3214
|
orderStoryTasksByDependencies(storyTasks, serviceRank, taskServiceByScope) {
|
|
2283
3215
|
const byLocalId = new Map(storyTasks.map((task) => [task.localId, task]));
|
|
2284
3216
|
const indegree = new Map();
|
|
@@ -2937,6 +3869,336 @@ export class CreateTasksService {
|
|
|
2937
3869
|
}
|
|
2938
3870
|
return uniqueStrings(sections).slice(0, limit);
|
|
2939
3871
|
}
|
|
3872
|
+
collectSdsSections(docs) {
|
|
3873
|
+
const sections = [];
|
|
3874
|
+
for (const doc of docs) {
|
|
3875
|
+
if (!looksLikeSdsDoc(doc))
|
|
3876
|
+
continue;
|
|
3877
|
+
const content = stripManagedSdsPreflightBlock(doc.content ?? "") ?? "";
|
|
3878
|
+
const lines = content.split(/\r?\n/);
|
|
3879
|
+
let currentHeading;
|
|
3880
|
+
let currentBody = [];
|
|
3881
|
+
let inCodeFence = false;
|
|
3882
|
+
const flush = () => {
|
|
3883
|
+
if (!currentHeading)
|
|
3884
|
+
return;
|
|
3885
|
+
sections.push({ heading: currentHeading, body: [...currentBody] });
|
|
3886
|
+
};
|
|
3887
|
+
for (const rawLine of lines) {
|
|
3888
|
+
const line = rawLine ?? "";
|
|
3889
|
+
const trimmed = line.trim();
|
|
3890
|
+
if (/^```/.test(trimmed)) {
|
|
3891
|
+
inCodeFence = !inCodeFence;
|
|
3892
|
+
currentBody.push(line);
|
|
3893
|
+
continue;
|
|
3894
|
+
}
|
|
3895
|
+
if (!inCodeFence) {
|
|
3896
|
+
const headingMatch = trimmed.match(MARKDOWN_HEADING_PATTERN);
|
|
3897
|
+
if (headingMatch) {
|
|
3898
|
+
flush();
|
|
3899
|
+
currentHeading = normalizeHeadingCandidate(headingMatch[2] ?? "");
|
|
3900
|
+
currentBody = [];
|
|
3901
|
+
continue;
|
|
3902
|
+
}
|
|
3903
|
+
}
|
|
3904
|
+
if (currentHeading)
|
|
3905
|
+
currentBody.push(line);
|
|
3906
|
+
}
|
|
3907
|
+
flush();
|
|
3908
|
+
}
|
|
3909
|
+
return sections;
|
|
3910
|
+
}
|
|
3911
|
+
normalizeRuntimeComponentCandidate(rawValue) {
|
|
3912
|
+
let candidate = rawValue.trim();
|
|
3913
|
+
if (!candidate)
|
|
3914
|
+
return undefined;
|
|
3915
|
+
const backtickMatch = candidate.match(/`([^`]+)`/);
|
|
3916
|
+
if (backtickMatch?.[1]) {
|
|
3917
|
+
candidate = backtickMatch[1];
|
|
3918
|
+
}
|
|
3919
|
+
const colonHead = candidate.split(/:\s+/, 2)[0]?.trim();
|
|
3920
|
+
if (colonHead && colonHead.split(/\s+/).length <= 5) {
|
|
3921
|
+
candidate = colonHead;
|
|
3922
|
+
}
|
|
3923
|
+
const dashHead = candidate.split(/\s+[—-]\s+/, 2)[0]?.trim();
|
|
3924
|
+
if (dashHead && dashHead.split(/\s+/).length <= 5) {
|
|
3925
|
+
candidate = dashHead;
|
|
3926
|
+
}
|
|
3927
|
+
candidate = candidate.replace(/\([^)]*\)/g, " ").replace(/[.;,]+$/, "").trim();
|
|
3928
|
+
if (!candidate)
|
|
3929
|
+
return undefined;
|
|
3930
|
+
const normalized = this.normalizeTextServiceName(candidate) ?? this.normalizeServiceName(candidate);
|
|
3931
|
+
if (!normalized)
|
|
3932
|
+
return undefined;
|
|
3933
|
+
if (normalized.split(" ").length > 4)
|
|
3934
|
+
return undefined;
|
|
3935
|
+
return normalized;
|
|
3936
|
+
}
|
|
3937
|
+
extractRuntimeComponentNames(docs) {
|
|
3938
|
+
const components = new Set();
|
|
3939
|
+
for (const section of this.collectSdsSections(docs)) {
|
|
3940
|
+
if (!RUNTIME_COMPONENTS_HEADING_PATTERN.test(section.heading))
|
|
3941
|
+
continue;
|
|
3942
|
+
let inCodeFence = false;
|
|
3943
|
+
for (const rawLine of section.body) {
|
|
3944
|
+
const trimmed = rawLine.trim();
|
|
3945
|
+
if (/^```/.test(trimmed)) {
|
|
3946
|
+
inCodeFence = !inCodeFence;
|
|
3947
|
+
continue;
|
|
3948
|
+
}
|
|
3949
|
+
if (inCodeFence)
|
|
3950
|
+
continue;
|
|
3951
|
+
const listMatch = trimmed.match(/^(?:[-*]|\d+[.)])\s+(.+)$/);
|
|
3952
|
+
if (!listMatch?.[1])
|
|
3953
|
+
continue;
|
|
3954
|
+
const candidate = this.normalizeRuntimeComponentCandidate(listMatch[1]);
|
|
3955
|
+
if (candidate)
|
|
3956
|
+
components.add(candidate);
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3959
|
+
return Array.from(components);
|
|
3960
|
+
}
|
|
3961
|
+
extractVerificationSuites(docs) {
|
|
3962
|
+
const suites = [];
|
|
3963
|
+
const seen = new Set();
|
|
3964
|
+
for (const section of this.collectSdsSections(docs)) {
|
|
3965
|
+
if (!VERIFICATION_MATRIX_HEADING_PATTERN.test(section.heading))
|
|
3966
|
+
continue;
|
|
3967
|
+
for (const rawLine of section.body) {
|
|
3968
|
+
const trimmed = rawLine.trim();
|
|
3969
|
+
if (!trimmed.startsWith("|"))
|
|
3970
|
+
continue;
|
|
3971
|
+
if (/^\|\s*-+\s*\|/i.test(trimmed))
|
|
3972
|
+
continue;
|
|
3973
|
+
const cells = trimmed
|
|
3974
|
+
.split("|")
|
|
3975
|
+
.map((cell) => cell.trim())
|
|
3976
|
+
.filter(Boolean);
|
|
3977
|
+
if (cells.length < 2)
|
|
3978
|
+
continue;
|
|
3979
|
+
if (/verification suite/i.test(cells[0] ?? ""))
|
|
3980
|
+
continue;
|
|
3981
|
+
const name = normalizeHeadingCandidate(cells[0] ?? "");
|
|
3982
|
+
if (!name)
|
|
3983
|
+
continue;
|
|
3984
|
+
const key = this.normalizeServiceLookupKey(name);
|
|
3985
|
+
if (!key || seen.has(key))
|
|
3986
|
+
continue;
|
|
3987
|
+
seen.add(key);
|
|
3988
|
+
suites.push({
|
|
3989
|
+
name,
|
|
3990
|
+
scope: cells[1] || undefined,
|
|
3991
|
+
sourceCoverage: cells[2] || undefined,
|
|
3992
|
+
});
|
|
3993
|
+
}
|
|
3994
|
+
}
|
|
3995
|
+
return suites;
|
|
3996
|
+
}
|
|
3997
|
+
extractAcceptanceScenarios(docs) {
|
|
3998
|
+
const scenarios = [];
|
|
3999
|
+
const seen = new Set();
|
|
4000
|
+
for (const section of this.collectSdsSections(docs)) {
|
|
4001
|
+
if (!ACCEPTANCE_SCENARIOS_HEADING_PATTERN.test(section.heading))
|
|
4002
|
+
continue;
|
|
4003
|
+
for (const rawLine of section.body) {
|
|
4004
|
+
const trimmed = rawLine.trim();
|
|
4005
|
+
const match = trimmed.match(/^(\d+)\.\s+(.+)$/);
|
|
4006
|
+
if (!match?.[1] || !match[2])
|
|
4007
|
+
continue;
|
|
4008
|
+
const index = Number.parseInt(match[1], 10);
|
|
4009
|
+
if (!Number.isFinite(index) || seen.has(index))
|
|
4010
|
+
continue;
|
|
4011
|
+
const details = match[2].trim();
|
|
4012
|
+
const title = normalizeHeadingCandidate(details.split(/:\s+/, 2)[0] ?? details) || `Scenario ${index}`;
|
|
4013
|
+
scenarios.push({ index, title, details });
|
|
4014
|
+
seen.add(index);
|
|
4015
|
+
}
|
|
4016
|
+
}
|
|
4017
|
+
return scenarios.sort((left, right) => left.index - right.index);
|
|
4018
|
+
}
|
|
4019
|
+
classifyBuildTarget(target) {
|
|
4020
|
+
const normalized = this.normalizeStructurePathToken(target) ?? target.replace(/\\/g, "/").trim();
|
|
4021
|
+
const segments = normalized
|
|
4022
|
+
.toLowerCase()
|
|
4023
|
+
.split("/")
|
|
4024
|
+
.map((segment) => segment.trim())
|
|
4025
|
+
.filter(Boolean);
|
|
4026
|
+
const basename = segments[segments.length - 1] ?? normalized.toLowerCase();
|
|
4027
|
+
const isFile = isStructuredFilePath(basename);
|
|
4028
|
+
const isServiceArtifact = SERVICE_ARTIFACT_BASENAME_PATTERN.test(basename);
|
|
4029
|
+
if (segments.some((segment) => BUILD_TARGET_DOC_SEGMENTS.has(segment))) {
|
|
4030
|
+
return { normalized, basename, segments, isFile, kind: "doc", isServiceArtifact };
|
|
4031
|
+
}
|
|
4032
|
+
if (MANIFEST_TARGET_BASENAME_PATTERN.test(basename) || isServiceArtifact) {
|
|
4033
|
+
return { normalized, basename, segments, isFile, kind: "manifest", isServiceArtifact };
|
|
4034
|
+
}
|
|
4035
|
+
if (segments.some((segment) => BUILD_TARGET_TEST_SEGMENTS.has(segment))) {
|
|
4036
|
+
return { normalized, basename, segments, isFile, kind: "test", isServiceArtifact };
|
|
4037
|
+
}
|
|
4038
|
+
if (segments.some((segment) => BUILD_TARGET_OPS_SEGMENTS.has(segment))) {
|
|
4039
|
+
return { normalized, basename, segments, isFile, kind: "ops", isServiceArtifact };
|
|
4040
|
+
}
|
|
4041
|
+
if (segments.some((segment) => BUILD_TARGET_INTERFACE_SEGMENTS.has(segment))) {
|
|
4042
|
+
return { normalized, basename, segments, isFile, kind: "interface", isServiceArtifact };
|
|
4043
|
+
}
|
|
4044
|
+
if (segments.some((segment) => BUILD_TARGET_DATA_SEGMENTS.has(segment))) {
|
|
4045
|
+
return { normalized, basename, segments, isFile, kind: "data", isServiceArtifact };
|
|
4046
|
+
}
|
|
4047
|
+
if (segments.some((segment) => BUILD_TARGET_RUNTIME_SEGMENTS.has(segment))) {
|
|
4048
|
+
return { normalized, basename, segments, isFile, kind: "runtime", isServiceArtifact };
|
|
4049
|
+
}
|
|
4050
|
+
return { normalized, basename, segments, isFile, kind: "unknown", isServiceArtifact };
|
|
4051
|
+
}
|
|
4052
|
+
isStrongImplementationTarget(target) {
|
|
4053
|
+
const classification = this.classifyBuildTarget(target);
|
|
4054
|
+
return (classification.kind === "runtime" ||
|
|
4055
|
+
classification.kind === "interface" ||
|
|
4056
|
+
classification.kind === "data" ||
|
|
4057
|
+
classification.kind === "test" ||
|
|
4058
|
+
classification.kind === "ops");
|
|
4059
|
+
}
|
|
4060
|
+
deriveBuildFocusProfile(texts) {
|
|
4061
|
+
const corpus = this.normalizeServiceLookupKey(texts.join("\n"));
|
|
4062
|
+
return {
|
|
4063
|
+
wantsOps: /\b(deploy|deployment|startup|release|rollback|recovery|rotation|drill|runbook|failover|proxy|operations?|runtime)\b/.test(corpus),
|
|
4064
|
+
wantsVerification: /\b(verify|verification|acceptance|scenario|quality|suite|test|tests|matrix|gate|drill)\b/.test(corpus),
|
|
4065
|
+
wantsData: /\b(data|storage|cache|ledger|pipeline|db|database|persistence)\b/.test(corpus),
|
|
4066
|
+
wantsInterface: /\b(contract|policy|provider|gateway|rpc|api|interface|schema|oracle|protocol)\b/.test(corpus),
|
|
4067
|
+
};
|
|
4068
|
+
}
|
|
4069
|
+
selectBuildTargets(unit, focusTexts, purpose, limit) {
|
|
4070
|
+
const candidates = uniqueStrings([...unit.files, ...unit.directories])
|
|
4071
|
+
.map((candidate) => this.normalizeStructurePathToken(candidate) ?? candidate.replace(/\\/g, "/").trim())
|
|
4072
|
+
.filter(Boolean);
|
|
4073
|
+
if (candidates.length === 0)
|
|
4074
|
+
return [];
|
|
4075
|
+
const focusCorpus = this.normalizeServiceLookupKey([unit.serviceName, ...unit.aliases, ...focusTexts].filter(Boolean).join("\n"));
|
|
4076
|
+
const focusTokens = focusCorpus
|
|
4077
|
+
.split(" ")
|
|
4078
|
+
.map((token) => token.trim())
|
|
4079
|
+
.filter((token) => token.length >= 3);
|
|
4080
|
+
const focusProfile = this.deriveBuildFocusProfile(focusTexts);
|
|
4081
|
+
const scored = candidates
|
|
4082
|
+
.map((target) => {
|
|
4083
|
+
const classification = this.classifyBuildTarget(target);
|
|
4084
|
+
const normalizedTarget = this.normalizeServiceLookupKey(target.replace(/\//g, " "));
|
|
4085
|
+
const overlap = focusTokens.filter((token) => normalizedTarget.includes(token)).length;
|
|
4086
|
+
let score = overlap * 25 + (classification.isFile ? 12 : 0);
|
|
4087
|
+
if (purpose === "structure") {
|
|
4088
|
+
if (classification.kind === "runtime" || classification.kind === "interface")
|
|
4089
|
+
score += 90;
|
|
4090
|
+
else if (classification.kind === "data")
|
|
4091
|
+
score += 75;
|
|
4092
|
+
else if (classification.kind === "ops")
|
|
4093
|
+
score += 30;
|
|
4094
|
+
else if (classification.kind === "manifest")
|
|
4095
|
+
score += 10;
|
|
4096
|
+
else if (classification.kind === "doc")
|
|
4097
|
+
score -= 80;
|
|
4098
|
+
}
|
|
4099
|
+
else if (purpose === "implementation") {
|
|
4100
|
+
if (classification.kind === "runtime")
|
|
4101
|
+
score += classification.isFile ? 170 : 140;
|
|
4102
|
+
else if (classification.kind === "interface")
|
|
4103
|
+
score += classification.isFile ? 160 : 135;
|
|
4104
|
+
else if (classification.kind === "data")
|
|
4105
|
+
score += classification.isFile ? 150 : 125;
|
|
4106
|
+
else if (classification.kind === "test")
|
|
4107
|
+
score += 70;
|
|
4108
|
+
else if (classification.kind === "ops")
|
|
4109
|
+
score += focusProfile.wantsOps ? 140 : 25;
|
|
4110
|
+
else if (classification.kind === "manifest")
|
|
4111
|
+
score -= 140;
|
|
4112
|
+
else if (classification.kind === "doc")
|
|
4113
|
+
score -= 180;
|
|
4114
|
+
}
|
|
4115
|
+
else {
|
|
4116
|
+
if (classification.kind === "test")
|
|
4117
|
+
score += 170;
|
|
4118
|
+
else if (classification.kind === "runtime")
|
|
4119
|
+
score += 120;
|
|
4120
|
+
else if (classification.kind === "interface")
|
|
4121
|
+
score += 105;
|
|
4122
|
+
else if (classification.kind === "data")
|
|
4123
|
+
score += 95;
|
|
4124
|
+
else if (classification.kind === "ops")
|
|
4125
|
+
score += focusProfile.wantsOps ? 160 : 90;
|
|
4126
|
+
else if (classification.kind === "manifest")
|
|
4127
|
+
score -= 120;
|
|
4128
|
+
else if (classification.kind === "doc")
|
|
4129
|
+
score -= 180;
|
|
4130
|
+
}
|
|
4131
|
+
if (focusProfile.wantsOps && classification.kind === "ops")
|
|
4132
|
+
score += 60;
|
|
4133
|
+
if (focusProfile.wantsVerification && classification.kind === "test")
|
|
4134
|
+
score += 60;
|
|
4135
|
+
if (focusProfile.wantsData && classification.kind === "data")
|
|
4136
|
+
score += 55;
|
|
4137
|
+
if (focusProfile.wantsInterface && classification.kind === "interface")
|
|
4138
|
+
score += 55;
|
|
4139
|
+
if (focusProfile.wantsInterface && classification.kind === "runtime")
|
|
4140
|
+
score += 20;
|
|
4141
|
+
return { target, classification, score };
|
|
4142
|
+
})
|
|
4143
|
+
.sort((left, right) => right.score - left.score || left.target.length - right.target.length || left.target.localeCompare(right.target));
|
|
4144
|
+
const strongExists = scored.some((entry) => entry.score > 0 &&
|
|
4145
|
+
(entry.classification.kind === "runtime" ||
|
|
4146
|
+
entry.classification.kind === "interface" ||
|
|
4147
|
+
entry.classification.kind === "data" ||
|
|
4148
|
+
entry.classification.kind === "test" ||
|
|
4149
|
+
entry.classification.kind === "ops"));
|
|
4150
|
+
const filtered = scored.filter((entry) => {
|
|
4151
|
+
if (entry.score <= 0)
|
|
4152
|
+
return false;
|
|
4153
|
+
if (!strongExists)
|
|
4154
|
+
return true;
|
|
4155
|
+
if (purpose === "structure")
|
|
4156
|
+
return entry.classification.kind !== "doc";
|
|
4157
|
+
if (entry.classification.kind === "manifest")
|
|
4158
|
+
return false;
|
|
4159
|
+
return entry.classification.kind !== "doc";
|
|
4160
|
+
});
|
|
4161
|
+
const ranked = (filtered.length > 0 ? filtered : scored.filter((entry) => entry.score > 0)).map((entry) => entry.target);
|
|
4162
|
+
return uniqueStrings(ranked).slice(0, Math.max(1, limit));
|
|
4163
|
+
}
|
|
4164
|
+
buildVerificationSuiteTaskDraft(suite, serviceName) {
|
|
4165
|
+
const normalized = this.normalizeServiceLookupKey([suite.name, suite.scope, suite.sourceCoverage].filter(Boolean).join(" "));
|
|
4166
|
+
const unitTests = [];
|
|
4167
|
+
const componentTests = [];
|
|
4168
|
+
const integrationTests = [];
|
|
4169
|
+
const apiTests = [];
|
|
4170
|
+
if (/\bunit\b/.test(normalized)) {
|
|
4171
|
+
unitTests.push(`Execute the named suite "${suite.name}" for ${serviceName}.`);
|
|
4172
|
+
}
|
|
4173
|
+
if (/\b(component|ui|render|client)\b/.test(normalized)) {
|
|
4174
|
+
componentTests.push(`Execute the named suite "${suite.name}" against the ${serviceName} surface.`);
|
|
4175
|
+
}
|
|
4176
|
+
if (/\b(integration|acceptance|end to end|end-to-end|drill|replay|failover)\b/.test(normalized)) {
|
|
4177
|
+
integrationTests.push(`Execute the named suite "${suite.name}" end to end for ${serviceName}.`);
|
|
4178
|
+
}
|
|
4179
|
+
if (/\b(api|gateway|rpc|provider)\b/.test(normalized)) {
|
|
4180
|
+
apiTests.push(`Execute the named suite "${suite.name}" against the ${serviceName} API/provider surface.`);
|
|
4181
|
+
}
|
|
4182
|
+
if (unitTests.length === 0 &&
|
|
4183
|
+
componentTests.length === 0 &&
|
|
4184
|
+
integrationTests.length === 0 &&
|
|
4185
|
+
apiTests.length === 0) {
|
|
4186
|
+
integrationTests.push(`Execute the named suite "${suite.name}" and capture deterministic evidence.`);
|
|
4187
|
+
}
|
|
4188
|
+
return {
|
|
4189
|
+
description: [
|
|
4190
|
+
`Implement and wire the named verification suite "${suite.name}" for ${serviceName}.`,
|
|
4191
|
+
suite.scope ? `Scope: ${suite.scope}` : undefined,
|
|
4192
|
+
suite.sourceCoverage ? `Source coverage: ${suite.sourceCoverage}` : undefined,
|
|
4193
|
+
]
|
|
4194
|
+
.filter(Boolean)
|
|
4195
|
+
.join("\n"),
|
|
4196
|
+
unitTests,
|
|
4197
|
+
componentTests,
|
|
4198
|
+
integrationTests,
|
|
4199
|
+
apiTests,
|
|
4200
|
+
};
|
|
4201
|
+
}
|
|
2940
4202
|
buildSdsCoverageHints(docs) {
|
|
2941
4203
|
const hints = this.extractSdsSectionCandidates(docs, SDS_COVERAGE_HINT_HEADING_LIMIT);
|
|
2942
4204
|
if (hints.length === 0)
|
|
@@ -3007,6 +4269,212 @@ export class CreateTasksService {
|
|
|
3007
4269
|
}
|
|
3008
4270
|
return { docSummary: blocks.join("\n\n") || "(no docs)", warnings };
|
|
3009
4271
|
}
|
|
4272
|
+
buildCreateTasksAgentMission(projectKey) {
|
|
4273
|
+
return [
|
|
4274
|
+
`You are the orchestration agent for mcoda create-tasks on project ${projectKey}.`,
|
|
4275
|
+
"Your job in this run is to turn the SDS and supporting docs into an executable backlog that is enough to build the documented product.",
|
|
4276
|
+
"You must understand the services/tools that need to exist, define implementation epics, define the user stories needed to finish each epic, and define the concrete tasks needed to finish each story.",
|
|
4277
|
+
"Keep every phase aligned to the SDS folder tree, runtime topology, dependency order, startup waves, verification suites, acceptance scenarios, and named implementation targets.",
|
|
4278
|
+
"Use only canonical documented names for services, modules, interfaces, commands, schemas, files, and runtime artifacts.",
|
|
4279
|
+
"Do not invent stack choices, rename documented targets, emit placeholder work, or defer SDS gaps to a later manual pass.",
|
|
4280
|
+
"If coverage gaps remain, refine the backlog itself until the backlog is enough to build the product.",
|
|
4281
|
+
].join("\n");
|
|
4282
|
+
}
|
|
4283
|
+
schemaSnippetForAction(action) {
|
|
4284
|
+
switch (action) {
|
|
4285
|
+
case "epics":
|
|
4286
|
+
return EPIC_SCHEMA_SNIPPET;
|
|
4287
|
+
case "stories":
|
|
4288
|
+
return STORY_SCHEMA_SNIPPET;
|
|
4289
|
+
case "tasks":
|
|
4290
|
+
return TASK_SCHEMA_SNIPPET;
|
|
4291
|
+
case "full_plan":
|
|
4292
|
+
return FULL_PLAN_SCHEMA_SNIPPET;
|
|
4293
|
+
default:
|
|
4294
|
+
return FULL_PLAN_SCHEMA_SNIPPET;
|
|
4295
|
+
}
|
|
4296
|
+
}
|
|
4297
|
+
buildPlanOutline(plan, options) {
|
|
4298
|
+
const maxEpics = options?.maxEpics ?? 16;
|
|
4299
|
+
const maxStoriesPerEpic = options?.maxStoriesPerEpic ?? 8;
|
|
4300
|
+
const maxTasksPerStory = options?.maxTasksPerStory ?? 10;
|
|
4301
|
+
const summary = {
|
|
4302
|
+
epics: plan.epics.slice(0, maxEpics).map((epic) => ({
|
|
4303
|
+
localId: epic.localId,
|
|
4304
|
+
title: epic.title,
|
|
4305
|
+
area: epic.area,
|
|
4306
|
+
serviceIds: normalizeStringArray(epic.serviceIds),
|
|
4307
|
+
tags: normalizeStringArray(epic.tags),
|
|
4308
|
+
acceptanceCriteria: (epic.acceptanceCriteria ?? []).slice(0, 8),
|
|
4309
|
+
stories: plan.stories
|
|
4310
|
+
.filter((story) => story.epicLocalId === epic.localId)
|
|
4311
|
+
.slice(0, maxStoriesPerEpic)
|
|
4312
|
+
.map((story) => ({
|
|
4313
|
+
localId: story.localId,
|
|
4314
|
+
title: story.title,
|
|
4315
|
+
userStory: story.userStory,
|
|
4316
|
+
acceptanceCriteria: (story.acceptanceCriteria ?? []).slice(0, 8),
|
|
4317
|
+
tasks: plan.tasks
|
|
4318
|
+
.filter((task) => task.epicLocalId === epic.localId && task.storyLocalId === story.localId)
|
|
4319
|
+
.slice(0, maxTasksPerStory)
|
|
4320
|
+
.map((task) => ({
|
|
4321
|
+
localId: task.localId,
|
|
4322
|
+
title: task.title,
|
|
4323
|
+
type: task.type,
|
|
4324
|
+
dependsOnKeys: normalizeStringArray(task.dependsOnKeys),
|
|
4325
|
+
description: task.description,
|
|
4326
|
+
unitTests: normalizeStringArray(task.unitTests),
|
|
4327
|
+
componentTests: normalizeStringArray(task.componentTests),
|
|
4328
|
+
integrationTests: normalizeStringArray(task.integrationTests),
|
|
4329
|
+
apiTests: normalizeStringArray(task.apiTests),
|
|
4330
|
+
})),
|
|
4331
|
+
})),
|
|
4332
|
+
})),
|
|
4333
|
+
};
|
|
4334
|
+
return JSON.stringify(summary, null, 2);
|
|
4335
|
+
}
|
|
4336
|
+
parseFullPlan(output, options) {
|
|
4337
|
+
const parsed = extractJson(output);
|
|
4338
|
+
if (!parsed || !Array.isArray(parsed.epics) || parsed.epics.length === 0) {
|
|
4339
|
+
throw new Error("Agent did not return a full backlog plan in expected format");
|
|
4340
|
+
}
|
|
4341
|
+
return this.materializePlanFromSeed(parsed, options);
|
|
4342
|
+
}
|
|
4343
|
+
async normalizeGeneratedPlan(params) {
|
|
4344
|
+
const normalizedPlanEpics = this.alignEpicsToServiceCatalog(params.plan.epics, params.serviceCatalog, params.unknownEpicServicePolicy);
|
|
4345
|
+
for (const warning of normalizedPlanEpics.warnings) {
|
|
4346
|
+
await this.jobService.appendLog(params.jobId, `[create-tasks] ${warning}\n`);
|
|
4347
|
+
}
|
|
4348
|
+
let plan = {
|
|
4349
|
+
...params.plan,
|
|
4350
|
+
epics: normalizedPlanEpics.epics.map((epic, index) => ({
|
|
4351
|
+
...epic,
|
|
4352
|
+
localId: epic.localId ?? `e${index + 1}`,
|
|
4353
|
+
stories: [],
|
|
4354
|
+
})),
|
|
4355
|
+
};
|
|
4356
|
+
plan = this.enforceStoryScopedDependencies(plan);
|
|
4357
|
+
plan = this.injectStructureBootstrapPlan(plan, params.docs, params.serviceCatalog.projectKey);
|
|
4358
|
+
plan = this.enforceStoryScopedDependencies(plan);
|
|
4359
|
+
this.validatePlanLocalIdentifiers(plan);
|
|
4360
|
+
plan = this.applyServiceDependencySequencing(plan, params.docs);
|
|
4361
|
+
plan = this.enforceStoryScopedDependencies(plan);
|
|
4362
|
+
this.validatePlanLocalIdentifiers(plan);
|
|
4363
|
+
this.derivePlanningArtifacts(params.serviceCatalog.projectKey, params.docs, plan, params.sourceTopologyExpectation);
|
|
4364
|
+
return plan;
|
|
4365
|
+
}
|
|
4366
|
+
buildRefinementPrompt(params) {
|
|
4367
|
+
const serviceCatalogSummary = this.buildServiceCatalogPromptSummary(params.serviceCatalog);
|
|
4368
|
+
const planOutline = this.buildPlanOutline(params.currentPlan, params.options);
|
|
4369
|
+
const refinementLimits = [
|
|
4370
|
+
params.options.maxEpics ? `- Limit epics to ${params.options.maxEpics}.` : "",
|
|
4371
|
+
params.options.maxStoriesPerEpic ? `- Limit stories per epic to ${params.options.maxStoriesPerEpic}.` : "",
|
|
4372
|
+
params.options.maxTasksPerStory ? `- Limit tasks per story to ${params.options.maxTasksPerStory}.` : "",
|
|
4373
|
+
]
|
|
4374
|
+
.filter(Boolean)
|
|
4375
|
+
.join("\n");
|
|
4376
|
+
const plannedGapBundles = params.audit.plannedGapBundles.length > 0
|
|
4377
|
+
? JSON.stringify(params.audit.plannedGapBundles.slice(0, 48), null, 2)
|
|
4378
|
+
: "[]";
|
|
4379
|
+
return [
|
|
4380
|
+
this.buildCreateTasksAgentMission(params.projectKey),
|
|
4381
|
+
`Refinement iteration ${params.iteration}. The current backlog did not yet satisfy the SDS sufficiency audit.`,
|
|
4382
|
+
"Return a complete replacement backlog as valid JSON only matching:",
|
|
4383
|
+
FULL_PLAN_SCHEMA_SNIPPET,
|
|
4384
|
+
"Refinement rules:",
|
|
4385
|
+
"- Return the full revised backlog, not a delta.",
|
|
4386
|
+
"- Preserve already-good backlog slices unless a stricter SDS-aligned replacement is required.",
|
|
4387
|
+
"- Every epic must map to one or more serviceIds from the phase-0 service catalog.",
|
|
4388
|
+
"- Every story must contain concrete tasks, and every task must stay scoped to its own story.",
|
|
4389
|
+
"- Every task must be implementation-concrete, name real targets when the SDS exposes them, and include unit/component/integration/api test arrays ([] when not applicable).",
|
|
4390
|
+
"- Fix the specific missing SDS coverage items listed below. Do not claim coverage unless the backlog contains executable work for them.",
|
|
4391
|
+
"- Maintain dependency-first sequencing from foundational/runtime prerequisites through verification and acceptance evidence.",
|
|
4392
|
+
refinementLimits || "- Use reasonable scope without over-generating backlog items.",
|
|
4393
|
+
"Why this revision is required:",
|
|
4394
|
+
formatBullets(params.reasons, "SDS coverage gaps remain."),
|
|
4395
|
+
"Current backlog outline:",
|
|
4396
|
+
planOutline,
|
|
4397
|
+
"Remaining section headings:",
|
|
4398
|
+
formatBullets(params.audit.remainingSectionHeadings, "none"),
|
|
4399
|
+
"Remaining folder entries:",
|
|
4400
|
+
formatBullets(params.audit.remainingFolderEntries, "none"),
|
|
4401
|
+
"Actionable gap bundles (anchor + concrete implementation targets):",
|
|
4402
|
+
plannedGapBundles,
|
|
4403
|
+
"Project construction method:",
|
|
4404
|
+
params.projectBuildMethod,
|
|
4405
|
+
"Phase 0 service catalog (allowed serviceIds):",
|
|
4406
|
+
serviceCatalogSummary,
|
|
4407
|
+
"Docs available:",
|
|
4408
|
+
params.docSummary || "- (no docs provided; propose sensible refinements).",
|
|
4409
|
+
].join("\n\n");
|
|
4410
|
+
}
|
|
4411
|
+
async refinePlanWithAgent(params) {
|
|
4412
|
+
const prompt = this.buildRefinementPrompt({
|
|
4413
|
+
projectKey: params.projectKey,
|
|
4414
|
+
currentPlan: params.currentPlan,
|
|
4415
|
+
audit: params.audit,
|
|
4416
|
+
reasons: params.reasons,
|
|
4417
|
+
docSummary: params.docSummary,
|
|
4418
|
+
projectBuildMethod: params.projectBuildMethod,
|
|
4419
|
+
serviceCatalog: params.serviceCatalog,
|
|
4420
|
+
options: params.options,
|
|
4421
|
+
iteration: params.iteration,
|
|
4422
|
+
});
|
|
4423
|
+
const { output } = await this.invokeAgentWithRetry(params.agent, prompt, "full_plan", params.agentStream, params.jobId, params.commandRunId, {
|
|
4424
|
+
refinementIteration: params.iteration,
|
|
4425
|
+
remainingGapCount: params.audit.remainingGaps.total,
|
|
4426
|
+
remainingSectionCount: params.audit.remainingSectionHeadings.length,
|
|
4427
|
+
remainingFolderCount: params.audit.remainingFolderEntries.length,
|
|
4428
|
+
});
|
|
4429
|
+
const refinedPlan = this.parseFullPlan(output, params.options);
|
|
4430
|
+
return this.normalizeGeneratedPlan({
|
|
4431
|
+
plan: refinedPlan,
|
|
4432
|
+
docs: params.docs,
|
|
4433
|
+
serviceCatalog: params.serviceCatalog,
|
|
4434
|
+
sourceTopologyExpectation: params.sourceTopologyExpectation,
|
|
4435
|
+
unknownEpicServicePolicy: params.unknownEpicServicePolicy,
|
|
4436
|
+
jobId: params.jobId,
|
|
4437
|
+
});
|
|
4438
|
+
}
|
|
4439
|
+
async runTaskSufficiencyAudit(params) {
|
|
4440
|
+
if (!this.taskSufficiencyFactory) {
|
|
4441
|
+
return { warnings: [] };
|
|
4442
|
+
}
|
|
4443
|
+
let audit;
|
|
4444
|
+
let error;
|
|
4445
|
+
let closeError;
|
|
4446
|
+
try {
|
|
4447
|
+
const sufficiencyService = await this.taskSufficiencyFactory(this.workspace);
|
|
4448
|
+
try {
|
|
4449
|
+
audit = await sufficiencyService.runAudit({
|
|
4450
|
+
workspace: params.workspace,
|
|
4451
|
+
projectKey: params.projectKey,
|
|
4452
|
+
sourceCommand: params.sourceCommand,
|
|
4453
|
+
dryRun: params.dryRun,
|
|
4454
|
+
});
|
|
4455
|
+
}
|
|
4456
|
+
finally {
|
|
4457
|
+
try {
|
|
4458
|
+
await sufficiencyService.close();
|
|
4459
|
+
}
|
|
4460
|
+
catch (caught) {
|
|
4461
|
+
closeError = caught?.message ?? String(caught);
|
|
4462
|
+
await this.jobService.appendLog(params.jobId, `Task sufficiency audit close warning: ${closeError}\n`);
|
|
4463
|
+
}
|
|
4464
|
+
}
|
|
4465
|
+
}
|
|
4466
|
+
catch (caught) {
|
|
4467
|
+
error = caught?.message ?? String(caught);
|
|
4468
|
+
}
|
|
4469
|
+
return {
|
|
4470
|
+
audit,
|
|
4471
|
+
error,
|
|
4472
|
+
warnings: uniqueStrings([
|
|
4473
|
+
...(audit?.warnings ?? []),
|
|
4474
|
+
...(closeError ? [`Task sufficiency audit close warning: ${closeError}`] : []),
|
|
4475
|
+
]),
|
|
4476
|
+
};
|
|
4477
|
+
}
|
|
3010
4478
|
buildPrompt(projectKey, docSummary, projectBuildMethod, serviceCatalog, options) {
|
|
3011
4479
|
const serviceCatalogSummary = this.buildServiceCatalogPromptSummary(serviceCatalog);
|
|
3012
4480
|
const limits = [
|
|
@@ -3017,12 +4485,14 @@ export class CreateTasksService {
|
|
|
3017
4485
|
.filter(Boolean)
|
|
3018
4486
|
.join(" ");
|
|
3019
4487
|
const prompt = [
|
|
3020
|
-
|
|
3021
|
-
|
|
4488
|
+
this.buildCreateTasksAgentMission(projectKey),
|
|
4489
|
+
`You are assisting in phase 1 of 3 for project ${projectKey}: understand the documented services/tools and generate epics only.`,
|
|
4490
|
+
"Process is strict and direct: understand services/tools -> epics -> stories -> tasks -> sufficiency refinement.",
|
|
3022
4491
|
"This step outputs only epics derived from the build plan and docs.",
|
|
3023
4492
|
"Return strictly valid JSON (no prose) matching:",
|
|
3024
4493
|
EPIC_SCHEMA_SNIPPET,
|
|
3025
4494
|
"Rules:",
|
|
4495
|
+
"- First reason through which documented services/tools must be created or changed, then express that understanding through executable implementation epics.",
|
|
3026
4496
|
"- Do NOT include final slugs; the system will assign keys.",
|
|
3027
4497
|
"- Use docdex handles when referencing docs.",
|
|
3028
4498
|
"- acceptanceCriteria must be an array of strings (5-10 items).",
|
|
@@ -3238,7 +4708,7 @@ export class CreateTasksService {
|
|
|
3238
4708
|
const attempt = 2;
|
|
3239
4709
|
const fixPrompt = [
|
|
3240
4710
|
"Rewrite the previous response into valid JSON matching the expected schema.",
|
|
3241
|
-
`Schema hint:\n${action
|
|
4711
|
+
`Schema hint:\n${this.schemaSnippetForAction(action)}`,
|
|
3242
4712
|
"Return JSON only; no prose.",
|
|
3243
4713
|
`Original content:\n${output}`,
|
|
3244
4714
|
].join("\n\n");
|
|
@@ -3342,8 +4812,9 @@ export class CreateTasksService {
|
|
|
3342
4812
|
}))
|
|
3343
4813
|
.filter((e) => e.title);
|
|
3344
4814
|
}
|
|
3345
|
-
async generateStoriesForEpic(agent, epic, docSummary, projectBuildMethod, stream, jobId, commandRunId) {
|
|
4815
|
+
async generateStoriesForEpic(agent, projectKey, epic, docSummary, projectBuildMethod, stream, jobId, commandRunId) {
|
|
3346
4816
|
const prompt = [
|
|
4817
|
+
this.buildCreateTasksAgentMission(projectKey),
|
|
3347
4818
|
`Generate user stories for epic "${epic.title}" (phase 2 of 3).`,
|
|
3348
4819
|
"This phase is stories-only. Do not generate tasks yet.",
|
|
3349
4820
|
"Return JSON only matching:",
|
|
@@ -3353,6 +4824,7 @@ export class CreateTasksService {
|
|
|
3353
4824
|
"- acceptanceCriteria must be an array of strings.",
|
|
3354
4825
|
"- Use docdex handles when citing docs.",
|
|
3355
4826
|
"- Keep stories direct and implementation-oriented; avoid placeholder-only narrative sections.",
|
|
4827
|
+
"- Define the minimum set of user stories that, when completed, will finish this epic according to the SDS and construction method.",
|
|
3356
4828
|
"- Keep story sequencing aligned with the project construction method.",
|
|
3357
4829
|
"- Preserve canonical documented names for modules, services, interfaces, commands, schemas, and files exactly as written.",
|
|
3358
4830
|
`Epic context (key=${epic.key ?? epic.localId ?? "TBD"}):`,
|
|
@@ -3383,7 +4855,7 @@ export class CreateTasksService {
|
|
|
3383
4855
|
}))
|
|
3384
4856
|
.filter((s) => s.title);
|
|
3385
4857
|
}
|
|
3386
|
-
async generateTasksForStory(agent, epic, story, docSummary, projectBuildMethod, stream, jobId, commandRunId) {
|
|
4858
|
+
async generateTasksForStory(agent, projectKey, epic, story, docSummary, projectBuildMethod, stream, jobId, commandRunId) {
|
|
3387
4859
|
const parseTestList = (value) => {
|
|
3388
4860
|
if (!Array.isArray(value))
|
|
3389
4861
|
return [];
|
|
@@ -3393,6 +4865,7 @@ export class CreateTasksService {
|
|
|
3393
4865
|
.filter(Boolean);
|
|
3394
4866
|
};
|
|
3395
4867
|
const prompt = [
|
|
4868
|
+
this.buildCreateTasksAgentMission(projectKey),
|
|
3396
4869
|
`Generate tasks for story "${story.title}" (Epic: ${epic.title}, phase 3 of 3).`,
|
|
3397
4870
|
"This phase is tasks-only for the given story.",
|
|
3398
4871
|
"Return JSON only matching:",
|
|
@@ -3411,6 +4884,7 @@ export class CreateTasksService {
|
|
|
3411
4884
|
"- Start from prerequisite codebase setup: add structure/bootstrap tasks before feature tasks when missing.",
|
|
3412
4885
|
"- Keep dependencies strictly inside this story; never reference tasks from other stories/epics.",
|
|
3413
4886
|
"- Order tasks from foundational prerequisites to dependents based on documented dependency direction and startup constraints.",
|
|
4887
|
+
"- Generate the concrete work that would actually complete this story in one automated backlog pass; do not leave implied implementation gaps behind.",
|
|
3414
4888
|
"- Avoid placeholder wording (TBD, TODO, to be defined, generic follow-up phrases).",
|
|
3415
4889
|
"- Avoid documentation-only or glossary-only tasks unless story acceptance explicitly requires them.",
|
|
3416
4890
|
"- Preserve canonical documented names for modules, services, interfaces, commands, schemas, and files exactly as written.",
|
|
@@ -3549,7 +5023,7 @@ export class CreateTasksService {
|
|
|
3549
5023
|
},
|
|
3550
5024
|
];
|
|
3551
5025
|
}
|
|
3552
|
-
async generatePlanFromAgent(epics, agent, docSummary, options) {
|
|
5026
|
+
async generatePlanFromAgent(projectKey, epics, agent, docSummary, options) {
|
|
3553
5027
|
const planEpics = epics.map((epic, idx) => ({
|
|
3554
5028
|
...epic,
|
|
3555
5029
|
localId: epic.localId ?? `e${idx + 1}`,
|
|
@@ -3561,7 +5035,7 @@ export class CreateTasksService {
|
|
|
3561
5035
|
let stories = [];
|
|
3562
5036
|
let usedFallbackStories = false;
|
|
3563
5037
|
try {
|
|
3564
|
-
stories = await this.generateStoriesForEpic(agent, { ...epic }, docSummary, options.projectBuildMethod, options.agentStream, options.jobId, options.commandRunId);
|
|
5038
|
+
stories = await this.generateStoriesForEpic(agent, projectKey, { ...epic }, docSummary, options.projectBuildMethod, options.agentStream, options.jobId, options.commandRunId);
|
|
3565
5039
|
}
|
|
3566
5040
|
catch (error) {
|
|
3567
5041
|
usedFallbackStories = true;
|
|
@@ -3594,7 +5068,7 @@ export class CreateTasksService {
|
|
|
3594
5068
|
}
|
|
3595
5069
|
else {
|
|
3596
5070
|
try {
|
|
3597
|
-
tasks = await this.generateTasksForStory(agent, { key: story.epicLocalId, title: story.title }, story, docSummary, options.projectBuildMethod, options.agentStream, options.jobId, options.commandRunId);
|
|
5071
|
+
tasks = await this.generateTasksForStory(agent, projectKey, { key: story.epicLocalId, title: story.title }, story, docSummary, options.projectBuildMethod, options.agentStream, options.jobId, options.commandRunId);
|
|
3598
5072
|
}
|
|
3599
5073
|
catch (error) {
|
|
3600
5074
|
await this.jobService.appendLog(options.jobId, `Task generation failed for story "${story.title}" (${storyScope}). Using deterministic fallback tasks. Reason: ${error.message ?? String(error)}\n`);
|
|
@@ -3652,31 +5126,62 @@ export class CreateTasksService {
|
|
|
3652
5126
|
throw new Error(`create-tasks produced inconsistent coverage artifacts for project "${projectKey}". coverage-report.json diverged from task sufficiency coverage.`);
|
|
3653
5127
|
}
|
|
3654
5128
|
}
|
|
5129
|
+
assertPlanningArtifactConsistency(projectKey, buildPlan, serviceCatalog) {
|
|
5130
|
+
const sort = (values) => [...values].sort((left, right) => left.localeCompare(right));
|
|
5131
|
+
const catalogSourceDocs = sort(uniqueStrings(serviceCatalog.sourceDocs));
|
|
5132
|
+
const buildPlanSourceDocs = sort(uniqueStrings(buildPlan.sourceDocs));
|
|
5133
|
+
if (JSON.stringify(catalogSourceDocs) !== JSON.stringify(buildPlanSourceDocs)) {
|
|
5134
|
+
throw new Error(`create-tasks produced inconsistent planning artifacts for project "${projectKey}". build-plan.json and services.json disagree on source docs.`);
|
|
5135
|
+
}
|
|
5136
|
+
const catalogServiceNames = serviceCatalog.services.map((service) => service.name);
|
|
5137
|
+
const catalogServiceIds = serviceCatalog.services.map((service) => service.id);
|
|
5138
|
+
const expectedBuildPlanServiceNames = catalogServiceNames.slice(0, buildPlan.services.length);
|
|
5139
|
+
const expectedBuildPlanServiceIds = catalogServiceIds.slice(0, buildPlan.serviceIds.length);
|
|
5140
|
+
if (JSON.stringify(buildPlan.services) !== JSON.stringify(expectedBuildPlanServiceNames) ||
|
|
5141
|
+
JSON.stringify(buildPlan.serviceIds) !== JSON.stringify(expectedBuildPlanServiceIds)) {
|
|
5142
|
+
throw new Error(`create-tasks produced inconsistent planning artifacts for project "${projectKey}". build-plan.json and services.json disagree on service identity ordering.`);
|
|
5143
|
+
}
|
|
5144
|
+
const catalogServicesByName = new Map(serviceCatalog.services.map((service) => [service.name, service]));
|
|
5145
|
+
const unknownWaveServices = buildPlan.startupWaves.flatMap((wave) => wave.services
|
|
5146
|
+
.filter((serviceName) => !catalogServicesByName.has(serviceName))
|
|
5147
|
+
.map((serviceName) => `wave ${wave.wave}:${serviceName}`));
|
|
5148
|
+
if (unknownWaveServices.length > 0) {
|
|
5149
|
+
throw new Error(`create-tasks produced inconsistent planning artifacts for project "${projectKey}". build-plan.json references services missing from services.json: ${unknownWaveServices.slice(0, 8).join(", ")}.`);
|
|
5150
|
+
}
|
|
5151
|
+
}
|
|
3655
5152
|
async loadExpectedCoverageFromSufficiencyReport(reportPath) {
|
|
3656
5153
|
if (!reportPath)
|
|
3657
5154
|
return undefined;
|
|
5155
|
+
let raw;
|
|
3658
5156
|
try {
|
|
3659
|
-
|
|
3660
|
-
const parsed = JSON.parse(raw);
|
|
3661
|
-
const finalCoverage = parsed.finalCoverage;
|
|
3662
|
-
if (!finalCoverage)
|
|
3663
|
-
return undefined;
|
|
3664
|
-
if (typeof finalCoverage.coverageRatio !== "number" ||
|
|
3665
|
-
typeof finalCoverage.totalSignals !== "number" ||
|
|
3666
|
-
!Array.isArray(finalCoverage.missingSectionHeadings) ||
|
|
3667
|
-
!Array.isArray(finalCoverage.missingFolderEntries)) {
|
|
3668
|
-
return undefined;
|
|
3669
|
-
}
|
|
3670
|
-
return {
|
|
3671
|
-
coverageRatio: finalCoverage.coverageRatio,
|
|
3672
|
-
totalSignals: finalCoverage.totalSignals,
|
|
3673
|
-
missingSectionHeadings: finalCoverage.missingSectionHeadings.filter((value) => typeof value === "string"),
|
|
3674
|
-
missingFolderEntries: finalCoverage.missingFolderEntries.filter((value) => typeof value === "string"),
|
|
3675
|
-
};
|
|
5157
|
+
raw = await fs.readFile(reportPath, "utf8");
|
|
3676
5158
|
}
|
|
3677
|
-
catch {
|
|
3678
|
-
|
|
5159
|
+
catch (error) {
|
|
5160
|
+
const message = error?.message ?? String(error);
|
|
5161
|
+
throw new Error(`create-tasks failed to load task sufficiency coverage report from "${reportPath}": ${message}`);
|
|
5162
|
+
}
|
|
5163
|
+
let parsed;
|
|
5164
|
+
try {
|
|
5165
|
+
parsed = JSON.parse(raw);
|
|
5166
|
+
}
|
|
5167
|
+
catch (error) {
|
|
5168
|
+
const message = error?.message ?? String(error);
|
|
5169
|
+
throw new Error(`create-tasks failed to parse task sufficiency coverage report from "${reportPath}": ${message}`);
|
|
5170
|
+
}
|
|
5171
|
+
const finalCoverage = parsed.finalCoverage;
|
|
5172
|
+
if (!finalCoverage ||
|
|
5173
|
+
typeof finalCoverage.coverageRatio !== "number" ||
|
|
5174
|
+
typeof finalCoverage.totalSignals !== "number" ||
|
|
5175
|
+
!Array.isArray(finalCoverage.missingSectionHeadings) ||
|
|
5176
|
+
!Array.isArray(finalCoverage.missingFolderEntries)) {
|
|
5177
|
+
throw new Error(`create-tasks failed to load task sufficiency coverage report from "${reportPath}": finalCoverage is incomplete.`);
|
|
3679
5178
|
}
|
|
5179
|
+
return {
|
|
5180
|
+
coverageRatio: finalCoverage.coverageRatio,
|
|
5181
|
+
totalSignals: finalCoverage.totalSignals,
|
|
5182
|
+
missingSectionHeadings: finalCoverage.missingSectionHeadings.filter((value) => typeof value === "string"),
|
|
5183
|
+
missingFolderEntries: finalCoverage.missingFolderEntries.filter((value) => typeof value === "string"),
|
|
5184
|
+
};
|
|
3680
5185
|
}
|
|
3681
5186
|
buildSdsCoverageReport(projectKey, docs, plan, existingAnchors = new Set()) {
|
|
3682
5187
|
const coverageSignals = collectSdsCoverageSignalsFromDocs(docs, {
|
|
@@ -3934,6 +5439,7 @@ export class CreateTasksService {
|
|
|
3934
5439
|
await fs.mkdir(baseDir, { recursive: true });
|
|
3935
5440
|
const releaseLock = await this.acquirePlanArtifactLock(baseDir);
|
|
3936
5441
|
try {
|
|
5442
|
+
this.assertCanonicalNameConsistency(projectKey, docs, plan);
|
|
3937
5443
|
const write = async (file, data) => {
|
|
3938
5444
|
const target = path.join(baseDir, file);
|
|
3939
5445
|
await this.writeJsonArtifactAtomic(target, data);
|
|
@@ -3951,6 +5457,7 @@ export class CreateTasksService {
|
|
|
3951
5457
|
await write("epics.json", plan.epics);
|
|
3952
5458
|
await write("stories.json", plan.stories);
|
|
3953
5459
|
await write("tasks.json", plan.tasks);
|
|
5460
|
+
this.assertPlanningArtifactConsistency(projectKey, buildPlan, serviceCatalog);
|
|
3954
5461
|
const coverageReport = this.buildSdsCoverageReport(projectKey, docs, plan, options?.existingCoverageAnchors ?? new Set());
|
|
3955
5462
|
if (options?.expectedCoverage) {
|
|
3956
5463
|
this.assertCoverageConsistency(projectKey, coverageReport, options.expectedCoverage);
|
|
@@ -4333,8 +5840,17 @@ export class CreateTasksService {
|
|
|
4333
5840
|
const docs = await this.prepareDocs(preflightDocInputs);
|
|
4334
5841
|
const { docSummary, warnings: indexedDocWarnings } = this.buildDocContext(docs);
|
|
4335
5842
|
const docWarnings = uniqueStrings([...(sdsPreflight?.warnings ?? []), ...indexedDocWarnings]);
|
|
4336
|
-
const
|
|
5843
|
+
const sourceTopologyExpectation = this.buildSourceTopologyExpectation(docs);
|
|
5844
|
+
const initialArtifacts = this.derivePlanningArtifacts(options.projectKey, docs, { epics: [], stories: [], tasks: [] }, sourceTopologyExpectation);
|
|
4337
5845
|
const { discoveryGraph, topologySignals, serviceCatalog, projectBuildMethod, projectBuildPlan } = initialArtifacts;
|
|
5846
|
+
const sdsDrivenPlan = this.buildSdsDrivenPlan(options.projectKey, docs, serviceCatalog, discoveryGraph);
|
|
5847
|
+
const deterministicFallbackPlan = this.hasStrongSdsPlanningEvidence(docs, serviceCatalog, sourceTopologyExpectation)
|
|
5848
|
+
? sdsDrivenPlan
|
|
5849
|
+
: this.materializePlanFromSeed(this.fallbackPlan(options.projectKey, docs), {
|
|
5850
|
+
maxEpics: options.maxEpics,
|
|
5851
|
+
maxStoriesPerEpic: options.maxStoriesPerEpic,
|
|
5852
|
+
maxTasksPerStory: options.maxTasksPerStory,
|
|
5853
|
+
});
|
|
4338
5854
|
const { prompt } = this.buildPrompt(options.projectKey, docSummary, projectBuildMethod, serviceCatalog, options);
|
|
4339
5855
|
const qaPreflight = await this.buildQaPreflight();
|
|
4340
5856
|
const qaOverrides = this.buildQaOverrides(options);
|
|
@@ -4396,7 +5912,7 @@ export class CreateTasksService {
|
|
|
4396
5912
|
timestamp: new Date().toISOString(),
|
|
4397
5913
|
details: { epics: epics.length, source: "agent" },
|
|
4398
5914
|
});
|
|
4399
|
-
plan = await this.generatePlanFromAgent(epics, agent, docSummary, {
|
|
5915
|
+
plan = await this.generatePlanFromAgent(options.projectKey, epics, agent, docSummary, {
|
|
4400
5916
|
agentStream,
|
|
4401
5917
|
jobId: job.id,
|
|
4402
5918
|
commandRunId: commandRun.id,
|
|
@@ -4411,38 +5927,41 @@ export class CreateTasksService {
|
|
|
4411
5927
|
/unknown service ids|phase-0 service references/i.test(fallbackReason)) {
|
|
4412
5928
|
throw error;
|
|
4413
5929
|
}
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
plan =
|
|
4417
|
-
maxEpics: options.maxEpics,
|
|
4418
|
-
maxStoriesPerEpic: options.maxStoriesPerEpic,
|
|
4419
|
-
maxTasksPerStory: options.maxTasksPerStory,
|
|
4420
|
-
});
|
|
5930
|
+
await this.jobService.appendLog(job.id, `Agent planning failed, using deterministic planner fallback: ${fallbackReason}\n`);
|
|
5931
|
+
planSource = deterministicFallbackPlan === sdsDrivenPlan ? "sds" : "fallback";
|
|
5932
|
+
plan = deterministicFallbackPlan;
|
|
4421
5933
|
await this.jobService.writeCheckpoint(job.id, {
|
|
4422
5934
|
stage: "epics_generated",
|
|
4423
5935
|
timestamp: new Date().toISOString(),
|
|
4424
5936
|
details: { epics: plan.epics.length, source: planSource, reason: fallbackReason },
|
|
4425
5937
|
});
|
|
4426
5938
|
}
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
5939
|
+
plan = await this.normalizeGeneratedPlan({
|
|
5940
|
+
plan,
|
|
5941
|
+
docs,
|
|
5942
|
+
serviceCatalog,
|
|
5943
|
+
sourceTopologyExpectation,
|
|
5944
|
+
unknownEpicServicePolicy,
|
|
5945
|
+
jobId: job.id,
|
|
5946
|
+
});
|
|
5947
|
+
if (this.planLooksTooWeakForSds(plan, docs, serviceCatalog, sourceTopologyExpectation)) {
|
|
5948
|
+
fallbackReason = [
|
|
5949
|
+
fallbackReason,
|
|
5950
|
+
`generated backlog was too weak for SDS-first acceptance (epics=${plan.epics.length}, tasks=${plan.tasks.length})`,
|
|
5951
|
+
]
|
|
5952
|
+
.filter(Boolean)
|
|
5953
|
+
.join("; ");
|
|
5954
|
+
planSource = "sds";
|
|
5955
|
+
plan = await this.normalizeGeneratedPlan({
|
|
5956
|
+
plan: sdsDrivenPlan,
|
|
5957
|
+
docs,
|
|
5958
|
+
serviceCatalog,
|
|
5959
|
+
sourceTopologyExpectation,
|
|
5960
|
+
unknownEpicServicePolicy,
|
|
5961
|
+
jobId: job.id,
|
|
5962
|
+
});
|
|
5963
|
+
await this.jobService.appendLog(job.id, `create-tasks replaced the weak generated backlog with the SDS-first deterministic plan. Reason: ${fallbackReason}\n`);
|
|
4430
5964
|
}
|
|
4431
|
-
plan = {
|
|
4432
|
-
...plan,
|
|
4433
|
-
epics: normalizedPlanEpics.epics.map((epic, index) => ({
|
|
4434
|
-
...epic,
|
|
4435
|
-
localId: epic.localId ?? `e${index + 1}`,
|
|
4436
|
-
stories: [],
|
|
4437
|
-
})),
|
|
4438
|
-
};
|
|
4439
|
-
plan = this.enforceStoryScopedDependencies(plan);
|
|
4440
|
-
plan = this.injectStructureBootstrapPlan(plan, docs, options.projectKey);
|
|
4441
|
-
plan = this.enforceStoryScopedDependencies(plan);
|
|
4442
|
-
this.validatePlanLocalIdentifiers(plan);
|
|
4443
|
-
plan = this.applyServiceDependencySequencing(plan, docs);
|
|
4444
|
-
plan = this.enforceStoryScopedDependencies(plan);
|
|
4445
|
-
this.validatePlanLocalIdentifiers(plan);
|
|
4446
5965
|
await this.jobService.writeCheckpoint(job.id, {
|
|
4447
5966
|
stage: "stories_generated",
|
|
4448
5967
|
timestamp: new Date().toISOString(),
|
|
@@ -4468,91 +5987,155 @@ export class CreateTasksService {
|
|
|
4468
5987
|
await this.seedPriorities(options.projectKey);
|
|
4469
5988
|
let sufficiencyAudit;
|
|
4470
5989
|
let sufficiencyAuditError;
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
await sufficiencyService.close();
|
|
4485
|
-
}
|
|
4486
|
-
catch (closeError) {
|
|
4487
|
-
sufficiencyCloseError = closeError?.message ?? String(closeError);
|
|
4488
|
-
await this.jobService.appendLog(job.id, `Task sufficiency audit close warning: ${sufficiencyCloseError}\n`);
|
|
4489
|
-
}
|
|
4490
|
-
}
|
|
4491
|
-
}
|
|
4492
|
-
catch (error) {
|
|
4493
|
-
sufficiencyAuditError = error?.message ?? String(error);
|
|
4494
|
-
}
|
|
5990
|
+
const sufficiencyWarnings = [];
|
|
5991
|
+
let refinementAttempt = 0;
|
|
5992
|
+
while (true) {
|
|
5993
|
+
const auditResult = await this.runTaskSufficiencyAudit({
|
|
5994
|
+
workspace: options.workspace,
|
|
5995
|
+
projectKey: options.projectKey,
|
|
5996
|
+
sourceCommand: "create-tasks",
|
|
5997
|
+
dryRun: true,
|
|
5998
|
+
jobId: job.id,
|
|
5999
|
+
});
|
|
6000
|
+
sufficiencyAudit = auditResult.audit;
|
|
6001
|
+
sufficiencyAuditError = auditResult.error;
|
|
6002
|
+
sufficiencyWarnings.push(...auditResult.warnings);
|
|
4495
6003
|
if (!sufficiencyAudit) {
|
|
4496
|
-
|
|
4497
|
-
await this.jobService.writeCheckpoint(job.id, {
|
|
4498
|
-
stage: "task_sufficiency_audit",
|
|
4499
|
-
timestamp: new Date().toISOString(),
|
|
4500
|
-
details: {
|
|
4501
|
-
status: "failed",
|
|
4502
|
-
error: message,
|
|
4503
|
-
jobId: undefined,
|
|
4504
|
-
commandRunId: undefined,
|
|
4505
|
-
satisfied: false,
|
|
4506
|
-
dryRun: undefined,
|
|
4507
|
-
totalTasksAdded: undefined,
|
|
4508
|
-
totalTasksUpdated: undefined,
|
|
4509
|
-
finalCoverageRatio: undefined,
|
|
4510
|
-
reportPath: undefined,
|
|
4511
|
-
remainingSectionCount: undefined,
|
|
4512
|
-
remainingFolderCount: undefined,
|
|
4513
|
-
remainingGapCount: undefined,
|
|
4514
|
-
warnings: [],
|
|
4515
|
-
},
|
|
4516
|
-
});
|
|
4517
|
-
throw new Error(message);
|
|
6004
|
+
break;
|
|
4518
6005
|
}
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
6006
|
+
if (sufficiencyAudit.satisfied) {
|
|
6007
|
+
sufficiencyAuditError = undefined;
|
|
6008
|
+
break;
|
|
6009
|
+
}
|
|
6010
|
+
const refinementReasons = [
|
|
6011
|
+
`SDS coverage target not reached (coverage=${sufficiencyAudit.finalCoverageRatio}, remaining gaps=${sufficiencyAudit.remainingGaps.total}).`,
|
|
6012
|
+
...(sufficiencyAudit.remainingSectionHeadings.length > 0
|
|
6013
|
+
? [
|
|
6014
|
+
`Remaining section headings: ${sufficiencyAudit.remainingSectionHeadings.slice(0, 12).join(", ")}`,
|
|
6015
|
+
]
|
|
6016
|
+
: []),
|
|
6017
|
+
...(sufficiencyAudit.remainingFolderEntries.length > 0
|
|
6018
|
+
? [
|
|
6019
|
+
`Remaining folder entries: ${sufficiencyAudit.remainingFolderEntries.slice(0, 12).join(", ")}`,
|
|
6020
|
+
]
|
|
6021
|
+
: []),
|
|
6022
|
+
];
|
|
6023
|
+
if (!agent || refinementAttempt >= CreateTasksService.MAX_AGENT_REFINEMENT_ATTEMPTS) {
|
|
6024
|
+
sufficiencyAuditError = refinementReasons[0];
|
|
6025
|
+
break;
|
|
4525
6026
|
}
|
|
6027
|
+
refinementAttempt += 1;
|
|
4526
6028
|
await this.jobService.writeCheckpoint(job.id, {
|
|
4527
|
-
stage: "
|
|
6029
|
+
stage: "backlog_refinement",
|
|
4528
6030
|
timestamp: new Date().toISOString(),
|
|
4529
6031
|
details: {
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
jobId: sufficiencyAudit.jobId,
|
|
4533
|
-
commandRunId: sufficiencyAudit.commandRunId,
|
|
4534
|
-
satisfied: sufficiencyAudit.satisfied,
|
|
4535
|
-
dryRun: sufficiencyAudit.dryRun,
|
|
4536
|
-
totalTasksAdded: sufficiencyAudit.totalTasksAdded,
|
|
4537
|
-
totalTasksUpdated: sufficiencyAudit.totalTasksUpdated,
|
|
4538
|
-
finalCoverageRatio: sufficiencyAudit.finalCoverageRatio,
|
|
4539
|
-
reportPath: sufficiencyAudit.reportPath,
|
|
6032
|
+
iteration: refinementAttempt,
|
|
6033
|
+
remainingGapCount: sufficiencyAudit.remainingGaps.total,
|
|
4540
6034
|
remainingSectionCount: sufficiencyAudit.remainingSectionHeadings.length,
|
|
4541
6035
|
remainingFolderCount: sufficiencyAudit.remainingFolderEntries.length,
|
|
4542
|
-
|
|
4543
|
-
|
|
6036
|
+
plannedGapBundleCount: sufficiencyAudit.plannedGapBundles.length,
|
|
6037
|
+
unresolvedBundleCount: sufficiencyAudit.unresolvedBundles.length,
|
|
4544
6038
|
},
|
|
4545
6039
|
});
|
|
4546
|
-
|
|
4547
|
-
|
|
6040
|
+
try {
|
|
6041
|
+
plan = await this.refinePlanWithAgent({
|
|
6042
|
+
agent,
|
|
6043
|
+
currentPlan: plan,
|
|
6044
|
+
audit: sufficiencyAudit,
|
|
6045
|
+
reasons: refinementReasons,
|
|
6046
|
+
docs,
|
|
6047
|
+
docSummary,
|
|
6048
|
+
projectKey: options.projectKey,
|
|
6049
|
+
projectBuildMethod,
|
|
6050
|
+
serviceCatalog,
|
|
6051
|
+
sourceTopologyExpectation,
|
|
6052
|
+
unknownEpicServicePolicy,
|
|
6053
|
+
options,
|
|
6054
|
+
agentStream,
|
|
6055
|
+
jobId: job.id,
|
|
6056
|
+
commandRunId: commandRun.id,
|
|
6057
|
+
iteration: refinementAttempt,
|
|
6058
|
+
});
|
|
6059
|
+
planSource = "agent";
|
|
6060
|
+
await this.persistPlanToDb(project.id, options.projectKey, plan, job.id, commandRun.id, {
|
|
6061
|
+
force: true,
|
|
6062
|
+
resetKeys: true,
|
|
6063
|
+
qaPreflight,
|
|
6064
|
+
qaOverrides,
|
|
6065
|
+
});
|
|
6066
|
+
await this.seedPriorities(options.projectKey);
|
|
6067
|
+
await this.jobService.appendLog(job.id, `create-tasks refinement iteration ${refinementAttempt} replaced the backlog with ${plan.epics.length} epics, ${plan.stories.length} stories, and ${plan.tasks.length} tasks.\n`);
|
|
6068
|
+
}
|
|
6069
|
+
catch (error) {
|
|
6070
|
+
const message = error?.message ?? String(error);
|
|
6071
|
+
await this.jobService.appendLog(job.id, `create-tasks refinement iteration ${refinementAttempt} failed: ${message}\n`);
|
|
6072
|
+
sufficiencyAuditError = refinementReasons[0];
|
|
6073
|
+
if (refinementAttempt >= CreateTasksService.MAX_AGENT_REFINEMENT_ATTEMPTS) {
|
|
6074
|
+
break;
|
|
6075
|
+
}
|
|
4548
6076
|
}
|
|
4549
6077
|
}
|
|
6078
|
+
if (!sufficiencyAudit) {
|
|
6079
|
+
const message = `create-tasks blocked: task sufficiency audit failed (${sufficiencyAuditError ?? "unknown error"}).`;
|
|
6080
|
+
await this.jobService.writeCheckpoint(job.id, {
|
|
6081
|
+
stage: "task_sufficiency_audit",
|
|
6082
|
+
timestamp: new Date().toISOString(),
|
|
6083
|
+
details: {
|
|
6084
|
+
status: "failed",
|
|
6085
|
+
error: message,
|
|
6086
|
+
jobId: undefined,
|
|
6087
|
+
commandRunId: undefined,
|
|
6088
|
+
satisfied: false,
|
|
6089
|
+
dryRun: undefined,
|
|
6090
|
+
totalTasksAdded: undefined,
|
|
6091
|
+
totalTasksUpdated: undefined,
|
|
6092
|
+
finalCoverageRatio: undefined,
|
|
6093
|
+
reportPath: undefined,
|
|
6094
|
+
remainingSectionCount: undefined,
|
|
6095
|
+
remainingFolderCount: undefined,
|
|
6096
|
+
remainingGapCount: undefined,
|
|
6097
|
+
plannedGapBundleCount: undefined,
|
|
6098
|
+
warnings: [],
|
|
6099
|
+
},
|
|
6100
|
+
});
|
|
6101
|
+
throw new Error(message);
|
|
6102
|
+
}
|
|
6103
|
+
const uniqueSufficiencyWarnings = uniqueStrings(sufficiencyWarnings);
|
|
6104
|
+
sufficiencyAudit = {
|
|
6105
|
+
...sufficiencyAudit,
|
|
6106
|
+
warnings: uniqueSufficiencyWarnings,
|
|
6107
|
+
};
|
|
6108
|
+
if (!sufficiencyAudit.satisfied) {
|
|
6109
|
+
sufficiencyAuditError = `SDS coverage target not reached (coverage=${sufficiencyAudit.finalCoverageRatio}, remaining gaps=${sufficiencyAudit.remainingGaps.total}).`;
|
|
6110
|
+
}
|
|
6111
|
+
await this.jobService.writeCheckpoint(job.id, {
|
|
6112
|
+
stage: "task_sufficiency_audit",
|
|
6113
|
+
timestamp: new Date().toISOString(),
|
|
6114
|
+
details: {
|
|
6115
|
+
status: sufficiencyAudit.satisfied ? "succeeded" : "blocked",
|
|
6116
|
+
error: sufficiencyAuditError,
|
|
6117
|
+
jobId: sufficiencyAudit.jobId,
|
|
6118
|
+
commandRunId: sufficiencyAudit.commandRunId,
|
|
6119
|
+
satisfied: sufficiencyAudit.satisfied,
|
|
6120
|
+
dryRun: sufficiencyAudit.dryRun,
|
|
6121
|
+
totalTasksAdded: sufficiencyAudit.totalTasksAdded,
|
|
6122
|
+
totalTasksUpdated: sufficiencyAudit.totalTasksUpdated,
|
|
6123
|
+
finalCoverageRatio: sufficiencyAudit.finalCoverageRatio,
|
|
6124
|
+
reportPath: sufficiencyAudit.reportPath,
|
|
6125
|
+
remainingSectionCount: sufficiencyAudit.remainingSectionHeadings.length,
|
|
6126
|
+
remainingFolderCount: sufficiencyAudit.remainingFolderEntries.length,
|
|
6127
|
+
remainingGapCount: sufficiencyAudit.remainingGaps.total,
|
|
6128
|
+
plannedGapBundleCount: sufficiencyAudit.plannedGapBundles.length,
|
|
6129
|
+
unresolvedBundleCount: sufficiencyAudit.unresolvedBundles.length,
|
|
6130
|
+
warnings: uniqueSufficiencyWarnings,
|
|
6131
|
+
},
|
|
6132
|
+
});
|
|
4550
6133
|
if ((sufficiencyAudit?.totalTasksAdded ?? 0) > 0) {
|
|
4551
6134
|
await this.seedPriorities(options.projectKey);
|
|
4552
6135
|
}
|
|
4553
6136
|
const finalBacklog = await this.loadPersistedBacklog(project.id);
|
|
4554
6137
|
const finalPlan = this.buildPlanFromPersistedBacklog(finalBacklog);
|
|
4555
|
-
const finalArtifacts = this.derivePlanningArtifacts(options.projectKey, docs, finalPlan);
|
|
6138
|
+
const finalArtifacts = this.derivePlanningArtifacts(options.projectKey, docs, finalPlan, sourceTopologyExpectation);
|
|
4556
6139
|
const finalCoverageAnchors = this.collectCoverageAnchorsFromBacklog(finalBacklog);
|
|
4557
6140
|
const expectedCoverage = await this.loadExpectedCoverageFromSufficiencyReport(sufficiencyAudit?.reportPath);
|
|
4558
6141
|
await this.writePlanArtifacts(options.projectKey, finalPlan, docSummary, docs, finalArtifacts.projectBuildPlan, finalArtifacts.serviceCatalog, {
|
|
@@ -4570,8 +6153,13 @@ export class CreateTasksService {
|
|
|
4570
6153
|
dependencies: finalBacklog.dependencies.length,
|
|
4571
6154
|
services: finalArtifacts.serviceCatalog.services.length,
|
|
4572
6155
|
startupWaves: finalArtifacts.projectBuildPlan.startupWaves.length,
|
|
6156
|
+
acceptedWithResidualSectionGaps: false,
|
|
4573
6157
|
},
|
|
4574
6158
|
});
|
|
6159
|
+
const acceptedWithResidualSectionGaps = false;
|
|
6160
|
+
if (sufficiencyAudit && !sufficiencyAudit.satisfied) {
|
|
6161
|
+
throw new Error(`create-tasks blocked: task sufficiency audit did not reach full coverage. Report: ${sufficiencyAudit.reportPath}`);
|
|
6162
|
+
}
|
|
4575
6163
|
await this.jobService.updateJobStatus(job.id, "completed", {
|
|
4576
6164
|
payload: {
|
|
4577
6165
|
epicsCreated: finalBacklog.epics.length,
|
|
@@ -4615,6 +6203,8 @@ export class CreateTasksService {
|
|
|
4615
6203
|
remainingSectionCount: sufficiencyAudit.remainingSectionHeadings.length,
|
|
4616
6204
|
remainingFolderCount: sufficiencyAudit.remainingFolderEntries.length,
|
|
4617
6205
|
remainingGapCount: sufficiencyAudit.remainingGaps.total,
|
|
6206
|
+
unresolvedBundleCount: sufficiencyAudit.unresolvedBundles.length,
|
|
6207
|
+
acceptedWithResidualSectionGaps,
|
|
4618
6208
|
warnings: sufficiencyAudit.warnings,
|
|
4619
6209
|
}
|
|
4620
6210
|
: undefined,
|
|
@@ -4812,3 +6402,4 @@ export class CreateTasksService {
|
|
|
4812
6402
|
}
|
|
4813
6403
|
CreateTasksService.MAX_BUSY_RETRIES = 6;
|
|
4814
6404
|
CreateTasksService.BUSY_BACKOFF_MS = 500;
|
|
6405
|
+
CreateTasksService.MAX_AGENT_REFINEMENT_ATTEMPTS = 3;
|