UNPKG

@eventcatalog/linter

Version:

A linter for EventCatalog to validate frontmatter and resource references

1,397 lines (1,377 loc) 67.3 kB
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { get: (a, b) => (typeof require !== "undefined" ? require : a)[b] }) : x)(function(x) { if (typeof require !== "undefined") return require.apply(this, arguments); throw Error('Dynamic require of "' + x + '" is not supported'); }); // src/schemas/common.ts import { z } from "zod"; var badgeSchema = z.object({ content: z.string(), backgroundColor: z.string(), textColor: z.string(), icon: z.string().optional(), url: z.string().optional() }); var ownerReferenceSchema = z.union([ // The ID of the user or team z.string(), // The full object with the ID and collection (keep compatibility with `reference`) z.object({ id: z.string(), collection: z.enum(["users", "teams"]) }) ]).transform( // This transformation is needed to keep compatibility with `reference`. // The utilities `getTeams` and `getUsers` rely on this transformation. (lookup) => ({ id: typeof lookup === "string" ? lookup : lookup.id }) ); var specificationSchema = z.union([ z.object({ openapiPath: z.string().optional(), asyncapiPath: z.string().optional(), graphqlPath: z.string().optional() }), z.array( z.object({ type: z.enum(["openapi", "asyncapi", "graphql"]), path: z.string(), name: z.string().optional(), headers: z.record(z.string()).optional() }) ) ]); var repositorySchema = z.object({ language: z.string().optional(), url: z.string().optional() }); var draftSchema = z.union([ z.boolean(), z.object({ title: z.string().optional(), message: z.string() }) ]); var deprecatedSchema = z.union([ z.object({ message: z.string().optional(), date: z.union([z.string(), z.date()]).optional() }), z.boolean().optional() ]); var pointerSchema = z.object({ id: z.string(), version: z.string().optional().default("latest") }); var resourcePointerSchema = z.object({ id: z.string(), version: z.string().optional().default("latest"), type: z.enum([ "agent", "service", "event", "command", "query", "flow", "channel", "domain", "system", "user", "team", "container", "entity", "diagram", "data-product", "adr" ]) }); var channelPointerSchema = z.object({ parameters: z.record(z.string()).optional() }).merge(pointerSchema); var detailPanelPropertySchema = z.object({ visible: z.boolean().optional() }).optional(); var sendsPointerSchema = z.object({ id: z.string(), version: z.string().optional().default("latest"), fields: z.array(z.string()).optional(), group: z.string().optional(), to: z.array( channelPointerSchema.extend({ delivery_mode: z.enum(["push", "pull", "push-pull"]).optional().default("push") }) ).optional() }); var receivesPointerSchema = z.object({ id: z.string(), version: z.string().optional().default("latest"), fields: z.array(z.string()).optional(), group: z.string().optional(), from: z.array( channelPointerSchema.extend({ delivery_mode: z.enum(["push", "pull", "push-pull"]).optional().default("push") }) ).optional() }); var resourceReferenceSchema = pointerSchema; var semverSchema = z.string().refine((version) => { if (version === "latest") return true; if (version.includes(".x")) { const xPattern = /^\d+(\.\d+)*\.x$/; return xPattern.test(version); } if (version.startsWith("^") || version.startsWith("~")) { const rangeVersion = version.substring(1); const semverRegex2 = /^\d+\.\d+\.\d+(-[\w\d-.]+)?(\+[\w\d-.]+)?$/; return semverRegex2.test(rangeVersion); } const semverRegex = /^\d+\.\d+\.\d+(-[\w\d-.]+)?(\+[\w\d-.]+)?$/; return semverRegex.test(version); }, "Invalid semantic version format"); var sidebarSchema = z.object({ label: z.string().optional(), badge: z.string().optional(), color: z.string().optional(), backgroundColor: z.string().optional() }).optional(); var stylesSchema = z.object({ icon: z.string().optional(), node: z.object({ color: z.string().optional(), label: z.string().optional() }).optional() }).optional(); var resourceGroupSchema = z.array( z.object({ id: z.string().optional(), title: z.string().optional(), items: z.array(resourcePointerSchema), limit: z.number().optional().default(10), sidebar: z.boolean().optional().default(true) }) ).optional(); var catalogMetadataSchema = z.object({ path: z.string(), filePath: z.string(), astroContentFilePath: z.string(), publicPath: z.string(), type: z.string() }).optional(); var baseSchema = z.object({ id: z.string(), name: z.string(), summary: z.string().optional(), version: semverSchema, draft: z.union([z.boolean(), z.object({ title: z.string().optional(), message: z.string() })]).optional(), badges: z.array(badgeSchema).optional(), owners: z.array(ownerReferenceSchema).optional(), schemaPath: z.string().optional(), sidebar: sidebarSchema, repository: repositorySchema.optional(), specifications: specificationSchema.optional(), hidden: z.boolean().optional(), editUrl: z.string().optional(), resourceGroups: resourceGroupSchema, styles: stylesSchema, deprecated: deprecatedSchema.optional(), visualiser: z.boolean().optional(), attachments: z.array( z.union([ z.string().url(), z.object({ url: z.string().url(), title: z.string().optional(), type: z.string().optional(), description: z.string().optional(), icon: z.string().optional() }) ]) ).optional(), diagrams: z.array(pointerSchema).optional(), versions: z.array(z.string()).optional(), latestVersion: z.string().optional(), catalog: catalogMetadataSchema }); // src/schemas/domain.ts import { z as z2 } from "zod"; var domainSchema = z2.object({ services: z2.array(pointerSchema).optional(), agents: z2.array(pointerSchema).optional(), domains: z2.array(pointerSchema).optional(), systems: z2.array(pointerSchema).optional(), entities: z2.array(pointerSchema).optional(), "data-products": z2.array(pointerSchema).optional(), dataProducts: z2.array(pointerSchema).optional(), flows: z2.array(pointerSchema).optional(), sends: z2.array(sendsPointerSchema).optional(), receives: z2.array(receivesPointerSchema).optional(), detailsPanel: z2.object({ parentDomains: detailPanelPropertySchema, subdomains: detailPanelPropertySchema, systems: detailPanelPropertySchema, services: detailPanelPropertySchema, agents: detailPanelPropertySchema, entities: detailPanelPropertySchema, messages: detailPanelPropertySchema, ubiquitousLanguage: detailPanelPropertySchema, repository: detailPanelPropertySchema, versions: detailPanelPropertySchema, owners: detailPanelPropertySchema, changelog: detailPanelPropertySchema, attachments: detailPanelPropertySchema }).optional() }).merge(baseSchema); // src/schemas/system.ts import { z as z3 } from "zod"; var systemRelationshipPointerSchema = z3.object({ id: z3.string(), version: z3.string().optional().default("latest"), label: z3.string().optional() }); var systemActorRelationshipSchema = z3.object({ id: z3.string(), name: z3.string().optional(), label: z3.string().optional(), direction: z3.enum(["inbound", "outbound"]).optional().default("inbound") }); var systemSchema = z3.object({ scope: z3.enum(["internal", "external"]).optional().default("internal"), services: z3.array(pointerSchema).optional(), flows: z3.array(pointerSchema).optional(), entities: z3.array(pointerSchema).optional(), containers: z3.array(pointerSchema).optional(), relationships: z3.array(systemRelationshipPointerSchema).optional(), actors: z3.array(systemActorRelationshipSchema).optional(), detailsPanel: z3.object({ versions: detailPanelPropertySchema, repository: detailPanelPropertySchema, owners: detailPanelPropertySchema, changelog: detailPanelPropertySchema, attachments: detailPanelPropertySchema, services: detailPanelPropertySchema, flows: detailPanelPropertySchema, entities: detailPanelPropertySchema, containers: detailPanelPropertySchema, diagrams: detailPanelPropertySchema }).optional() }).merge(baseSchema); // src/schemas/service.ts import { z as z4 } from "zod"; var serviceSchema = z4.object({ sends: z4.array(sendsPointerSchema).optional(), receives: z4.array(receivesPointerSchema).optional(), entities: z4.array(pointerSchema).optional(), writesTo: z4.array(pointerSchema).optional(), readsFrom: z4.array(pointerSchema).optional(), flows: z4.array(pointerSchema).optional(), externalSystem: z4.boolean().optional(), detailsPanel: z4.object({ domains: detailPanelPropertySchema, messages: detailPanelPropertySchema, versions: detailPanelPropertySchema, specifications: detailPanelPropertySchema, entities: detailPanelPropertySchema, repository: detailPanelPropertySchema, owners: detailPanelPropertySchema, changelog: detailPanelPropertySchema, containers: detailPanelPropertySchema }).optional() }).merge(baseSchema); // src/schemas/message.ts import { z as z5 } from "zod"; var operationSchema = z5.object({ method: z5.enum(["GET", "POST", "PUT", "DELETE", "PATCH"]).optional(), path: z5.string().optional(), statusCodes: z5.array(z5.string()).optional() }).optional(); var messageDetailsPanelSchema = z5.object({ producers: detailPanelPropertySchema, consumers: detailPanelPropertySchema, channels: detailPanelPropertySchema, versions: detailPanelPropertySchema, repository: detailPanelPropertySchema, owners: detailPanelPropertySchema, changelog: detailPanelPropertySchema, attachments: detailPanelPropertySchema }).optional(); var baseMessageSchema = z5.object({ operation: operationSchema, producers: z5.array(z5.any()).optional(), // reference('services') consumers: z5.array(z5.any()).optional(), // reference('services') channels: z5.array(channelPointerSchema).optional(), schemas: z5.array( z5.object({ id: z5.string().optional(), ref: z5.string().optional(), file: z5.string().optional(), path: z5.string().optional(), name: z5.string().optional(), format: z5.string().optional(), environments: z5.array(z5.string()).optional(), default: z5.boolean().optional() }) ).optional(), messageChannels: z5.array(z5.any()).optional(), // reference('channels') detailsPanel: messageDetailsPanelSchema }).merge(baseSchema); var eventSchema = baseMessageSchema; var commandSchema = baseMessageSchema; var querySchema = baseMessageSchema; // src/schemas/channel.ts import { z as z6 } from "zod"; var parameterSchema = z6.object({ enum: z6.array(z6.string()).optional(), default: z6.string().optional(), examples: z6.array(z6.string()).optional(), description: z6.string().optional() }); var channelSchema = z6.object({ channels: z6.array(channelPointerSchema).optional(), address: z6.string().optional(), protocols: z6.array(z6.string()).optional(), deliveryGuarantee: z6.enum(["at-most-once", "at-least-once", "exactly-once"]).optional(), routes: z6.array(channelPointerSchema).optional(), parameters: z6.record(parameterSchema).optional(), messages: z6.array(z6.object({ collection: z6.string(), name: z6.string(), ...pointerSchema.shape })).optional(), detailsPanel: z6.object({ producers: detailPanelPropertySchema, consumers: detailPanelPropertySchema, messages: detailPanelPropertySchema, protocols: detailPanelPropertySchema, parameters: detailPanelPropertySchema, versions: detailPanelPropertySchema, repository: detailPanelPropertySchema, owners: detailPanelPropertySchema, changelog: detailPanelPropertySchema, attachments: detailPanelPropertySchema }).optional() }).merge(baseSchema); // src/schemas/flow.ts import { z as z7 } from "zod"; var flowStep = z7.union([ z7.union([z7.string(), z7.number()]), z7.object({ id: z7.union([z7.string(), z7.number()]), label: z7.string().optional() }).optional() ]).optional(); var flowStepSchema = z7.object({ id: z7.union([z7.string(), z7.number()]), type: z7.enum(["node", "message", "agent", "user", "actor"]).optional(), title: z7.string(), summary: z7.string().optional(), message: pointerSchema.optional(), agent: pointerSchema.optional(), service: pointerSchema.optional(), flow: pointerSchema.optional(), container: pointerSchema.optional(), dataProduct: pointerSchema.optional(), actor: z7.object({ name: z7.string(), summary: z7.string().optional() }).optional(), custom: z7.object({ title: z7.string(), icon: z7.string().optional(), type: z7.string().optional(), summary: z7.string().optional(), url: z7.string().url().optional(), color: z7.string().optional(), properties: z7.record(z7.union([z7.string(), z7.number()])).optional(), height: z7.number().optional(), menu: z7.array( z7.object({ label: z7.string(), url: z7.string().url().optional() }) ).optional() }).optional(), externalSystem: z7.object({ name: z7.string(), summary: z7.string().optional(), url: z7.string().url().optional() }).optional(), next_step: flowStep, next_steps: z7.array(flowStep).optional() }).refine((data) => { if (data.next_step && data.next_steps) return false; const typesUsed = [ data.message, data.agent, data.service, data.flow, data.container, data.dataProduct, data.actor, data.custom ].filter((v) => v).length; return typesUsed === 0 || typesUsed === 1; }); var flowSchema = z7.object({ steps: z7.array(flowStepSchema) }).merge(baseSchema); // src/schemas/entity.ts import { z as z8 } from "zod"; var propertySchema = z8.lazy( () => z8.object({ name: z8.string(), type: z8.string(), required: z8.boolean().optional(), description: z8.string().optional(), references: z8.string().optional(), referencesIdentifier: z8.string().optional(), referenceTarget: z8.literal("entity").optional(), relationType: z8.string().optional(), enum: z8.array(z8.string()).optional(), properties: z8.array(propertySchema).optional(), items: z8.object({ type: z8.string(), properties: z8.array(propertySchema).optional() }).optional() }) ); var entitySchema = z8.object({ aggregateRoot: z8.boolean().optional(), identifier: z8.string().optional(), properties: z8.array(propertySchema).optional(), services: z8.array(z8.any()).optional(), // reference('services') domains: z8.array(z8.any()).optional(), // reference('domains') detailsPanel: z8.object({ domains: detailPanelPropertySchema, services: detailPanelPropertySchema, messages: detailPanelPropertySchema, versions: detailPanelPropertySchema, owners: detailPanelPropertySchema, changelog: detailPanelPropertySchema, attachments: detailPanelPropertySchema }).optional() }).merge(baseSchema); // src/schemas/user.ts import { z as z9 } from "zod"; var userSchema = z9.object({ id: z9.string(), name: z9.string(), avatarUrl: z9.string().optional(), role: z9.string().optional(), hidden: z9.boolean().optional(), source: z9.object({ provider: z9.string(), id: z9.string().optional(), url: z9.string().optional() }).optional(), readOnly: z9.boolean().optional(), email: z9.string().email().optional(), slackDirectMessageUrl: z9.string().optional(), msTeamsDirectMessageUrl: z9.string().optional(), ownedAgents: z9.array(z9.any()).optional(), // reference('agents') ownedDomains: z9.array(z9.any()).optional(), // reference('domains') ownedSystems: z9.array(z9.any()).optional(), // reference('systems') ownedServices: z9.array(z9.any()).optional(), // reference('services') ownedEvents: z9.array(z9.any()).optional(), // reference('events') ownedCommands: z9.array(z9.any()).optional(), // reference('commands') ownedQueries: z9.array(z9.any()).optional(), // reference('queries') associatedTeams: z9.array(z9.any()).optional() // reference('teams') }); // src/schemas/team.ts import { z as z10 } from "zod"; var teamSchema = z10.object({ id: z10.string(), name: z10.string(), summary: z10.string().optional(), email: z10.string().email().optional(), hidden: z10.boolean().optional(), source: z10.object({ provider: z10.string(), id: z10.string().optional(), url: z10.string().optional() }).optional(), readOnly: z10.boolean().optional(), slackDirectMessageUrl: z10.string().optional(), msTeamsDirectMessageUrl: z10.string().optional(), members: z10.array(z10.any()).optional(), // reference('users') ownedAgents: z10.array(z10.any()).optional(), // reference('agents') ownedCommands: z10.array(z10.any()).optional(), // reference('commands') ownedQueries: z10.array(z10.any()).optional(), // reference('queries') ownedDomains: z10.array(z10.any()).optional(), // reference('domains') ownedSystems: z10.array(z10.any()).optional(), // reference('systems') ownedServices: z10.array(z10.any()).optional(), // reference('services') ownedEvents: z10.array(z10.any()).optional() // reference('events') }); // src/schemas/agent.ts import { z as z11 } from "zod"; var agentToolSchema = z11.object({ name: z11.string(), type: z11.string(), icon: z11.string().optional(), url: z11.string().optional(), description: z11.string().optional() }); var agentModelSchema = z11.object({ provider: z11.string().optional(), name: z11.string().optional(), version: z11.string().optional() }); var agentSchema = z11.object({ sends: z11.array(sendsPointerSchema).optional(), receives: z11.array(receivesPointerSchema).optional(), writesTo: z11.array(pointerSchema).optional(), readsFrom: z11.array(pointerSchema).optional(), flows: z11.array(pointerSchema).optional(), model: agentModelSchema.optional(), tools: z11.array(agentToolSchema).optional(), specifications: z11.undefined().optional(), detailsPanel: z11.object({ domains: detailPanelPropertySchema, messages: detailPanelPropertySchema, versions: detailPanelPropertySchema, repository: detailPanelPropertySchema, owners: detailPanelPropertySchema, changelog: detailPanelPropertySchema, containers: detailPanelPropertySchema, tools: detailPanelPropertySchema, model: detailPanelPropertySchema }).optional() }).merge(baseSchema.omit({ specifications: true })); // src/schemas/adr.ts import { z as z12 } from "zod"; var adrSchema = z12.object({ status: z12.enum(["proposed", "accepted", "rejected", "deprecated", "superseded"]), date: z12.coerce.date(), decisionMakers: z12.array(z12.any()).optional(), appliesTo: z12.array(resourcePointerSchema).optional(), supersedes: z12.array(pointerSchema).optional(), supersededBy: z12.array(pointerSchema).optional(), amends: z12.array(pointerSchema).optional(), amendedBy: z12.array(pointerSchema).optional(), related: z12.array(pointerSchema).optional(), detailsPanel: z12.object({ status: detailPanelPropertySchema, date: detailPanelPropertySchema, decisionMakers: detailPanelPropertySchema, appliesTo: detailPanelPropertySchema, relationships: detailPanelPropertySchema, owners: detailPanelPropertySchema, repository: detailPanelPropertySchema, changelog: detailPanelPropertySchema }).optional() }).merge(baseSchema); // src/schemas/container.ts import { z as z13 } from "zod"; var containerSchema = z13.object({ container_type: z13.enum([ "database", "cache", "objectStore", "searchIndex", "dataWarehouse", "dataLake", "externalSaaS", "other" ]), technology: z13.string().optional(), authoritative: z13.boolean().optional().default(false), access_mode: z13.enum(["read", "write", "readWrite", "appendOnly"]).optional(), classification: z13.enum(["public", "internal", "confidential", "regulated"]).optional(), residency: z13.string().optional(), retention: z13.string().optional(), detailsPanel: z13.object({ versions: detailPanelPropertySchema, repository: detailPanelPropertySchema, owners: detailPanelPropertySchema, changelog: detailPanelPropertySchema, attachments: detailPanelPropertySchema, services: detailPanelPropertySchema, flows: detailPanelPropertySchema }).optional(), services: z13.array(z13.any()).optional(), servicesThatWriteToContainer: z13.array(z13.any()).optional(), servicesThatReadFromContainer: z13.array(z13.any()).optional(), dataProductsThatWriteToContainer: z13.array(z13.any()).optional(), dataProductsThatReadFromContainer: z13.array(z13.any()).optional() }).merge(baseSchema); // src/schemas/data-product.ts import { z as z14 } from "zod"; var dataProductOutputPointerSchema = z14.object({ id: z14.string(), version: z14.string().optional().default("latest"), contract: z14.object({ path: z14.string(), name: z14.string(), type: z14.string().optional() }).optional() }); var dataProductSchema = z14.object({ inputs: z14.array(pointerSchema).optional(), outputs: z14.array(dataProductOutputPointerSchema).optional(), detailsPanel: z14.object({ domains: detailPanelPropertySchema, inputs: detailPanelPropertySchema, outputs: detailPanelPropertySchema, versions: detailPanelPropertySchema, repository: detailPanelPropertySchema, owners: detailPanelPropertySchema, changelog: detailPanelPropertySchema, flows: detailPanelPropertySchema }).optional() }).merge(baseSchema); // src/schemas/diagram.ts import { z as z15 } from "zod"; var diagramSchema = z15.object({ detailsPanel: z15.object({ versions: detailPanelPropertySchema, owners: detailPanelPropertySchema, changelog: detailPanelPropertySchema, attachments: detailPanelPropertySchema }).optional() }).merge(baseSchema); // src/schemas/index.ts var schemas = { domain: domainSchema, system: systemSchema, service: serviceSchema, agent: agentSchema, adr: adrSchema, event: eventSchema, command: commandSchema, query: querySchema, channel: channelSchema, flow: flowSchema, entity: entitySchema, container: containerSchema, dataProduct: dataProductSchema, diagram: diagramSchema, user: userSchema, team: teamSchema, // Backwards-compatible alias for older linter integrations. dataStore: containerSchema }; // src/scanner/index.ts import fg from "fast-glob"; import path from "path"; var RESOURCE_DIRECTORY_NAMES = { dataProduct: "data-products", dataStore: "containers", container: "containers", adr: "adrs" }; var RESOURCE_PATTERNS = { domain: [ "domains/*/index.{md,mdx}", "domains/*/versioned/*/index.{md,mdx}", "domains/*/subdomains/*/index.{md,mdx}", "domains/*/subdomains/*/versioned/*/index.{md,mdx}" ], system: [ "**/systems/**/index.{md,mdx}", "**/systems/**/versioned/*/index.{md,mdx}", "!**/systems/**/agents/**", "!**/systems/**/services/**", "!**/systems/**/events/**", "!**/systems/**/commands/**", "!**/systems/**/queries/**", "!**/systems/**/flows/**", "!**/systems/**/channels/**", "!**/systems/**/entities/**", "!**/systems/**/containers/**", "!**/systems/**/diagrams/**", "!**/systems/**/data-products/**", "!**/systems/**/adrs/**", "!**/systems/**/docs/**" ], service: [ "domains/*/services/*/index.{md,mdx}", "domains/*/services/*/versioned/*/index.{md,mdx}", "domains/*/subdomains/*/services/*/index.{md,mdx}", "domains/*/subdomains/*/services/*/versioned/*/index.{md,mdx}", "**/systems/**/services/*/index.{md,mdx}", "**/systems/**/services/*/versioned/*/index.{md,mdx}", "services/*/index.{md,mdx}", "services/*/versioned/*/index.{md,mdx}" ], agent: ["**/agents/*/index.{md,mdx}", "**/agents/*/versioned/*/index.{md,mdx}"], adr: ["**/adrs/*/index.{md,mdx}", "**/adrs/*/versioned/*/index.{md,mdx}"], event: ["**/events/*/index.{md,mdx}", "**/events/*/versioned/*/index.{md,mdx}"], command: ["**/commands/*/index.{md,mdx}", "**/commands/*/versioned/*/index.{md,mdx}"], query: ["**/queries/*/index.{md,mdx}", "**/queries/*/versioned/*/index.{md,mdx}"], channel: ["**/channels/**/index.{md,mdx}", "**/channels/**/versioned/*/index.{md,mdx}"], flow: ["**/flows/**/index.{md,mdx}", "**/flows/**/versioned/*/index.{md,mdx}"], entity: ["**/entities/*/index.{md,mdx}", "**/entities/*/versioned/*/index.{md,mdx}"], container: ["**/containers/**/index.{md,mdx}", "**/containers/**/versioned/*/index.{md,mdx}"], dataProduct: ["**/data-products/*/index.{md,mdx}", "**/data-products/*/versioned/*/index.{md,mdx}"], diagram: ["**/diagrams/**/index.{md,mdx}", "**/diagrams/**/versioned/*/index.{md,mdx}"], user: ["users/*.{md,mdx}"], team: ["teams/*.{md,mdx}"] }; var extractResourceInfo = (filePath, resourceType) => { const normalizedPath = filePath.replace(/\\/g, "/"); const relativePath = normalizedPath.split("/"); if (resourceType === "user" || resourceType === "team") { const filename = path.basename(filePath, path.extname(filePath)); return { id: filename }; } const resourceTypePattern = RESOURCE_DIRECTORY_NAMES[resourceType] || `${resourceType}s`; const resourceTypeIndex = relativePath.findIndex((part) => part === resourceTypePattern); if (resourceTypeIndex === -1) { const parts2 = relativePath.slice(1, -1); return { id: parts2[parts2.length - 1] }; } const parts = relativePath.slice(resourceTypeIndex + 1, -1); if (parts.length === 0) { return { id: "unknown" }; } if (parts.length >= 3 && parts[parts.length - 2] === "versioned") { const version = parts[parts.length - 1]; const resourceId = parts.slice(0, -2).join("/"); return { id: resourceId, version }; } if (resourceType === "domain" && parts.length >= 2) { const versionedIndex = parts.findIndex((part) => part === "versioned"); if (versionedIndex !== -1 && versionedIndex < parts.length - 1) { const version = parts[versionedIndex + 1]; const resourceId = parts.slice(0, versionedIndex).join("/"); return { id: resourceId, version }; } } if (parts.length === 1) { return { id: parts[0] }; } return { id: parts.join("/") }; }; var scanCatalogFiles = async (rootDir) => { const files = []; for (const [resourceType, patterns] of Object.entries(RESOURCE_PATTERNS)) { const foundFiles = await fg(patterns, { cwd: rootDir, absolute: true, onlyFiles: true, followSymbolicLinks: false }); for (const filePath of foundFiles) { const relativePath = path.relative(rootDir, filePath); const { id, version } = extractResourceInfo(relativePath, resourceType); files.push({ path: filePath, relativePath, resourceType, resourceId: id, version }); } } return files; }; // src/parser/index.ts import fs from "fs/promises"; import matter from "gray-matter"; var parseFrontmatter = async (file) => { try { const fileContent = await fs.readFile(file.path, "utf-8"); const { data, content } = matter(fileContent); return { file, frontmatter: data, content, raw: fileContent }; } catch (error) { return { file, error: error instanceof Error ? error : new Error(String(error)) }; } }; var parseAllFiles = async (files) => { const results = await Promise.all(files.map(parseFrontmatter)); const parsed = []; const errors = []; for (const result of results) { if ("error" in result) { errors.push(result); } else { parsed.push(result); } } return { parsed, errors }; }; // src/validators/schema-validator.ts import { z as z16 } from "zod"; var validateSchema = (parsedFile) => { const { file, frontmatter } = parsedFile; const schema = schemas[file.resourceType]; const errors = []; try { schema.parse(frontmatter); } catch (error) { if (error instanceof z16.ZodError) { for (const issue of error.issues) { const field = issue.path.join("."); let message = issue.message; if (issue.code === "invalid_type") { message = `Expected ${issue.expected}, but received ${issue.received}`; } let rule = "schema/required-fields"; if (issue.code === "invalid_type") { rule = "schema/valid-type"; } else if (issue.code === "invalid_string" && issue.validation === "email") { rule = "schema/valid-email"; } else if (field && field.includes("version")) { rule = "schema/valid-semver"; } errors.push({ type: "schema", resource: `${file.resourceType}/${file.resourceId}`, field: field || void 0, message: field ? `${field}: ${message}` : message, file: file.relativePath, rule }); } } else { errors.push({ type: "schema", resource: `${file.resourceType}/${file.resourceId}`, message: error instanceof Error ? error.message : String(error), file: file.relativePath, rule: "schema/validation-error" }); } } return errors; }; var validateAllSchemas = (parsedFiles) => { const errors = []; for (const parsedFile of parsedFiles) { errors.push(...validateSchema(parsedFile)); } return errors; }; // src/validators/reference-validator.ts import semver from "semver"; var MESSAGE_TYPES = ["event", "command", "query"]; var RESOURCE_TYPE_ALIASES = { container: ["container", "dataStore"], dataStore: ["container", "dataStore"] }; var normalizeResourceType = (type) => { if (typeof type !== "string") return void 0; const normalizedTypes = { agent: "agent", agents: "agent", adr: "adr", adrs: "adr", service: "service", services: "service", event: "event", events: "event", command: "command", commands: "command", query: "query", queries: "query", flow: "flow", flows: "flow", channel: "channel", channels: "channel", domain: "domain", domains: "domain", system: "system", systems: "system", user: "user", users: "user", team: "team", teams: "team", container: "container", containers: "container", dataStore: "container", dataStores: "container", "data-product": "dataProduct", "data-products": "dataProduct", dataProduct: "dataProduct", dataProducts: "dataProduct", entity: "entity", entities: "entity", diagram: "diagram", diagrams: "diagram" }; return normalizedTypes[type]; }; var getReference = (value) => { if (typeof value === "string") { return { id: value }; } if (!value || typeof value !== "object") { return void 0; } const ref = value; if (typeof ref.id !== "string") { return void 0; } return { id: ref.id, version: typeof ref.version === "string" ? ref.version : void 0 }; }; var addReference = (references, value, possibleTypes, field) => { const ref = getReference(value); if (ref) { references.push({ ref, possibleTypes, field }); } }; var addReferences = (references, values, possibleTypes, field) => { if (!Array.isArray(values)) return; values.forEach((value) => { addReference(references, value, possibleTypes, field); }); }; var addTypedReference = (references, value, field) => { if (!value || typeof value !== "object") return; const resourceType = normalizeResourceType(value.type); if (!resourceType) return; addReference(references, value, [resourceType], field); }; var buildResourceIndex = (parsedFiles, dependencies) => { const index = {}; for (const parsedFile of parsedFiles) { const { file, frontmatter } = parsedFile; const { resourceType } = file; let resourceId = file.resourceId; if (frontmatter.id && typeof frontmatter.id === "string") { resourceId = frontmatter.id; } if (!index[resourceType]) { index[resourceType] = {}; } if (!index[resourceType][resourceId]) { index[resourceType][resourceId] = /* @__PURE__ */ new Set(); } if (frontmatter.version && typeof frontmatter.version === "string") { index[resourceType][resourceId].add(frontmatter.version); } else { index[resourceType][resourceId].add("latest"); } } if (dependencies) { for (const [resourceType, entries] of Object.entries(dependencies)) { if (!index[resourceType]) { index[resourceType] = {}; } for (const entry of entries) { if (!index[resourceType][entry.id]) { index[resourceType][entry.id] = /* @__PURE__ */ new Set(); } index[resourceType][entry.id].add(entry.version || "latest"); } } } return index; }; var checkResourceExists = (ref, resourceType, index) => { const resourceTypes = RESOURCE_TYPE_ALIASES[resourceType] || [resourceType]; const resourceVersions = /* @__PURE__ */ new Set(); for (const type of resourceTypes) { const versions = index[type]?.[ref.id]; if (versions) { versions.forEach((version) => resourceVersions.add(version)); } } if (!resourceVersions || resourceVersions.size === 0) { return false; } if (!ref.version) { return true; } const refVersion = ref.version === "latest" ? ref.version : ref.version; const availableVersions = Array.from(resourceVersions); if (refVersion === "latest") { return availableVersions.includes("latest") || availableVersions.length > 0; } if (availableVersions.includes(refVersion)) { return true; } try { const semverVersions = availableVersions.filter((v) => v !== "latest" && semver.valid(v)); for (const availableVersion of semverVersions) { if (semver.satisfies(availableVersion, refVersion)) { return true; } } if (refVersion.includes(".x")) { const pattern = refVersion.replace(/\.x/g, ""); for (const availableVersion of semverVersions) { if (availableVersion.startsWith(pattern)) { return true; } } } return false; } catch (error) { return availableVersions.includes(refVersion); } }; var extractReferences = (parsedFile) => { const { file, frontmatter } = parsedFile; const references = []; if (frontmatter.owners && Array.isArray(frontmatter.owners)) { frontmatter.owners.forEach((owner, index) => { if (owner && typeof owner === "object" && "collection" in owner) { const ownerType = normalizeResourceType(owner.collection); if (ownerType) { addReference(references, owner, [ownerType], `owners[${index}]`); return; } } addReference(references, owner, ["user", "team"], `owners[${index}]`); }); } addReferences(references, frontmatter.diagrams, ["diagram"], "diagrams"); if (frontmatter.resourceGroups && Array.isArray(frontmatter.resourceGroups)) { frontmatter.resourceGroups.forEach((group, groupIndex) => { if (!group || typeof group !== "object") return; const items = group.items; if (!Array.isArray(items)) return; items.forEach( (item, itemIndex) => addTypedReference(references, item, `resourceGroups[${groupIndex}].items[${itemIndex}]`) ); }); } if (file.resourceType === "domain") { addReferences(references, frontmatter.services, ["service"], "services"); addReferences(references, frontmatter.agents, ["agent"], "agents"); addReferences(references, frontmatter.domains, ["domain"], "domains"); addReferences(references, frontmatter.systems, ["system"], "systems"); addReferences(references, frontmatter.entities, ["entity"], "entities"); addReferences(references, frontmatter["data-products"], ["dataProduct"], "data-products"); addReferences(references, frontmatter.dataProducts, ["dataProduct"], "dataProducts"); addReferences(references, frontmatter.flows, ["flow"], "flows"); addReferences(references, frontmatter.sends, MESSAGE_TYPES, "sends"); addReferences(references, frontmatter.receives, MESSAGE_TYPES, "receives"); } if (file.resourceType === "service" || file.resourceType === "agent") { addReferences(references, frontmatter.sends, MESSAGE_TYPES, "sends"); addReferences(references, frontmatter.receives, MESSAGE_TYPES, "receives"); if (frontmatter.writesTo && Array.isArray(frontmatter.writesTo)) { frontmatter.writesTo.forEach((ref) => addReference(references, ref, ["container"], "writesTo")); } if (frontmatter.readsFrom && Array.isArray(frontmatter.readsFrom)) { frontmatter.readsFrom.forEach((ref) => addReference(references, ref, ["container"], "readsFrom")); } addReferences(references, frontmatter.flows, ["flow"], "flows"); } if (file.resourceType === "service") { addReferences(references, frontmatter.entities, ["entity"], "entities"); } if (file.resourceType === "system") { addReferences(references, frontmatter.services, ["service"], "services"); addReferences(references, frontmatter.flows, ["flow"], "flows"); addReferences(references, frontmatter.entities, ["entity"], "entities"); addReferences(references, frontmatter.containers, ["container"], "containers"); addReferences(references, frontmatter.relationships, ["system"], "relationships"); } if (file.resourceType === "flow" && frontmatter.steps && Array.isArray(frontmatter.steps)) { frontmatter.steps.forEach((step, index) => { if (step.message) { addReference(references, step.message, MESSAGE_TYPES, `steps[${index}].message`); } if (step.service) { addReference(references, step.service, ["service"], `steps[${index}].service`); } if (step.agent) { addReference(references, step.agent, ["agent"], `steps[${index}].agent`); } if (step.flow) { addReference(references, step.flow, ["flow"], `steps[${index}].flow`); } if (step.container) { addReference(references, step.container, ["container"], `steps[${index}].container`); } if (step.dataProduct) { addReference(references, step.dataProduct, ["dataProduct"], `steps[${index}].dataProduct`); } }); } if (file.resourceType === "entity" && frontmatter.properties && Array.isArray(frontmatter.properties)) { frontmatter.properties.forEach((prop, index) => { if (prop.references) { references.push({ ref: { id: prop.references }, possibleTypes: ["entity"], field: `properties[${index}].references` }); } }); addReferences(references, frontmatter.services, ["service"], "services"); addReferences(references, frontmatter.domains, ["domain"], "domains"); } if (MESSAGE_TYPES.includes(file.resourceType)) { addReferences(references, frontmatter.producers, ["service"], "producers"); addReferences(references, frontmatter.consumers, ["service"], "consumers"); addReferences(references, frontmatter.channels, ["channel"], "channels"); addReferences(references, frontmatter.messageChannels, ["channel"], "messageChannels"); } if (file.resourceType === "channel") { addReferences(references, frontmatter.channels, ["channel"], "channels"); addReferences(references, frontmatter.routes, ["channel"], "routes"); if (frontmatter.messages && Array.isArray(frontmatter.messages)) { frontmatter.messages.forEach((message, index) => { if (!message || typeof message !== "object") return; const messageType = normalizeResourceType(message.collection); if (messageType && MESSAGE_TYPES.includes(messageType)) { addReference(references, message, [messageType], `messages[${index}]`); } }); } } if (file.resourceType === "container" || file.resourceType === "dataStore") { addReferences(references, frontmatter.services, ["service"], "services"); addReferences(references, frontmatter.servicesThatWriteToContainer, ["service"], "servicesThatWriteToContainer"); addReferences(references, frontmatter.servicesThatReadFromContainer, ["service"], "servicesThatReadFromContainer"); addReferences(references, frontmatter.dataProductsThatWriteToContainer, ["dataProduct"], "dataProductsThatWriteToContainer"); addReferences( references, frontmatter.dataProductsThatReadFromContainer, ["dataProduct"], "dataProductsThatReadFromContainer" ); } if (file.resourceType === "dataProduct") { const dataProductLinkTypes = [ "event", "command", "query", "service", "agent", "system", "container", "channel", "dataProduct" ]; addReferences(references, frontmatter.inputs, dataProductLinkTypes, "inputs"); addReferences(references, frontmatter.outputs, dataProductLinkTypes, "outputs"); } if (file.resourceType === "adr") { if (frontmatter.decisionMakers && Array.isArray(frontmatter.decisionMakers)) { frontmatter.decisionMakers.forEach((owner, index) => { addReference(references, owner, ["user", "team"], `decisionMakers[${index}]`); }); } if (frontmatter.appliesTo && Array.isArray(frontmatter.appliesTo)) { frontmatter.appliesTo.forEach((target, index) => { addTypedReference(references, target, `appliesTo[${index}]`); }); } addReferences(references, frontmatter.supersedes, ["adr"], "supersedes"); addReferences(references, frontmatter.supersededBy, ["adr"], "supersededBy"); addReferences(references, frontmatter.amends, ["adr"], "amends"); addReferences(references, frontmatter.amendedBy, ["adr"], "amendedBy"); addReferences(references, frontmatter.related, ["adr"], "related"); } if (file.resourceType === "team" && frontmatter.members && Array.isArray(frontmatter.members)) { frontmatter.members.forEach((member, index) => { addReference(references, member, ["user"], `members[${index}]`); }); } if (file.resourceType === "user" || file.resourceType === "team") { addReferences(references, frontmatter.ownedAgents, ["agent"], "ownedAgents"); addReferences(references, frontmatter.ownedDomains, ["domain"], "ownedDomains"); addReferences(references, frontmatter.ownedSystems, ["system"], "ownedSystems"); addReferences(references, frontmatter.ownedServices, ["service"], "ownedServices"); addReferences(references, frontmatter.ownedEvents, ["event"], "ownedEvents"); addReferences(references, frontmatter.ownedCommands, ["command"], "ownedCommands"); addReferences(references, frontmatter.ownedQueries, ["query"], "ownedQueries"); } if (file.resourceType === "user") { addReferences(references, frontmatter.associatedTeams, ["team"], "associatedTeams"); } return references; }; var extractChannelReferences = (parsedFile) => { const { file, frontmatter } = parsedFile; const references = []; if (file.resourceType !== "service" && file.resourceType !== "domain" && file.resourceType !== "agent") { return references; } const extractFromPointers = (pointers, parentField) => { if (!Array.isArray(pointers)) return; pointers.forEach((pointer, idx) => { if (pointer.to && Array.isArray(pointer.to)) { pointer.to.forEach((channelRef, cIdx) => { if (channelRef && channelRef.id) { references.push({ ref: { id: channelRef.id, version: channelRef.version }, possibleTypes: ["channel"], field: `${parentField}[${idx}].to[${cIdx}]` }); } }); } if (pointer.from && Array.isArray(pointer.from)) { pointer.from.forEach((channelRef, cIdx) => { if (channelRef && channelRef.id) { references.push({ ref: { id: channelRef.id, version: channelRef.version }, possibleTypes: ["channel"], field: `${parentField}[${idx}].from[${cIdx}]` }); } }); } }); }; if (frontmatter.sends && Array.isArray(frontmatter.sends)) { extractFromPointers(frontmatter.sends, "sends"); } if (frontmatter.receives && Array.isArray(frontmatter.receives)) { extractFromPointers(frontmatter.receives, "receives"); } return references; }; var validateReferences = (parsedFiles, dependencies) => { const index = buildResourceIndex(parsedFiles, dependencies); const errors = []; for (const parsedFile of parsedFiles) { const references = extractReferences(parsedFile); for (const { ref, possibleTypes, field } of references) { const found = possibleTypes.some((type) => checkResourceExists(ref, type, index)); if (!found) { const versionStr = ref.version ? ` (version: ${ref.version})` : ""; const typeStr = possibleTypes.length === 1 ? possibleTypes[0] : possibleTypes.join("/"); let rule = "refs/resource-exists"; if (field === "owners" || field.startsWith("owners[") || field.startsWith("decisionMakers[")) { rule = "refs/owner-exists"; } else if (field === "writesTo" || field === "readsFrom" || field.includes("container") || field.includes("Container")) { rule = "refs/container-exists"; } else if (field.includes("channel") || field.includes("Channel") || field.includes(".to[") || field.includes(".from[") || field === "routes" || field.startsWith("routes[")) { rule = "refs/channel-exists"; } else if (ref.version) { rule = "refs/valid-version-range"; } errors.push({ type: "reference", resource: `${parsedFile.file.resourceType}/${parsedFile.file.resourceId}`, field, message: `Referenced ${typeStr} "${ref.id}"${versionStr} does not exist`, file: parsedFile.file.relativePath, rule }); } } const channelRefs = extractChannelReferences(parsedFile); for (const { ref, possibleTypes, field } of channelRefs) { const found = possibleTypes.some((type) => checkResourceExists(ref, type, index)); if (!found) { const versionStr = ref.version ? ` (version: ${ref.version})` : ""; errors.push({ type: "reference", resource: `${parsedFile.file.resourceType}/${parsedFile.file.resourceId}`, field, message: `Referenced channel "${ref.id}"${versionStr} does not exist`, file: parsedFile.file.relativePath, rule: "refs/channel-exists" }); } } } return errors; }; var validateOrphanMessages = (parsedFiles, dependencies) => { const errors = []; const messageTypes = ["event", "command", "query"]; const messageFiles = parsedFiles.filter((pf) => messageTypes.includes(pf.file.resourceType)); if (messageFiles.length === 0) return errors; const producedMessages = /* @__PURE__ */ new Set(); const consumedMessages = /* @__PURE__ */ new Set(); for (const parsedFile of parsedFiles) { const { file, frontmatter } = parsedFile; if (file.resourceType === "service" || file.resourceType === "domain" || file.resourceType === "agent") { if (frontmatter.sends && Array.isArray(frontmatter.sends)) { frontmatter.sends.forEach((ref) => { if (ref && ref.id) producedMessages.add(ref.id); }); } if (frontmatter.receives && Array.isArray(frontmatter.receives)) { frontmatter.receives.forEach((ref) => { if (ref && ref.id) consumedMessages.add(ref.id); }); } } if (messageTypes.includes(file.resourceType)) { if (frontmatter.producers && Array.isArray(frontmatter.producers) && frontmatter.producers.length > 0) { const msgId = frontmatter.id || file.resourceId; producedMessages.add(msgId); } if (frontmatter.consumers && Array.isArray(frontmatter.consumers) && frontmatter.consumers.length > 0) { const msgId = frontmatter.id || file.resourceId; consumedMessages.add(msgId); } } } if (dependencies) { for (const [type, entries] of Object.entries(dependencies)) { if (messageTypes.includes(type)) { entries.forEach((entry) => { producedMessages.add(entry.id); consumedMessages.add(entry.id); }); } } } for (const parsedFile of messageFiles) { const msgId = parsedFile.frontmatter.id || parsedFile.file.resourceId; const isProduced = producedMessages.has(msgId); const isConsumed = consumedMessages.has(msgId); if (!isProduced && !isConsumed) { errors.push({ type: "reference", resource: `${parsedFile.file.resourceType}/${parsedFile.file.resourceId}`, field: "id", message: `${parsedFile.file.resourceType} "${msgId}" has no producer and no consumer`, file: parsedFile.file.relativePath, rule: "refs/orphan-messages" }); } } return errors; }; var validateDeprecatedReferences = (parsedFiles) => { const errors = []; const deprecatedIndex = {}; for (const parsedFile of parsedFiles) { const { file, frontmatter } = parsedFile; if (frontmatter.deprecated && frontmatter.deprecated !== false) { const resourceId = frontmatter.id || file.resourceId; const key = `${file.resourceType}:${resourceId}`; if (!deprecatedIndex[key]) { deprecatedIndex[key] = /* @__PURE__ */ new Set(); } if (frontmatter.version && typeof frontmatter.version === "string") { deprecatedIndex[key].add(frontmatter.version); } else { deprecatedIndex[key].add("*"); } } } if (Object.keys(deprecatedIndex).length === 0) return errors; const isDeprecated = (type, id, version) => { const key = `${type}:${id}`; const versions = deprecatedIndex[key]; if