@eventcatalog/linter
Version:
A linter for EventCatalog to validate frontmatter and resource references
1,401 lines (1,379 loc) • 68.4 kB
JavaScript
#!/usr/bin/env node
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/cli/index.ts
import { Command } from "commander";
import chalk2 from "chalk";
import ora from "ora";
import path3 from "path";
// 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";
// 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 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/validators/schema-validator.ts
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 ===