@mcoda/core 0.1.32 → 0.1.34
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.
|
@@ -25,6 +25,7 @@ export interface CreateTasksOptions {
|
|
|
25
25
|
qaRequires?: string[];
|
|
26
26
|
sdsPreflightCommit?: boolean;
|
|
27
27
|
sdsPreflightCommitMessage?: string;
|
|
28
|
+
unknownEpicServicePolicy?: EpicServiceValidationPolicy;
|
|
28
29
|
}
|
|
29
30
|
export interface CreateTasksResult {
|
|
30
31
|
jobId: string;
|
|
@@ -34,6 +35,7 @@ export interface CreateTasksResult {
|
|
|
34
35
|
tasks: TaskRow[];
|
|
35
36
|
dependencies: TaskDependencyRow[];
|
|
36
37
|
}
|
|
38
|
+
type EpicServiceValidationPolicy = "auto-remediate" | "fail";
|
|
37
39
|
type TaskOrderingClient = Pick<TaskOrderingService, "orderTasks" | "close">;
|
|
38
40
|
type TaskOrderingFactory = (workspace: WorkspaceResolution, options?: {
|
|
39
41
|
recordTelemetry?: boolean;
|
|
@@ -99,6 +101,12 @@ export declare class CreateTasksService {
|
|
|
99
101
|
private extractStartupWaveHints;
|
|
100
102
|
private sortServicesByDependency;
|
|
101
103
|
private buildServiceDependencyGraph;
|
|
104
|
+
private normalizeServiceId;
|
|
105
|
+
private normalizeServiceLookupKey;
|
|
106
|
+
private createUniqueServiceId;
|
|
107
|
+
private buildServiceCatalogArtifact;
|
|
108
|
+
private buildServiceCatalogPromptSummary;
|
|
109
|
+
private alignEpicsToServiceCatalog;
|
|
102
110
|
private buildProjectConstructionMethod;
|
|
103
111
|
private buildProjectPlanArtifact;
|
|
104
112
|
private orderStoryTasksByDependencies;
|
|
@@ -125,6 +133,8 @@ export declare class CreateTasksService {
|
|
|
125
133
|
private buildFallbackTasksForStory;
|
|
126
134
|
private generatePlanFromAgent;
|
|
127
135
|
private buildSdsCoverageReport;
|
|
136
|
+
private acquirePlanArtifactLock;
|
|
137
|
+
private writeJsonArtifactAtomic;
|
|
128
138
|
private writePlanArtifacts;
|
|
129
139
|
private persistPlanToDb;
|
|
130
140
|
createTasks(options: CreateTasksOptions): Promise<CreateTasksResult>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CreateTasksService.d.ts","sourceRoot":"","sources":["../../../src/services/planning/CreateTasksService.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAEL,OAAO,EACP,gBAAgB,EAEhB,QAAQ,EAER,iBAAiB,EAEjB,OAAO,EACP,mBAAmB,EACpB,MAAM,WAAW,CAAC;AAGnB,OAAO,EAAE,YAAY,EAAkB,MAAM,qBAAqB,CAAC;AACnE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAErE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAOxE,OAAO,EAAE,sBAAsB,EAAmC,MAAM,6BAA6B,CAAC;AACtG,OAAO,EAAE,mBAAmB,EAA2B,MAAM,0BAA0B,CAAC;AAExF,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,mBAAmB,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,yBAAyB,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"CreateTasksService.d.ts","sourceRoot":"","sources":["../../../src/services/planning/CreateTasksService.ts"],"names":[],"mappings":"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"}
|
|
@@ -172,6 +172,8 @@ const DEPENDENCY_SCAN_LINE_LIMIT = 1400;
|
|
|
172
172
|
const STARTUP_WAVE_SCAN_LINE_LIMIT = 4000;
|
|
173
173
|
const VALID_AREAS = new Set(["web", "adm", "bck", "ops", "infra", "mobile"]);
|
|
174
174
|
const VALID_TASK_TYPES = new Set(["feature", "bug", "chore", "spike"]);
|
|
175
|
+
const VALID_EPIC_SERVICE_POLICIES = new Set(["auto-remediate", "fail"]);
|
|
176
|
+
const CROSS_SERVICE_TAG = "cross_service";
|
|
175
177
|
const inferDocType = (filePath) => {
|
|
176
178
|
const name = path.basename(filePath).toLowerCase();
|
|
177
179
|
if (name.includes("openapi") || name.includes("swagger"))
|
|
@@ -212,6 +214,17 @@ const normalizeTaskType = (value) => {
|
|
|
212
214
|
}
|
|
213
215
|
return undefined;
|
|
214
216
|
};
|
|
217
|
+
const normalizeEpicServicePolicy = (value) => {
|
|
218
|
+
if (typeof value !== "string")
|
|
219
|
+
return undefined;
|
|
220
|
+
const normalized = value.trim().toLowerCase();
|
|
221
|
+
if (!VALID_EPIC_SERVICE_POLICIES.has(normalized))
|
|
222
|
+
return undefined;
|
|
223
|
+
return normalized;
|
|
224
|
+
};
|
|
225
|
+
const normalizeEpicTags = (value) => uniqueStrings(normalizeStringArray(value)
|
|
226
|
+
.map((item) => item.toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, ""))
|
|
227
|
+
.filter(Boolean));
|
|
215
228
|
const normalizeRelatedDocs = (value) => {
|
|
216
229
|
if (!Array.isArray(value))
|
|
217
230
|
return [];
|
|
@@ -759,7 +772,9 @@ const EPIC_SCHEMA_SNIPPET = `{
|
|
|
759
772
|
"description": "Epic description using the epic template",
|
|
760
773
|
"acceptanceCriteria": ["criterion"],
|
|
761
774
|
"relatedDocs": ["docdex:..."],
|
|
762
|
-
"priorityHint": 50
|
|
775
|
+
"priorityHint": 50,
|
|
776
|
+
"serviceIds": ["backend-api"],
|
|
777
|
+
"tags": ["cross_service"]
|
|
763
778
|
}
|
|
764
779
|
]
|
|
765
780
|
}`;
|
|
@@ -1514,6 +1529,11 @@ export class CreateTasksService {
|
|
|
1514
1529
|
...plan.stories.map((story) => `${story.title}\n${story.description ?? ""}\n${story.userStory ?? ""}`),
|
|
1515
1530
|
...plan.tasks.map((task) => `${task.title}\n${task.description ?? ""}`),
|
|
1516
1531
|
].join("\n");
|
|
1532
|
+
for (const epic of plan.epics) {
|
|
1533
|
+
for (const serviceId of normalizeStringArray(epic.serviceIds)) {
|
|
1534
|
+
register(serviceId);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1517
1537
|
const structureTargets = this.extractStructureTargets(docs);
|
|
1518
1538
|
for (const token of [...structureTargets.directories, ...structureTargets.files]) {
|
|
1519
1539
|
register(this.deriveServiceFromPathToken(token));
|
|
@@ -1550,6 +1570,322 @@ export class CreateTasksService {
|
|
|
1550
1570
|
foundationalDependencies: waveHints.foundationalDependencies,
|
|
1551
1571
|
};
|
|
1552
1572
|
}
|
|
1573
|
+
normalizeServiceId(value) {
|
|
1574
|
+
const normalizedName = this.normalizeServiceName(value);
|
|
1575
|
+
if (!normalizedName)
|
|
1576
|
+
return undefined;
|
|
1577
|
+
const slug = normalizedName
|
|
1578
|
+
.replace(/\s+/g, "-")
|
|
1579
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
1580
|
+
.replace(/-+/g, "-")
|
|
1581
|
+
.replace(/^-+|-+$/g, "");
|
|
1582
|
+
if (!slug)
|
|
1583
|
+
return undefined;
|
|
1584
|
+
return /^[a-z]/.test(slug) ? slug : `svc-${slug}`;
|
|
1585
|
+
}
|
|
1586
|
+
normalizeServiceLookupKey(value) {
|
|
1587
|
+
return value
|
|
1588
|
+
.toLowerCase()
|
|
1589
|
+
.replace(/[`"'()[\]{}]/g, " ")
|
|
1590
|
+
.replace(/[._/-]+/g, " ")
|
|
1591
|
+
.replace(/[^a-z0-9\s]+/g, " ")
|
|
1592
|
+
.replace(/\s+/g, " ")
|
|
1593
|
+
.trim();
|
|
1594
|
+
}
|
|
1595
|
+
createUniqueServiceId(baseId, used) {
|
|
1596
|
+
if (!used.has(baseId)) {
|
|
1597
|
+
used.add(baseId);
|
|
1598
|
+
return baseId;
|
|
1599
|
+
}
|
|
1600
|
+
let suffix = 2;
|
|
1601
|
+
while (used.has(`${baseId}-${suffix}`))
|
|
1602
|
+
suffix += 1;
|
|
1603
|
+
const next = `${baseId}-${suffix}`;
|
|
1604
|
+
used.add(next);
|
|
1605
|
+
return next;
|
|
1606
|
+
}
|
|
1607
|
+
buildServiceCatalogArtifact(projectKey, docs, graph) {
|
|
1608
|
+
const serviceNames = new Set(graph.services);
|
|
1609
|
+
for (const [dependent, dependencies] of graph.dependencies.entries()) {
|
|
1610
|
+
serviceNames.add(dependent);
|
|
1611
|
+
for (const dependency of dependencies)
|
|
1612
|
+
serviceNames.add(dependency);
|
|
1613
|
+
}
|
|
1614
|
+
for (const foundation of graph.foundationalDependencies) {
|
|
1615
|
+
const normalized = this.normalizeServiceName(foundation);
|
|
1616
|
+
if (normalized)
|
|
1617
|
+
serviceNames.add(normalized);
|
|
1618
|
+
}
|
|
1619
|
+
const orderedNames = [
|
|
1620
|
+
...graph.services,
|
|
1621
|
+
...Array.from(serviceNames)
|
|
1622
|
+
.filter((name) => !graph.services.includes(name))
|
|
1623
|
+
.sort((a, b) => a.localeCompare(b)),
|
|
1624
|
+
];
|
|
1625
|
+
const usedServiceIds = new Set();
|
|
1626
|
+
const serviceIdByName = new Map();
|
|
1627
|
+
for (const name of orderedNames) {
|
|
1628
|
+
const baseId = this.normalizeServiceId(name);
|
|
1629
|
+
if (!baseId)
|
|
1630
|
+
continue;
|
|
1631
|
+
serviceIdByName.set(name, this.createUniqueServiceId(baseId, usedServiceIds));
|
|
1632
|
+
}
|
|
1633
|
+
const services = [];
|
|
1634
|
+
for (const name of orderedNames) {
|
|
1635
|
+
const id = serviceIdByName.get(name);
|
|
1636
|
+
if (!id)
|
|
1637
|
+
continue;
|
|
1638
|
+
const aliases = uniqueStrings([
|
|
1639
|
+
name,
|
|
1640
|
+
...(graph.aliases.get(name) ? Array.from(graph.aliases.get(name) ?? []) : []),
|
|
1641
|
+
]).sort((a, b) => a.localeCompare(b));
|
|
1642
|
+
const dependencyNames = Array.from(graph.dependencies.get(name) ?? []);
|
|
1643
|
+
const dependsOnServiceIds = uniqueStrings(dependencyNames
|
|
1644
|
+
.map((dependency) => serviceIdByName.get(dependency))
|
|
1645
|
+
.filter((value) => Boolean(value)));
|
|
1646
|
+
const startupWave = graph.waveRank.get(name);
|
|
1647
|
+
const wave = typeof startupWave === "number" && Number.isFinite(startupWave) ? startupWave : undefined;
|
|
1648
|
+
services.push({
|
|
1649
|
+
id,
|
|
1650
|
+
name,
|
|
1651
|
+
aliases,
|
|
1652
|
+
startupWave: wave,
|
|
1653
|
+
dependsOnServiceIds,
|
|
1654
|
+
isFoundational: graph.foundationalDependencies.some((foundation) => this.normalizeServiceName(foundation) === name || this.normalizeServiceId(foundation) === id) || dependsOnServiceIds.length === 0,
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
if (services.length === 0) {
|
|
1658
|
+
const fallbackServiceId = this.normalizeServiceId(`${projectKey} core`) ?? `${projectKey}-core`;
|
|
1659
|
+
services.push({
|
|
1660
|
+
id: fallbackServiceId,
|
|
1661
|
+
name: `${projectKey} core`,
|
|
1662
|
+
aliases: uniqueStrings([`${projectKey} core`, projectKey, "core"]),
|
|
1663
|
+
dependsOnServiceIds: [],
|
|
1664
|
+
isFoundational: true,
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
const sourceDocs = docs
|
|
1668
|
+
.map((doc) => doc.path ?? (doc.id ? `docdex:${doc.id}` : doc.title ?? "doc"))
|
|
1669
|
+
.filter((value) => Boolean(value))
|
|
1670
|
+
.slice(0, 24);
|
|
1671
|
+
return {
|
|
1672
|
+
projectKey,
|
|
1673
|
+
generatedAt: new Date().toISOString(),
|
|
1674
|
+
sourceDocs,
|
|
1675
|
+
services,
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
buildServiceCatalogPromptSummary(catalog) {
|
|
1679
|
+
if (!catalog.services.length) {
|
|
1680
|
+
return "- No services detected. Infer services from SDS and ensure every epic includes at least one service id.";
|
|
1681
|
+
}
|
|
1682
|
+
const allIds = catalog.services.map((service) => service.id);
|
|
1683
|
+
const idChunks = [];
|
|
1684
|
+
for (let index = 0; index < allIds.length; index += 12) {
|
|
1685
|
+
idChunks.push(allIds.slice(index, index + 12).join(", "));
|
|
1686
|
+
}
|
|
1687
|
+
const detailLimit = Math.min(catalog.services.length, 40);
|
|
1688
|
+
const detailLines = catalog.services.slice(0, detailLimit).map((service) => {
|
|
1689
|
+
const deps = service.dependsOnServiceIds.length > 0 ? service.dependsOnServiceIds.join(", ") : "none";
|
|
1690
|
+
const wave = typeof service.startupWave === "number" ? `wave=${service.startupWave}` : "wave=unspecified";
|
|
1691
|
+
return `- ${service.id} (${wave}; deps: ${deps}; aliases: ${service.aliases.slice(0, 5).join(", ")})`;
|
|
1692
|
+
});
|
|
1693
|
+
if (catalog.services.length > detailLimit) {
|
|
1694
|
+
detailLines.push(`- ${catalog.services.length - detailLimit} additional services omitted from detailed lines (still listed in allowed serviceIds).`);
|
|
1695
|
+
}
|
|
1696
|
+
return [`- Allowed serviceIds (${allIds.length}):`, ...idChunks.map((chunk) => ` ${chunk}`), "- Service details:", ...detailLines].join("\n");
|
|
1697
|
+
}
|
|
1698
|
+
alignEpicsToServiceCatalog(epics, catalog, policy) {
|
|
1699
|
+
const warnings = [];
|
|
1700
|
+
const validServiceIds = new Set(catalog.services.map((service) => service.id));
|
|
1701
|
+
const serviceOrder = new Map(catalog.services.map((service, index) => [service.id, index]));
|
|
1702
|
+
const aliasToIds = new Map();
|
|
1703
|
+
const idLookup = new Map();
|
|
1704
|
+
const registerAlias = (rawValue, serviceId) => {
|
|
1705
|
+
const normalized = this.normalizeServiceLookupKey(rawValue);
|
|
1706
|
+
if (!normalized)
|
|
1707
|
+
return;
|
|
1708
|
+
const bucket = aliasToIds.get(normalized) ?? new Set();
|
|
1709
|
+
bucket.add(serviceId);
|
|
1710
|
+
aliasToIds.set(normalized, bucket);
|
|
1711
|
+
};
|
|
1712
|
+
for (const service of catalog.services) {
|
|
1713
|
+
const normalizedId = this.normalizeServiceLookupKey(service.id);
|
|
1714
|
+
if (normalizedId)
|
|
1715
|
+
idLookup.set(normalizedId, service.id);
|
|
1716
|
+
registerAlias(service.id, service.id);
|
|
1717
|
+
registerAlias(service.name, service.id);
|
|
1718
|
+
for (const alias of service.aliases) {
|
|
1719
|
+
registerAlias(alias, service.id);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
const containsLookupPhrase = (haystack, phrase) => {
|
|
1723
|
+
if (!haystack || !phrase)
|
|
1724
|
+
return false;
|
|
1725
|
+
if (!phrase.includes(" ") && phrase.length < 4)
|
|
1726
|
+
return false;
|
|
1727
|
+
return ` ${haystack} `.includes(` ${phrase} `);
|
|
1728
|
+
};
|
|
1729
|
+
const mapCandidateToServiceId = (value) => {
|
|
1730
|
+
const normalized = this.normalizeServiceLookupKey(value);
|
|
1731
|
+
if (!normalized)
|
|
1732
|
+
return {};
|
|
1733
|
+
const directId = idLookup.get(normalized);
|
|
1734
|
+
if (directId)
|
|
1735
|
+
return { resolvedId: directId };
|
|
1736
|
+
const aliasMatches = aliasToIds.get(normalized);
|
|
1737
|
+
if (aliasMatches && aliasMatches.size === 1) {
|
|
1738
|
+
return { resolvedId: Array.from(aliasMatches)[0] };
|
|
1739
|
+
}
|
|
1740
|
+
if (aliasMatches && aliasMatches.size > 1) {
|
|
1741
|
+
return {
|
|
1742
|
+
ambiguousIds: Array.from(aliasMatches).sort((a, b) => (serviceOrder.get(a) ?? Number.MAX_SAFE_INTEGER) - (serviceOrder.get(b) ?? Number.MAX_SAFE_INTEGER)),
|
|
1743
|
+
};
|
|
1744
|
+
}
|
|
1745
|
+
const idFromName = this.normalizeServiceId(normalized);
|
|
1746
|
+
if (!idFromName)
|
|
1747
|
+
return {};
|
|
1748
|
+
if (validServiceIds.has(idFromName))
|
|
1749
|
+
return { resolvedId: idFromName };
|
|
1750
|
+
const candidates = Array.from(validServiceIds)
|
|
1751
|
+
.filter((id) => id === idFromName || id.startsWith(`${idFromName}-`))
|
|
1752
|
+
.sort((a, b) => (serviceOrder.get(a) ?? Number.MAX_SAFE_INTEGER) - (serviceOrder.get(b) ?? Number.MAX_SAFE_INTEGER));
|
|
1753
|
+
if (candidates.length === 1)
|
|
1754
|
+
return { resolvedId: candidates[0] };
|
|
1755
|
+
if (candidates.length > 1)
|
|
1756
|
+
return { ambiguousIds: candidates };
|
|
1757
|
+
return {};
|
|
1758
|
+
};
|
|
1759
|
+
const inferServiceIdsFromEpicText = (epic) => {
|
|
1760
|
+
const text = this.normalizeServiceLookupKey([epic.title, epic.description ?? "", ...(epic.acceptanceCriteria ?? [])]
|
|
1761
|
+
.filter(Boolean)
|
|
1762
|
+
.join("\n"));
|
|
1763
|
+
if (!text)
|
|
1764
|
+
return [];
|
|
1765
|
+
const scored = new Map();
|
|
1766
|
+
for (const service of catalog.services) {
|
|
1767
|
+
let score = 0;
|
|
1768
|
+
const idToken = this.normalizeServiceLookupKey(service.id);
|
|
1769
|
+
if (idToken && containsLookupPhrase(text, idToken)) {
|
|
1770
|
+
score = Math.max(score, 120 + idToken.length);
|
|
1771
|
+
}
|
|
1772
|
+
const nameToken = this.normalizeServiceLookupKey(service.name);
|
|
1773
|
+
if (nameToken && containsLookupPhrase(text, nameToken)) {
|
|
1774
|
+
score = Math.max(score, 90 + nameToken.length);
|
|
1775
|
+
}
|
|
1776
|
+
for (const alias of service.aliases) {
|
|
1777
|
+
const aliasToken = this.normalizeServiceLookupKey(alias);
|
|
1778
|
+
if (!aliasToken || aliasToken === idToken || aliasToken === nameToken)
|
|
1779
|
+
continue;
|
|
1780
|
+
if (containsLookupPhrase(text, aliasToken)) {
|
|
1781
|
+
score = Math.max(score, 60 + aliasToken.length);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
if (score > 0)
|
|
1785
|
+
scored.set(service.id, score);
|
|
1786
|
+
}
|
|
1787
|
+
return Array.from(scored.entries())
|
|
1788
|
+
.sort((a, b) => b[1] - a[1] || (serviceOrder.get(a[0]) ?? 0) - (serviceOrder.get(b[0]) ?? 0))
|
|
1789
|
+
.map(([id]) => id)
|
|
1790
|
+
.slice(0, 4);
|
|
1791
|
+
};
|
|
1792
|
+
const pickFallbackServiceIds = (epic, count) => {
|
|
1793
|
+
const text = this.normalizeServiceLookupKey([epic.title, epic.description ?? "", ...(epic.acceptanceCriteria ?? [])]
|
|
1794
|
+
.filter(Boolean)
|
|
1795
|
+
.join("\n"));
|
|
1796
|
+
const ranked = catalog.services
|
|
1797
|
+
.map((service) => {
|
|
1798
|
+
let score = 0;
|
|
1799
|
+
if (service.isFoundational)
|
|
1800
|
+
score += 100;
|
|
1801
|
+
if (typeof service.startupWave === "number")
|
|
1802
|
+
score += Math.max(0, 40 - service.startupWave * 2);
|
|
1803
|
+
if (service.dependsOnServiceIds.length === 0)
|
|
1804
|
+
score += 20;
|
|
1805
|
+
const tokens = uniqueStrings([service.id, service.name, ...service.aliases])
|
|
1806
|
+
.map((value) => this.normalizeServiceLookupKey(value))
|
|
1807
|
+
.filter(Boolean);
|
|
1808
|
+
for (const token of tokens) {
|
|
1809
|
+
if (containsLookupPhrase(text, token)) {
|
|
1810
|
+
score += 25 + token.length;
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
return { id: service.id, score };
|
|
1814
|
+
})
|
|
1815
|
+
.sort((a, b) => b.score - a.score ||
|
|
1816
|
+
(serviceOrder.get(a.id) ?? Number.MAX_SAFE_INTEGER) - (serviceOrder.get(b.id) ?? Number.MAX_SAFE_INTEGER));
|
|
1817
|
+
return ranked.slice(0, Math.max(1, count)).map((entry) => entry.id);
|
|
1818
|
+
};
|
|
1819
|
+
const normalizedEpics = epics.map((epic, index) => {
|
|
1820
|
+
const explicitServiceIds = normalizeStringArray(epic.serviceIds);
|
|
1821
|
+
const resolvedServiceIds = [];
|
|
1822
|
+
const unresolvedServiceIds = [];
|
|
1823
|
+
const ambiguousServiceIds = [];
|
|
1824
|
+
for (const candidate of explicitServiceIds) {
|
|
1825
|
+
const mapped = mapCandidateToServiceId(candidate);
|
|
1826
|
+
if (mapped.resolvedId) {
|
|
1827
|
+
resolvedServiceIds.push(mapped.resolvedId);
|
|
1828
|
+
}
|
|
1829
|
+
else if ((mapped.ambiguousIds?.length ?? 0) > 1) {
|
|
1830
|
+
ambiguousServiceIds.push({ candidate, options: mapped.ambiguousIds ?? [] });
|
|
1831
|
+
unresolvedServiceIds.push(candidate);
|
|
1832
|
+
}
|
|
1833
|
+
else {
|
|
1834
|
+
unresolvedServiceIds.push(candidate);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
const inferredServiceIds = inferServiceIdsFromEpicText(epic);
|
|
1838
|
+
const targetServiceCount = Math.max(1, Math.min(3, explicitServiceIds.length || 1));
|
|
1839
|
+
if (policy === "auto-remediate") {
|
|
1840
|
+
for (const inferred of inferredServiceIds) {
|
|
1841
|
+
if (resolvedServiceIds.includes(inferred))
|
|
1842
|
+
continue;
|
|
1843
|
+
resolvedServiceIds.push(inferred);
|
|
1844
|
+
if (resolvedServiceIds.length >= targetServiceCount)
|
|
1845
|
+
break;
|
|
1846
|
+
}
|
|
1847
|
+
if (resolvedServiceIds.length === 0 && catalog.services.length > 0) {
|
|
1848
|
+
resolvedServiceIds.push(...pickFallbackServiceIds(epic, 1));
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
else if (resolvedServiceIds.length === 0 && inferredServiceIds.length > 0) {
|
|
1852
|
+
resolvedServiceIds.push(...inferredServiceIds.slice(0, Math.max(1, targetServiceCount)));
|
|
1853
|
+
}
|
|
1854
|
+
const dedupedServiceIds = uniqueStrings(resolvedServiceIds)
|
|
1855
|
+
.filter((serviceId) => validServiceIds.has(serviceId))
|
|
1856
|
+
.sort((a, b) => (serviceOrder.get(a) ?? Number.MAX_SAFE_INTEGER) - (serviceOrder.get(b) ?? Number.MAX_SAFE_INTEGER));
|
|
1857
|
+
if (dedupedServiceIds.length === 0) {
|
|
1858
|
+
throw new Error(`Epic ${epic.localId ?? index + 1} (${epic.title}) has no valid phase-0 service references. Allowed service ids: ${Array.from(validServiceIds).join(", ")}`);
|
|
1859
|
+
}
|
|
1860
|
+
if (unresolvedServiceIds.length > 0 || ambiguousServiceIds.length > 0) {
|
|
1861
|
+
const unresolvedLabel = unresolvedServiceIds.length > 0 ? unresolvedServiceIds.join(", ") : "(none)";
|
|
1862
|
+
const issueLabel = unresolvedServiceIds.length > 0 ? "unknown service ids" : "ambiguous service ids";
|
|
1863
|
+
const ambiguity = ambiguousServiceIds.length > 0
|
|
1864
|
+
? ` Ambiguous mappings: ${ambiguousServiceIds
|
|
1865
|
+
.map((item) => `${item.candidate} -> [${item.options.join(", ")}]`)
|
|
1866
|
+
.join("; ")}.`
|
|
1867
|
+
: "";
|
|
1868
|
+
const message = `Epic ${epic.localId ?? index + 1} (${epic.title}) referenced ${issueLabel}: ${unresolvedLabel}.${ambiguity}`;
|
|
1869
|
+
if (policy === "fail") {
|
|
1870
|
+
throw new Error(`${message} Allowed service ids: ${Array.from(validServiceIds).join(", ")}`);
|
|
1871
|
+
}
|
|
1872
|
+
warnings.push(`${message} Auto-remediated to: ${dedupedServiceIds.join(", ")}`);
|
|
1873
|
+
}
|
|
1874
|
+
const tags = normalizeEpicTags(epic.tags);
|
|
1875
|
+
if (dedupedServiceIds.length > 1 && !tags.includes(CROSS_SERVICE_TAG)) {
|
|
1876
|
+
tags.push(CROSS_SERVICE_TAG);
|
|
1877
|
+
}
|
|
1878
|
+
if (dedupedServiceIds.length <= 1 && tags.includes(CROSS_SERVICE_TAG)) {
|
|
1879
|
+
warnings.push(`Epic ${epic.localId ?? index + 1} (${epic.title}) has tag ${CROSS_SERVICE_TAG} but only one service id (${dedupedServiceIds.join(", ")}). Keeping tag as explicit cross-cutting marker.`);
|
|
1880
|
+
}
|
|
1881
|
+
return {
|
|
1882
|
+
...epic,
|
|
1883
|
+
serviceIds: dedupedServiceIds,
|
|
1884
|
+
tags: uniqueStrings(tags),
|
|
1885
|
+
};
|
|
1886
|
+
});
|
|
1887
|
+
return { epics: normalizedEpics, warnings };
|
|
1888
|
+
}
|
|
1553
1889
|
buildProjectConstructionMethod(docs, graph) {
|
|
1554
1890
|
const toLabel = (value) => value.replace(/\s+/g, "-");
|
|
1555
1891
|
const structureTargets = this.extractStructureTargets(docs);
|
|
@@ -1600,6 +1936,7 @@ export class CreateTasksService {
|
|
|
1600
1936
|
sourceDocs,
|
|
1601
1937
|
startupWaves: graph.startupWaves.slice(0, 12),
|
|
1602
1938
|
services: graph.services.slice(0, 40),
|
|
1939
|
+
serviceIds: graph.services.map((service) => this.normalizeServiceId(service) ?? service.replace(/\s+/g, "-")).slice(0, 40),
|
|
1603
1940
|
foundationalDependencies: graph.foundationalDependencies.slice(0, 16),
|
|
1604
1941
|
buildMethod,
|
|
1605
1942
|
};
|
|
@@ -1675,14 +2012,36 @@ export class CreateTasksService {
|
|
|
1675
2012
|
return [service, wave * 10000 + order];
|
|
1676
2013
|
}));
|
|
1677
2014
|
const resolveEntityService = (text) => this.resolveServiceMentionFromPhrase(text, graph.aliases);
|
|
2015
|
+
const resolveServiceFromIds = (serviceIds) => {
|
|
2016
|
+
for (const serviceId of normalizeStringArray(serviceIds)) {
|
|
2017
|
+
const resolved = resolveEntityService(serviceId) ?? this.addServiceAlias(graph.aliases, serviceId);
|
|
2018
|
+
if (resolved)
|
|
2019
|
+
return resolved;
|
|
2020
|
+
}
|
|
2021
|
+
return undefined;
|
|
2022
|
+
};
|
|
1678
2023
|
const epics = plan.epics.map((epic) => ({ ...epic }));
|
|
1679
2024
|
const stories = plan.stories.map((story) => ({ ...story }));
|
|
1680
2025
|
const tasks = plan.tasks.map((task) => ({ ...task, dependsOnKeys: uniqueStrings(task.dependsOnKeys ?? []) }));
|
|
1681
2026
|
const storyByScope = new Map(stories.map((story) => [this.scopeStory(story), story]));
|
|
2027
|
+
const epicServiceByLocalId = new Map();
|
|
2028
|
+
const storyServiceByScope = new Map();
|
|
1682
2029
|
const taskServiceByScope = new Map();
|
|
2030
|
+
for (const epic of epics) {
|
|
2031
|
+
const serviceFromIds = resolveServiceFromIds(epic.serviceIds);
|
|
2032
|
+
const serviceFromText = resolveEntityService(`${epic.title}\n${epic.description ?? ""}`);
|
|
2033
|
+
epicServiceByLocalId.set(epic.localId, serviceFromIds ?? serviceFromText);
|
|
2034
|
+
}
|
|
2035
|
+
for (const story of stories) {
|
|
2036
|
+
const storyScope = this.scopeStory(story);
|
|
2037
|
+
const inherited = epicServiceByLocalId.get(story.epicLocalId);
|
|
2038
|
+
const serviceFromText = resolveEntityService(`${story.title}\n${story.description ?? ""}\n${story.userStory ?? ""}`);
|
|
2039
|
+
storyServiceByScope.set(storyScope, serviceFromText ?? inherited);
|
|
2040
|
+
}
|
|
1683
2041
|
for (const task of tasks) {
|
|
2042
|
+
const storyScope = this.storyScopeKey(task.epicLocalId, task.storyLocalId);
|
|
1684
2043
|
const text = `${task.title ?? ""}\n${task.description ?? ""}`;
|
|
1685
|
-
taskServiceByScope.set(this.scopeTask(task), resolveEntityService(text));
|
|
2044
|
+
taskServiceByScope.set(this.scopeTask(task), resolveEntityService(text) ?? storyServiceByScope.get(storyScope));
|
|
1686
2045
|
}
|
|
1687
2046
|
const tasksByStory = new Map();
|
|
1688
2047
|
for (const task of tasks) {
|
|
@@ -1728,7 +2087,7 @@ export class CreateTasksService {
|
|
|
1728
2087
|
const taskRanks = storyTasks
|
|
1729
2088
|
.map((task) => serviceRank.get(taskServiceByScope.get(this.scopeTask(task)) ?? ""))
|
|
1730
2089
|
.filter((value) => typeof value === "number");
|
|
1731
|
-
const storyTextRank = serviceRank.get(
|
|
2090
|
+
const storyTextRank = serviceRank.get(storyServiceByScope.get(storyScope) ?? "");
|
|
1732
2091
|
const rank = taskRanks.length > 0 ? Math.min(...taskRanks) : storyTextRank ?? Number.MAX_SAFE_INTEGER;
|
|
1733
2092
|
storyRankByScope.set(storyScope, rank);
|
|
1734
2093
|
}
|
|
@@ -1738,7 +2097,7 @@ export class CreateTasksService {
|
|
|
1738
2097
|
const storyRanks = epicStories
|
|
1739
2098
|
.map((story) => storyRankByScope.get(this.scopeStory(story)))
|
|
1740
2099
|
.filter((value) => typeof value === "number");
|
|
1741
|
-
const epicTextRank = serviceRank.get(
|
|
2100
|
+
const epicTextRank = serviceRank.get(epicServiceByLocalId.get(epic.localId) ?? "");
|
|
1742
2101
|
const rank = storyRanks.length > 0 ? Math.min(...storyRanks) : epicTextRank ?? Number.MAX_SAFE_INTEGER;
|
|
1743
2102
|
epicRankByLocalId.set(epic.localId, rank);
|
|
1744
2103
|
}
|
|
@@ -2295,8 +2654,9 @@ export class CreateTasksService {
|
|
|
2295
2654
|
}
|
|
2296
2655
|
return { docSummary: blocks.join("\n\n") || "(no docs)", warnings };
|
|
2297
2656
|
}
|
|
2298
|
-
buildPrompt(projectKey, docs, projectBuildMethod, options) {
|
|
2657
|
+
buildPrompt(projectKey, docs, projectBuildMethod, serviceCatalog, options) {
|
|
2299
2658
|
const docSummary = docs.map((doc, idx) => describeDoc(doc, idx)).join("\n");
|
|
2659
|
+
const serviceCatalogSummary = this.buildServiceCatalogPromptSummary(serviceCatalog);
|
|
2300
2660
|
const limits = [
|
|
2301
2661
|
options.maxEpics ? `Limit epics to ${options.maxEpics}.` : "",
|
|
2302
2662
|
options.maxStoriesPerEpic ? `Limit stories per epic to ${options.maxStoriesPerEpic}.` : "",
|
|
@@ -2317,8 +2677,12 @@ export class CreateTasksService {
|
|
|
2317
2677
|
"- Keep epics actionable and implementation-oriented; avoid glossary/admin-only epics.",
|
|
2318
2678
|
"- Prefer dependency-first sequencing: foundational setup epics before dependent feature epics.",
|
|
2319
2679
|
"- Keep output derived from docs; do not assume stacks unless docs state them.",
|
|
2680
|
+
"- serviceIds is required and must contain one or more ids from the phase-0 service catalog below.",
|
|
2681
|
+
`- If an epic spans multiple services, include tag \"${CROSS_SERVICE_TAG}\" in tags.`,
|
|
2320
2682
|
"Project construction method to follow:",
|
|
2321
2683
|
projectBuildMethod,
|
|
2684
|
+
"Phase 0 service catalog (allowed serviceIds):",
|
|
2685
|
+
serviceCatalogSummary,
|
|
2322
2686
|
limits || "Use reasonable scope without over-generating epics.",
|
|
2323
2687
|
"Docs available:",
|
|
2324
2688
|
docSummary || "- (no docs provided; propose sensible epics).",
|
|
@@ -2618,6 +2982,8 @@ export class CreateTasksService {
|
|
|
2618
2982
|
acceptanceCriteria: Array.isArray(epic.acceptanceCriteria) ? epic.acceptanceCriteria : [],
|
|
2619
2983
|
relatedDocs: normalizeRelatedDocs(epic.relatedDocs),
|
|
2620
2984
|
priorityHint: typeof epic.priorityHint === "number" ? epic.priorityHint : undefined,
|
|
2985
|
+
serviceIds: normalizeStringArray(epic.serviceIds),
|
|
2986
|
+
tags: normalizeEpicTags(epic.tags),
|
|
2621
2987
|
stories: [],
|
|
2622
2988
|
}))
|
|
2623
2989
|
.filter((e) => e.title);
|
|
@@ -2636,6 +3002,8 @@ export class CreateTasksService {
|
|
|
2636
3002
|
"- Keep story sequencing aligned with the project construction method.",
|
|
2637
3003
|
`Epic context (key=${epic.key ?? epic.localId ?? "TBD"}):`,
|
|
2638
3004
|
epic.description ?? "(no description provided)",
|
|
3005
|
+
`Epic serviceIds: ${(epic.serviceIds ?? []).join(", ") || "(not provided)"}`,
|
|
3006
|
+
`Epic tags: ${(epic.tags ?? []).join(", ") || "(none)"}`,
|
|
2639
3007
|
"Project construction method:",
|
|
2640
3008
|
projectBuildMethod,
|
|
2641
3009
|
`Docs: ${docSummary || "none"}`,
|
|
@@ -2939,19 +3307,91 @@ export class CreateTasksService {
|
|
|
2939
3307
|
: ["Coverage is heading-based heuristic match between SDS sections and generated epic/story/task corpus."],
|
|
2940
3308
|
};
|
|
2941
3309
|
}
|
|
2942
|
-
async
|
|
3310
|
+
async acquirePlanArtifactLock(baseDir, options) {
|
|
3311
|
+
const timeoutMs = options?.timeoutMs ?? 10000;
|
|
3312
|
+
const pollIntervalMs = options?.pollIntervalMs ?? 80;
|
|
3313
|
+
const staleLockMs = options?.staleLockMs ?? 120000;
|
|
3314
|
+
const lockPath = path.join(baseDir, ".plan-artifacts.lock");
|
|
3315
|
+
const startedAtMs = Date.now();
|
|
3316
|
+
while (true) {
|
|
3317
|
+
try {
|
|
3318
|
+
const handle = await fs.open(lockPath, "wx");
|
|
3319
|
+
await handle.writeFile(JSON.stringify({ pid: process.pid, acquiredAt: new Date().toISOString() }), "utf8");
|
|
3320
|
+
return async () => {
|
|
3321
|
+
try {
|
|
3322
|
+
await handle.close();
|
|
3323
|
+
}
|
|
3324
|
+
catch { }
|
|
3325
|
+
await fs.rm(lockPath, { force: true });
|
|
3326
|
+
};
|
|
3327
|
+
}
|
|
3328
|
+
catch (error) {
|
|
3329
|
+
const code = error.code;
|
|
3330
|
+
if (code !== "EEXIST")
|
|
3331
|
+
throw error;
|
|
3332
|
+
try {
|
|
3333
|
+
const stat = await fs.stat(lockPath);
|
|
3334
|
+
if (Date.now() - stat.mtimeMs > staleLockMs) {
|
|
3335
|
+
await fs.rm(lockPath, { force: true });
|
|
3336
|
+
continue;
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
catch {
|
|
3340
|
+
continue;
|
|
3341
|
+
}
|
|
3342
|
+
if (Date.now() - startedAtMs >= timeoutMs) {
|
|
3343
|
+
throw new Error(`Timed out acquiring plan artifact lock for ${baseDir}`);
|
|
3344
|
+
}
|
|
3345
|
+
await delay(pollIntervalMs);
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
async writeJsonArtifactAtomic(targetPath, data) {
|
|
3350
|
+
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`;
|
|
3351
|
+
const payload = JSON.stringify(data, null, 2);
|
|
3352
|
+
await fs.writeFile(tempPath, payload, "utf8");
|
|
3353
|
+
try {
|
|
3354
|
+
await fs.rename(tempPath, targetPath);
|
|
3355
|
+
}
|
|
3356
|
+
catch (error) {
|
|
3357
|
+
const code = error.code;
|
|
3358
|
+
if (code === "EEXIST" || code === "EPERM") {
|
|
3359
|
+
await fs.rm(targetPath, { force: true });
|
|
3360
|
+
await fs.rename(tempPath, targetPath);
|
|
3361
|
+
}
|
|
3362
|
+
else {
|
|
3363
|
+
await fs.rm(tempPath, { force: true });
|
|
3364
|
+
throw error;
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
async writePlanArtifacts(projectKey, plan, docSummary, docs, buildPlan, serviceCatalog) {
|
|
2943
3369
|
const baseDir = path.join(this.workspace.mcodaDir, "tasks", projectKey);
|
|
2944
3370
|
await fs.mkdir(baseDir, { recursive: true });
|
|
2945
|
-
const
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
3371
|
+
const releaseLock = await this.acquirePlanArtifactLock(baseDir);
|
|
3372
|
+
try {
|
|
3373
|
+
const write = async (file, data) => {
|
|
3374
|
+
const target = path.join(baseDir, file);
|
|
3375
|
+
await this.writeJsonArtifactAtomic(target, data);
|
|
3376
|
+
};
|
|
3377
|
+
await write("plan.json", {
|
|
3378
|
+
projectKey,
|
|
3379
|
+
generatedAt: new Date().toISOString(),
|
|
3380
|
+
docSummary,
|
|
3381
|
+
buildPlan,
|
|
3382
|
+
serviceCatalog,
|
|
3383
|
+
...plan,
|
|
3384
|
+
});
|
|
3385
|
+
await write("build-plan.json", buildPlan);
|
|
3386
|
+
await write("services.json", serviceCatalog);
|
|
3387
|
+
await write("epics.json", plan.epics);
|
|
3388
|
+
await write("stories.json", plan.stories);
|
|
3389
|
+
await write("tasks.json", plan.tasks);
|
|
3390
|
+
await write("coverage-report.json", this.buildSdsCoverageReport(projectKey, docs, plan));
|
|
3391
|
+
}
|
|
3392
|
+
finally {
|
|
3393
|
+
await releaseLock();
|
|
3394
|
+
}
|
|
2955
3395
|
return { folder: baseDir };
|
|
2956
3396
|
}
|
|
2957
3397
|
async persistPlanToDb(projectId, projectKey, plan, jobId, commandRunId, options) {
|
|
@@ -2969,7 +3409,13 @@ export class CreateTasksService {
|
|
|
2969
3409
|
description: buildEpicDescription(key, epic.title || `Epic ${key}`, epic.description, epic.acceptanceCriteria, epic.relatedDocs),
|
|
2970
3410
|
storyPointsTotal: null,
|
|
2971
3411
|
priority: epic.priorityHint ?? (epicInserts.length + 1),
|
|
2972
|
-
metadata: epic.relatedDocs
|
|
3412
|
+
metadata: epic.relatedDocs || (epic.serviceIds?.length ?? 0) > 0 || (epic.tags?.length ?? 0) > 0
|
|
3413
|
+
? {
|
|
3414
|
+
...(epic.relatedDocs ? { doc_links: epic.relatedDocs } : {}),
|
|
3415
|
+
...(epic.serviceIds && epic.serviceIds.length > 0 ? { service_ids: epic.serviceIds } : {}),
|
|
3416
|
+
...(epic.tags && epic.tags.length > 0 ? { tags: epic.tags } : {}),
|
|
3417
|
+
}
|
|
3418
|
+
: undefined,
|
|
2973
3419
|
});
|
|
2974
3420
|
epicMeta.push({ key, node: epic });
|
|
2975
3421
|
}
|
|
@@ -3187,6 +3633,7 @@ export class CreateTasksService {
|
|
|
3187
3633
|
}
|
|
3188
3634
|
async createTasks(options) {
|
|
3189
3635
|
const agentStream = options.agentStream !== false;
|
|
3636
|
+
const unknownEpicServicePolicy = normalizeEpicServicePolicy(options.unknownEpicServicePolicy) ?? "auto-remediate";
|
|
3190
3637
|
const commandRun = await this.jobService.startCommandRun("create-tasks", options.projectKey);
|
|
3191
3638
|
const job = await this.jobService.startJob("create_tasks", commandRun.id, options.projectKey, {
|
|
3192
3639
|
commandName: "create-tasks",
|
|
@@ -3196,6 +3643,7 @@ export class CreateTasksService {
|
|
|
3196
3643
|
agent: options.agentName,
|
|
3197
3644
|
agentStream,
|
|
3198
3645
|
sdsPreflightCommit: options.sdsPreflightCommit === true,
|
|
3646
|
+
unknownEpicServicePolicy,
|
|
3199
3647
|
},
|
|
3200
3648
|
});
|
|
3201
3649
|
let lastError;
|
|
@@ -3311,9 +3759,10 @@ export class CreateTasksService {
|
|
|
3311
3759
|
const { docSummary, warnings: indexedDocWarnings } = this.buildDocContext(docs);
|
|
3312
3760
|
const docWarnings = uniqueStrings([...(sdsPreflight?.warnings ?? []), ...indexedDocWarnings]);
|
|
3313
3761
|
const discoveryGraph = this.buildServiceDependencyGraph({ epics: [], stories: [], tasks: [] }, docs);
|
|
3762
|
+
const serviceCatalog = this.buildServiceCatalogArtifact(options.projectKey, docs, discoveryGraph);
|
|
3314
3763
|
const projectBuildMethod = this.buildProjectConstructionMethod(docs, discoveryGraph);
|
|
3315
3764
|
const projectBuildPlan = this.buildProjectPlanArtifact(options.projectKey, docs, discoveryGraph, projectBuildMethod);
|
|
3316
|
-
const { prompt } = this.buildPrompt(options.projectKey, docs, projectBuildMethod, options);
|
|
3765
|
+
const { prompt } = this.buildPrompt(options.projectKey, docs, projectBuildMethod, serviceCatalog, options);
|
|
3317
3766
|
const qaPreflight = await this.buildQaPreflight();
|
|
3318
3767
|
const qaOverrides = this.buildQaOverrides(options);
|
|
3319
3768
|
await this.jobService.writeCheckpoint(job.id, {
|
|
@@ -3330,6 +3779,18 @@ export class CreateTasksService {
|
|
|
3330
3779
|
startupWaves: projectBuildPlan.startupWaves.length,
|
|
3331
3780
|
},
|
|
3332
3781
|
});
|
|
3782
|
+
await this.jobService.writeCheckpoint(job.id, {
|
|
3783
|
+
stage: "phase0_services_defined",
|
|
3784
|
+
timestamp: new Date().toISOString(),
|
|
3785
|
+
details: {
|
|
3786
|
+
services: serviceCatalog.services.length,
|
|
3787
|
+
foundational: serviceCatalog.services.filter((service) => service.isFoundational).length,
|
|
3788
|
+
startupWaves: uniqueStrings(serviceCatalog.services
|
|
3789
|
+
.map((service) => (typeof service.startupWave === "number" ? String(service.startupWave) : ""))
|
|
3790
|
+
.filter(Boolean)).length,
|
|
3791
|
+
sourceDocs: serviceCatalog.sourceDocs.length,
|
|
3792
|
+
},
|
|
3793
|
+
});
|
|
3333
3794
|
await this.jobService.writeCheckpoint(job.id, {
|
|
3334
3795
|
stage: "qa_preflight",
|
|
3335
3796
|
timestamp: new Date().toISOString(),
|
|
@@ -3342,7 +3803,12 @@ export class CreateTasksService {
|
|
|
3342
3803
|
try {
|
|
3343
3804
|
agent = await this.resolveAgent(options.agentName);
|
|
3344
3805
|
const { output: epicOutput } = await this.invokeAgentWithRetry(agent, prompt, "epics", agentStream, job.id, commandRun.id, { docWarnings });
|
|
3345
|
-
const
|
|
3806
|
+
const parsedEpics = this.parseEpics(epicOutput, docs, options.projectKey).slice(0, options.maxEpics ?? Number.MAX_SAFE_INTEGER);
|
|
3807
|
+
const normalizedEpics = this.alignEpicsToServiceCatalog(parsedEpics, serviceCatalog, unknownEpicServicePolicy);
|
|
3808
|
+
for (const warning of normalizedEpics.warnings) {
|
|
3809
|
+
await this.jobService.appendLog(job.id, `[create-tasks] ${warning}\n`);
|
|
3810
|
+
}
|
|
3811
|
+
const epics = normalizedEpics.epics;
|
|
3346
3812
|
await this.jobService.writeCheckpoint(job.id, {
|
|
3347
3813
|
stage: "epics_generated",
|
|
3348
3814
|
timestamp: new Date().toISOString(),
|
|
@@ -3359,6 +3825,10 @@ export class CreateTasksService {
|
|
|
3359
3825
|
}
|
|
3360
3826
|
catch (error) {
|
|
3361
3827
|
fallbackReason = error.message ?? String(error);
|
|
3828
|
+
if (unknownEpicServicePolicy === "fail" &&
|
|
3829
|
+
/unknown service ids|phase-0 service references/i.test(fallbackReason)) {
|
|
3830
|
+
throw error;
|
|
3831
|
+
}
|
|
3362
3832
|
planSource = "fallback";
|
|
3363
3833
|
await this.jobService.appendLog(job.id, `Agent planning failed, using deterministic fallback plan: ${fallbackReason}\n`);
|
|
3364
3834
|
plan = this.materializePlanFromSeed(this.fallbackPlan(options.projectKey, docs), {
|
|
@@ -3372,6 +3842,18 @@ export class CreateTasksService {
|
|
|
3372
3842
|
details: { epics: plan.epics.length, source: planSource, reason: fallbackReason },
|
|
3373
3843
|
});
|
|
3374
3844
|
}
|
|
3845
|
+
const normalizedPlanEpics = this.alignEpicsToServiceCatalog(plan.epics, serviceCatalog, unknownEpicServicePolicy);
|
|
3846
|
+
for (const warning of normalizedPlanEpics.warnings) {
|
|
3847
|
+
await this.jobService.appendLog(job.id, `[create-tasks] ${warning}\n`);
|
|
3848
|
+
}
|
|
3849
|
+
plan = {
|
|
3850
|
+
...plan,
|
|
3851
|
+
epics: normalizedPlanEpics.epics.map((epic, index) => ({
|
|
3852
|
+
...epic,
|
|
3853
|
+
localId: epic.localId ?? `e${index + 1}`,
|
|
3854
|
+
stories: [],
|
|
3855
|
+
})),
|
|
3856
|
+
};
|
|
3375
3857
|
plan = this.enforceStoryScopedDependencies(plan);
|
|
3376
3858
|
plan = this.injectStructureBootstrapPlan(plan, docs, options.projectKey);
|
|
3377
3859
|
plan = this.enforceStoryScopedDependencies(plan);
|
|
@@ -3389,7 +3871,7 @@ export class CreateTasksService {
|
|
|
3389
3871
|
timestamp: new Date().toISOString(),
|
|
3390
3872
|
details: { tasks: plan.tasks.length, source: planSource, fallbackReason },
|
|
3391
3873
|
});
|
|
3392
|
-
const { folder } = await this.writePlanArtifacts(options.projectKey, plan, docSummary, docs, projectBuildPlan);
|
|
3874
|
+
const { folder } = await this.writePlanArtifacts(options.projectKey, plan, docSummary, docs, projectBuildPlan, serviceCatalog);
|
|
3393
3875
|
await this.jobService.writeCheckpoint(job.id, {
|
|
3394
3876
|
stage: "plan_written",
|
|
3395
3877
|
timestamp: new Date().toISOString(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcoda/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.34",
|
|
4
4
|
"description": "Core services and APIs for the mcoda CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -32,11 +32,11 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@apidevtools/swagger-parser": "^10.1.0",
|
|
34
34
|
"yaml": "^2.4.2",
|
|
35
|
-
"@mcoda/shared": "0.1.
|
|
36
|
-
"@mcoda/db": "0.1.
|
|
37
|
-
"@mcoda/agents": "0.1.
|
|
38
|
-
"@mcoda/
|
|
39
|
-
"@mcoda/
|
|
35
|
+
"@mcoda/shared": "0.1.34",
|
|
36
|
+
"@mcoda/db": "0.1.34",
|
|
37
|
+
"@mcoda/agents": "0.1.34",
|
|
38
|
+
"@mcoda/generators": "0.1.34",
|
|
39
|
+
"@mcoda/integrations": "0.1.34"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"build": "tsc -p tsconfig.json",
|