yaml-flow 2.2.0 → 2.3.0

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/index.cjs CHANGED
@@ -1735,6 +1735,213 @@ ${serialized}`;
1735
1735
  return String(obj);
1736
1736
  }
1737
1737
 
1738
+ // src/event-graph/validate.ts
1739
+ function buildProducerMap2(tasks) {
1740
+ const map = {};
1741
+ for (const [name, config] of Object.entries(tasks)) {
1742
+ for (const token of getProvides(config)) {
1743
+ if (!map[token]) map[token] = [];
1744
+ map[token].push(name);
1745
+ }
1746
+ if (config.on) {
1747
+ for (const tokens of Object.values(config.on)) {
1748
+ for (const token of tokens) {
1749
+ if (!map[token]) map[token] = [];
1750
+ if (!map[token].includes(name)) map[token].push(name);
1751
+ }
1752
+ }
1753
+ }
1754
+ if (config.on_failure) {
1755
+ for (const token of config.on_failure) {
1756
+ if (!map[token]) map[token] = [];
1757
+ if (!map[token].includes(name)) map[token].push(name);
1758
+ }
1759
+ }
1760
+ }
1761
+ return map;
1762
+ }
1763
+ function buildTaskDeps(tasks, producerMap) {
1764
+ const deps = {};
1765
+ for (const [name, config] of Object.entries(tasks)) {
1766
+ deps[name] = /* @__PURE__ */ new Set();
1767
+ for (const token of getRequires(config)) {
1768
+ for (const producer of producerMap[token] || []) {
1769
+ if (producer !== name) deps[name].add(producer);
1770
+ }
1771
+ }
1772
+ }
1773
+ return deps;
1774
+ }
1775
+ function detectCycles(taskNames, deps) {
1776
+ const WHITE = 0, GRAY = 1, BLACK = 2;
1777
+ const color = {};
1778
+ const parent = {};
1779
+ const cycles = [];
1780
+ for (const name of taskNames) {
1781
+ color[name] = WHITE;
1782
+ parent[name] = null;
1783
+ }
1784
+ function dfs(node) {
1785
+ color[node] = GRAY;
1786
+ for (const dep of deps[node] || []) {
1787
+ if (color[dep] === GRAY) {
1788
+ const cycle = [dep];
1789
+ let cur = node;
1790
+ while (cur !== dep) {
1791
+ cycle.push(cur);
1792
+ cur = parent[cur];
1793
+ }
1794
+ cycle.push(dep);
1795
+ cycle.reverse();
1796
+ cycles.push(cycle);
1797
+ } else if (color[dep] === WHITE) {
1798
+ parent[dep] = node;
1799
+ dfs(dep);
1800
+ }
1801
+ }
1802
+ color[node] = BLACK;
1803
+ }
1804
+ for (const name of taskNames) {
1805
+ if (color[name] === WHITE) {
1806
+ dfs(name);
1807
+ }
1808
+ }
1809
+ return cycles;
1810
+ }
1811
+ function validateGraph(graph) {
1812
+ const issues = [];
1813
+ const tasks = getAllTasks(graph);
1814
+ const taskNames = Object.keys(tasks);
1815
+ if (taskNames.length === 0) {
1816
+ issues.push({
1817
+ severity: "error",
1818
+ code: "EMPTY_GRAPH",
1819
+ message: "Graph has no tasks"
1820
+ });
1821
+ return buildResult(issues);
1822
+ }
1823
+ const producerMap = buildProducerMap2(tasks);
1824
+ const deps = buildTaskDeps(tasks, producerMap);
1825
+ for (const [name, config] of Object.entries(tasks)) {
1826
+ for (const token of getRequires(config)) {
1827
+ if (!producerMap[token]) {
1828
+ issues.push({
1829
+ severity: "error",
1830
+ code: "DANGLING_REQUIRES",
1831
+ message: `Task "${name}" requires token "${token}" but no task produces it`,
1832
+ tasks: [name],
1833
+ tokens: [token]
1834
+ });
1835
+ }
1836
+ }
1837
+ }
1838
+ const cycles = detectCycles(taskNames, deps);
1839
+ for (const cycle of cycles) {
1840
+ issues.push({
1841
+ severity: "error",
1842
+ code: "CIRCULAR_DEPENDENCY",
1843
+ message: `Circular dependency: ${cycle.join(" \u2192 ")}`,
1844
+ tasks: cycle.filter((_t, i) => i < cycle.length - 1)
1845
+ // dedupe last = first
1846
+ });
1847
+ }
1848
+ for (const [name, config] of Object.entries(tasks)) {
1849
+ const req = getRequires(config);
1850
+ const prov = getProvides(config);
1851
+ const self = req.filter((token) => prov.includes(token));
1852
+ if (self.length > 0) {
1853
+ issues.push({
1854
+ severity: "error",
1855
+ code: "SELF_DEPENDENCY",
1856
+ message: `Task "${name}" requires tokens it provides itself: [${self.join(", ")}]`,
1857
+ tasks: [name],
1858
+ tokens: self
1859
+ });
1860
+ }
1861
+ }
1862
+ for (const [token, producers] of Object.entries(producerMap)) {
1863
+ if (producers.length > 1) {
1864
+ issues.push({
1865
+ severity: "warning",
1866
+ code: "PROVIDE_CONFLICT",
1867
+ message: `Token "${token}" is produced by multiple tasks: [${producers.join(", ")}]. This requires a conflict strategy.`,
1868
+ tasks: producers,
1869
+ tokens: [token]
1870
+ });
1871
+ }
1872
+ }
1873
+ if (graph.settings.completion === "goal-reached" && graph.settings.goal) {
1874
+ for (const goalToken of graph.settings.goal) {
1875
+ if (!producerMap[goalToken]) {
1876
+ issues.push({
1877
+ severity: "error",
1878
+ code: "UNREACHABLE_GOAL",
1879
+ message: `Goal token "${goalToken}" cannot be produced by any task`,
1880
+ tokens: [goalToken]
1881
+ });
1882
+ }
1883
+ }
1884
+ }
1885
+ if (taskNames.length > 1) {
1886
+ for (const [name, config] of Object.entries(tasks)) {
1887
+ const prov = getProvides(config);
1888
+ const onProv = config.on ? Object.values(config.on).flat() : [];
1889
+ const failProv = config.on_failure || [];
1890
+ if (prov.length === 0 && onProv.length === 0 && failProv.length === 0) {
1891
+ issues.push({
1892
+ severity: "warning",
1893
+ code: "DEAD_END_TASK",
1894
+ message: `Task "${name}" has no provides \u2014 it cannot unblock any downstream task`,
1895
+ tasks: [name]
1896
+ });
1897
+ }
1898
+ }
1899
+ }
1900
+ const allRequired = /* @__PURE__ */ new Set();
1901
+ for (const config of Object.values(tasks)) {
1902
+ for (const token of getRequires(config)) {
1903
+ allRequired.add(token);
1904
+ }
1905
+ }
1906
+ for (const [name, config] of Object.entries(tasks)) {
1907
+ const req = getRequires(config);
1908
+ const prov = getProvides(config);
1909
+ const onProv = config.on ? Object.values(config.on).flat() : [];
1910
+ const allProv = [...prov, ...onProv];
1911
+ const isEntryPoint = req.length === 0;
1912
+ const hasDownstream = allProv.some((token) => allRequired.has(token));
1913
+ if (isEntryPoint && !hasDownstream && taskNames.length > 1) {
1914
+ const isGoalRelevant = graph.settings.completion === "goal-reached" && graph.settings.goal?.some((g) => allProv.includes(g));
1915
+ if (!isGoalRelevant) {
1916
+ issues.push({
1917
+ severity: "info",
1918
+ code: "ISOLATED_TASK",
1919
+ message: `Task "${name}" is disconnected \u2014 it has no requires and nothing depends on its provides`,
1920
+ tasks: [name]
1921
+ });
1922
+ }
1923
+ }
1924
+ }
1925
+ if (graph.settings.completion === "goal-reached" && !graph.settings.goal) {
1926
+ issues.push({
1927
+ severity: "error",
1928
+ code: "MISSING_GOAL",
1929
+ message: 'Completion strategy is "goal-reached" but no goal tokens are defined'
1930
+ });
1931
+ }
1932
+ return buildResult(issues);
1933
+ }
1934
+ function buildResult(issues) {
1935
+ const errors = issues.filter((i) => i.severity === "error");
1936
+ const warnings = issues.filter((i) => i.severity === "warning");
1937
+ return {
1938
+ valid: errors.length === 0,
1939
+ issues,
1940
+ errors,
1941
+ warnings
1942
+ };
1943
+ }
1944
+
1738
1945
  // src/stores/localStorage.ts
1739
1946
  var LocalStorageStore = class {
1740
1947
  prefix;
@@ -2142,6 +2349,7 @@ exports.next = next;
2142
2349
  exports.planExecution = planExecution;
2143
2350
  exports.resolveConfigTemplates = resolveConfigTemplates;
2144
2351
  exports.resolveVariables = resolveVariables;
2352
+ exports.validateGraph = validateGraph;
2145
2353
  exports.validateGraphConfig = validateGraphConfig;
2146
2354
  exports.validateStepFlowConfig = validateStepFlowConfig;
2147
2355
  //# sourceMappingURL=index.cjs.map