@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":"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"}
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(resolveEntityService(`${story.title}\n${story.description ?? ""}\n${story.userStory ?? ""}`) ?? "");
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(resolveEntityService(`${epic.title}\n${epic.description ?? ""}`) ?? "");
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 writePlanArtifacts(projectKey, plan, docSummary, docs, buildPlan) {
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 write = async (file, data) => {
2946
- const target = path.join(baseDir, file);
2947
- await fs.writeFile(target, JSON.stringify(data, null, 2), "utf8");
2948
- };
2949
- await write("plan.json", { projectKey, generatedAt: new Date().toISOString(), docSummary, buildPlan, ...plan });
2950
- await write("build-plan.json", buildPlan);
2951
- await write("epics.json", plan.epics);
2952
- await write("stories.json", plan.stories);
2953
- await write("tasks.json", plan.tasks);
2954
- await write("coverage-report.json", this.buildSdsCoverageReport(projectKey, docs, plan));
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 ? { doc_links: epic.relatedDocs } : undefined,
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 epics = this.parseEpics(epicOutput, docs, options.projectKey).slice(0, options.maxEpics ?? Number.MAX_SAFE_INTEGER);
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.32",
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.32",
36
- "@mcoda/db": "0.1.32",
37
- "@mcoda/agents": "0.1.32",
38
- "@mcoda/integrations": "0.1.32",
39
- "@mcoda/generators": "0.1.32"
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",