@eventcatalog/core 4.7.4 → 4.7.5

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.
Files changed (37) hide show
  1. package/dist/analytics/analytics.cjs +1 -1
  2. package/dist/analytics/analytics.js +2 -2
  3. package/dist/analytics/log-build.cjs +1 -1
  4. package/dist/analytics/log-build.js +3 -3
  5. package/dist/{chunk-FDXJIZ74.js → chunk-2BYTIH4L.js} +1 -1
  6. package/dist/{chunk-7QTCSRAC.js → chunk-EUZT4RKY.js} +1 -1
  7. package/dist/{chunk-RIUHZZRA.js → chunk-GYYYAUJD.js} +128 -56
  8. package/dist/chunk-JJPB6EOZ.js +159 -0
  9. package/dist/{chunk-ZIABX6SP.js → chunk-QJRY6Q7S.js} +1 -1
  10. package/dist/{chunk-ZXVQXBTT.js → chunk-QQPH6RCB.js} +24 -1
  11. package/dist/chunk-WRNH5C3U.js +118 -0
  12. package/dist/{chunk-TLWM7WRZ.js → chunk-ZBPMCEXS.js} +1 -1
  13. package/dist/constants.cjs +1 -1
  14. package/dist/constants.js +1 -1
  15. package/dist/eventcatalog.cjs +540 -194
  16. package/dist/eventcatalog.config.d.cts +9 -3
  17. package/dist/eventcatalog.config.d.ts +9 -3
  18. package/dist/eventcatalog.js +37 -39
  19. package/dist/federation/diagnostics.cjs +187 -0
  20. package/dist/federation/diagnostics.d.cts +23 -0
  21. package/dist/federation/diagnostics.d.ts +23 -0
  22. package/dist/federation/diagnostics.js +14 -0
  23. package/dist/federation/federate.cjs +442 -117
  24. package/dist/federation/federate.d.cts +8 -1
  25. package/dist/federation/federate.d.ts +8 -1
  26. package/dist/federation/federate.js +6 -2
  27. package/dist/federation/output-transaction.cjs +152 -0
  28. package/dist/federation/output-transaction.d.cts +3 -0
  29. package/dist/federation/output-transaction.d.ts +3 -0
  30. package/dist/federation/output-transaction.js +6 -0
  31. package/dist/generate.cjs +24 -1
  32. package/dist/generate.js +3 -3
  33. package/dist/utils/cli-logger.cjs +24 -1
  34. package/dist/utils/cli-logger.d.cts +6 -0
  35. package/dist/utils/cli-logger.d.ts +6 -0
  36. package/dist/utils/cli-logger.js +2 -2
  37. package/package.json +3 -3
@@ -29,10 +29,10 @@ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
29
29
  // src/eventcatalog.ts
30
30
  var import_commander = require("commander");
31
31
  var import_node_child_process2 = require("child_process");
32
- var import_node_path16 = require("path");
32
+ var import_node_path17 = require("path");
33
33
  var import_node_http = __toESM(require("http"), 1);
34
34
  var import_fs3 = __toESM(require("fs"), 1);
35
- var import_node_path17 = __toESM(require("path"), 1);
35
+ var import_node_path18 = __toESM(require("path"), 1);
36
36
  var import_node_url = require("url");
37
37
 
38
38
  // src/generate.js
