@eventcatalog/linter
Version:
A linter for EventCatalog to validate frontmatter and resource references
1 lines • 128 kB
Source Map (JSON)
{"version":3,"sources":["../src/schemas/common.ts","../src/schemas/domain.ts","../src/schemas/system.ts","../src/schemas/service.ts","../src/schemas/message.ts","../src/schemas/channel.ts","../src/schemas/flow.ts","../src/schemas/entity.ts","../src/schemas/user.ts","../src/schemas/team.ts","../src/schemas/agent.ts","../src/schemas/adr.ts","../src/schemas/container.ts","../src/schemas/data-product.ts","../src/schemas/diagram.ts","../src/schemas/index.ts","../src/scanner/index.ts","../src/parser/index.ts","../src/validators/schema-validator.ts","../src/validators/reference-validator.ts","../src/validators/best-practices-validator.ts","../src/validators/index.ts","../src/reporters/index.ts","../src/config/index.ts"],"sourcesContent":["import { z } from 'zod';\n\nexport const badgeSchema = z.object({\n content: z.string(),\n backgroundColor: z.string(),\n textColor: z.string(),\n icon: z.string().optional(),\n url: z.string().optional(),\n});\n\nexport const ownerReferenceSchema = z\n .union([\n // The ID of the user or team\n z.string(),\n // The full object with the ID and collection (keep compatibility with `reference`)\n z.object({\n id: z.string(),\n collection: z.enum(['users', 'teams']),\n }),\n ])\n .transform(\n // This transformation is needed to keep compatibility with `reference`.\n // The utilities `getTeams` and `getUsers` rely on this transformation.\n (lookup) => ({ id: typeof lookup === 'string' ? lookup : lookup.id })\n );\n\nexport const specificationSchema = z.union([\n z.object({\n openapiPath: z.string().optional(),\n asyncapiPath: z.string().optional(),\n graphqlPath: z.string().optional(),\n }),\n z.array(\n z.object({\n type: z.enum(['openapi', 'asyncapi', 'graphql']),\n path: z.string(),\n name: z.string().optional(),\n headers: z.record(z.string()).optional(),\n })\n ),\n]);\n\nexport const repositorySchema = z.object({\n language: z.string().optional(),\n url: z.string().optional(),\n});\n\nexport const draftSchema = z.union([\n z.boolean(),\n z.object({\n title: z.string().optional(),\n message: z.string(),\n }),\n]);\n\nexport const deprecatedSchema = z.union([\n z.object({\n message: z.string().optional(),\n date: z.union([z.string(), z.date()]).optional(),\n }),\n z.boolean().optional(),\n]);\n\nexport const pointerSchema = z.object({\n id: z.string(),\n version: z.string().optional().default('latest'),\n});\n\nexport const resourcePointerSchema = z.object({\n id: z.string(),\n version: z.string().optional().default('latest'),\n type: z.enum([\n 'agent',\n 'service',\n 'event',\n 'command',\n 'query',\n 'flow',\n 'channel',\n 'domain',\n 'system',\n 'user',\n 'team',\n 'container',\n 'entity',\n 'diagram',\n 'data-product',\n 'adr',\n ]),\n});\n\nexport const channelPointerSchema = z\n .object({\n parameters: z.record(z.string()).optional(),\n })\n .merge(pointerSchema);\n\nexport const detailPanelPropertySchema = z\n .object({\n visible: z.boolean().optional(),\n })\n .optional();\n\nexport const sendsPointerSchema = z.object({\n id: z.string(),\n version: z.string().optional().default('latest'),\n fields: z.array(z.string()).optional(),\n group: z.string().optional(),\n to: z\n .array(\n channelPointerSchema.extend({\n delivery_mode: z.enum(['push', 'pull', 'push-pull']).optional().default('push'),\n })\n )\n .optional(),\n});\n\nexport const receivesPointerSchema = z.object({\n id: z.string(),\n version: z.string().optional().default('latest'),\n fields: z.array(z.string()).optional(),\n group: z.string().optional(),\n from: z\n .array(\n channelPointerSchema.extend({\n delivery_mode: z.enum(['push', 'pull', 'push-pull']).optional().default('push'),\n })\n )\n .optional(),\n});\n\nexport const resourceReferenceSchema = pointerSchema;\n\nexport const semverSchema = z.string().refine((version) => {\n // Allow common patterns used in EventCatalog\n if (version === 'latest') return true;\n\n // Allow x patterns like 0.0.x, 1.x, 2.1.x but not x.x.x\n if (version.includes('.x')) {\n const xPattern = /^\\d+(\\.\\d+)*\\.x$/;\n return xPattern.test(version);\n }\n\n // Allow semver ranges like ^1.0.0, ~1.2.0\n if (version.startsWith('^') || version.startsWith('~')) {\n const rangeVersion = version.substring(1);\n const semverRegex = /^\\d+\\.\\d+\\.\\d+(-[\\w\\d-.]+)?(\\+[\\w\\d-.]+)?$/;\n return semverRegex.test(rangeVersion);\n }\n\n // For strict semver, use a regex that matches the semver spec\n const semverRegex = /^\\d+\\.\\d+\\.\\d+(-[\\w\\d-.]+)?(\\+[\\w\\d-.]+)?$/;\n return semverRegex.test(version);\n}, 'Invalid semantic version format');\n\nexport const sidebarSchema = z\n .object({\n label: z.string().optional(),\n badge: z.string().optional(),\n color: z.string().optional(),\n backgroundColor: z.string().optional(),\n })\n .optional();\n\nexport const stylesSchema = z\n .object({\n icon: z.string().optional(),\n node: z\n .object({\n color: z.string().optional(),\n label: z.string().optional(),\n })\n .optional(),\n })\n .optional();\n\nexport const resourceGroupSchema = z\n .array(\n z.object({\n id: z.string().optional(),\n title: z.string().optional(),\n items: z.array(resourcePointerSchema),\n limit: z.number().optional().default(10),\n sidebar: z.boolean().optional().default(true),\n })\n )\n .optional();\n\nexport const catalogMetadataSchema = z\n .object({\n path: z.string(),\n filePath: z.string(),\n astroContentFilePath: z.string(),\n publicPath: z.string(),\n type: z.string(),\n })\n .optional();\n\nexport const baseSchema = z.object({\n id: z.string(),\n name: z.string(),\n summary: z.string().optional(),\n version: semverSchema,\n draft: z.union([z.boolean(), z.object({ title: z.string().optional(), message: z.string() })]).optional(),\n badges: z.array(badgeSchema).optional(),\n owners: z.array(ownerReferenceSchema).optional(),\n schemaPath: z.string().optional(),\n sidebar: sidebarSchema,\n repository: repositorySchema.optional(),\n specifications: specificationSchema.optional(),\n hidden: z.boolean().optional(),\n editUrl: z.string().optional(),\n resourceGroups: resourceGroupSchema,\n styles: stylesSchema,\n deprecated: deprecatedSchema.optional(),\n visualiser: z.boolean().optional(),\n attachments: z\n .array(\n z.union([\n z.string().url(),\n z.object({\n url: z.string().url(),\n title: z.string().optional(),\n type: z.string().optional(),\n description: z.string().optional(),\n icon: z.string().optional(),\n }),\n ])\n )\n .optional(),\n diagrams: z.array(pointerSchema).optional(),\n versions: z.array(z.string()).optional(),\n latestVersion: z.string().optional(),\n catalog: catalogMetadataSchema,\n});\n","import { z } from 'zod';\nimport { baseSchema, pointerSchema, receivesPointerSchema, sendsPointerSchema, detailPanelPropertySchema } from './common';\n\nexport const domainSchema = z\n .object({\n services: z.array(pointerSchema).optional(),\n agents: z.array(pointerSchema).optional(),\n domains: z.array(pointerSchema).optional(),\n systems: z.array(pointerSchema).optional(),\n entities: z.array(pointerSchema).optional(),\n 'data-products': z.array(pointerSchema).optional(),\n dataProducts: z.array(pointerSchema).optional(),\n flows: z.array(pointerSchema).optional(),\n sends: z.array(sendsPointerSchema).optional(),\n receives: z.array(receivesPointerSchema).optional(),\n detailsPanel: z\n .object({\n parentDomains: detailPanelPropertySchema,\n subdomains: detailPanelPropertySchema,\n systems: detailPanelPropertySchema,\n services: detailPanelPropertySchema,\n agents: detailPanelPropertySchema,\n entities: detailPanelPropertySchema,\n messages: detailPanelPropertySchema,\n ubiquitousLanguage: detailPanelPropertySchema,\n repository: detailPanelPropertySchema,\n versions: detailPanelPropertySchema,\n owners: detailPanelPropertySchema,\n changelog: detailPanelPropertySchema,\n attachments: detailPanelPropertySchema,\n })\n .optional(),\n })\n .merge(baseSchema);\n","import { z } from 'zod';\nimport { baseSchema, detailPanelPropertySchema, pointerSchema } from './common';\n\nconst systemRelationshipPointerSchema = z.object({\n id: z.string(),\n version: z.string().optional().default('latest'),\n label: z.string().optional(),\n});\n\nconst systemActorRelationshipSchema = z.object({\n id: z.string(),\n name: z.string().optional(),\n label: z.string().optional(),\n direction: z.enum(['inbound', 'outbound']).optional().default('inbound'),\n});\n\nexport const systemSchema = z\n .object({\n scope: z.enum(['internal', 'external']).optional().default('internal'),\n services: z.array(pointerSchema).optional(),\n flows: z.array(pointerSchema).optional(),\n entities: z.array(pointerSchema).optional(),\n containers: z.array(pointerSchema).optional(),\n relationships: z.array(systemRelationshipPointerSchema).optional(),\n actors: z.array(systemActorRelationshipSchema).optional(),\n detailsPanel: z\n .object({\n versions: detailPanelPropertySchema,\n repository: detailPanelPropertySchema,\n owners: detailPanelPropertySchema,\n changelog: detailPanelPropertySchema,\n attachments: detailPanelPropertySchema,\n services: detailPanelPropertySchema,\n flows: detailPanelPropertySchema,\n entities: detailPanelPropertySchema,\n containers: detailPanelPropertySchema,\n diagrams: detailPanelPropertySchema,\n })\n .optional(),\n })\n .merge(baseSchema);\n","import { z } from 'zod';\nimport { baseSchema, pointerSchema, receivesPointerSchema, sendsPointerSchema, detailPanelPropertySchema } from './common';\n\nexport const serviceSchema = z\n .object({\n sends: z.array(sendsPointerSchema).optional(),\n receives: z.array(receivesPointerSchema).optional(),\n entities: z.array(pointerSchema).optional(),\n writesTo: z.array(pointerSchema).optional(),\n readsFrom: z.array(pointerSchema).optional(),\n flows: z.array(pointerSchema).optional(),\n externalSystem: z.boolean().optional(),\n detailsPanel: z\n .object({\n domains: detailPanelPropertySchema,\n messages: detailPanelPropertySchema,\n versions: detailPanelPropertySchema,\n specifications: detailPanelPropertySchema,\n entities: detailPanelPropertySchema,\n repository: detailPanelPropertySchema,\n owners: detailPanelPropertySchema,\n changelog: detailPanelPropertySchema,\n containers: detailPanelPropertySchema,\n })\n .optional(),\n })\n .merge(baseSchema);\n","import { z } from 'zod';\nimport { baseSchema, channelPointerSchema, detailPanelPropertySchema } from './common';\n\nconst operationSchema = z\n .object({\n method: z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH']).optional(),\n path: z.string().optional(),\n statusCodes: z.array(z.string()).optional(),\n })\n .optional();\n\nconst messageDetailsPanelSchema = z\n .object({\n producers: detailPanelPropertySchema,\n consumers: detailPanelPropertySchema,\n channels: detailPanelPropertySchema,\n versions: detailPanelPropertySchema,\n repository: detailPanelPropertySchema,\n owners: detailPanelPropertySchema,\n changelog: detailPanelPropertySchema,\n attachments: detailPanelPropertySchema,\n })\n .optional();\n\nconst baseMessageSchema = z\n .object({\n operation: operationSchema,\n producers: z.array(z.any()).optional(), // reference('services')\n consumers: z.array(z.any()).optional(), // reference('services')\n channels: z.array(channelPointerSchema).optional(),\n schemas: z\n .array(\n z.object({\n id: z.string().optional(),\n ref: z.string().optional(),\n file: z.string().optional(),\n path: z.string().optional(),\n name: z.string().optional(),\n format: z.string().optional(),\n environments: z.array(z.string()).optional(),\n default: z.boolean().optional(),\n })\n )\n .optional(),\n messageChannels: z.array(z.any()).optional(), // reference('channels')\n detailsPanel: messageDetailsPanelSchema,\n })\n .merge(baseSchema);\n\nexport const eventSchema = baseMessageSchema;\nexport const commandSchema = baseMessageSchema;\nexport const querySchema = baseMessageSchema;\n","import { z } from 'zod';\nimport { baseSchema, detailPanelPropertySchema, pointerSchema, channelPointerSchema } from './common';\n\nconst parameterSchema = z.object({\n enum: z.array(z.string()).optional(),\n default: z.string().optional(),\n examples: z.array(z.string()).optional(),\n description: z.string().optional(),\n});\n\nexport const channelSchema = z\n .object({\n channels: z.array(channelPointerSchema).optional(),\n address: z.string().optional(),\n protocols: z.array(z.string()).optional(),\n deliveryGuarantee: z.enum(['at-most-once', 'at-least-once', 'exactly-once']).optional(),\n routes: z.array(channelPointerSchema).optional(),\n parameters: z.record(parameterSchema).optional(),\n messages: z.array(z.object({ collection: z.string(), name: z.string(), ...pointerSchema.shape })).optional(),\n detailsPanel: z\n .object({\n producers: detailPanelPropertySchema,\n consumers: detailPanelPropertySchema,\n messages: detailPanelPropertySchema,\n protocols: detailPanelPropertySchema,\n parameters: detailPanelPropertySchema,\n versions: detailPanelPropertySchema,\n repository: detailPanelPropertySchema,\n owners: detailPanelPropertySchema,\n changelog: detailPanelPropertySchema,\n attachments: detailPanelPropertySchema,\n })\n .optional(),\n })\n .merge(baseSchema);\n","import { z } from 'zod';\nimport { baseSchema, pointerSchema } from './common';\n\nconst flowStep = z\n .union([\n z.union([z.string(), z.number()]),\n z\n .object({\n id: z.union([z.string(), z.number()]),\n label: z.string().optional(),\n })\n .optional(),\n ])\n .optional();\n\nconst flowStepSchema = z\n .object({\n id: z.union([z.string(), z.number()]),\n type: z.enum(['node', 'message', 'agent', 'user', 'actor']).optional(),\n title: z.string(),\n summary: z.string().optional(),\n message: pointerSchema.optional(),\n agent: pointerSchema.optional(),\n service: pointerSchema.optional(),\n flow: pointerSchema.optional(),\n container: pointerSchema.optional(),\n dataProduct: pointerSchema.optional(),\n actor: z\n .object({\n name: z.string(),\n summary: z.string().optional(),\n })\n .optional(),\n custom: z\n .object({\n title: z.string(),\n icon: z.string().optional(),\n type: z.string().optional(),\n summary: z.string().optional(),\n url: z.string().url().optional(),\n color: z.string().optional(),\n properties: z.record(z.union([z.string(), z.number()])).optional(),\n height: z.number().optional(),\n menu: z\n .array(\n z.object({\n label: z.string(),\n url: z.string().url().optional(),\n })\n )\n .optional(),\n })\n .optional(),\n externalSystem: z\n .object({\n name: z.string(),\n summary: z.string().optional(),\n url: z.string().url().optional(),\n })\n .optional(),\n next_step: flowStep,\n next_steps: z.array(flowStep).optional(),\n })\n .refine((data) => {\n if (data.next_step && data.next_steps) return false;\n const typesUsed = [\n data.message,\n data.agent,\n data.service,\n data.flow,\n data.container,\n data.dataProduct,\n data.actor,\n data.custom,\n ].filter((v) => v).length;\n return typesUsed === 0 || typesUsed === 1;\n });\n\nexport const flowSchema = z\n .object({\n steps: z.array(flowStepSchema),\n })\n .merge(baseSchema);\n","import { z } from 'zod';\nimport { baseSchema, detailPanelPropertySchema } from './common';\n\nexport interface EntityPropertySchema {\n name: string;\n type: string;\n required?: boolean;\n description?: string;\n references?: string;\n referencesIdentifier?: string;\n referenceTarget?: 'entity';\n relationType?: string;\n enum?: string[];\n properties?: EntityPropertySchema[];\n items?: {\n type: string;\n properties?: EntityPropertySchema[];\n };\n}\n\nconst propertySchema: z.ZodType<EntityPropertySchema> = z.lazy(() =>\n z.object({\n name: z.string(),\n type: z.string(),\n required: z.boolean().optional(),\n description: z.string().optional(),\n references: z.string().optional(),\n referencesIdentifier: z.string().optional(),\n referenceTarget: z.literal('entity').optional(),\n relationType: z.string().optional(),\n enum: z.array(z.string()).optional(),\n properties: z.array(propertySchema).optional(),\n items: z\n .object({\n type: z.string(),\n properties: z.array(propertySchema).optional(),\n })\n .optional(),\n })\n);\n\nexport const entitySchema = z\n .object({\n aggregateRoot: z.boolean().optional(),\n identifier: z.string().optional(),\n properties: z.array(propertySchema).optional(),\n services: z.array(z.any()).optional(), // reference('services')\n domains: z.array(z.any()).optional(), // reference('domains')\n detailsPanel: z\n .object({\n domains: detailPanelPropertySchema,\n services: detailPanelPropertySchema,\n messages: detailPanelPropertySchema,\n versions: detailPanelPropertySchema,\n owners: detailPanelPropertySchema,\n changelog: detailPanelPropertySchema,\n attachments: detailPanelPropertySchema,\n })\n .optional(),\n })\n .merge(baseSchema);\n","import { z } from 'zod';\n\nexport const userSchema = z.object({\n id: z.string(),\n name: z.string(),\n avatarUrl: z.string().optional(),\n role: z.string().optional(),\n hidden: z.boolean().optional(),\n source: z\n .object({\n provider: z.string(),\n id: z.string().optional(),\n url: z.string().optional(),\n })\n .optional(),\n readOnly: z.boolean().optional(),\n email: z.string().email().optional(),\n slackDirectMessageUrl: z.string().optional(),\n msTeamsDirectMessageUrl: z.string().optional(),\n ownedAgents: z.array(z.any()).optional(), // reference('agents')\n ownedDomains: z.array(z.any()).optional(), // reference('domains')\n ownedSystems: z.array(z.any()).optional(), // reference('systems')\n ownedServices: z.array(z.any()).optional(), // reference('services')\n ownedEvents: z.array(z.any()).optional(), // reference('events')\n ownedCommands: z.array(z.any()).optional(), // reference('commands')\n ownedQueries: z.array(z.any()).optional(), // reference('queries')\n associatedTeams: z.array(z.any()).optional(), // reference('teams')\n});\n","import { z } from 'zod';\n\nexport const teamSchema = z.object({\n id: z.string(),\n name: z.string(),\n summary: z.string().optional(),\n email: z.string().email().optional(),\n hidden: z.boolean().optional(),\n source: z\n .object({\n provider: z.string(),\n id: z.string().optional(),\n url: z.string().optional(),\n })\n .optional(),\n readOnly: z.boolean().optional(),\n slackDirectMessageUrl: z.string().optional(),\n msTeamsDirectMessageUrl: z.string().optional(),\n members: z.array(z.any()).optional(), // reference('users')\n ownedAgents: z.array(z.any()).optional(), // reference('agents')\n ownedCommands: z.array(z.any()).optional(), // reference('commands')\n ownedQueries: z.array(z.any()).optional(), // reference('queries')\n ownedDomains: z.array(z.any()).optional(), // reference('domains')\n ownedSystems: z.array(z.any()).optional(), // reference('systems')\n ownedServices: z.array(z.any()).optional(), // reference('services')\n ownedEvents: z.array(z.any()).optional(), // reference('events')\n});\n","import { z } from 'zod';\nimport { baseSchema, detailPanelPropertySchema, pointerSchema, receivesPointerSchema, sendsPointerSchema } from './common';\n\nconst agentToolSchema = z.object({\n name: z.string(),\n type: z.string(),\n icon: z.string().optional(),\n url: z.string().optional(),\n description: z.string().optional(),\n});\n\nconst agentModelSchema = z.object({\n provider: z.string().optional(),\n name: z.string().optional(),\n version: z.string().optional(),\n});\n\nexport const agentSchema = z\n .object({\n sends: z.array(sendsPointerSchema).optional(),\n receives: z.array(receivesPointerSchema).optional(),\n writesTo: z.array(pointerSchema).optional(),\n readsFrom: z.array(pointerSchema).optional(),\n flows: z.array(pointerSchema).optional(),\n model: agentModelSchema.optional(),\n tools: z.array(agentToolSchema).optional(),\n specifications: z.undefined().optional(),\n detailsPanel: z\n .object({\n domains: detailPanelPropertySchema,\n messages: detailPanelPropertySchema,\n versions: detailPanelPropertySchema,\n repository: detailPanelPropertySchema,\n owners: detailPanelPropertySchema,\n changelog: detailPanelPropertySchema,\n containers: detailPanelPropertySchema,\n tools: detailPanelPropertySchema,\n model: detailPanelPropertySchema,\n })\n .optional(),\n })\n .merge(baseSchema.omit({ specifications: true }));\n","import { z } from 'zod';\nimport { baseSchema, detailPanelPropertySchema, pointerSchema, resourcePointerSchema } from './common';\n\nexport const adrSchema = z\n .object({\n status: z.enum(['proposed', 'accepted', 'rejected', 'deprecated', 'superseded']),\n date: z.coerce.date(),\n decisionMakers: z.array(z.any()).optional(),\n appliesTo: z.array(resourcePointerSchema).optional(),\n supersedes: z.array(pointerSchema).optional(),\n supersededBy: z.array(pointerSchema).optional(),\n amends: z.array(pointerSchema).optional(),\n amendedBy: z.array(pointerSchema).optional(),\n related: z.array(pointerSchema).optional(),\n detailsPanel: z\n .object({\n status: detailPanelPropertySchema,\n date: detailPanelPropertySchema,\n decisionMakers: detailPanelPropertySchema,\n appliesTo: detailPanelPropertySchema,\n relationships: detailPanelPropertySchema,\n owners: detailPanelPropertySchema,\n repository: detailPanelPropertySchema,\n changelog: detailPanelPropertySchema,\n })\n .optional(),\n })\n .merge(baseSchema);\n","import { z } from 'zod';\nimport { baseSchema, detailPanelPropertySchema } from './common';\n\nexport const containerSchema = z\n .object({\n container_type: z.enum([\n 'database',\n 'cache',\n 'objectStore',\n 'searchIndex',\n 'dataWarehouse',\n 'dataLake',\n 'externalSaaS',\n 'other',\n ]),\n technology: z.string().optional(),\n authoritative: z.boolean().optional().default(false),\n access_mode: z.enum(['read', 'write', 'readWrite', 'appendOnly']).optional(),\n classification: z.enum(['public', 'internal', 'confidential', 'regulated']).optional(),\n residency: z.string().optional(),\n retention: z.string().optional(),\n detailsPanel: z\n .object({\n versions: detailPanelPropertySchema,\n repository: detailPanelPropertySchema,\n owners: detailPanelPropertySchema,\n changelog: detailPanelPropertySchema,\n attachments: detailPanelPropertySchema,\n services: detailPanelPropertySchema,\n flows: detailPanelPropertySchema,\n })\n .optional(),\n services: z.array(z.any()).optional(),\n servicesThatWriteToContainer: z.array(z.any()).optional(),\n servicesThatReadFromContainer: z.array(z.any()).optional(),\n dataProductsThatWriteToContainer: z.array(z.any()).optional(),\n dataProductsThatReadFromContainer: z.array(z.any()).optional(),\n })\n .merge(baseSchema);\n","import { z } from 'zod';\nimport { baseSchema, detailPanelPropertySchema, pointerSchema } from './common';\n\nconst dataProductOutputPointerSchema = z.object({\n id: z.string(),\n version: z.string().optional().default('latest'),\n contract: z\n .object({\n path: z.string(),\n name: z.string(),\n type: z.string().optional(),\n })\n .optional(),\n});\n\nexport const dataProductSchema = z\n .object({\n inputs: z.array(pointerSchema).optional(),\n outputs: z.array(dataProductOutputPointerSchema).optional(),\n detailsPanel: z\n .object({\n domains: detailPanelPropertySchema,\n inputs: detailPanelPropertySchema,\n outputs: detailPanelPropertySchema,\n versions: detailPanelPropertySchema,\n repository: detailPanelPropertySchema,\n owners: detailPanelPropertySchema,\n changelog: detailPanelPropertySchema,\n flows: detailPanelPropertySchema,\n })\n .optional(),\n })\n .merge(baseSchema);\n","import { z } from 'zod';\nimport { baseSchema, detailPanelPropertySchema } from './common';\n\nexport const diagramSchema = z\n .object({\n detailsPanel: z\n .object({\n versions: detailPanelPropertySchema,\n owners: detailPanelPropertySchema,\n changelog: detailPanelPropertySchema,\n attachments: detailPanelPropertySchema,\n })\n .optional(),\n })\n .merge(baseSchema);\n","export * from './common';\nexport * from './domain';\nexport * from './system';\nexport * from './service';\nexport * from './message';\nexport * from './channel';\nexport * from './flow';\nexport * from './entity';\nexport * from './user';\nexport * from './team';\nexport * from './agent';\nexport * from './adr';\nexport * from './container';\nexport * from './data-product';\nexport * from './diagram';\nexport * from './data-store';\n\nimport { domainSchema } from './domain';\nimport { systemSchema } from './system';\nimport { serviceSchema } from './service';\nimport { eventSchema, commandSchema, querySchema } from './message';\nimport { channelSchema } from './channel';\nimport { flowSchema } from './flow';\nimport { entitySchema } from './entity';\nimport { userSchema } from './user';\nimport { teamSchema } from './team';\nimport { agentSchema } from './agent';\nimport { adrSchema } from './adr';\nimport { containerSchema } from './container';\nimport { dataProductSchema } from './data-product';\nimport { diagramSchema } from './diagram';\nimport { dataStoreSchema } from './data-store';\n\nexport const schemas = {\n domain: domainSchema,\n system: systemSchema,\n service: serviceSchema,\n agent: agentSchema,\n adr: adrSchema,\n event: eventSchema,\n command: commandSchema,\n query: querySchema,\n channel: channelSchema,\n flow: flowSchema,\n entity: entitySchema,\n container: containerSchema,\n dataProduct: dataProductSchema,\n diagram: diagramSchema,\n user: userSchema,\n team: teamSchema,\n // Backwards-compatible alias for older linter integrations.\n dataStore: dataStoreSchema,\n} as const;\n\nexport type ResourceType = keyof typeof schemas;\n","import fg from 'fast-glob';\nimport path from 'path';\nimport { ResourceType } from '../schemas';\n\nexport interface CatalogFile {\n path: string;\n relativePath: string;\n resourceType: ResourceType;\n resourceId: string;\n version?: string;\n}\n\nconst RESOURCE_DIRECTORY_NAMES: Partial<Record<ResourceType, string>> = {\n dataProduct: 'data-products',\n dataStore: 'containers',\n container: 'containers',\n adr: 'adrs',\n};\n\nconst RESOURCE_PATTERNS: Partial<Record<ResourceType, string[]>> = {\n domain: [\n 'domains/*/index.{md,mdx}',\n 'domains/*/versioned/*/index.{md,mdx}',\n 'domains/*/subdomains/*/index.{md,mdx}',\n 'domains/*/subdomains/*/versioned/*/index.{md,mdx}',\n ],\n system: [\n '**/systems/**/index.{md,mdx}',\n '**/systems/**/versioned/*/index.{md,mdx}',\n '!**/systems/**/agents/**',\n '!**/systems/**/services/**',\n '!**/systems/**/events/**',\n '!**/systems/**/commands/**',\n '!**/systems/**/queries/**',\n '!**/systems/**/flows/**',\n '!**/systems/**/channels/**',\n '!**/systems/**/entities/**',\n '!**/systems/**/containers/**',\n '!**/systems/**/diagrams/**',\n '!**/systems/**/data-products/**',\n '!**/systems/**/adrs/**',\n '!**/systems/**/docs/**',\n ],\n service: [\n 'domains/*/services/*/index.{md,mdx}',\n 'domains/*/services/*/versioned/*/index.{md,mdx}',\n 'domains/*/subdomains/*/services/*/index.{md,mdx}',\n 'domains/*/subdomains/*/services/*/versioned/*/index.{md,mdx}',\n '**/systems/**/services/*/index.{md,mdx}',\n '**/systems/**/services/*/versioned/*/index.{md,mdx}',\n 'services/*/index.{md,mdx}',\n 'services/*/versioned/*/index.{md,mdx}',\n ],\n agent: ['**/agents/*/index.{md,mdx}', '**/agents/*/versioned/*/index.{md,mdx}'],\n adr: ['**/adrs/*/index.{md,mdx}', '**/adrs/*/versioned/*/index.{md,mdx}'],\n event: ['**/events/*/index.{md,mdx}', '**/events/*/versioned/*/index.{md,mdx}'],\n command: ['**/commands/*/index.{md,mdx}', '**/commands/*/versioned/*/index.{md,mdx}'],\n query: ['**/queries/*/index.{md,mdx}', '**/queries/*/versioned/*/index.{md,mdx}'],\n channel: ['**/channels/**/index.{md,mdx}', '**/channels/**/versioned/*/index.{md,mdx}'],\n flow: ['**/flows/**/index.{md,mdx}', '**/flows/**/versioned/*/index.{md,mdx}'],\n entity: ['**/entities/*/index.{md,mdx}', '**/entities/*/versioned/*/index.{md,mdx}'],\n container: ['**/containers/**/index.{md,mdx}', '**/containers/**/versioned/*/index.{md,mdx}'],\n dataProduct: ['**/data-products/*/index.{md,mdx}', '**/data-products/*/versioned/*/index.{md,mdx}'],\n diagram: ['**/diagrams/**/index.{md,mdx}', '**/diagrams/**/versioned/*/index.{md,mdx}'],\n user: ['users/*.{md,mdx}'],\n team: ['teams/*.{md,mdx}'],\n};\n\nexport const extractResourceInfo = (filePath: string, resourceType: ResourceType): { id: string; version?: string } => {\n // Normalize path separators to forward slashes for consistent parsing across platforms\n const normalizedPath = filePath.replace(/\\\\/g, '/');\n const relativePath = normalizedPath.split('/');\n\n if (resourceType === 'user' || resourceType === 'team') {\n const filename = path.basename(filePath, path.extname(filePath));\n return { id: filename };\n }\n\n // Find the resource type directory in the path\n const resourceTypePattern = RESOURCE_DIRECTORY_NAMES[resourceType] || `${resourceType}s`;\n const resourceTypeIndex = relativePath.findIndex((part) => part === resourceTypePattern);\n\n if (resourceTypeIndex === -1) {\n // Fallback to original logic if pattern not found\n const parts = relativePath.slice(1, -1);\n return { id: parts[parts.length - 1] };\n }\n\n // Extract parts after the resource type directory, excluding index.mdx\n const parts = relativePath.slice(resourceTypeIndex + 1, -1);\n\n if (parts.length === 0) {\n return { id: 'unknown' };\n }\n\n // Check for versioned structure: resourceId/versioned/version/index.mdx\n if (parts.length >= 3 && parts[parts.length - 2] === 'versioned') {\n const version = parts[parts.length - 1];\n const resourceId = parts.slice(0, -2).join('/');\n return { id: resourceId, version };\n }\n\n // Check for domain versioned structure: domains/domainId/versioned/version/index.mdx\n if (resourceType === 'domain' && parts.length >= 2) {\n const versionedIndex = parts.findIndex((part) => part === 'versioned');\n if (versionedIndex !== -1 && versionedIndex < parts.length - 1) {\n const version = parts[versionedIndex + 1];\n const resourceId = parts.slice(0, versionedIndex).join('/');\n return { id: resourceId, version };\n }\n }\n\n // Standard structure: resourceId/index.mdx\n if (parts.length === 1) {\n return { id: parts[0] };\n }\n\n // Handle subdomain structure or nested resources\n return { id: parts.join('/') };\n};\n\nexport const scanCatalogFiles = async (rootDir: string): Promise<CatalogFile[]> => {\n const files: CatalogFile[] = [];\n\n for (const [resourceType, patterns] of Object.entries(RESOURCE_PATTERNS)) {\n const foundFiles = await fg(patterns, {\n cwd: rootDir,\n absolute: true,\n onlyFiles: true,\n followSymbolicLinks: false,\n });\n\n for (const filePath of foundFiles) {\n const relativePath = path.relative(rootDir, filePath);\n const { id, version } = extractResourceInfo(relativePath, resourceType as ResourceType);\n\n files.push({\n path: filePath,\n relativePath,\n resourceType: resourceType as ResourceType,\n resourceId: id,\n version,\n });\n }\n }\n\n return files;\n};\n","import fs from 'fs/promises';\nimport matter from 'gray-matter';\nimport { CatalogFile } from '../scanner';\n\nexport interface ParsedFile {\n file: CatalogFile;\n frontmatter: Record<string, unknown>;\n content: string;\n raw: string;\n}\n\nexport interface ParseError {\n file: CatalogFile;\n error: Error;\n}\n\nexport const parseFrontmatter = async (file: CatalogFile): Promise<ParsedFile | ParseError> => {\n try {\n const fileContent = await fs.readFile(file.path, 'utf-8');\n const { data, content } = matter(fileContent);\n\n return {\n file,\n frontmatter: data,\n content,\n raw: fileContent,\n };\n } catch (error) {\n return {\n file,\n error: error instanceof Error ? error : new Error(String(error)),\n };\n }\n};\n\nexport const parseAllFiles = async (\n files: CatalogFile[]\n): Promise<{\n parsed: ParsedFile[];\n errors: ParseError[];\n}> => {\n const results = await Promise.all(files.map(parseFrontmatter));\n\n const parsed: ParsedFile[] = [];\n const errors: ParseError[] = [];\n\n for (const result of results) {\n if ('error' in result) {\n errors.push(result);\n } else {\n parsed.push(result);\n }\n }\n\n return { parsed, errors };\n};\n","import { z } from 'zod';\nimport { schemas } from '../schemas';\nimport { ParsedFile } from '../parser';\nimport { ValidationError } from '../types';\n\nexport const validateSchema = (parsedFile: ParsedFile): ValidationError[] => {\n const { file, frontmatter } = parsedFile;\n const schema = schemas[file.resourceType];\n const errors: ValidationError[] = [];\n\n try {\n schema.parse(frontmatter);\n } catch (error) {\n if (error instanceof z.ZodError) {\n for (const issue of error.issues) {\n const field = issue.path.join('.');\n let message = issue.message;\n\n if (issue.code === 'invalid_type') {\n message = `Expected ${issue.expected}, but received ${issue.received}`;\n }\n\n let rule = 'schema/required-fields';\n if (issue.code === 'invalid_type') {\n rule = 'schema/valid-type';\n } else if (issue.code === 'invalid_string' && issue.validation === 'email') {\n rule = 'schema/valid-email';\n } else if (field && field.includes('version')) {\n rule = 'schema/valid-semver';\n }\n\n errors.push({\n type: 'schema',\n resource: `${file.resourceType}/${file.resourceId}`,\n field: field || undefined,\n message: field ? `${field}: ${message}` : message,\n file: file.relativePath,\n rule,\n });\n }\n } else {\n errors.push({\n type: 'schema',\n resource: `${file.resourceType}/${file.resourceId}`,\n message: error instanceof Error ? error.message : String(error),\n file: file.relativePath,\n rule: 'schema/validation-error',\n });\n }\n }\n\n return errors;\n};\n\nexport const validateAllSchemas = (parsedFiles: ParsedFile[]): ValidationError[] => {\n const errors: ValidationError[] = [];\n\n for (const parsedFile of parsedFiles) {\n errors.push(...validateSchema(parsedFile));\n }\n\n return errors;\n};\n","import { ParsedFile } from '../parser';\nimport { ValidationError, ResourceReference } from '../types';\nimport { ResourceType } from '../schemas';\nimport { CatalogDependencies } from '../config';\nimport semver from 'semver';\n\ninterface ResourceIndex {\n [resourceType: string]: {\n [resourceId: string]: Set<string>;\n };\n}\n\nconst MESSAGE_TYPES: ResourceType[] = ['event', 'command', 'query'];\n\nconst RESOURCE_TYPE_ALIASES: Partial<Record<ResourceType, ResourceType[]>> = {\n container: ['container', 'dataStore'],\n dataStore: ['container', 'dataStore'],\n};\n\nconst normalizeResourceType = (type: unknown): ResourceType | undefined => {\n if (typeof type !== 'string') return undefined;\n\n const normalizedTypes: Record<string, ResourceType> = {\n agent: 'agent',\n agents: 'agent',\n adr: 'adr',\n adrs: 'adr',\n service: 'service',\n services: 'service',\n event: 'event',\n events: 'event',\n command: 'command',\n commands: 'command',\n query: 'query',\n queries: 'query',\n flow: 'flow',\n flows: 'flow',\n channel: 'channel',\n channels: 'channel',\n domain: 'domain',\n domains: 'domain',\n system: 'system',\n systems: 'system',\n user: 'user',\n users: 'user',\n team: 'team',\n teams: 'team',\n container: 'container',\n containers: 'container',\n dataStore: 'container',\n dataStores: 'container',\n 'data-product': 'dataProduct',\n 'data-products': 'dataProduct',\n dataProduct: 'dataProduct',\n dataProducts: 'dataProduct',\n entity: 'entity',\n entities: 'entity',\n diagram: 'diagram',\n diagrams: 'diagram',\n };\n\n return normalizedTypes[type];\n};\n\nconst getReference = (value: unknown): ResourceReference | undefined => {\n if (typeof value === 'string') {\n return { id: value };\n }\n\n if (!value || typeof value !== 'object') {\n return undefined;\n }\n\n const ref = value as Record<string, unknown>;\n if (typeof ref.id !== 'string') {\n return undefined;\n }\n\n return {\n id: ref.id,\n version: typeof ref.version === 'string' ? ref.version : undefined,\n };\n};\n\nconst addReference = (references: ReferenceInfo[], value: unknown, possibleTypes: ResourceType[], field: string): void => {\n const ref = getReference(value);\n if (ref) {\n references.push({ ref, possibleTypes, field });\n }\n};\n\nconst addReferences = (references: ReferenceInfo[], values: unknown, possibleTypes: ResourceType[], field: string): void => {\n if (!Array.isArray(values)) return;\n values.forEach((value) => {\n addReference(references, value, possibleTypes, field);\n });\n};\n\nconst addTypedReference = (references: ReferenceInfo[], value: unknown, field: string): void => {\n if (!value || typeof value !== 'object') return;\n const resourceType = normalizeResourceType((value as Record<string, unknown>).type);\n if (!resourceType) return;\n addReference(references, value, [resourceType], field);\n};\n\nexport const buildResourceIndex = (parsedFiles: ParsedFile[], dependencies?: CatalogDependencies): ResourceIndex => {\n const index: ResourceIndex = {};\n\n for (const parsedFile of parsedFiles) {\n const { file, frontmatter } = parsedFile;\n const { resourceType } = file;\n\n // Use the frontmatter id field if present, otherwise fall back to filename/directory\n // This handles cases where:\n // - Filename is \"aSmith.mdx\" but frontmatter has id: \"asmith\"\n // - Directory is \"e-commerce\" but frontmatter has id: \"E-Commerce\"\n // - Directory is \"UserService\" but frontmatter has id: \"user-service\" (SentenceCase vs kebab-case)\n let resourceId = file.resourceId;\n if (frontmatter.id && typeof frontmatter.id === 'string') {\n resourceId = frontmatter.id;\n }\n\n if (!index[resourceType]) {\n index[resourceType] = {};\n }\n\n if (!index[resourceType][resourceId]) {\n index[resourceType][resourceId] = new Set();\n }\n\n if (frontmatter.version && typeof frontmatter.version === 'string') {\n index[resourceType][resourceId].add(frontmatter.version);\n } else {\n index[resourceType][resourceId].add('latest');\n }\n }\n\n // Add dependency entries to the index\n if (dependencies) {\n for (const [resourceType, entries] of Object.entries(dependencies)) {\n if (!index[resourceType]) {\n index[resourceType] = {};\n }\n for (const entry of entries) {\n if (!index[resourceType][entry.id]) {\n index[resourceType][entry.id] = new Set();\n }\n index[resourceType][entry.id].add(entry.version || 'latest');\n }\n }\n }\n\n return index;\n};\n\nconst checkResourceExists = (ref: ResourceReference, resourceType: ResourceType, index: ResourceIndex): boolean => {\n const resourceTypes = RESOURCE_TYPE_ALIASES[resourceType] || [resourceType];\n const resourceVersions = new Set<string>();\n\n for (const type of resourceTypes) {\n const versions = index[type]?.[ref.id];\n if (versions) {\n versions.forEach((version) => resourceVersions.add(version));\n }\n }\n\n if (!resourceVersions || resourceVersions.size === 0) {\n return false;\n }\n\n if (!ref.version) {\n return true;\n }\n\n const refVersion = ref.version === 'latest' ? ref.version : ref.version;\n const availableVersions = Array.from(resourceVersions);\n\n // Handle 'latest' specifically\n if (refVersion === 'latest') {\n return availableVersions.includes('latest') || availableVersions.length > 0;\n }\n\n // Check for exact match first\n if (availableVersions.includes(refVersion)) {\n return true;\n }\n\n // Handle semver patterns like '0.0.x', '^1.0.0', '~1.2.0', etc.\n try {\n // Filter out 'latest' from available versions for semver matching\n const semverVersions = availableVersions.filter((v) => v !== 'latest' && semver.valid(v));\n\n // Check if any available version satisfies the requested version pattern\n for (const availableVersion of semverVersions) {\n if (semver.satisfies(availableVersion, refVersion)) {\n return true;\n }\n }\n\n // Special handling for patterns like '0.0.x' which aren't standard semver ranges\n if (refVersion.includes('.x')) {\n const pattern = refVersion.replace(/\\.x/g, '');\n for (const availableVersion of semverVersions) {\n if (availableVersion.startsWith(pattern)) {\n return true;\n }\n }\n }\n\n return false;\n } catch (error) {\n // If semver parsing fails, fall back to exact string match\n return availableVersions.includes(refVersion);\n }\n};\n\ninterface ReferenceInfo {\n ref: ResourceReference;\n possibleTypes: ResourceType[];\n field: string;\n}\n\nconst extractReferences = (parsedFile: ParsedFile): ReferenceInfo[] => {\n const { file, frontmatter } = parsedFile;\n const references: ReferenceInfo[] = [];\n\n if (frontmatter.owners && Array.isArray(frontmatter.owners)) {\n frontmatter.owners.forEach((owner: unknown, index: number) => {\n if (owner && typeof owner === 'object' && 'collection' in owner) {\n const ownerType = normalizeResourceType((owner as Record<string, unknown>).collection);\n if (ownerType) {\n addReference(references, owner, [ownerType], `owners[${index}]`);\n return;\n }\n }\n addReference(references, owner, ['user', 'team'], `owners[${index}]`);\n });\n }\n\n addReferences(references, frontmatter.diagrams, ['diagram'], 'diagrams');\n\n if (frontmatter.resourceGroups && Array.isArray(frontmatter.resourceGroups)) {\n frontmatter.resourceGroups.forEach((group: unknown, groupIndex: number) => {\n if (!group || typeof group !== 'object') return;\n const items = (group as Record<string, unknown>).items;\n if (!Array.isArray(items)) return;\n items.forEach((item, itemIndex) =>\n addTypedReference(references, item, `resourceGroups[${groupIndex}].items[${itemIndex}]`)\n );\n });\n }\n\n if (file.resourceType === 'domain') {\n addReferences(references, frontmatter.services, ['service'], 'services');\n addReferences(references, frontmatter.agents, ['agent'], 'agents');\n addReferences(references, frontmatter.domains, ['domain'], 'domains');\n addReferences(references, frontmatter.systems, ['system'], 'systems');\n addReferences(references, frontmatter.entities, ['entity'], 'entities');\n addReferences(references, frontmatter['data-products'], ['dataProduct'], 'data-products');\n addReferences(references, frontmatter.dataProducts, ['dataProduct'], 'dataProducts');\n addReferences(references, frontmatter.flows, ['flow'], 'flows');\n addReferences(references, frontmatter.sends, MESSAGE_TYPES, 'sends');\n addReferences(references, frontmatter.receives, MESSAGE_TYPES, 'receives');\n }\n\n if (file.resourceType === 'service' || file.resourceType === 'agent') {\n addReferences(references, frontmatter.sends, MESSAGE_TYPES, 'sends');\n addReferences(references, frontmatter.receives, MESSAGE_TYPES, 'receives');\n if (frontmatter.writesTo && Array.isArray(frontmatter.writesTo)) {\n frontmatter.writesTo.forEach((ref: unknown) => addReference(references, ref, ['container'], 'writesTo'));\n }\n if (frontmatter.readsFrom && Array.isArray(frontmatter.readsFrom)) {\n frontmatter.readsFrom.forEach((ref: unknown) => addReference(references, ref, ['container'], 'readsFrom'));\n }\n addReferences(references, frontmatter.flows, ['flow'], 'flows');\n }\n\n if (file.resourceType === 'service') {\n addReferences(references, frontmatter.entities, ['entity'], 'entities');\n }\n\n if (file.resourceType === 'system') {\n addReferences(references, frontmatter.services, ['service'], 'services');\n addReferences(references, frontmatter.flows, ['flow'], 'flows');\n addReferences(references, frontmatter.entities, ['entity'], 'entities');\n addReferences(references, frontmatter.containers, ['container'], 'containers');\n addReferences(references, frontmatter.relationships, ['system'], 'relationships');\n }\n\n if (file.resourceType === 'flow' && frontmatter.steps && Array.isArray(frontmatter.steps)) {\n frontmatter.steps.forEach((step: Record<string, unknown>, index: number) => {\n if (step.message) {\n addReference(references, step.message, MESSAGE_TYPES, `steps[${index}].message`);\n }\n if (step.service) {\n addReference(references, step.service, ['service'], `steps[${index}].service`);\n }\n if (step.agent) {\n addReference(references, step.agent, ['agent'], `steps[${index}].agent`);\n }\n if (step.flow) {\n addReference(references, step.flow, ['flow'], `steps[${index}].flow`);\n }\n if (step.container) {\n addReference(references, step.container, ['container'], `steps[${index}].container`);\n }\n if (step.dataProduct) {\n addReference(references, step.dataProduct, ['dataProduct'], `steps[${index}].dataProduct`);\n }\n });\n }\n\n if (file.resourceType === 'entity' && frontmatter.properties && Array.isArray(frontmatter.properties)) {\n frontmatter.properties.forEach((prop: Record<string, unknown>, index: number) => {\n if (prop.references) {\n references.push({\n ref: { id: prop.references as string },\n possibleTypes: ['entity'],\n field: `properties[${index}].references`,\n });\n }\n });\n addReferences(references, frontmatter.services, ['service'], 'services');\n addReferences(references, frontmatter.domains, ['domain'], 'domains');\n }\n\n if (MESSAGE_TYPES.includes(file.resourceType)) {\n addReferences(references, frontmatter.producers, ['service'], 'producers');\n addReferences(references, frontmatter.consumers, ['service'], 'consumers');\n addReferences(references, frontmatter.channels, ['channel'], 'channels');\n addReferences(references, frontmatter.messageChannels, ['channel'], 'messageChannels');\n }\n\n if (file.resourceType === 'channel') {\n addReferences(references, frontmatter.channels, ['channel'], 'channels');\n addReferences(references, frontmatter.routes, ['channel'], 'routes');\n if (frontmatter.messages && Array.isArray(frontmatter.messages)) {\n frontmatter.messages.forEach((message: unknown, index: number) => {\n if (!message || typeof message !== 'object') return;\n const messageType = normalizeResourceType((message as Record<string, unknown>).collection);\n if (messageType && MESSAGE_TYPES.includes(messageType)) {\n addReference(references, message, [messageType], `messages[${index}]`);\n }\n });\n }\n }\n\n if (file.resourceType === 'container' || file.resourceType === 'dataStore') {\n addReferences(references, frontmatter.services, ['service'], 'services');\n addReferences(references, frontmatter.servicesThatWriteToContainer, ['service'], 'servicesThatWriteToContainer');\n addReferences(references, frontmatter.servicesThatReadFromContainer, ['service'], 'servicesThatReadFromContainer');\n addReferences(references, frontmatter.dataProductsThatWriteToContainer, ['dataProduct'], 'dataProductsThatWriteToContainer');\n addReferences(\n references,\n frontmatter.dataProductsThatReadFromContainer,\n ['dataProduct'],\n 'dataProductsThatReadFromContain