@@ -144,7 +144,7 @@ var verifyRequiredFieldsAreInCatalogConfigFile = async (projectDirectory) => {
144
144
  var import_picocolors = __toESM(require("picocolors"), 1);
145
145
 
146
146
  // package.json
147
- var version = "4.7.4";
147
+ var version = "4.7.5";
148
148
 
149
149
  // src/constants.ts
150
150
  var VERSION = version;
@@ -183,6 +183,29 @@ var logger = {
183
183
  warning: (message, tag = "warn") => {
184
184
  console.log(formatMessage(tag, message, import_picocolors.default.yellow));
185
185
  },
186
+ line: (message = "") => {
187
+ console.log(message);
188
+ },
189
+ diagnostic: (severity, message, rule, attributes = []) => {
190
+ const isError = severity === "error";
191
+ const color = isError ? import_picocolors.default.red : import_picocolors.default.yellow;
192
+ const icon = isError ? "\u2716" : "\u26A0";
193
+ const label = `${icon} ${severity.padEnd(7)} ${message}`;
194
+ const ruleSpacing = " ".repeat(Math.max(2, 72 - label.length));
195
+ console.log(` ${color(icon)} ${color(severity.padEnd(7))} ${message}${ruleSpacing}${import_picocolors.default.gray(rule)}`);
196
+ for (const attribute of attributes) {
197
+ console.log(` - ${import_picocolors.default.dim(`${attribute.label}:`)} ${attribute.value}`);
198
+ }
199
+ },
200
+ diagnosticSummary: (errors, warnings) => {
201
+ const total = errors + warnings;
202
+ const color = errors > 0 ? import_picocolors.default.red : import_picocolors.default.yellow;
203
+ const icon = errors > 0 ? "\u2716" : "\u26A0";
204
+ const problems = `${total} problem${total === 1 ? "" : "s"}`;
205
+ const errorCount = `${errors} error${errors === 1 ? "" : "s"}`;
206
+ const warningCount = `${warnings} warning${warnings === 1 ? "" : "s"}`;
207
+ console.log(color(`${icon} ${problems} (${errorCount}, ${warningCount})`));
208
+ },
186
209
  dim: (message) => {
187
210
  console.log(import_picocolors.default.dim(message));
188
211
  }
@@ -1310,22 +1333,22 @@ function getAvroTypeName(type) {
1310
1333
  function walkAvroRecord(schema, prefix, fields) {
1311
1334
  if (!schema.fields || !Array.isArray(schema.fields)) return;
1312
1335
  for (const field of schema.fields) {
1313
- const path17 = prefix ? `${prefix}.${field.name}` : field.name;
1336
+ const path18 = prefix ? `${prefix}.${field.name}` : field.name;
1314
1337
  const isOptional = Array.isArray(field.type) && field.type.includes("null");
1315
1338
  const typeName = getAvroTypeName(field.type);
1316
1339
  fields.push({
1317
- path: path17,
1340
+ path: path18,
1318
1341
  type: typeName,
1319
1342
  description: field.doc || "",
1320
1343
  required: !isOptional
1321
1344
  });
1322
1345
  const innerType = Array.isArray(field.type) ? field.type.find((t) => typeof t === "object" && t.type === "record") : typeof field.type === "object" && field.type.type === "record" ? field.type : null;
1323
1346
  if (innerType) {
1324
- walkAvroRecord(innerType, path17, fields);
1347
+ walkAvroRecord(innerType, path18, fields);
1325
1348
  }
1326
1349
  const arrayType = Array.isArray(field.type) ? field.type.find((t) => typeof t === "object" && t.type === "array") : typeof field.type === "object" && field.type.type === "array" ? field.type : null;
1327
1350
  if (arrayType && typeof arrayType.items === "object" && arrayType.items.type === "record") {
1328
- walkAvroRecord(arrayType.items, `${path17}[]`, fields);
1351
+ walkAvroRecord(arrayType.items, `${path18}[]`, fields);
1329
1352
  }
1330
1353
  }
1331
1354
  }
@@ -1356,32 +1379,32 @@ function walkJsonSchema(node, prefix, requiredList, rootSchema, fields) {
1356
1379
  }
1357
1380
  if (!node.properties) return;
1358
1381
  for (const [name, prop] of Object.entries(node.properties)) {
1359
- const path17 = prefix ? `${prefix}.${name}` : name;
1382
+ const path18 = prefix ? `${prefix}.${name}` : name;
1360
1383
  const isRequired = requiredList.includes(name);
1361
1384
  if (prop.$ref) {
1362
1385
  const resolved = resolveLocalRef(prop.$ref, rootSchema);
1363
1386
  if (resolved) {
1364
1387
  const rawRefType = resolved.type || "object";
1365
1388
  const type2 = Array.isArray(rawRefType) ? [...rawRefType].sort().join(" | ") : rawRefType;
1366
- fields.push({ path: path17, type: type2, description: resolved.description || "", required: isRequired });
1389
+ fields.push({ path: path18, type: type2, description: resolved.description || "", required: isRequired });
1367
1390
  if (resolved.properties) {
1368
- walkJsonSchema(resolved, path17, resolved.required || [], rootSchema, fields);
1391
+ walkJsonSchema(resolved, path18, resolved.required || [], rootSchema, fields);
1369
1392
  }
1370
1393
  } else {
1371
- fields.push({ path: path17, type: "$ref", description: "", required: isRequired });
1394
+ fields.push({ path: path18, type: "$ref", description: "", required: isRequired });
1372
1395
  }
1373
1396
  continue;
1374
1397
  }
1375
1398
  const rawType = prop.type || (prop.enum ? "enum" : prop.$ref ? "$ref" : "object");
1376
1399
  const typeList = Array.isArray(rawType) ? rawType : [rawType];
1377
1400
  const type = Array.isArray(rawType) ? [...rawType].sort().join(" | ") : rawType;
1378
- fields.push({ path: path17, type, description: prop.description || "", required: isRequired });
1401
+ fields.push({ path: path18, type, description: prop.description || "", required: isRequired });
1379
1402
  if (typeList.includes("object") && prop.properties) {
1380
- walkJsonSchema(prop, path17, prop.required || [], rootSchema, fields);
1403
+ walkJsonSchema(prop, path18, prop.required || [], rootSchema, fields);
1381
1404
  }
1382
1405
  if (typeList.includes("array") && prop.items) {
1383
1406
  if (prop.items.type === "object" && prop.items.properties) {
1384
- walkJsonSchema(prop.items, `${path17}[]`, prop.items.required || [], rootSchema, fields);
1407
+ walkJsonSchema(prop.items, `${path18}[]`, prop.items.required || [], rootSchema, fields);
1385
1408
  }
1386
1409
  }
1387
1410
  }
@@ -1901,8 +1924,8 @@ var createAstroDevLineFilter = () => {
1901
1924
 
1902
1925
  // src/federation/federate.ts
1903
1926
  var import_node_crypto4 = require("crypto");
1904
- var import_promises8 = __toESM(require("fs/promises"), 1);
1905
- var import_node_path15 = __toESM(require("path"), 1);
1927
+ var import_promises9 = __toESM(require("fs/promises"), 1);
1928
+ var import_node_path16 = __toESM(require("path"), 1);
1906
1929
  var import_sdk3 = __toESM(require("@eventcatalog/sdk"), 1);
1907
1930
 
1908
1931
  // src/federation/content-cache.ts
@@ -1946,28 +1969,295 @@ var createFederationContentCache = (projectDirectory, options = {}) => {
1946
1969
  };
1947
1970
  };
1948
1971
 
1949
- // src/federation/public-assets.ts
1950
- var import_node_crypto2 = require("crypto");
1972
+ // src/federation/diagnostics.ts
1973
+ var federationRuleDefaults = {
1974
+ "federation/duplicate-source": "error",
1975
+ "federation/type-collision": "error",
1976
+ "federation/pointer-type-mismatch": "error",
1977
+ "federation/facet-disagreement": "error",
1978
+ "federation/asset-collision": "warn",
1979
+ "federation/missing-resource": "warn",
1980
+ "federation/unresolved-version": "warn"
1981
+ };
1982
+ var resolveFederationRules = (rules = {}) => {
1983
+ for (const [rule, level] of Object.entries(rules)) {
1984
+ if (!Object.hasOwn(federationRuleDefaults, rule)) throw new Error(`Unknown federation rule "${rule}".`);
1985
+ if (level !== "off" && level !== "warn" && level !== "error") {
1986
+ throw new Error(`Invalid level "${String(level)}" for federation rule "${rule}". Expected off, warn, or error.`);
1987
+ }
1988
+ }
1989
+ return { ...federationRuleDefaults, ...rules };
1990
+ };
1991
+ var getExternalResource = (external) => `${external.id}${external.version === void 0 ? "" : `@${external.version}`}`;
1992
+ var getVersionedResource = (id, version2) => `${id}${version2 == null ? "" : `@${version2}`}`;
1993
+ var getDocumentedResourceType = (type) => {
1994
+ const usesAn = type === "adr" || type === "agent" || type === "entity" || type === "event";
1995
+ return `documented as ${usesAn ? "an" : "a"} ${type}`;
1996
+ };
1997
+ var getConflictDiagnostic = (conflict, graph) => {
1998
+ const catalogs = { label: "catalogs", value: conflict.sources.join(", ") };
1999
+ switch (conflict.kind) {
2000
+ case "duplicate-source":
2001
+ return {
2002
+ severity: "error",
2003
+ message: "Resource has multiple owners",
2004
+ rule: "federation/duplicate-source",
2005
+ attributes: [
2006
+ { label: "resource", value: conflict.id },
2007
+ catalogs,
2008
+ { label: "resolution", value: "assign a single owning catalog" }
2009
+ ]
2010
+ };
2011
+ case "type-collision": {
2012
+ const types = [
2013
+ ...new Map(
2014
+ graph.entities.filter((entity) => entity.id === conflict.id).map(
2015
+ (entity) => [
2016
+ `${entity.resolvedFrom.source}:${entity.type}`,
2017
+ { label: entity.resolvedFrom.source, value: getDocumentedResourceType(entity.type) }
2018
+ ]
2019
+ )
2020
+ ).values()
2021
+ ];
2022
+ return {
2023
+ severity: "error",
2024
+ message: "Resource ID has conflicting types",
2025
+ rule: "federation/type-collision",
2026
+ attributes: [{ label: "resource", value: conflict.id }, ...types.length > 0 ? types : [catalogs]]
2027
+ };
2028
+ }
2029
+ case "pointer-type-mismatch": {
2030
+ const typeMismatch = /^Expected (.+) but found (.+)$/.exec(conflict.detail ?? "");
2031
+ return {
2032
+ severity: "error",
2033
+ message: "Reference type does not match resource",
2034
+ rule: "federation/pointer-type-mismatch",
2035
+ attributes: [
2036
+ { label: "resource", value: conflict.id },
2037
+ ...typeMismatch ? [
2038
+ { label: "expected type", value: typeMismatch[1] },
2039
+ { label: "actual type", value: typeMismatch[2] }
2040
+ ] : conflict.detail ? [{ label: "detail", value: conflict.detail }] : [],
2041
+ catalogs
2042
+ ]
2043
+ };
2044
+ }
2045
+ case "facet-disagreement":
2046
+ return {
2047
+ severity: "error",
2048
+ message: "Catalogs disagree about this resource",
2049
+ rule: "federation/facet-disagreement",
2050
+ attributes: [
2051
+ { label: "resource", value: conflict.id },
2052
+ ...conflict.detail ? [{ label: "detail", value: conflict.detail }] : [],
2053
+ catalogs
2054
+ ]
2055
+ };
2056
+ }
2057
+ };
2058
+ var getFederationDiagnostics = (graph, rules = {}) => {
2059
+ const resolvedRules = resolveFederationRules(rules);
2060
+ const diagnostics = graph.conflicts.map((conflict) => getConflictDiagnostic(conflict, graph));
2061
+ for (const external of graph.externals) {
2062
+ const resource = getExternalResource(external);
2063
+ for (const referencedBy of external.referencedBy) {
2064
+ const referrer = graph.entities.find((entity) => entity.id === referencedBy);
2065
+ diagnostics.push({
2066
+ severity: "warning",
2067
+ message: "Referenced EventCatalog resource does not exist",
2068
+ rule: "federation/missing-resource",
2069
+ attributes: [
2070
+ { label: "source catalog", value: referrer?.resolvedFrom.source ?? "unknown" },
2071
+ { label: "referenced by", value: referencedBy },
2072
+ { label: "missing resource", value: resource }
2073
+ ]
2074
+ });
2075
+ }
2076
+ }
2077
+ for (const edge of graph.edges.filter((edge2) => edge2.status === "unresolved")) {
2078
+ const availableVersions = [
2079
+ ...new Set(graph.entities.filter((entity) => entity.id === edge.to).map((entity) => entity.version ?? "unversioned"))
2080
+ ].sort((left, right) => left.localeCompare(right, void 0, { numeric: true }));
2081
+ diagnostics.push({
2082
+ severity: "warning",
2083
+ message: "Referenced EventCatalog resource version does not exist",
2084
+ rule: "federation/unresolved-version",
2085
+ attributes: [
2086
+ { label: "source catalog", value: edge.fromResolvedFrom.source },
2087
+ { label: "referenced by", value: getVersionedResource(edge.from, edge.fromVersion) },
2088
+ { label: "resource", value: edge.to },
2089
+ { label: "requested version", value: edge.pointer ?? "unspecified" },
2090
+ { label: "available versions", value: availableVersions.join(", ") }
2091
+ ]
2092
+ });
2093
+ }
2094
+ for (const warning of graph.warnings) {
2095
+ diagnostics.push({
2096
+ severity: "warning",
2097
+ message: "Asset collision",
2098
+ rule: "federation/asset-collision",
2099
+ attributes: [
2100
+ { label: "asset", value: warning.path },
2101
+ { label: "sources", value: warning.sources.join(", ") },
2102
+ { label: "winner", value: warning.winner },
2103
+ { label: "resolution", value: "last configured source wins" }
2104
+ ]
2105
+ });
2106
+ }
2107
+ return diagnostics.flatMap((diagnostic) => {
2108
+ const level = resolvedRules[diagnostic.rule];
2109
+ if (level === "off") return [];
2110
+ return [{ ...diagnostic, severity: level === "error" ? "error" : "warning" }];
2111
+ }).sort(
2112
+ (left, right) => (left.severity === right.severity ? 0 : left.severity === "error" ? -1 : 1) || left.rule.localeCompare(right.rule) || (left.attributes[0]?.value ?? "").localeCompare(right.attributes[0]?.value ?? "")
2113
+ );
2114
+ };
2115
+ var getFederationDiagnosticCounts = (diagnostics) => diagnostics.reduce(
2116
+ (counts, diagnostic) => ({
2117
+ errors: counts.errors + (diagnostic.severity === "error" ? 1 : 0),
2118
+ warnings: counts.warnings + (diagnostic.severity === "warning" ? 1 : 0)
2119
+ }),
2120
+ { errors: 0, warnings: 0 }
2121
+ );
2122
+ var getVisibleFederationDiagnostics = (diagnostics, verbose) => diagnostics.filter((diagnostic) => verbose || diagnostic.severity === "error");
2123
+
2124
+ // src/federation/output-transaction.ts
1951
2125
  var import_promises5 = __toESM(require("fs/promises"), 1);
1952
2126
  var import_node_path12 = __toESM(require("path"), 1);
2127
+ var getSafePublicPath = (publicDirectory, relativePath) => {
2128
+ const normalizedPath = import_node_path12.default.posix.normalize(relativePath);
2129
+ const unsafe = relativePath.length === 0 || relativePath.includes("\0") || relativePath.includes("\\") || import_node_path12.default.posix.isAbsolute(relativePath) || /^[a-zA-Z]:\//.test(relativePath) || normalizedPath !== relativePath || normalizedPath === ".." || normalizedPath.startsWith("../");
2130
+ if (unsafe) return void 0;
2131
+ const destinationPath = import_node_path12.default.resolve(publicDirectory, relativePath);
2132
+ const relativeDestinationPath = import_node_path12.default.relative(publicDirectory, destinationPath);
2133
+ return relativeDestinationPath !== "" && relativeDestinationPath !== ".." && !relativeDestinationPath.startsWith(`..${import_node_path12.default.sep}`) && !import_node_path12.default.isAbsolute(relativeDestinationPath) ? destinationPath : void 0;
2134
+ };
2135
+ var beginFederationOutputTransaction = async (projectDirectory, outDir, relativePublicPaths) => {
2136
+ const transactionDirectory = await import_promises5.default.mkdtemp(import_node_path12.default.join(projectDirectory, ".eventcatalog-federation-transaction-"));
2137
+ const previousOutDir = import_node_path12.default.join(transactionDirectory, "federated");
2138
+ const publicBackupDirectory = import_node_path12.default.join(transactionDirectory, "public");
2139
+ const publicDirectory = import_node_path12.default.resolve(projectDirectory, "public");
2140
+ const publicSnapshots = [];
2141
+ const missingPublicDirectories = /* @__PURE__ */ new Set();
2142
+ let hadPreviousOutDir = false;
2143
+ try {
2144
+ for (const relativePath of new Set(relativePublicPaths)) {
2145
+ const destinationPath = getSafePublicPath(publicDirectory, relativePath);
2146
+ if (!destinationPath) continue;
2147
+ try {
2148
+ const stat = await import_promises5.default.lstat(destinationPath);
2149
+ if (!stat.isFile()) continue;
2150
+ const backupPath = import_node_path12.default.join(publicBackupDirectory, `${publicSnapshots.length}`);
2151
+ await import_promises5.default.mkdir(import_node_path12.default.dirname(backupPath), { recursive: true });
2152
+ await import_promises5.default.copyFile(destinationPath, backupPath);
2153
+ publicSnapshots.push({ backupPath, destinationPath, state: "file" });
2154
+ } catch (error) {
2155
+ const code = error.code;
2156
+ if (code === "ENOTDIR") continue;
2157
+ if (code !== "ENOENT") throw error;
2158
+ publicSnapshots.push({ destinationPath, state: "missing" });
2159
+ let directory = import_node_path12.default.dirname(destinationPath);
2160
+ while (directory !== publicDirectory) {
2161
+ try {
2162
+ await import_promises5.default.lstat(directory);
2163
+ break;
2164
+ } catch (directoryError) {
2165
+ const directoryCode = directoryError.code;
2166
+ if (directoryCode === "ENOTDIR") break;
2167
+ if (directoryCode !== "ENOENT") throw directoryError;
2168
+ missingPublicDirectories.add(directory);
2169
+ directory = import_node_path12.default.dirname(directory);
2170
+ }
2171
+ }
2172
+ }
2173
+ }
2174
+ try {
2175
+ await import_promises5.default.rename(outDir, previousOutDir);
2176
+ hadPreviousOutDir = true;
2177
+ } catch (error) {
2178
+ if (error.code !== "ENOENT") throw error;
2179
+ }
2180
+ } catch (error) {
2181
+ await import_promises5.default.rm(transactionDirectory, { recursive: true, force: true });
2182
+ throw error;
2183
+ }
2184
+ return {
2185
+ commit: async () => {
2186
+ await import_promises5.default.rm(transactionDirectory, { recursive: true, force: true }).catch(() => void 0);
2187
+ },
2188
+ rollback: async () => {
2189
+ const rollbackErrors = [];
2190
+ const attempt = async (operation) => {
2191
+ try {
2192
+ await operation();
2193
+ } catch (error) {
2194
+ rollbackErrors.push(error);
2195
+ }
2196
+ };
2197
+ await attempt(() => import_promises5.default.rm(outDir, { recursive: true, force: true }));
2198
+ if (hadPreviousOutDir) await attempt(() => import_promises5.default.rename(previousOutDir, outDir));
2199
+ for (const snapshot of publicSnapshots) {
2200
+ if (snapshot.state === "missing") {
2201
+ await attempt(() => import_promises5.default.rm(snapshot.destinationPath, { force: true }));
2202
+ continue;
2203
+ }
2204
+ await attempt(async () => {
2205
+ await import_promises5.default.mkdir(import_node_path12.default.dirname(snapshot.destinationPath), { recursive: true });
2206
+ await import_promises5.default.copyFile(snapshot.backupPath, snapshot.destinationPath);
2207
+ });
2208
+ }
2209
+ for (const directory of [...missingPublicDirectories].sort((left, right) => right.length - left.length)) {
2210
+ await attempt(async () => {
2211
+ try {
2212
+ await import_promises5.default.rmdir(directory);
2213
+ } catch (error) {
2214
+ if (!["ENOENT", "ENOTEMPTY"].includes(error.code ?? "")) throw error;
2215
+ }
2216
+ });
2217
+ }
2218
+ await attempt(() => import_promises5.default.rm(transactionDirectory, { recursive: true, force: true }));
2219
+ if (rollbackErrors.length > 0) throw new AggregateError(rollbackErrors, "Failed to restore the previous federation output");
2220
+ }
2221
+ };
2222
+ };
2223
+ var withFederationOutputTransaction = async (projectDirectory, outDir, relativePublicPaths, update) => {
2224
+ const transaction = await beginFederationOutputTransaction(projectDirectory, outDir, relativePublicPaths);
2225
+ try {
2226
+ const result = await update();
2227
+ await transaction.commit();
2228
+ return result;
2229
+ } catch (error) {
2230
+ try {
2231
+ await transaction.rollback();
2232
+ } catch (rollbackError) {
2233
+ throw new AggregateError([error, rollbackError], "Federation failed and the previous output could not be restored");
2234
+ }
2235
+ throw error;
2236
+ }
2237
+ };
2238
+
2239
+ // src/federation/public-assets.ts
2240
+ var import_node_crypto2 = require("crypto");
2241
+ var import_promises6 = __toESM(require("fs/promises"), 1);
2242
+ var import_node_path13 = __toESM(require("path"), 1);
1953
2243
  var getContentHash2 = (content) => `sha256:${(0, import_node_crypto2.createHash)("sha256").update(content).digest("hex")}`;
1954
2244
  var getFileHash = async (filePath) => {
1955
2245
  try {
1956
- const stat = await import_promises5.default.lstat(filePath);
1957
- return stat.isFile() ? getContentHash2(await import_promises5.default.readFile(filePath)) : void 0;
2246
+ const stat = await import_promises6.default.lstat(filePath);
2247
+ return stat.isFile() ? getContentHash2(await import_promises6.default.readFile(filePath)) : void 0;
1958
2248
  } catch (error) {
1959
2249
  if (error.code === "ENOENT") return void 0;
1960
2250
  throw error;
1961
2251
  }
1962
2252
  };
1963
2253
  var getSafePath = (directory, relativePath) => {
1964
- const normalizedPath = import_node_path12.default.posix.normalize(relativePath);
1965
- const unsafe = relativePath.length === 0 || relativePath.includes("\0") || relativePath.includes("\\") || import_node_path12.default.posix.isAbsolute(relativePath) || /^[a-zA-Z]:\//.test(relativePath) || normalizedPath !== relativePath || normalizedPath === ".." || normalizedPath.startsWith("../");
2254
+ const normalizedPath = import_node_path13.default.posix.normalize(relativePath);
2255
+ const unsafe = relativePath.length === 0 || relativePath.includes("\0") || relativePath.includes("\\") || import_node_path13.default.posix.isAbsolute(relativePath) || /^[a-zA-Z]:\//.test(relativePath) || normalizedPath !== relativePath || normalizedPath === ".." || normalizedPath.startsWith("../");
1966
2256
  if (unsafe) return void 0;
1967
- const resolvedDirectory = import_node_path12.default.resolve(directory);
1968
- const resolvedPath = import_node_path12.default.resolve(resolvedDirectory, relativePath);
1969
- const relativeResolvedPath = import_node_path12.default.relative(resolvedDirectory, resolvedPath);
1970
- if (relativeResolvedPath === "" || relativeResolvedPath === ".." || relativeResolvedPath.startsWith(`..${import_node_path12.default.sep}`) || import_node_path12.default.isAbsolute(relativeResolvedPath)) {
2257
+ const resolvedDirectory = import_node_path13.default.resolve(directory);
2258
+ const resolvedPath = import_node_path13.default.resolve(resolvedDirectory, relativePath);
2259
+ const relativeResolvedPath = import_node_path13.default.relative(resolvedDirectory, resolvedPath);
2260
+ if (relativeResolvedPath === "" || relativeResolvedPath === ".." || relativeResolvedPath.startsWith(`..${import_node_path13.default.sep}`) || import_node_path13.default.isAbsolute(relativeResolvedPath)) {
1971
2261
  return void 0;
1972
2262
  }
1973
2263
  return resolvedPath;
@@ -1975,23 +2265,23 @@ var getSafePath = (directory, relativePath) => {
1975
2265
  var listFiles = async (directory, relativeDirectory = "") => {
1976
2266
  let entries;
1977
2267
  try {
1978
- entries = await import_promises5.default.readdir(import_node_path12.default.join(directory, relativeDirectory), { withFileTypes: true });
2268
+ entries = await import_promises6.default.readdir(import_node_path13.default.join(directory, relativeDirectory), { withFileTypes: true });
1979
2269
  } catch (error) {
1980
2270
  if (error.code === "ENOENT") return [];
1981
2271
  throw error;
1982
2272
  }
1983
2273
  const files = await Promise.all(
1984
2274
  entries.map(async (entry) => {
1985
- const relativePath = import_node_path12.default.join(relativeDirectory, entry.name);
2275
+ const relativePath = import_node_path13.default.join(relativeDirectory, entry.name);
1986
2276
  if (entry.isDirectory()) return listFiles(directory, relativePath);
1987
- return entry.isFile() ? [relativePath.split(import_node_path12.default.sep).join("/")] : [];
2277
+ return entry.isFile() ? [relativePath.split(import_node_path13.default.sep).join("/")] : [];
1988
2278
  })
1989
2279
  );
1990
2280
  return files.flat().sort();
1991
2281
  };
1992
2282
  var pathExists = async (filePath) => {
1993
2283
  try {
1994
- await import_promises5.default.lstat(filePath);
2284
+ await import_promises6.default.lstat(filePath);
1995
2285
  return true;
1996
2286
  } catch (error) {
1997
2287
  if (error.code === "ENOENT") return false;
@@ -1999,27 +2289,27 @@ var pathExists = async (filePath) => {
1999
2289
  }
2000
2290
  };
2001
2291
  var hasBlockingParent = async (publicDirectory, destinationPath) => {
2002
- let currentPath = import_node_path12.default.dirname(destinationPath);
2292
+ let currentPath = import_node_path13.default.dirname(destinationPath);
2003
2293
  while (currentPath !== publicDirectory) {
2004
2294
  try {
2005
- if (!(await import_promises5.default.lstat(currentPath)).isDirectory()) return true;
2295
+ if (!(await import_promises6.default.lstat(currentPath)).isDirectory()) return true;
2006
2296
  } catch (error) {
2007
2297
  if (error.code !== "ENOENT") throw error;
2008
2298
  }
2009
- currentPath = import_node_path12.default.dirname(currentPath);
2299
+ currentPath = import_node_path13.default.dirname(currentPath);
2010
2300
  }
2011
2301
  return false;
2012
2302
  };
2013
2303
  var pruneEmptyDirectories = async (publicDirectory, filePath) => {
2014
- let currentPath = import_node_path12.default.dirname(filePath);
2304
+ let currentPath = import_node_path13.default.dirname(filePath);
2015
2305
  while (currentPath !== publicDirectory) {
2016
2306
  try {
2017
- await import_promises5.default.rmdir(currentPath);
2307
+ await import_promises6.default.rmdir(currentPath);
2018
2308
  } catch (error) {
2019
2309
  if (!["ENOENT", "ENOTEMPTY"].includes(error.code ?? "")) throw error;
2020
2310
  if (error.code === "ENOTEMPTY") return;
2021
2311
  }
2022
- currentPath = import_node_path12.default.dirname(currentPath);
2312
+ currentPath = import_node_path13.default.dirname(currentPath);
2023
2313
  }
2024
2314
  };
2025
2315
  var composePublicAssets = async ({
@@ -2029,8 +2319,8 @@ var composePublicAssets = async ({
2029
2319
  previousFiles = {},
2030
2320
  collisionPaths = /* @__PURE__ */ new Set()
2031
2321
  }) => {
2032
- const publicDirectory = import_node_path12.default.resolve(projectDirectory, "public");
2033
- const federatedPublicDirectory = import_node_path12.default.resolve(federatedDirectory, "public");
2322
+ const publicDirectory = import_node_path13.default.resolve(projectDirectory, "public");
2323
+ const federatedPublicDirectory = import_node_path13.default.resolve(federatedDirectory, "public");
2034
2324
  const sourceFiles = await listFiles(federatedPublicDirectory);
2035
2325
  const sourceFileSet = new Set(sourceFiles);
2036
2326
  const managedFiles = /* @__PURE__ */ new Set();
@@ -2043,7 +2333,7 @@ var composePublicAssets = async ({
2043
2333
  if (sourceFileSet.has(relativePath)) continue;
2044
2334
  const destinationPath = getSafePath(publicDirectory, relativePath);
2045
2335
  if (!destinationPath) continue;
2046
- await import_promises5.default.rm(destinationPath, { force: true });
2336
+ await import_promises6.default.rm(destinationPath, { force: true });
2047
2337
  await pruneEmptyDirectories(publicDirectory, destinationPath);
2048
2338
  removed += 1;
2049
2339
  }
@@ -2065,31 +2355,31 @@ var composePublicAssets = async ({
2065
2355
  }
2066
2356
  const asset = publicAssetsByPath.get(relativePath);
2067
2357
  if (!asset) throw new Error(`Cannot identify the source of federated public asset "${relativePath}"`);
2068
- const content = await import_promises5.default.readFile(sourcePath);
2069
- await import_promises5.default.mkdir(import_node_path12.default.dirname(destinationPath), { recursive: true });
2070
- await import_promises5.default.writeFile(destinationPath, content);
2358
+ const content = await import_promises6.default.readFile(sourcePath);
2359
+ await import_promises6.default.mkdir(import_node_path13.default.dirname(destinationPath), { recursive: true });
2360
+ await import_promises6.default.writeFile(destinationPath, content);
2071
2361
  files[relativePath] = { source: asset.resolvedFrom.source, hash: getContentHash2(content) };
2072
2362
  copied += 1;
2073
2363
  if (collisionPaths.has(`public/${relativePath}`)) overwritten += 1;
2074
2364
  }
2075
- await import_promises5.default.rm(federatedPublicDirectory, { recursive: true, force: true });
2365
+ await import_promises6.default.rm(federatedPublicDirectory, { recursive: true, force: true });
2076
2366
  return { files, copied, skipped, overwritten, removed };
2077
2367
  };
2078
2368
 
2079
2369
  // src/federation/filesystem-source-provider.ts
2080
2370
  var import_node_crypto3 = require("crypto");
2081
- var import_promises6 = __toESM(require("fs/promises"), 1);
2082
- var import_node_path13 = __toESM(require("path"), 1);
2371
+ var import_promises7 = __toESM(require("fs/promises"), 1);
2372
+ var import_node_path14 = __toESM(require("path"), 1);
2083
2373
  var import_sdk = __toESM(require("@eventcatalog/sdk"), 1);
2084
2374
  var FILESYSTEM_SOURCE_PREFIX = "file:";
2085
2375
  var isWithinDirectory = (directory, target) => {
2086
- const relativePath = import_node_path13.default.relative(directory, target);
2087
- return relativePath === "" || !relativePath.startsWith(`..${import_node_path13.default.sep}`) && relativePath !== ".." && !import_node_path13.default.isAbsolute(relativePath);
2376
+ const relativePath = import_node_path14.default.relative(directory, target);
2377
+ return relativePath === "" || !relativePath.startsWith(`..${import_node_path14.default.sep}`) && relativePath !== ".." && !import_node_path14.default.isAbsolute(relativePath);
2088
2378
  };
2089
2379
  var assertPortableRelativePath = (filePath, label, allowRoot = true) => {
2090
2380
  const portablePath = filePath.replaceAll("\\", "/");
2091
- const normalizedPath = import_node_path13.default.posix.normalize(portablePath);
2092
- const isUnsafe = filePath.includes("\\") || filePath.includes("\0") || import_node_path13.default.posix.isAbsolute(normalizedPath) || /^[a-zA-Z]:\//.test(normalizedPath) || normalizedPath === ".." || normalizedPath.startsWith("../") || !allowRoot && (normalizedPath === "." || normalizedPath === "");
2381
+ const normalizedPath = import_node_path14.default.posix.normalize(portablePath);
2382
+ const isUnsafe = filePath.includes("\\") || filePath.includes("\0") || import_node_path14.default.posix.isAbsolute(normalizedPath) || /^[a-zA-Z]:\//.test(normalizedPath) || normalizedPath === ".." || normalizedPath.startsWith("../") || !allowRoot && (normalizedPath === "." || normalizedPath === "");
2093
2383
  if (isUnsafe) throw new Error(`${label} "${filePath}" escapes its filesystem source`);
2094
2384
  return normalizedPath;
2095
2385
  };
@@ -2099,22 +2389,22 @@ var getSourceRoot = (projectDirectory, source) => {
2099
2389
  }
2100
2390
  const locator = source.source.slice(FILESYSTEM_SOURCE_PREFIX.length);
2101
2391
  if (!locator.trim()) throw new Error(`Filesystem federation source "${source.id}" requires a path after "file:".`);
2102
- return import_node_path13.default.resolve(projectDirectory, locator);
2392
+ return import_node_path14.default.resolve(projectDirectory, locator);
2103
2393
  };
2104
2394
  var getCatalogDirectory = async (projectDirectory, source) => {
2105
2395
  if (source.ref) throw new Error(`Filesystem federation source "${source.id}" does not support "ref".`);
2106
2396
  const sourceRoot = getSourceRoot(projectDirectory, source);
2107
2397
  const catalogPath = assertPortableRelativePath(source.path ?? ".", "Catalog path");
2108
- const catalogDirectory = import_node_path13.default.resolve(sourceRoot, ...catalogPath.split("/"));
2398
+ const catalogDirectory = import_node_path14.default.resolve(sourceRoot, ...catalogPath.split("/"));
2109
2399
  if (!isWithinDirectory(sourceRoot, catalogDirectory)) {
2110
2400
  throw new Error(`Catalog path "${source.path}" escapes source "${source.id}"`);
2111
2401
  }
2112
2402
  try {
2113
- const [realSourceRoot, realCatalogDirectory] = await Promise.all([import_promises6.default.realpath(sourceRoot), import_promises6.default.realpath(catalogDirectory)]);
2403
+ const [realSourceRoot, realCatalogDirectory] = await Promise.all([import_promises7.default.realpath(sourceRoot), import_promises7.default.realpath(catalogDirectory)]);
2114
2404
  if (!isWithinDirectory(realSourceRoot, realCatalogDirectory)) {
2115
2405
  throw new Error(`Catalog path "${source.path}" escapes source "${source.id}"`);
2116
2406
  }
2117
- if (!(await import_promises6.default.stat(realCatalogDirectory)).isDirectory()) {
2407
+ if (!(await import_promises7.default.stat(realCatalogDirectory)).isDirectory()) {
2118
2408
  throw new Error(`Filesystem federation source "${source.id}" is not a directory: ${catalogDirectory}`);
2119
2409
  }
2120
2410
  return realCatalogDirectory;
@@ -2127,12 +2417,12 @@ var getCatalogDirectory = async (projectDirectory, source) => {
2127
2417
  };
2128
2418
  var getArtifactPath = async (catalogDirectory, source, artifactPath) => {
2129
2419
  const normalizedPath = assertPortableRelativePath(artifactPath, "Federated artifact path", false);
2130
- const filePath = import_node_path13.default.resolve(catalogDirectory, ...normalizedPath.split("/"));
2420
+ const filePath = import_node_path14.default.resolve(catalogDirectory, ...normalizedPath.split("/"));
2131
2421
  if (!isWithinDirectory(catalogDirectory, filePath)) {
2132
2422
  throw new Error(`Federated artifact path "${artifactPath}" escapes source "${source.id}"`);
2133
2423
  }
2134
2424
  try {
2135
- const realFilePath = await import_promises6.default.realpath(filePath);
2425
+ const realFilePath = await import_promises7.default.realpath(filePath);
2136
2426
  if (!isWithinDirectory(catalogDirectory, realFilePath)) {
2137
2427
  throw new Error(`Federated artifact path "${artifactPath}" escapes source "${source.id}"`);
2138
2428
  }
@@ -2159,15 +2449,15 @@ var createFileSystemSourceProvider = (projectDirectory) => ({
2159
2449
  },
2160
2450
  async fetchContent({ source, path: artifactPath }) {
2161
2451
  const catalogDirectory = await getCatalogDirectory(projectDirectory, source);
2162
- return import_promises6.default.readFile(await getArtifactPath(catalogDirectory, source, artifactPath));
2452
+ return import_promises7.default.readFile(await getArtifactPath(catalogDirectory, source, artifactPath));
2163
2453
  }
2164
2454
  });
2165
2455
 
2166
2456
  // src/federation/github-source-provider.ts
2167
2457
  var import_node_child_process = require("child_process");
2168
- var import_promises7 = __toESM(require("fs/promises"), 1);
2458
+ var import_promises8 = __toESM(require("fs/promises"), 1);
2169
2459
  var import_node_os4 = __toESM(require("os"), 1);
2170
- var import_node_path14 = __toESM(require("path"), 1);
2460
+ var import_node_path15 = __toESM(require("path"), 1);
2171
2461
  var import_node_util = require("util");
2172
2462
  var import_sdk2 = __toESM(require("@eventcatalog/sdk"), 1);
2173
2463
  var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
@@ -2178,8 +2468,8 @@ var parseGitHubSource = (source) => {
2178
2468
  };
2179
2469
  var assertSafeCatalogPath = (source) => {
2180
2470
  const catalogPath = source.path ?? ".";
2181
- const normalized = import_node_path14.default.posix.normalize(catalogPath.replaceAll("\\", "/"));
2182
- if (catalogPath.includes("\\") || import_node_path14.default.posix.isAbsolute(normalized) || normalized === ".." || normalized.startsWith("../")) {
2471
+ const normalized = import_node_path15.default.posix.normalize(catalogPath.replaceAll("\\", "/"));
2472
+ if (catalogPath.includes("\\") || import_node_path15.default.posix.isAbsolute(normalized) || normalized === ".." || normalized.startsWith("../")) {
2183
2473
  throw new Error(`Catalog path "${catalogPath}" escapes source "${source.id}"`);
2184
2474
  }
2185
2475
  };
@@ -2222,8 +2512,8 @@ var getGitEnvironment = (token) => {
2222
2512
  };
2223
2513
  var createCheckout = (executeFile, token) => async (source, ref, callback) => {
2224
2514
  const { owner, repository } = parseGitHubSource(source);
2225
- const catalogPath = import_node_path14.default.posix.normalize(source.path ?? ".");
2226
- const directory = await import_promises7.default.mkdtemp(import_node_path14.default.join(import_node_os4.default.tmpdir(), "eventcatalog-federation-"));
2515
+ const catalogPath = import_node_path15.default.posix.normalize(source.path ?? ".");
2516
+ const directory = await import_promises8.default.mkdtemp(import_node_path15.default.join(import_node_os4.default.tmpdir(), "eventcatalog-federation-"));
2227
2517
  const env = getGitEnvironment(token);
2228
2518
  try {
2229
2519
  const git = (args) => executeFile("git", args, { cwd: directory, env, encoding: "utf8" });
@@ -2237,22 +2527,22 @@ var createCheckout = (executeFile, token) => async (source, ref, callback) => {
2237
2527
  await git(["checkout", "--quiet", "--detach", "FETCH_HEAD"]);
2238
2528
  return await callback(directory);
2239
2529
  } finally {
2240
- await import_promises7.default.rm(directory, { recursive: true, force: true });
2530
+ await import_promises8.default.rm(directory, { recursive: true, force: true });
2241
2531
  }
2242
2532
  };
2243
2533
  var generateIndex = async (source, ref, checkout, executeFile) => checkout(source, ref, async (directory) => {
2244
2534
  const { stdout } = await executeFile("git", ["rev-parse", "HEAD"], { cwd: directory, encoding: "utf8" });
2245
2535
  const commit = stdout.trim();
2246
- const catalogDirectory = import_node_path14.default.resolve(directory, source.path ?? ".");
2247
- const relativeCatalogDirectory = import_node_path14.default.relative(directory, catalogDirectory);
2248
- if (relativeCatalogDirectory.startsWith("..") || import_node_path14.default.isAbsolute(relativeCatalogDirectory)) {
2536
+ const catalogDirectory = import_node_path15.default.resolve(directory, source.path ?? ".");
2537
+ const relativeCatalogDirectory = import_node_path15.default.relative(directory, catalogDirectory);
2538
+ if (relativeCatalogDirectory.startsWith("..") || import_node_path15.default.isAbsolute(relativeCatalogDirectory)) {
2249
2539
  throw new Error(`Catalog path "${source.path}" escapes source "${source.id}"`);
2250
2540
  }
2251
2541
  const index = await (0, import_sdk2.default)(catalogDirectory).buildIndex({ source: source.id, commit });
2252
2542
  return { bytes: Buffer.from(JSON.stringify(index)), index, commit, generated: true };
2253
2543
  });
2254
2544
  var fetchPublishedIndex = async (source, ref, fetcher, token) => {
2255
- const indexPath = import_node_path14.default.posix.join(source.path ?? ".", "catalog.index.json");
2545
+ const indexPath = import_node_path15.default.posix.join(source.path ?? ".", "catalog.index.json");
2256
2546
  const bytes = await fetchBytes(source, ref, indexPath, fetcher, token);
2257
2547
  if (!bytes) return void 0;
2258
2548
  const index = (0, import_sdk2.parseIndex)(JSON.parse(bytes.toString("utf8")));
@@ -2275,7 +2565,7 @@ var createGitHubSourceProvider = (options = {}) => {
2275
2565
  },
2276
2566
  async fetchContent({ source, commit, path: artifactPath }) {
2277
2567
  assertSafeCatalogPath(source);
2278
- const catalogPath = import_node_path14.default.posix.join(source.path ?? ".", artifactPath);
2568
+ const catalogPath = import_node_path15.default.posix.join(source.path ?? ".", artifactPath);
2279
2569
  const content = await fetchBytes(source, commit, catalogPath, fetcher, token);
2280
2570
  if (!content) throw new Error(`Federated artifact not found for "${source.id}": ${artifactPath}`);
2281
2571
  return content;
@@ -2313,6 +2603,16 @@ ${details}`);
2313
2603
  this.conflicts = conflicts;
2314
2604
  }
2315
2605
  };
2606
+ var FederationDiagnosticError = class extends Error {
2607
+ diagnostics;
2608
+ constructor(diagnostics) {
2609
+ const details = diagnostics.map((diagnostic) => `${diagnostic.rule}: ${diagnostic.message}`).join("\n");
2610
+ super(`Federation diagnostics prevent hydration:
2611
+ ${details}`);
2612
+ this.name = "FederationDiagnosticError";
2613
+ this.diagnostics = diagnostics;
2614
+ }
2615
+ };
2316
2616
  var validateSources = (sources) => {
2317
2617
  const ids = /* @__PURE__ */ new Set();
2318
2618
  for (const source of sources) {
@@ -2321,19 +2621,46 @@ var validateSources = (sources) => {
2321
2621
  ids.add(source.id);
2322
2622
  }
2323
2623
  };
2624
+ var createDiagnosticGraph = (graph, ownershipGraph, remoteSourceIds, localSourceId) => {
2625
+ const edges = ownershipGraph.edges.filter((edge) => remoteSourceIds.has(edge.fromResolvedFrom.source));
2626
+ const externalsByPointer = /* @__PURE__ */ new Map();
2627
+ for (const edge of edges.filter((edge2) => edge2.status === "external")) {
2628
+ const version2 = edge.pointer ?? void 0;
2629
+ const key = `${edge.to}@${version2 ?? ""}`;
2630
+ const external = externalsByPointer.get(key) ?? {
2631
+ id: edge.to,
2632
+ ...version2 === void 0 ? {} : { version: version2 },
2633
+ referencedBy: []
2634
+ };
2635
+ if (!external.referencedBy.includes(edge.from)) external.referencedBy.push(edge.from);
2636
+ externalsByPointer.set(key, external);
2637
+ }
2638
+ return {
2639
+ ...graph,
2640
+ entities: [
2641
+ ...graph.entities,
2642
+ ...ownershipGraph.entities.filter(
2643
+ (entity) => entity.resolvedFrom.source === localSourceId && entity.resolvedFrom.commit === "local"
2644
+ )
2645
+ ],
2646
+ edges,
2647
+ conflicts: ownershipGraph.conflicts,
2648
+ externals: [...externalsByPointer.values()]
2649
+ };
2650
+ };
2324
2651
  var writeLock = async (lockPath, lock) => {
2325
2652
  const temporaryPath = `${lockPath}.tmp-${process.pid}`;
2326
2653
  try {
2327
- await import_promises8.default.writeFile(temporaryPath, `${JSON.stringify(lock, null, 2)}
2654
+ await import_promises9.default.writeFile(temporaryPath, `${JSON.stringify(lock, null, 2)}
2328
2655
  `, "utf8");
2329
- await import_promises8.default.rename(temporaryPath, lockPath);
2656
+ await import_promises9.default.rename(temporaryPath, lockPath);
2330
2657
  } finally {
2331
- await import_promises8.default.rm(temporaryPath, { force: true });
2658
+ await import_promises9.default.rm(temporaryPath, { force: true });
2332
2659
  }
2333
2660
  };
2334
2661
  var readLock = async (lockPath) => {
2335
2662
  try {
2336
- return JSON.parse(await import_promises8.default.readFile(lockPath, "utf8"));
2663
+ return JSON.parse(await import_promises9.default.readFile(lockPath, "utf8"));
2337
2664
  } catch (error) {
2338
2665
  if (error.code === "ENOENT") return void 0;
2339
2666
  throw new Error(`Cannot read federation lock at "${lockPath}"`, { cause: error });
@@ -2341,7 +2668,7 @@ var readLock = async (lockPath) => {
2341
2668
  };
2342
2669
  var pathExists2 = async (filePath) => {
2343
2670
  try {
2344
- await import_promises8.default.access(filePath);
2671
+ await import_promises9.default.access(filePath);
2345
2672
  return true;
2346
2673
  } catch (error) {
2347
2674
  if (error.code === "ENOENT") return false;
@@ -2349,20 +2676,27 @@ var pathExists2 = async (filePath) => {
2349
2676
  }
2350
2677
  };
2351
2678
  var cleanupPreviousFederation = async (projectDirectory, onProgress) => {
2352
- const outDir = import_node_path15.default.join(projectDirectory, "federated");
2353
- const lockPath = import_node_path15.default.join(projectDirectory, "eventcatalog.lock");
2679
+ const outDir = import_node_path16.default.join(projectDirectory, "federated");
2680
+ const lockPath = import_node_path16.default.join(projectDirectory, "eventcatalog.lock");
2354
2681
  const previousLock = await readLock(lockPath);
2355
2682
  const hadFederatedOutput = await pathExists2(outDir);
2356
2683
  const hadLock = previousLock !== void 0;
2357
2684
  if (!hadFederatedOutput && !hadLock) return;
2358
- await import_promises8.default.rm(outDir, { recursive: true, force: true });
2359
- const publicResult = await composePublicAssets({
2685
+ const publicResult = await withFederationOutputTransaction(
2360
2686
  projectDirectory,
2361
- federatedDirectory: outDir,
2362
- assets: [],
2363
- previousFiles: previousLock?.publicFiles
2364
- });
2365
- await import_promises8.default.rm(lockPath, { force: true });
2687
+ outDir,
2688
+ Object.keys(previousLock?.publicFiles ?? {}),
2689
+ async () => {
2690
+ const result = await composePublicAssets({
2691
+ projectDirectory,
2692
+ federatedDirectory: outDir,
2693
+ assets: [],
2694
+ previousFiles: previousLock?.publicFiles
2695
+ });
2696
+ await import_promises9.default.rm(lockPath, { force: true });
2697
+ return result;
2698
+ }
2699
+ );
2366
2700
  onProgress?.({
2367
2701
  type: "cleanup:complete",
2368
2702
  federated: hadFederatedOutput,
@@ -2384,6 +2718,7 @@ var federateCatalog = async (projectDirectory, options = {}) => {
2384
2718
  );
2385
2719
  }
2386
2720
  validateSources(sources);
2721
+ const rules = resolveFederationRules(config.federation?.rules);
2387
2722
  if (options.useCache === false) options.onProgress?.({ type: "cache:disabled" });
2388
2723
  const provider = options.provider ?? createFederationSourceProvider(projectDirectory);
2389
2724
  const resolvedSources = [];
@@ -2407,8 +2742,8 @@ var federateCatalog = async (projectDirectory, options = {}) => {
2407
2742
  throw new Error(`Failed to federate source "${source.id}": ${message}`, { cause: error });
2408
2743
  }
2409
2744
  }
2410
- const outDir = import_node_path15.default.join(projectDirectory, "federated");
2411
- const lockPath = import_node_path15.default.join(projectDirectory, "eventcatalog.lock");
2745
+ const outDir = import_node_path16.default.join(projectDirectory, "federated");
2746
+ const lockPath = import_node_path16.default.join(projectDirectory, "eventcatalog.lock");
2412
2747
  const previousLock = await readLock(lockPath);
2413
2748
  const resources = resolvedSources.reduce((total, source) => total + source.resolved.index.resources.length, 0);
2414
2749
  const remoteIndexes = resolvedSources.map(({ resolved }) => resolved.index);
@@ -2422,60 +2757,79 @@ var federateCatalog = async (projectDirectory, options = {}) => {
2422
2757
  options.onProgress?.({ type: "local:complete", resources: localIndex.resources.length });
2423
2758
  options.onProgress?.({ type: "resolving", resources, localResources: localIndex.resources.length });
2424
2759
  const ownershipGraph = (0, import_sdk3.resolve)([localIndex, ...remoteIndexes]);
2425
- if (ownershipGraph.conflicts.length > 0) {
2426
- options.onProgress?.({ type: "resolved", graph: ownershipGraph });
2427
- throw new FederationConflictError(ownershipGraph.conflicts);
2428
- }
2429
2760
  const graph = (0, import_sdk3.resolve)(remoteIndexes);
2430
- options.onProgress?.({ type: "resolved", graph });
2431
- const sourcesById = new Map(sources.map((source) => [source.id, source]));
2432
- options.onProgress?.({ type: "hydrating", outDir });
2433
- let hydratedFiles = 0;
2434
- let cachedFiles = 0;
2435
- const hydrateResult = await (0, import_sdk3.hydrate)(graph, {
2761
+ const diagnosticGraph = createDiagnosticGraph(
2762
+ graph,
2763
+ ownershipGraph,
2764
+ new Set(remoteIndexes.map((index) => index.source)),
2765
+ localIndex.source
2766
+ );
2767
+ const diagnostics = getFederationDiagnostics(diagnosticGraph, rules);
2768
+ options.onProgress?.({ type: "resolved", graph, diagnostics });
2769
+ const blockingConflicts = ownershipGraph.conflicts.filter((conflict) => rules[`federation/${conflict.kind}`] === "error");
2770
+ if (blockingConflicts.length > 0) throw new FederationConflictError(blockingConflicts);
2771
+ const blockingDiagnostics = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
2772
+ if (blockingDiagnostics.length > 0) throw new FederationDiagnosticError(blockingDiagnostics);
2773
+ const publicPaths = [
2774
+ ...Object.keys(previousLock?.publicFiles ?? {}),
2775
+ ...graph.assets.filter((asset) => asset.path.startsWith("public/")).map((asset) => asset.path.slice("public/".length))
2776
+ ];
2777
+ const { hydrateResult, publicResult } = await withFederationOutputTransaction(
2778
+ projectDirectory,
2436
2779
  outDir,
2437
- cache: createFederationContentCache(projectDirectory, {
2438
- read: options.useCache !== false,
2439
- onHit: () => {
2440
- cachedFiles += 1;
2441
- options.onProgress?.({ type: "hydrate:cache", files: cachedFiles });
2442
- }
2443
- }),
2444
- modes: Object.fromEntries(sources.map((source) => [source.id, source.mode ?? "hydrate"])),
2445
- fetch: async ({ source: sourceId, commit, path: artifactPath }) => {
2446
- const source = sourcesById.get(sourceId);
2447
- if (!source) throw new Error(`Cannot fetch content for unconfigured source "${sourceId}"`);
2448
- const content = await provider.fetchContent({ source, commit, path: artifactPath });
2449
- hydratedFiles += 1;
2450
- options.onProgress?.({ type: "hydrate:file", files: hydratedFiles, source: sourceId, path: artifactPath });
2451
- return content;
2780
+ publicPaths,
2781
+ async () => {
2782
+ const sourcesById = new Map(sources.map((source) => [source.id, source]));
2783
+ options.onProgress?.({ type: "hydrating", outDir });
2784
+ let hydratedFiles = 0;
2785
+ let cachedFiles = 0;
2786
+ const hydrateResult2 = await (0, import_sdk3.hydrate)(graph, {
2787
+ outDir,
2788
+ cache: createFederationContentCache(projectDirectory, {
2789
+ read: options.useCache !== false,
2790
+ onHit: () => {
2791
+ cachedFiles += 1;
2792
+ options.onProgress?.({ type: "hydrate:cache", files: cachedFiles });
2793
+ }
2794
+ }),
2795
+ fetch: async ({ source: sourceId, commit, path: artifactPath }) => {
2796
+ const source = sourcesById.get(sourceId);
2797
+ if (!source) throw new Error(`Cannot fetch content for unconfigured source "${sourceId}"`);
2798
+ const content = await provider.fetchContent({ source, commit, path: artifactPath });
2799
+ hydratedFiles += 1;
2800
+ options.onProgress?.({ type: "hydrate:file", files: hydratedFiles, source: sourceId, path: artifactPath });
2801
+ return content;
2802
+ }
2803
+ });
2804
+ const publicResult2 = await composePublicAssets({
2805
+ projectDirectory,
2806
+ federatedDirectory: outDir,
2807
+ assets: graph.assets,
2808
+ previousFiles: previousLock?.publicFiles,
2809
+ collisionPaths: new Set(
2810
+ graph.warnings.filter((warning) => warning.kind === "asset-collision").map((warning) => warning.path)
2811
+ )
2812
+ });
2813
+ options.onProgress?.({ type: "public:complete", result: publicResult2 });
2814
+ const resolvedAt = (options.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
2815
+ await writeLock(lockPath, {
2816
+ lockVersion: 1,
2817
+ sources: resolvedSources.map(({ config: source, resolved }) => ({
2818
+ id: source.id,
2819
+ digest: `sha256:${(0, import_node_crypto4.createHash)("sha256").update(resolved.bytes).digest("hex")}`,
2820
+ commit: resolved.commit,
2821
+ resolvedAt
2822
+ })).sort((left, right) => left.id.localeCompare(right.id)),
2823
+ publicFiles: publicResult2.files
2824
+ });
2825
+ return { hydrateResult: hydrateResult2, publicResult: publicResult2 };
2452
2826
  }
2453
- });
2454
- const publicResult = await composePublicAssets({
2455
- projectDirectory,
2456
- federatedDirectory: outDir,
2457
- assets: graph.assets,
2458
- previousFiles: previousLock?.publicFiles,
2459
- collisionPaths: new Set(
2460
- graph.warnings.filter((warning) => warning.kind === "asset-collision").map((warning) => warning.path)
2461
- )
2462
- });
2463
- options.onProgress?.({ type: "public:complete", result: publicResult });
2464
- const resolvedAt = (options.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
2465
- await writeLock(lockPath, {
2466
- lockVersion: 1,
2467
- sources: resolvedSources.map(({ config: source, resolved }) => ({
2468
- id: source.id,
2469
- digest: `sha256:${(0, import_node_crypto4.createHash)("sha256").update(resolved.bytes).digest("hex")}`,
2470
- commit: resolved.commit,
2471
- resolvedAt
2472
- })).sort((left, right) => left.id.localeCompare(right.id)),
2473
- publicFiles: publicResult.files
2474
- });
2827
+ );
2475
2828
  const result = {
2476
2829
  sources: sources.length,
2477
2830
  resources,
2478
2831
  graph,
2832
+ diagnostics,
2479
2833
  hydrate: hydrateResult,
2480
2834
  public: publicResult,
2481
2835
  outDir,
@@ -2487,14 +2841,14 @@ var federateCatalog = async (projectDirectory, options = {}) => {
2487
2841
 
2488
2842
  // src/eventcatalog.ts
2489
2843
  var import_license = require("@eventcatalog/license");
2490
- var currentDir = import_node_path17.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
2844
+ var currentDir = import_node_path18.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
2491
2845
  var program = new import_commander.Command().version(VERSION);
2492
- var dir = import_node_path17.default.resolve(process.env.PROJECT_DIR || process.cwd());
2493
- var core = import_node_path17.default.resolve(process.env.CATALOG_DIR || (0, import_node_path16.join)(dir, ".eventcatalog-core"));
2494
- var eventCatalogDir = import_node_path17.default.resolve((0, import_node_path16.join)(currentDir, "../eventcatalog/"));
2846
+ var dir = import_node_path18.default.resolve(process.env.PROJECT_DIR || process.cwd());
2847
+ var core = import_node_path18.default.resolve(process.env.CATALOG_DIR || (0, import_node_path17.join)(dir, ".eventcatalog-core"));
2848
+ var eventCatalogDir = import_node_path18.default.resolve((0, import_node_path17.join)(currentDir, "../eventcatalog/"));
2495
2849
  var getInstalledEventCatalogVersion = () => {
2496
2850
  try {
2497
- const pkg = import_fs3.default.readFileSync((0, import_node_path16.join)(dir, "package.json"), "utf8");
2851
+ const pkg = import_fs3.default.readFileSync((0, import_node_path17.join)(dir, "package.json"), "utf8");
2498
2852
  const json = JSON.parse(pkg);
2499
2853
  return json.dependencies["@eventcatalog/core"];
2500
2854
  } catch (error) {
@@ -2560,12 +2914,12 @@ var startDevPrewarm = ({
2560
2914
  var buildDevSearchIndex = async ({ config }) => {
2561
2915
  const result = await buildSearchIndex({
2562
2916
  projectDir: dir,
2563
- outDir: import_node_path17.default.join(core, "public"),
2564
- searchOutputPath: import_node_path17.default.join(core, "public", "pagefind"),
2917
+ outDir: import_node_path18.default.join(core, "public"),
2918
+ searchOutputPath: import_node_path18.default.join(core, "public", "pagefind"),
2565
2919
  config,
2566
2920
  isServer: false
2567
2921
  });
2568
- logger.info(`Indexed ${result.records} page(s) into ${import_node_path17.default.relative(core, result.outputPath)}`, "search");
2922
+ logger.info(`Indexed ${result.records} page(s) into ${import_node_path18.default.relative(core, result.outputPath)}`, "search");
2569
2923
  };
2570
2924
  var warnIfIndexedSearchUsesAuth = async () => {
2571
2925
  if (!await isAuthEnabled()) {
@@ -2680,12 +3034,12 @@ var copyCore = () => {
2680
3034
  import_fs3.default.cpSync(eventCatalogDir, core, {
2681
3035
  recursive: true,
2682
3036
  filter: (src) => {
2683
- const relativePath = import_node_path17.default.relative(eventCatalogDir, src);
2684
- const pathParts = relativePath.split(import_node_path17.default.sep);
3037
+ const relativePath = import_node_path18.default.relative(eventCatalogDir, src);
3038
+ const pathParts = relativePath.split(import_node_path18.default.sep);
2685
3039
  return !pathParts.some((part) => [".astro", "dist", "node_modules"].includes(part));
2686
3040
  }
2687
3041
  });
2688
- const coreNodeModules = import_node_path17.default.join(core, "node_modules");
3042
+ const coreNodeModules = import_node_path18.default.join(core, "node_modules");
2689
3043
  const installedCoreNodeModules = resolveInstalledCoreNodeModules(currentDir);
2690
3044
  linkCoreNodeModules({ coreNodeModules, installedCoreNodeModules });
2691
3045
  };
@@ -2744,8 +3098,8 @@ program.command("dev").description("Run development server of EventCatalog").opt
2744
3098
  logger.info("Setting up EventCatalog...", "eventcatalog");
2745
3099
  const isServer = await isOutputServer();
2746
3100
  logger.info(isServer ? "EventCatalog is running in Server Mode" : "EventCatalog is running in Static Mode", "config");
2747
- if (import_fs3.default.existsSync(import_node_path17.default.join(dir, ".env"))) {
2748
- import_dotenv.default.config({ path: import_node_path17.default.join(dir, ".env") });
3101
+ if (import_fs3.default.existsSync(import_node_path18.default.join(dir, ".env"))) {
3102
+ import_dotenv.default.config({ path: import_node_path18.default.join(dir, ".env") });
2749
3103
  }
2750
3104
  if (options.debug) {
2751
3105
  logger.info("Debug mode enabled", "debug");
@@ -2825,8 +3179,8 @@ program.command("build").description("Run build of EventCatalog").action(async (
2825
3179
  logger.info("Building EventCatalog...", "build");
2826
3180
  const isServer = await isOutputServer();
2827
3181
  logger.info(isServer ? "EventCatalog is running in Server Mode" : "EventCatalog is running in Static Mode", "config");
2828
- if (import_fs3.default.existsSync(import_node_path17.default.join(dir, ".env"))) {
2829
- import_dotenv.default.config({ path: import_node_path17.default.join(dir, ".env") });
3182
+ if (import_fs3.default.existsSync(import_node_path18.default.join(dir, ".env"))) {
3183
+ import_dotenv.default.config({ path: import_node_path18.default.join(dir, ".env") });
2830
3184
  }
2831
3185
  await verifyRequiredFieldsAreInCatalogConfigFile(dir);
2832
3186
  copyCore();
@@ -2876,7 +3230,7 @@ program.command("build").description("Run build of EventCatalog").action(async (
2876
3230
  if (await isIndexedSearchEnabled()) {
2877
3231
  await warnIfIndexedSearchUsesAuth();
2878
3232
  const config = await getEventCatalogConfigFile(dir);
2879
- const outDir = import_node_path17.default.resolve(dir, await getProjectOutDir());
3233
+ const outDir = import_node_path18.default.resolve(dir, await getProjectOutDir());
2880
3234
  logger.info("Building indexed search...", "search");
2881
3235
  const result = await buildSearchIndex({
2882
3236
  projectDir: dir,
@@ -2884,7 +3238,7 @@ program.command("build").description("Run build of EventCatalog").action(async (
2884
3238
  config,
2885
3239
  isServer
2886
3240
  });
2887
- logger.info(`Indexed ${result.records} page(s) into ${import_node_path17.default.relative(dir, result.outputPath)}`, "search");
3241
+ logger.info(`Indexed ${result.records} page(s) into ${import_node_path18.default.relative(dir, result.outputPath)}`, "search");
2888
3242
  }
2889
3243
  });
2890
3244
  var previewCatalog = async ({
@@ -2911,7 +3265,7 @@ var startServerCatalog = async ({
2911
3265
  isEventCatalogStarter = false,
2912
3266
  isEventCatalogScale = false
2913
3267
  }) => {
2914
- const serverEntryPath = import_node_path17.default.join(dir, "dist", "server", "entry.mjs");
3268
+ const serverEntryPath = import_node_path18.default.join(dir, "dist", "server", "entry.mjs");
2915
3269
  await runCommandWithFilteredOutput({
2916
3270
  command: `node "${serverEntryPath}"`,
2917
3271
  cwd: core,
@@ -2928,8 +3282,8 @@ var startServerCatalog = async ({
2928
3282
  program.command("preview").description("Serves the contents of your eventcatalog build directory").action(async (options, command) => {
2929
3283
  logger.welcome();
2930
3284
  logger.info("Starting preview of your build...", "preview");
2931
- if (import_fs3.default.existsSync(import_node_path17.default.join(dir, ".env"))) {
2932
- import_dotenv.default.config({ path: import_node_path17.default.join(dir, ".env") });
3285
+ if (import_fs3.default.existsSync(import_node_path18.default.join(dir, ".env"))) {
3286
+ import_dotenv.default.config({ path: import_node_path18.default.join(dir, ".env") });
2933
3287
  }
2934
3288
  const canEmbedPages = await (0, import_license.isFeatureEnabled)(
2935
3289
  "@eventcatalog/backstage-plugin-eventcatalog",
@@ -2947,8 +3301,8 @@ program.command("preview").description("Serves the contents of your eventcatalog
2947
3301
  program.command("start").description("Serves the contents of your eventcatalog build directory").action(async (options, command) => {
2948
3302
  logger.welcome();
2949
3303
  logger.info("Starting preview of your build...", "preview");
2950
- if (import_fs3.default.existsSync(import_node_path17.default.join(dir, ".env"))) {
2951
- import_dotenv.default.config({ path: import_node_path17.default.join(dir, ".env") });
3304
+ if (import_fs3.default.existsSync(import_node_path18.default.join(dir, ".env"))) {
3305
+ import_dotenv.default.config({ path: import_node_path18.default.join(dir, ".env") });
2952
3306
  }
2953
3307
  const canEmbedPages = await (0, import_license.isFeatureEnabled)(
2954
3308
  "@eventcatalog/backstage-plugin-eventcatalog",
@@ -2975,26 +3329,26 @@ program.command("start").description("Serves the contents of your eventcatalog b
2975
3329
  program.command("export").description("Export your EventCatalog using the SDK dumpCatalog function").option("--include-markdown", "Include markdown content in the export", false).action(async (options) => {
2976
3330
  logger.welcome();
2977
3331
  logger.info("Exporting EventCatalog...", "export");
2978
- if (import_fs3.default.existsSync(import_node_path17.default.join(dir, ".env"))) {
2979
- import_dotenv.default.config({ path: import_node_path17.default.join(dir, ".env") });
3332
+ if (import_fs3.default.existsSync(import_node_path18.default.join(dir, ".env"))) {
3333
+ import_dotenv.default.config({ path: import_node_path18.default.join(dir, ".env") });
2980
3334
  }
2981
3335
  const { default: initSDK } = await import("@eventcatalog/sdk");
2982
3336
  const sdk = initSDK(dir);
2983
3337
  const catalog = await sdk.dumpCatalog({ includeMarkdown: options.includeMarkdown });
2984
- const exportsDir = import_node_path17.default.join(dir, "exports");
3338
+ const exportsDir = import_node_path18.default.join(dir, "exports");
2985
3339
  ensureDir(exportsDir);
2986
3340
  const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
2987
- const exportFile = import_node_path17.default.join(exportsDir, `catalog-${date}.json`);
3341
+ const exportFile = import_node_path18.default.join(exportsDir, `catalog-${date}.json`);
2988
3342
  import_fs3.default.writeFileSync(exportFile, JSON.stringify(catalog, null, 2), "utf-8");
2989
3343
  logger.info(`Catalog exported to ${exportFile}`, "export");
2990
3344
  });
2991
3345
  program.command("generate [siteDir]").description("Start the generator scripts.").action(async () => {
2992
- if (import_fs3.default.existsSync(import_node_path17.default.join(dir, ".env"))) {
2993
- import_dotenv.default.config({ path: import_node_path17.default.join(dir, ".env") });
3346
+ if (import_fs3.default.existsSync(import_node_path18.default.join(dir, ".env"))) {
3347
+ import_dotenv.default.config({ path: import_node_path18.default.join(dir, ".env") });
2994
3348
  }
2995
3349
  await generate(dir);
2996
3350
  });
2997
- var reportFederationProgress = (event) => {
3351
+ var reportFederationProgress = (event, verbose = false) => {
2998
3352
  switch (event.type) {
2999
3353
  case "configured":
3000
3354
  if (event.sources > 0)
@@ -3034,38 +3388,36 @@ var reportFederationProgress = (event) => {
3034
3388
  );
3035
3389
  return;
3036
3390
  case "resolved":
3037
- if (event.graph.conflicts.length > 0) {
3038
- logger.error(
3039
- `${event.graph.conflicts.length} ownership conflict${event.graph.conflicts.length === 1 ? "" : "s"} found`,
3391
+ const counts = getFederationDiagnosticCounts(event.diagnostics);
3392
+ const showDiagnosticDetails = verbose || counts.errors > 0;
3393
+ if (counts.errors === 0) {
3394
+ logger.success(
3395
+ `Graph resolved: ${event.graph.entities.length} remote resources, ${event.graph.edges.length} relationships`,
3040
3396
  "federation"
3041
3397
  );
3042
- for (const conflict of event.graph.conflicts.slice(0, 10)) {
3043
- logger.error(`${conflict.id}: ${conflict.sources.join(", ")}`, "federation");
3044
- }
3045
- if (event.graph.conflicts.length > 10) {
3046
- logger.error(`...and ${event.graph.conflicts.length - 10} more`, "federation");
3047
- }
3048
- return;
3049
3398
  }
3050
- logger.success(
3051
- `Graph resolved: ${event.graph.entities.length} remote resources, ${event.graph.edges.length} relationships`,
3052
- "federation"
3053
- );
3054
- if (event.graph.warnings.length > 0) {
3399
+ if (counts.errors + counts.warnings === 0) return;
3400
+ if (showDiagnosticDetails) {
3401
+ const diagnostics = getVisibleFederationDiagnostics(event.diagnostics, verbose);
3402
+ logger.info("Federation diagnostics", "federation");
3403
+ logger.line();
3404
+ for (const diagnostic of diagnostics) {
3405
+ logger.diagnostic(diagnostic.severity, diagnostic.message, diagnostic.rule, diagnostic.attributes);
3406
+ logger.line();
3407
+ }
3408
+ logger.diagnosticSummary(counts.errors, counts.warnings);
3409
+ } else {
3055
3410
  logger.warning(
3056
- `${event.graph.warnings.length} federation warning${event.graph.warnings.length === 1 ? "" : "s"}`,
3411
+ `${counts.warnings} problem${counts.warnings === 1 ? "" : "s"} (0 errors, ${counts.warnings} warning${counts.warnings === 1 ? "" : "s"})`,
3057
3412
  "federation"
3058
3413
  );
3059
3414
  }
3060
- if (event.graph.externals.length > 0) {
3061
- logger.warning(
3062
- `${event.graph.externals.length} reference${event.graph.externals.length === 1 ? "" : "s"} remain external`,
3063
- "federation"
3064
- );
3415
+ if (!verbose && counts.warnings > 0) {
3416
+ logger.info("Run with --verbose to see warning details.", "federation");
3065
3417
  }
3066
3418
  return;
3067
3419
  case "hydrating":
3068
- logger.info(`Hydrating federated content into ${import_node_path17.default.relative(dir, event.outDir)}/...`, "federation");
3420
+ logger.info(`Hydrating federated content into ${import_node_path18.default.relative(dir, event.outDir)}/...`, "federation");
3069
3421
  return;
3070
3422
  case "hydrate:cache":
3071
3423
  if (event.files === 1 || event.files % 25 === 0) {
@@ -3082,25 +3434,19 @@ var reportFederationProgress = (event) => {
3082
3434
  `Public assets: ${event.result.copied} copied, ${event.result.skipped} preserved from the main catalog, ${event.result.removed} stale removed`,
3083
3435
  "federation"
3084
3436
  );
3085
- if (event.result.overwritten > 0) {
3086
- logger.warning(
3087
- `${event.result.overwritten} public asset conflict${event.result.overwritten === 1 ? "" : "s"} resolved using the last configured source`,
3088
- "federation"
3089
- );
3090
- }
3091
3437
  return;
3092
3438
  case "complete":
3093
3439
  logger.success(
3094
3440
  `Federation complete: ${event.result.sources} sources, ${event.result.resources} remote resources, ${event.result.hydrate.written} files written (${event.result.hydrate.fetched} downloaded, ${event.result.hydrate.written - event.result.hydrate.fetched} cached)`,
3095
3441
  "federation"
3096
3442
  );
3097
- logger.info(`Pinned source commits in ${import_node_path17.default.relative(dir, event.result.lockPath)}`, "federation");
3443
+ logger.info(`Recorded resolved source state in ${import_node_path18.default.relative(dir, event.result.lockPath)}`, "federation");
3098
3444
  }
3099
3445
  };
3100
- program.command("federate").description("Fetch, resolve, and hydrate the catalogs configured in federation.sources.").option("--no-cache", "Download all federation content and refresh the cache.").action(async (commandOptions) => {
3446
+ program.command("federate").description("Fetch, resolve, and hydrate the catalogs configured in federation.sources.").option("--no-cache", "Download all federation content and refresh the cache.").option("-v, --verbose", "Show detailed federation diagnostics.").action(async (commandOptions) => {
3101
3447
  logger.welcome();
3102
- if (import_fs3.default.existsSync(import_node_path17.default.join(dir, ".env"))) {
3103
- import_dotenv.default.config({ path: import_node_path17.default.join(dir, ".env") });
3448
+ if (import_fs3.default.existsSync(import_node_path18.default.join(dir, ".env"))) {
3449
+ import_dotenv.default.config({ path: import_node_path18.default.join(dir, ".env") });
3104
3450
  }
3105
3451
  logger.info("Starting federation...", "federation");
3106
3452
  let cleanedPreviousOutput = false;
@@ -3109,7 +3455,7 @@ program.command("federate").description("Fetch, resolve, and hydrate the catalog
3109
3455
  isFederationEnabled: import_license.isEventCatalogScaleEnabled,
3110
3456
  onProgress: (event) => {
3111
3457
  if (event.type === "cleanup:complete") cleanedPreviousOutput = true;
3112
- reportFederationProgress(event);
3458
+ reportFederationProgress(event, commandOptions.verbose);
3113
3459
  }
3114
3460
  });
3115
3461
  if (!result && !cleanedPreviousOutput) logger.warning("No federation sources configured; nothing to do.", "federation");
@@ -3123,6 +3469,6 @@ program.addHelpText(
3123
3469
  `
3124
3470
  );
3125
3471
  program.parseAsync().then(() => process.exit(0)).catch((err) => {
3126
- if (!(err instanceof FederationConflictError)) console.error(err);
3472
+ if (!(err instanceof FederationConflictError) && !(err instanceof FederationDiagnosticError)) console.error(err);
3127
3473
  process.exit(1);
3128
3474
  });