chore: harden repository guardrails
CI / verify (push) Has been cancelled
CI / infrastructure-smoke (push) Has been cancelled

This commit is contained in:
ekklesia
2026-07-12 21:20:58 -05:00
parent 192a6514cf
commit d9489384c1
100 changed files with 1303 additions and 42 deletions
+11
View File
@@ -36,3 +36,14 @@ jobs:
- name: Verify repository - name: Verify repository
run: pnpm ci run: pnpm ci
infrastructure-smoke:
needs: verify
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Check out repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Exercise PostgreSQL and S3 infrastructure
run: bash scripts/smoke-infrastructure.sh
+2
View File
@@ -22,6 +22,8 @@ pnpm infrastructure:up
pnpm verify pnpm verify
``` ```
Run `pnpm infrastructure:smoke` for an isolated end-to-end PostgreSQL and authenticated S3 check.
The development services bind only to loopback: The development services bind only to loopback:
- PostgreSQL: `127.0.0.1:5432` - PostgreSQL: `127.0.0.1:5432`
+18 -6
View File
@@ -17,24 +17,36 @@ These rules map platform ownership to filesystem and package boundaries. The exe
- Core never imports a feature module, surface, Builder implementation, or app. - Core never imports a feature module, surface, Builder implementation, or app.
- A module never imports Core implementations, another module, a surface, Builder code, or an app. - A module never imports Core implementations, another module, a surface, Builder code, or an app.
- A module cannot contain `.tsx`, `.jsx`, `.css`, `.scss`, `.less`, `.vue`, or `.svelte` source - A module cannot contain presentation source, including JSX/TSX, single-file components,
and cannot depend on a UI framework. Modules expose data and behavior only. Astro/Marko/MDX/HTML views, stylesheets, or SVG presentation assets, and cannot depend on UI
frameworks or shared UI-role packages. Modules expose data and behavior only.
- A surface never imports Core or a feature module. It uses public API/client contracts. - A surface never imports Core or a feature module. It uses public API/client contracts.
- Separate surfaces do not import each other's internals. - Separate surfaces do not import each other's internals.
- Builder code composes contracts and surface renderer identifiers; it does not import module or - Builder code composes contracts and surface renderer identifiers; it does not import module or
surface internals. surface internals.
- Contract and SDK packages cannot depend upward on implementations. - Contract and SDK packages cannot depend upward on implementations.
- Workspace package consumers use declared `exports`, not private subpaths. - Workspace package consumers use declared `exports`, not private subpaths.
- Every internal dependency uses a `workspace:` range and every imported workspace package is
declared by the consumer.
- Workspace aliases are forbidden because they obscure package ownership in the dependency graph.
- Workspace package names are unique and shared packages declare a recognized architectural role.
- Circular source and workspace-package dependencies are rejected. - Circular source and workspace-package dependencies are rejected.
- Cross-module data access uses services, events, capabilities, or opaque identifiers rather than - Cross-module data access uses services, events, capabilities, or opaque identifiers rather than
direct table imports and foreign keys. direct table imports and foreign keys.
The boundary test suite includes intentionally invalid fixture graphs. This proves that the guard The boundary test suite includes intentionally invalid fixture graphs. This proves that the guard
rejects forbidden dependency directions even before production packages fill every directory. rejects every currently machine-checkable repository rule even before production packages fill
every directory. The UI guard rejects component, view, template, stylesheet, shared UI-role, and
common UI-framework dependencies inside modules. This deny list is defense in depth; package
review remains required when adopting a new frontend technology.
Cross-module table ownership, foreign keys, and schema namespace rules require Drizzle metadata
and are intentionally deferred to the Phase 3 schema validator.
## Generated code ## Generated code
Generated artifacts are reproducible outputs with a named generator and validation step. A Generated artifacts are reproducible outputs with a named generator and validation step. A
generated module bootstrap may import discovered module entry points at an application generated module bootstrap under the root `generated/` boundary may import discovered module entry
composition edge, but Core itself remains feature-agnostic. Generated files are never an excuse points, and apps may import that generated bootstrap. Handwritten app and Core code still cannot
for a hand-maintained central module registry. import feature modules directly. Generated files are never an excuse for a hand-maintained central
module registry.
+18 -3
View File
@@ -24,7 +24,12 @@ pnpm build # Turborepo package build graph
pnpm verify # every code gate above pnpm verify # every code gate above
``` ```
CI runs `pnpm ci`, which adds Compose configuration validation to `pnpm verify`. The lint, typecheck, and test commands run both repository-root checks and every matching
package-level Turborepo task. New packages add their own `lint`, `typecheck`, and `test` scripts;
the root verification command discovers them automatically.
CI runs `pnpm ci`, which adds Compose configuration validation to `pnpm verify`. A separate CI job
runs the live infrastructure smoke test so code-only local verification does not require Docker.
## Development infrastructure ## Development infrastructure
@@ -42,6 +47,16 @@ pnpm infrastructure:down
pnpm infrastructure:logs pnpm infrastructure:logs
``` ```
Exercise both services with isolated, throwaway credentials and Docker-assigned host ports:
```bash
pnpm infrastructure:smoke
```
The smoke test verifies PostgreSQL password authentication and authenticated S3 list, put, get,
head, and delete operations. It also proves invalid database passwords, anonymous object access,
and invalid S3 signatures are rejected. Its containers, network, and volumes are removed on exit.
PostgreSQL and the S3 endpoint bind to `127.0.0.1` only. Compose uses persistent named volumes; PostgreSQL and the S3 endpoint bind to `127.0.0.1` only. Compose uses persistent named volumes;
`infrastructure:down` preserves data. The S3 service creates the configured development bucket `infrastructure:down` preserves data. The S3 service creates the configured development bucket
idempotently at startup. idempotently at startup.
@@ -49,5 +64,5 @@ idempotently at startup.
The checked-in credentials are safe only for local development. Deployed secrets must come from The checked-in credentials are safe only for local development. Deployed secrets must come from
an environment-appropriate secret provider and must not be committed. an environment-appropriate secret provider and must not be committed.
Docker is not installed in every contributor environment. `pnpm infrastructure:config` and live Docker is not installed in every contributor environment. Compose validation and live service
service health therefore run separately from the code-only `pnpm verify` gate. health therefore run separately from the code-only `pnpm verify` gate.
+1 -1
View File
@@ -6,7 +6,7 @@ services:
restart: unless-stopped restart: unless-stopped
environment: environment:
POSTGRES_DB: ${POSTGRES_DB:?POSTGRES_DB is required} POSTGRES_DB: ${POSTGRES_DB:?POSTGRES_DB is required}
POSTGRES_INITDB_ARGS: --data-checksums POSTGRES_INITDB_ARGS: --data-checksums --auth-host=scram-sha-256 --auth-local=trust
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
POSTGRES_USER: ${POSTGRES_USER:?POSTGRES_USER is required} POSTGRES_USER: ${POSTGRES_USER:?POSTGRES_USER is required}
TZ: UTC TZ: UTC
+7 -3
View File
@@ -18,11 +18,15 @@
"infrastructure:config": "docker compose --env-file .env.example -f infrastructure/compose.yaml config --quiet", "infrastructure:config": "docker compose --env-file .env.example -f infrastructure/compose.yaml config --quiet",
"infrastructure:down": "docker compose --env-file .env -f infrastructure/compose.yaml down", "infrastructure:down": "docker compose --env-file .env -f infrastructure/compose.yaml down",
"infrastructure:logs": "docker compose --env-file .env -f infrastructure/compose.yaml logs --follow", "infrastructure:logs": "docker compose --env-file .env -f infrastructure/compose.yaml logs --follow",
"infrastructure:smoke": "bash scripts/smoke-infrastructure.sh",
"infrastructure:up": "docker compose --env-file .env -f infrastructure/compose.yaml up --detach --wait", "infrastructure:up": "docker compose --env-file .env -f infrastructure/compose.yaml up --detach --wait",
"lint": "biome lint .", "lint": "turbo run lint lint:root",
"test": "vitest run", "lint:root": "biome lint .",
"test": "turbo run test test:root",
"test:root": "vitest run --config vitest.config.ts",
"test:watch": "vitest", "test:watch": "vitest",
"typecheck": "tsc --noEmit --project tsconfig.json", "typecheck": "turbo run typecheck typecheck:root",
"typecheck:root": "tsc --noEmit --project tsconfig.json",
"verify": "pnpm format:check && pnpm lint && pnpm boundaries && pnpm typecheck && pnpm test && pnpm build" "verify": "pnpm format:check && pnpm lint && pnpm boundaries && pnpm typecheck && pnpm test && pnpm build"
}, },
"devDependencies": { "devDependencies": {
+1
View File
@@ -1,5 +1,6 @@
packages: packages:
- "apps/*" - "apps/*"
- "builders/*"
- "core" - "core"
- "modules/*" - "modules/*"
- "packages/*" - "packages/*"
+432 -22
View File
@@ -1,6 +1,7 @@
import { readdir, readFile, stat } from "node:fs/promises"; import { readdir, readFile, stat } from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { pathToFileURL } from "node:url"; import { pathToFileURL } from "node:url";
import { parse } from "yaml";
type Layer = "app" | "builder" | "core" | "module" | "neutral" | "shared" | "surface"; type Layer = "app" | "builder" | "core" | "module" | "neutral" | "shared" | "surface";
@@ -28,6 +29,10 @@ interface WorkspacePackage {
name: string; name: string;
} }
interface WorkspaceConfiguration {
packages?: string[];
}
export interface BoundaryViolation { export interface BoundaryViolation {
from: string; from: string;
message: string; message: string;
@@ -42,19 +47,82 @@ export interface BoundaryResult {
} }
const CODE_EXTENSIONS = new Set([".cjs", ".cts", ".js", ".jsx", ".mjs", ".mts", ".ts", ".tsx"]); const CODE_EXTENSIONS = new Set([".cjs", ".cts", ".js", ".jsx", ".mjs", ".mts", ".ts", ".tsx"]);
const MODULE_UI_EXTENSIONS = new Set([".css", ".jsx", ".less", ".scss", ".svelte", ".tsx", ".vue"]); const MODULE_UI_EXTENSIONS = new Set([
".astro",
".css",
".htm",
".html",
".jsx",
".less",
".marko",
".mdx",
".pcss",
".postcss",
".sass",
".scss",
".styl",
".stylus",
".svelte",
".svg",
".tsx",
".vue",
]);
const MODULE_UI_PACKAGES = new Set([ const MODULE_UI_PACKAGES = new Set([
"@angular/core", "@angular/core",
"@builder.io/qwik",
"@emotion/react",
"@emotion/styled",
"@lit/react",
"@mui/material",
"@preact/signals",
"@storybook/react",
"@tamagui/core",
"alpinejs",
"astro",
"bootstrap",
"bulma",
"expo", "expo",
"gatsby",
"htmx.org",
"lit",
"marko",
"nativewind",
"next", "next",
"nuxt",
"preact",
"qwik",
"react", "react",
"react-dom", "react-dom",
"react-native", "react-native",
"react-router-dom",
"remix",
"solid-js", "solid-js",
"solid-start",
"styled-components",
"svelte", "svelte",
"tamagui",
"tailwindcss", "tailwindcss",
"unocss",
"vue", "vue",
"vue-router",
"vuetify",
]); ]);
const MODULE_UI_PACKAGE_PREFIXES = [
"@angular/",
"@chakra-ui/",
"@emotion/",
"@mui/",
"@radix-ui/",
"@remix-run/",
"@solidjs/",
"@storybook/",
"@sveltejs/",
"@tamagui/",
"@tanstack/react-",
"@unocss/",
"@vitejs/plugin-react",
"@vue/",
];
const IGNORED_DIRECTORIES = new Set([ const IGNORED_DIRECTORIES = new Set([
".git", ".git",
".next", ".next",
@@ -69,6 +137,19 @@ const DEPENDENCY_FIELDS = [
"optionalDependencies", "optionalDependencies",
"peerDependencies", "peerDependencies",
] as const; ] as const;
const SHARED_PACKAGE_ROLES = new Set([
"api-client",
"auth-client",
"builder-sdk",
"contracts",
"design-tokens",
"module-sdk",
"shared",
"surface-sdk",
"testing",
"tooling",
"ui",
]);
function normalizePath(value: string): string { function normalizePath(value: string): string {
return value.split(path.sep).join("/").replace(/^\.\//, ""); return value.split(path.sep).join("/").replace(/^\.\//, "");
@@ -129,6 +210,12 @@ function boundaryRule(from: Location, to: Location): string | undefined {
if (to.layer === "module" && from.owner !== to.owner) { if (to.layer === "module" && from.owner !== to.owner) {
return "modules-do-not-import-other-modules"; return "modules-do-not-import-other-modules";
} }
if (
to.layer === "shared" &&
!["contracts", "module-sdk", "testing", "tooling"].includes(to.role ?? "")
) {
return "modules-use-public-platform-contracts";
}
} }
if (from.layer === "surface") { if (from.layer === "surface") {
@@ -144,6 +231,10 @@ function boundaryRule(from: Location, to: Location): string | undefined {
return "builder-composes-contracts"; return "builder-composes-contracts";
} }
if (from.layer === "app" && ["app", "builder", "module"].includes(to.layer)) {
return "apps-are-composition-roots";
}
if ( if (
from.layer === "shared" && from.layer === "shared" &&
from.role === "contracts" && from.role === "contracts" &&
@@ -154,12 +245,24 @@ function boundaryRule(from: Location, to: Location): string | undefined {
if ( if (
from.layer === "shared" && from.layer === "shared" &&
["module-sdk", "surface-sdk"].includes(from.role ?? "") && ["builder-sdk", "module-sdk", "surface-sdk"].includes(from.role ?? "") &&
["app", "builder", "core", "module", "surface"].includes(to.layer) ["app", "builder", "core", "module", "surface"].includes(to.layer)
) { ) {
return "sdks-do-not-depend-on-implementations"; return "sdks-do-not-depend-on-implementations";
} }
if (from.layer === "shared" && to.layer === "shared") {
if (from.role === "contracts" && to.role !== "contracts") {
return "contracts-do-not-depend-upward";
}
if (["module-sdk", "surface-sdk"].includes(from.role ?? "") && to.role !== "contracts") {
return "sdks-do-not-depend-on-implementations";
}
if (from.role === "builder-sdk" && !["contracts", "surface-sdk"].includes(to.role ?? "")) {
return "sdks-do-not-depend-on-implementations";
}
}
return undefined; return undefined;
} }
@@ -171,7 +274,28 @@ function packageNameFromSpecifier(specifier: string): string {
} }
function isModuleUiPackage(packageName: string): boolean { function isModuleUiPackage(packageName: string): boolean {
return MODULE_UI_PACKAGES.has(packageName) || packageName.startsWith("@angular/"); const normalizedName = packageName.toLowerCase();
return (
MODULE_UI_PACKAGES.has(normalizedName) ||
MODULE_UI_PACKAGE_PREFIXES.some((prefix) => normalizedName.startsWith(prefix))
);
}
function npmAliasTarget(range: string): string | undefined {
if (!range.startsWith("npm:")) {
return undefined;
}
const alias = range.slice(4);
if (alias.startsWith("@")) {
const versionMarker = alias.lastIndexOf("@");
return versionMarker > 0 ? alias.slice(0, versionMarker) : alias;
}
return alias.split("@")[0];
}
function isModuleUiDependency(packageName: string, range: string): boolean {
const aliasTarget = npmAliasTarget(range);
return isModuleUiPackage(packageName) || (aliasTarget ? isModuleUiPackage(aliasTarget) : false);
} }
async function isFile(filePath: string): Promise<boolean> { async function isFile(filePath: string): Promise<boolean> {
@@ -210,13 +334,56 @@ async function walkFiles(root: string, current = root): Promise<string[]> {
return files; return files;
} }
function workspacePatternMatches(relativeDirectory: string, pattern: string): boolean {
let expression = "^";
for (let index = 0; index < pattern.length; index += 1) {
const character = pattern[index];
if (character === "*" && pattern[index + 1] === "*") {
expression += ".*";
index += 1;
} else if (character === "*") {
expression += "[^/]*";
} else if (character && /[\\^$.*+?()[\]{}|]/u.test(character)) {
expression += `\\${character}`;
} else {
expression += character;
}
}
expression += "$";
return new RegExp(expression, "u").test(relativeDirectory);
}
function workspaceDirectoryIsIncluded(relativeDirectory: string, patterns: string[]): boolean {
const included = patterns
.filter((pattern) => !pattern.startsWith("!"))
.some((pattern) => workspacePatternMatches(relativeDirectory, pattern));
const excluded = patterns
.filter((pattern) => pattern.startsWith("!"))
.some((pattern) => workspacePatternMatches(relativeDirectory, pattern.slice(1)));
return included && !excluded;
}
function workspacePatternIsSupported(pattern: string): boolean {
const normalizedPattern = pattern.startsWith("!") ? pattern.slice(1) : pattern;
return !/[?[\]{}]/u.test(normalizedPattern);
}
async function readWorkspacePatterns(root: string): Promise<string[] | undefined> {
const workspaceFile = path.join(root, "pnpm-workspace.yaml");
if (!(await isFile(workspaceFile))) {
return undefined;
}
const workspace = parse(await readFile(workspaceFile, "utf8")) as WorkspaceConfiguration;
return workspace.packages;
}
function collectImportSpecifiers(sourceText: string): string[] { function collectImportSpecifiers(sourceText: string): string[] {
const specifiers = new Set<string>(); const specifiers = new Set<string>();
const staticImport = const staticImport = /\b(?:import|export)\s+(?:type\s+)?[^;]*?\bfrom\s*["']([^"'\n]+)["']/gu;
/\b(?:import|export)\s+(?:type\s+)?(?:[^"'\n;]*?\s+from\s+)?["']([^"'\n]+)["']/gu; const sideEffectImport = /\bimport\s*["']([^"'\n]+)["']/gu;
const dynamicImport = /\b(?:import|require)\s*\(\s*["']([^"'\n]+)["']\s*\)/gu; const dynamicImport = /\b(?:import|require)\s*\(\s*["']([^"'\n]+)["']\s*\)/gu;
for (const pattern of [staticImport, dynamicImport]) { for (const pattern of [staticImport, sideEffectImport, dynamicImport]) {
for (const match of sourceText.matchAll(pattern)) { for (const match of sourceText.matchAll(pattern)) {
const specifier = match[1]; const specifier = match[1];
if (specifier) { if (specifier) {
@@ -313,41 +480,210 @@ function exportedSubpath(
specifier: string, specifier: string,
packageName: string, packageName: string,
): boolean { ): boolean {
const suffix = specifier.slice(packageName.length); function hasAvailableTarget(target: unknown): boolean {
if (!suffix) { if (typeof target === "string") {
return true; return true;
} }
if (Array.isArray(target)) {
if (!manifest.exports || typeof manifest.exports !== "object") { return target.some(hasAvailableTarget);
}
if (target && typeof target === "object") {
return Object.values(target).some(hasAvailableTarget);
}
return false; return false;
} }
const suffix = specifier.slice(packageName.length);
const packageExports = manifest.exports;
if (packageExports === undefined || packageExports === null) {
return false;
}
if (!suffix) {
if (typeof packageExports === "string") {
return true;
}
if (Array.isArray(packageExports)) {
return hasAvailableTarget(packageExports);
}
if (typeof packageExports !== "object") {
return false;
}
const exportMap = packageExports as Record<string, unknown>;
const exportKeys = Object.keys(exportMap);
if (Object.hasOwn(exportMap, ".")) {
return hasAvailableTarget(exportMap["."]);
}
return exportKeys.some((key) => !key.startsWith(".") && hasAvailableTarget(exportMap[key]));
}
if (typeof packageExports !== "object" || Array.isArray(packageExports)) {
return false;
}
const exportMap = packageExports as Record<string, unknown>;
const requested = `.${suffix}`; const requested = `.${suffix}`;
return Object.hasOwn(manifest.exports, requested); if (Object.hasOwn(exportMap, requested)) {
return hasAvailableTarget(exportMap[requested]);
}
function comparePatternKeys(left: string, right: string): number {
const leftBaseLength = left.indexOf("*") + 1;
const rightBaseLength = right.indexOf("*") + 1;
if (leftBaseLength > rightBaseLength) {
return -1;
}
if (rightBaseLength > leftBaseLength) {
return 1;
}
if (left.length > right.length) {
return -1;
}
if (right.length > left.length) {
return 1;
}
return 0;
}
const matchingPatterns = Object.keys(exportMap)
.filter((exportKey) => {
const wildcard = exportKey.indexOf("*");
if (wildcard === -1) {
return false;
}
return (
requested.startsWith(exportKey.slice(0, wildcard)) &&
requested.endsWith(exportKey.slice(wildcard + 1))
);
})
.sort(comparePatternKeys);
const matchingPattern = matchingPatterns[0];
return matchingPattern ? hasAvailableTarget(exportMap[matchingPattern]) : false;
}
function findContainingPackage(
filePath: string,
packagesByDepth: WorkspacePackage[],
): WorkspacePackage | undefined {
return packagesByDepth.find(
(workspacePackage) =>
filePath === workspacePackage.directory ||
filePath.startsWith(`${workspacePackage.directory}${path.sep}`),
);
}
function dependencyRange(manifest: PackageManifest, dependencyName: string): string | undefined {
for (const dependencyField of DEPENDENCY_FIELDS) {
const range = manifest[dependencyField]?.[dependencyName];
if (range !== undefined) {
return range;
}
}
return undefined;
}
function workspaceAliasPackageName(range: string): string | undefined {
if (!range.startsWith("workspace:")) {
return undefined;
}
const target = range.slice("workspace:".length);
if (target.startsWith("@")) {
const versionMarker = target.lastIndexOf("@");
return versionMarker > 0 ? target.slice(0, versionMarker) : target;
}
const versionMarker = target.lastIndexOf("@");
return versionMarker > 0 ? target.slice(0, versionMarker) : undefined;
}
function resolveWorkspaceDependency(
dependencyName: string,
range: string,
sourcePackage: WorkspacePackage,
workspacePackages: Map<string, WorkspacePackage>,
packageByDirectory: Map<string, WorkspacePackage>,
): { aliased: boolean; target: WorkspacePackage | undefined } {
if (range.startsWith("workspace:.")) {
const relativeTarget = range.slice("workspace:".length);
return {
aliased: true,
target: packageByDirectory.get(path.resolve(sourcePackage.directory, relativeTarget)),
};
}
const aliasName = workspaceAliasPackageName(range);
if (aliasName) {
return { aliased: true, target: workspacePackages.get(aliasName) };
}
const directTarget = workspacePackages.get(dependencyName);
if (directTarget) {
return { aliased: false, target: directTarget };
}
return { aliased: false, target: undefined };
} }
export async function checkBoundaries(rootDirectory: string): Promise<BoundaryResult> { export async function checkBoundaries(rootDirectory: string): Promise<BoundaryResult> {
const root = path.resolve(rootDirectory); const root = path.resolve(rootDirectory);
const allFiles = await walkFiles(root); const allFiles = await walkFiles(root);
const workspacePatterns = await readWorkspacePatterns(root);
const manifestFiles = allFiles.filter((file) => path.basename(file) === "package.json"); const manifestFiles = allFiles.filter((file) => path.basename(file) === "package.json");
const codeFiles = allFiles.filter((file) => CODE_EXTENSIONS.has(path.extname(file))); const codeFiles = allFiles.filter((file) => CODE_EXTENSIONS.has(path.extname(file)));
const workspacePackages = new Map<string, WorkspacePackage>(); const workspacePackages = new Map<string, WorkspacePackage>();
const packageByDirectory = new Map<string, WorkspacePackage>(); const packageByDirectory = new Map<string, WorkspacePackage>();
const violations: BoundaryViolation[] = []; const violations: BoundaryViolation[] = [];
for (const pattern of workspacePatterns ?? []) {
if (!workspacePatternIsSupported(pattern)) {
violations.push({
from: "pnpm-workspace.yaml",
message: `Workspace pattern ${pattern} uses unsupported glob syntax.`,
rule: "workspace-patterns-use-supported-syntax",
});
}
}
for (const manifestFile of manifestFiles) { for (const manifestFile of manifestFiles) {
const manifest = JSON.parse(await readFile(manifestFile, "utf8")) as PackageManifest; const manifest = JSON.parse(await readFile(manifestFile, "utf8")) as PackageManifest;
if (!manifest.name || manifestFile === path.join(root, "package.json")) { if (!manifest.name) {
continue; continue;
} }
const directory = path.dirname(manifestFile); const directory = path.dirname(manifestFile);
const relativeDirectory = normalizePath(path.relative(root, directory)); const relativeDirectory = normalizePath(path.relative(root, directory)) || ".";
if (
directory !== root &&
workspacePatterns &&
!workspaceDirectoryIsIncluded(relativeDirectory, workspacePatterns)
) {
continue;
}
const location = describeRepositoryPath(relativeDirectory); const location = describeRepositoryPath(relativeDirectory);
if (manifest.ekklesia?.layer) { if (manifest.ekklesia?.layer) {
location.role = manifest.ekklesia.layer; location.role = manifest.ekklesia.layer;
} }
if (
location.layer === "shared" &&
(!manifest.ekklesia?.layer || !SHARED_PACKAGE_ROLES.has(manifest.ekklesia.layer))
) {
violations.push({
from: relativeDirectory,
message: `Shared package ${manifest.name} must declare a recognized ekklesia.layer role.`,
rule: "shared-packages-declare-known-roles",
});
}
const workspacePackage = { directory, location, manifest, name: manifest.name }; const workspacePackage = { directory, location, manifest, name: manifest.name };
workspacePackages.set(manifest.name, workspacePackage); const existingPackage = workspacePackages.get(manifest.name);
if (existingPackage) {
violations.push({
from: relativeDirectory,
message: `Workspace package name ${manifest.name} is already used by ${existingPackage.location.relativePath}.`,
rule: "workspace-package-names-are-unique",
to: existingPackage.location.relativePath,
});
} else {
workspacePackages.set(manifest.name, workspacePackage);
}
packageByDirectory.set(directory, workspacePackage); packageByDirectory.set(directory, workspacePackage);
} }
@@ -355,8 +691,13 @@ export async function checkBoundaries(rootDirectory: string): Promise<BoundaryRe
for (const workspacePackage of workspacePackages.values()) { for (const workspacePackage of workspacePackages.values()) {
packageGraph.set(workspacePackage.name, new Set()); packageGraph.set(workspacePackage.name, new Set());
for (const dependencyField of DEPENDENCY_FIELDS) { for (const dependencyField of DEPENDENCY_FIELDS) {
for (const dependencyName of Object.keys(workspacePackage.manifest[dependencyField] ?? {})) { for (const [dependencyName, range] of Object.entries(
if (workspacePackage.location.layer === "module" && isModuleUiPackage(dependencyName)) { workspacePackage.manifest[dependencyField] ?? {},
)) {
if (
workspacePackage.location.layer === "module" &&
isModuleUiDependency(dependencyName, range)
) {
violations.push({ violations.push({
from: workspacePackage.location.relativePath, from: workspacePackage.location.relativePath,
message: `Module package depends on UI framework ${dependencyName}.`, message: `Module package depends on UI framework ${dependencyName}.`,
@@ -364,10 +705,33 @@ export async function checkBoundaries(rootDirectory: string): Promise<BoundaryRe
}); });
} }
const targetPackage = workspacePackages.get(dependencyName); const resolvedDependency = resolveWorkspaceDependency(
dependencyName,
range,
workspacePackage,
workspacePackages,
packageByDirectory,
);
const targetPackage = resolvedDependency.target;
if (!targetPackage) { if (!targetPackage) {
continue; continue;
} }
if (resolvedDependency.aliased) {
violations.push({
from: workspacePackage.location.relativePath,
message: `Internal dependency alias ${dependencyName} obscures ${targetPackage.name}; use the real package name.`,
rule: "workspace-dependency-aliases-are-forbidden",
to: targetPackage.location.relativePath,
});
}
if (!range.startsWith("workspace:")) {
violations.push({
from: workspacePackage.location.relativePath,
message: `Internal dependency ${dependencyName} must use the workspace: protocol, not ${range}.`,
rule: "internal-dependencies-use-workspace-protocol",
to: targetPackage.location.relativePath,
});
}
packageGraph.get(workspacePackage.name)?.add(targetPackage.name); packageGraph.get(workspacePackage.name)?.add(targetPackage.name);
const rule = boundaryRule(workspacePackage.location, targetPackage.location); const rule = boundaryRule(workspacePackage.location, targetPackage.location);
if (rule) { if (rule) {
@@ -382,6 +746,10 @@ export async function checkBoundaries(rootDirectory: string): Promise<BoundaryRe
} }
} }
const packagesByDepth = [...packageByDirectory.values()].sort(
(left, right) => right.directory.length - left.directory.length,
);
for (const cycle of findCycles(packageGraph)) { for (const cycle of findCycles(packageGraph)) {
violations.push({ violations.push({
from: cycle[0] ?? "unknown-package", from: cycle[0] ?? "unknown-package",
@@ -393,7 +761,10 @@ export async function checkBoundaries(rootDirectory: string): Promise<BoundaryRe
for (const repositoryFile of allFiles) { for (const repositoryFile of allFiles) {
const relativeFile = normalizePath(path.relative(root, repositoryFile)); const relativeFile = normalizePath(path.relative(root, repositoryFile));
const location = describeRepositoryPath(relativeFile); const location = describeRepositoryPath(relativeFile);
if (location.layer === "module" && MODULE_UI_EXTENSIONS.has(path.extname(repositoryFile))) { if (
location.layer === "module" &&
MODULE_UI_EXTENSIONS.has(path.extname(repositoryFile).toLowerCase())
) {
violations.push({ violations.push({
from: relativeFile, from: relativeFile,
message: "UI source files belong in a surface, not a module.", message: "UI source files belong in a surface, not a module.",
@@ -428,13 +799,52 @@ export async function checkBoundaries(rootDirectory: string): Promise<BoundaryRe
to = describeRepositoryPath(normalizePath(path.relative(root, targetFile))); to = describeRepositoryPath(normalizePath(path.relative(root, targetFile)));
if (await isFile(targetFile)) { if (await isFile(targetFile)) {
sourceGraph.get(relativeFile)?.add(normalizePath(path.relative(root, targetFile))); sourceGraph.get(relativeFile)?.add(normalizePath(path.relative(root, targetFile)));
const sourcePackage = findContainingPackage(codeFile, packagesByDepth);
const targetPackage = findContainingPackage(targetFile, packagesByDepth);
if (
sourcePackage &&
targetPackage &&
sourcePackage.directory !== targetPackage.directory
) {
violations.push({
from: relativeFile,
message: `Cross-package import ${specifier} must use ${targetPackage.name}'s public exports.`,
rule: "workspace-imports-use-public-exports",
to: targetPackage.location.relativePath,
});
}
} }
} }
} else { } else {
const targetPackage = workspacePackages.get(dependencyName); const sourcePackage = findContainingPackage(codeFile, packagesByDepth);
const declaredRange = sourcePackage
? dependencyRange(sourcePackage.manifest, dependencyName)
: undefined;
const targetPackage =
sourcePackage && declaredRange
? resolveWorkspaceDependency(
dependencyName,
declaredRange,
sourcePackage,
workspacePackages,
packageByDirectory,
).target
: workspacePackages.get(dependencyName);
if (targetPackage) { if (targetPackage) {
to = targetPackage.location; to = targetPackage.location;
if (!exportedSubpath(targetPackage.manifest, specifier, dependencyName)) { if (sourcePackage && sourcePackage.directory !== targetPackage.directory) {
if (declaredRange === undefined) {
violations.push({
from: relativeFile,
message: `${sourcePackage.name} imports ${dependencyName} without declaring it.`,
rule: "workspace-dependencies-are-declared",
to: targetPackage.location.relativePath,
});
}
}
const publicSpecifier =
dependencyName === targetPackage.name ? specifier : targetPackage.name;
if (!exportedSubpath(targetPackage.manifest, publicSpecifier, targetPackage.name)) {
violations.push({ violations.push({
from: relativeFile, from: relativeFile,
message: `${specifier} is not a public package export.`, message: `${specifier} is not a public package export.`,
+227
View File
@@ -0,0 +1,227 @@
#!/usr/bin/env bash
set -Eeuo pipefail
repository_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$repository_root"
command -v docker >/dev/null || {
echo "Docker is required for the infrastructure smoke test." >&2
exit 1
}
command -v curl >/dev/null || {
echo "curl is required for the infrastructure smoke test." >&2
exit 1
}
command -v cmp >/dev/null || {
echo "cmp is required for the infrastructure smoke test." >&2
exit 1
}
docker compose version >/dev/null
curl_help="$(curl --help all)"
if [[ "$curl_help" != *"--aws-sigv4"* ]]; then
echo "curl must support --aws-sigv4 for authenticated S3 checks." >&2
exit 1
fi
raw_project="ekklesia-smoke-${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ATTEMPT:-0}-$$"
project="${raw_project//[^a-zA-Z0-9_-]/-}"
project="${project,,}"
temporary_directory="$(mktemp -d)"
payload_file="$temporary_directory/payload.txt"
download_file="$temporary_directory/download.txt"
bucket_list_file="$temporary_directory/buckets.xml"
: >"$bucket_list_file"
export POSTGRES_DB="ekklesia_smoke"
export POSTGRES_USER="ekklesia_smoke"
export POSTGRES_PASSWORD="ekklesia-smoke-postgres-password"
export POSTGRES_PORT="${EKKLESIA_SMOKE_POSTGRES_PORT:-0}"
export S3_ACCESS_KEY="ekklesia-smoke-access"
export S3_SECRET_KEY="ekklesia-smoke-secret-key"
export S3_BUCKET="ekklesia-smoke"
export S3_REGION="us-east-1"
export S3_PORT="${EKKLESIA_SMOKE_S3_PORT:-0}"
compose=(
docker compose
--project-name "$project"
--env-file .env.example
--file infrastructure/compose.yaml
)
cleanup() {
local exit_status=$?
local cleanup_output
trap - EXIT INT TERM
if ((exit_status != 0)); then
"${compose[@]}" logs --no-color >&2 || true
fi
if ! cleanup_output="$(
"${compose[@]}" down --volumes --remove-orphans --timeout 10 2>&1
)"; then
echo "Infrastructure smoke cleanup failed:" >&2
echo "$cleanup_output" >&2
if ((exit_status == 0)); then
exit_status=1
fi
fi
rm -rf -- "$temporary_directory"
exit "$exit_status"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
"${compose[@]}" config --quiet
"${compose[@]}" up --detach --wait --wait-timeout 120
"${compose[@]}" up --detach --wait --wait-timeout 120
database_result="$(
"${compose[@]}" exec -T \
-e "PGPASSWORD=$POSTGRES_PASSWORD" \
postgres \
psql \
--no-psqlrc \
--host 127.0.0.1 \
--username "$POSTGRES_USER" \
--dbname "$POSTGRES_DB" \
--tuples-only \
--no-align \
--set ON_ERROR_STOP=1 \
--command "SELECT current_database(), current_user"
)"
if [[ "$database_result" != "$POSTGRES_DB|$POSTGRES_USER" ]]; then
echo "PostgreSQL returned an unexpected identity: $database_result" >&2
exit 1
fi
if "${compose[@]}" exec -T \
-e "PGPASSWORD=incorrect-password" \
postgres \
psql \
--no-psqlrc \
--host 127.0.0.1 \
--username "$POSTGRES_USER" \
--dbname "$POSTGRES_DB" \
--command "SELECT 1" >/dev/null 2>&1; then
echo "PostgreSQL unexpectedly accepted an invalid password." >&2
exit 1
fi
s3_binding="$("${compose[@]}" port object-storage 8333)"
s3_port="${s3_binding##*:}"
if [[ ! "$s3_port" =~ ^[0-9]+$ ]]; then
echo "Compose returned an invalid S3 port binding: $s3_binding" >&2
exit 1
fi
s3_endpoint="http://127.0.0.1:$s3_port"
signed_curl=(
curl
--silent
--show-error
--aws-sigv4 "aws:amz:$S3_REGION:s3"
--user "$S3_ACCESS_KEY:$S3_SECRET_KEY"
)
s3_ready=false
for _ in {1..30}; do
status="$(
"${signed_curl[@]}" \
--output "$bucket_list_file" \
--write-out "%{http_code}" \
"$s3_endpoint/" || true
)"
bucket_list="$(<"$bucket_list_file")"
if [[ "$status" == "200" && "$bucket_list" == *"<Name>$S3_BUCKET</Name>"* ]]; then
s3_ready=true
break
fi
sleep 2
done
if [[ "$s3_ready" != "true" ]]; then
echo "The authenticated S3 endpoint and bucket did not become ready." >&2
exit 1
fi
printf '%s' "ekklesia-storage-smoke" >"$payload_file"
object_key="smoke/$project/object.txt"
object_url="$s3_endpoint/$S3_BUCKET/$object_key"
put_status="$(
"${signed_curl[@]}" \
--upload-file "$payload_file" \
--header "Content-Type: text/plain" \
--output /dev/null \
--write-out "%{http_code}" \
"$object_url"
)"
if [[ "$put_status" != "200" ]]; then
echo "S3 PUT returned $put_status instead of 200." >&2
exit 1
fi
get_status="$(
"${signed_curl[@]}" \
--output "$download_file" \
--write-out "%{http_code}" \
"$object_url"
)"
if [[ "$get_status" != "200" ]] || ! cmp --silent "$payload_file" "$download_file"; then
echo "S3 GET did not return the uploaded object unchanged." >&2
exit 1
fi
head_status="$(
"${signed_curl[@]}" --head --output /dev/null --write-out "%{http_code}" "$object_url"
)"
if [[ "$head_status" != "200" ]]; then
echo "S3 HEAD returned $head_status instead of 200." >&2
exit 1
fi
anonymous_status="$(
curl --silent --show-error --output /dev/null --write-out "%{http_code}" "$object_url"
)"
if [[ "$anonymous_status" != "403" ]]; then
echo "S3 anonymous GET returned $anonymous_status instead of 403." >&2
exit 1
fi
bad_signature_status="$(
curl \
--silent \
--show-error \
--aws-sigv4 "aws:amz:$S3_REGION:s3" \
--user "$S3_ACCESS_KEY:incorrect-secret" \
--output /dev/null \
--write-out "%{http_code}" \
"$object_url"
)"
if [[ "$bad_signature_status" != "403" ]]; then
echo "S3 invalid signature returned $bad_signature_status instead of 403." >&2
exit 1
fi
delete_status="$(
"${signed_curl[@]}" \
--request DELETE \
--output /dev/null \
--write-out "%{http_code}" \
"$object_url"
)"
if [[ "$delete_status" != "204" ]]; then
echo "S3 DELETE returned $delete_status instead of 204." >&2
exit 1
fi
missing_status="$(
"${signed_curl[@]}" --head --output /dev/null --write-out "%{http_code}" "$object_url"
)"
if [[ "$missing_status" != "404" ]]; then
echo "Deleted S3 object returned $missing_status instead of 404." >&2
exit 1
fi
echo "Infrastructure smoke test passed for PostgreSQL and authenticated S3 operations."
+73 -1
View File
@@ -12,21 +12,93 @@ describe("repository dependency boundaries", () => {
expect(result.violations).toEqual([]); expect(result.violations).toEqual([]);
}); });
it("accepts generated module composition only through the generated bootstrap boundary", async () => {
const result = await checkBoundaries(path.join(fixtures, "legal-generated-bootstrap"));
expect(result.violations).toEqual([]);
});
it("honors explicit pnpm workspace exclusions", async () => {
const result = await checkBoundaries(path.join(fixtures, "workspace-exclusion"));
expect(result.violations).toEqual([]);
});
it.each([ it.each([
["core-imports-module", "core-is-feature-agnostic"], ["core-imports-module", "core-is-feature-agnostic"],
["app-imports-module", "apps-are-composition-roots"],
["module-imports-core", "modules-use-public-platform-contracts"],
["module-imports-surface", "modules-use-public-platform-contracts"], ["module-imports-surface", "modules-use-public-platform-contracts"],
["package-boundary-direction", "modules-use-public-platform-contracts"],
["cross-module-import", "modules-do-not-import-other-modules"], ["cross-module-import", "modules-do-not-import-other-modules"],
["surface-imports-module", "surfaces-use-public-contracts"],
["cross-surface-import", "surfaces-are-independent"],
["builder-imports-module", "builder-composes-contracts"],
["contracts-import-core", "contracts-do-not-depend-upward"],
["sdk-imports-core", "sdks-do-not-depend-on-implementations"],
["shared-role-order", "sdks-do-not-depend-on-implementations"],
["package-without-exports", "workspace-imports-use-public-exports"],
["private-package-import", "workspace-imports-use-public-exports"],
["non-workspace-version", "internal-dependencies-use-workspace-protocol"],
["workspace-alias", "workspace-dependency-aliases-are-forbidden"],
["undeclared-workspace-dependency", "workspace-dependencies-are-declared"],
["duplicate-package-name", "workspace-package-names-are-unique"],
["unknown-shared-role", "shared-packages-declare-known-roles"],
["workspace-unsupported-pattern", "workspace-patterns-use-supported-syntax"],
["package-cycle", "no-circular-package-dependencies"],
["module-has-ui", "modules-do-not-own-ui"], ["module-has-ui", "modules-do-not-own-ui"],
["module-ui-framework", "modules-do-not-own-ui"],
["module-imports-ui-package", "modules-use-public-platform-contracts"],
["cross-package-relative", "workspace-imports-use-public-exports"],
["cycle", "no-circular-source-dependencies"], ["cycle", "no-circular-source-dependencies"],
])("rejects %s", async (fixture, expectedRule) => { ])("rejects %s", async (fixture, expectedRule) => {
const result = await checkBoundaries(path.join(fixtures, fixture)); const result = await checkBoundaries(path.join(fixtures, fixture));
expect(result.violations.map((violation) => violation.rule)).toContain(expectedRule); expect(result.violations.map((violation) => violation.rule)).toContain(expectedRule);
}); });
it("rejects both component and stylesheet files inside a module", async () => { it("rejects component, stylesheet, and case-insensitive view files inside a module", async () => {
const result = await checkBoundaries(path.join(fixtures, "module-has-ui")); const result = await checkBoundaries(path.join(fixtures, "module-has-ui"));
expect( expect(
result.violations.filter((violation) => violation.rule === "modules-do-not-own-ui"), result.violations.filter((violation) => violation.rule === "modules-do-not-own-ui"),
).toHaveLength(3);
});
it("rejects UI framework dependencies, imports, and view formats inside a module", async () => {
const result = await checkBoundaries(path.join(fixtures, "module-ui-framework"));
expect(
result.violations.filter((violation) => violation.rule === "modules-do-not-own-ui"),
).toHaveLength(4);
});
it("requires workspace: ranges in every dependency field", async () => {
const result = await checkBoundaries(path.join(fixtures, "non-workspace-version"));
expect(
result.violations.filter(
(violation) => violation.rule === "internal-dependencies-use-workspace-protocol",
),
).toHaveLength(4);
});
it("resolves forbidden workspace aliases before applying layer rules", async () => {
const result = await checkBoundaries(path.join(fixtures, "workspace-alias"));
expect(
result.violations.filter(
(violation) => violation.rule === "workspace-dependency-aliases-are-forbidden",
),
).toHaveLength(2); ).toHaveLength(2);
expect(result.violations.map((violation) => violation.rule)).toEqual(
expect.arrayContaining([
"workspace-dependency-aliases-are-forbidden",
"modules-use-public-platform-contracts",
]),
);
});
it("prevents contracts and SDKs from depending upward through shared packages", async () => {
const result = await checkBoundaries(path.join(fixtures, "shared-role-order"));
expect(result.violations.map((violation) => violation.rule)).toEqual(
expect.arrayContaining([
"contracts-do-not-depend-upward",
"sdks-do-not-depend-on-implementations",
]),
);
}); });
}); });
@@ -0,0 +1,3 @@
import { calendar } from "../../../modules/calendar/src/index.js";
export const invalidAppFeature = calendar;
@@ -0,0 +1 @@
export const calendar = "calendar";
@@ -0,0 +1,3 @@
import { calendar } from "../../../modules/calendar/src/index.js";
export const invalidBuilderFeature = calendar;
@@ -0,0 +1 @@
export const calendar = "calendar";
@@ -0,0 +1 @@
export const core = "core";
@@ -0,0 +1,3 @@
import { core } from "../../../core/src/index.js";
export const invalidContract = core;
@@ -0,0 +1,11 @@
{
"name": "@ekklesia/core",
"version": "0.0.0",
"type": "module",
"dependencies": {
"@ekklesia/contracts": "workspace:*"
},
"exports": {
".": "./src/index.ts"
}
}
@@ -0,0 +1,3 @@
import { contract } from "../../packages/contracts/src/index.js";
export const invalidPrivatePath = contract;
@@ -0,0 +1,11 @@
{
"name": "@ekklesia/contracts",
"version": "0.0.0",
"type": "module",
"exports": {
".": "./src/index.ts"
},
"ekklesia": {
"layer": "contracts"
}
}
@@ -0,0 +1 @@
export const contract = "contract";
@@ -0,0 +1,3 @@
import { website } from "../../../surfaces/website/src/index.js";
export const invalidSurfaceDependency = website;
@@ -0,0 +1 @@
export const website = "website";
@@ -0,0 +1,11 @@
{
"name": "@ekklesia/duplicate",
"version": "0.0.0",
"type": "module",
"exports": {
".": "./src/index.ts"
},
"ekklesia": {
"layer": "tooling"
}
}
@@ -0,0 +1 @@
export const one = 1;
@@ -0,0 +1,11 @@
{
"name": "@ekklesia/duplicate",
"version": "0.0.0",
"type": "module",
"exports": {
".": "./src/index.ts"
},
"ekklesia": {
"layer": "tooling"
}
}
@@ -0,0 +1 @@
export const two = 2;
@@ -0,0 +1,3 @@
import { installedModules } from "../../../generated/module-registry.js";
export const bootstrap = installedModules;
@@ -0,0 +1,3 @@
import { calendar } from "../modules/calendar/src/index.js";
export const installedModules = [calendar];
@@ -0,0 +1 @@
export const calendar = "calendar";
@@ -0,0 +1,12 @@
{
"name": "@ekklesia/conditional",
"version": "0.0.0",
"type": "module",
"exports": {
"types": "./src/index.ts",
"import": "./src/index.ts"
},
"ekklesia": {
"layer": "shared"
}
}
@@ -0,0 +1 @@
export const conditional = "conditional";
@@ -0,0 +1,15 @@
{
"name": "@ekklesia/consumer",
"version": "0.0.0",
"type": "module",
"dependencies": {
"@ekklesia/conditional": "workspace:*",
"@ekklesia/contracts": "workspace:*"
},
"exports": {
".": "./src/index.ts"
},
"ekklesia": {
"layer": "shared"
}
}
@@ -0,0 +1,9 @@
import type { HealthContract } from "@ekklesia/contracts";
import { conditional } from "@ekklesia/conditional";
import {
publicValue,
} from "@ekklesia/contracts/public/value";
export const health: HealthContract = { status: "ok" };
export { conditional };
export { publicValue };
@@ -3,7 +3,8 @@
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"exports": { "exports": {
".": "./src/index.ts" ".": "./src/index.ts",
"./public/*": "./src/public/*.ts"
}, },
"ekklesia": { "ekklesia": {
"layer": "contracts" "layer": "contracts"
@@ -0,0 +1 @@
export const publicValue = "public";
@@ -0,0 +1 @@
<main>Modules do not own UI.</main>
@@ -0,0 +1 @@
export const core = "core";
@@ -0,0 +1,3 @@
import { core } from "../../../core/src/index.js";
export const invalidCoreImport = core;
@@ -0,0 +1,11 @@
{
"name": "@ekklesia/module-calendar",
"version": "0.0.0",
"type": "module",
"dependencies": {
"@ekklesia/ui": "workspace:*"
},
"exports": {
".": "./src/index.ts"
}
}
@@ -0,0 +1,3 @@
import { button } from "@ekklesia/ui";
export const invalidModuleUiDependency = button;
@@ -0,0 +1,11 @@
{
"name": "@ekklesia/ui",
"version": "0.0.0",
"type": "module",
"exports": {
".": "./src/index.ts"
},
"ekklesia": {
"layer": "ui"
}
}
@@ -0,0 +1 @@
export const button = "button";
@@ -0,0 +1,12 @@
{
"name": "@ekklesia/module-calendar",
"version": "0.0.0",
"type": "module",
"dependencies": {
"compat": "npm:[email protected]",
"preact": "10.27.2"
},
"exports": {
".": "./src/index.ts"
}
}
@@ -0,0 +1,3 @@
import { h } from "preact";
export const invalidUiFramework = h;
@@ -0,0 +1 @@
# Modules do not own views
@@ -0,0 +1,20 @@
{
"name": "@ekklesia/core",
"version": "0.0.0",
"type": "module",
"dependencies": {
"@ekklesia/contracts": "^1.0.0"
},
"devDependencies": {
"@ekklesia/testing": "1.0.0"
},
"optionalDependencies": {
"@ekklesia/module-sdk": "~1.0.0"
},
"peerDependencies": {
"@ekklesia/surface-sdk": ">=1.0.0"
},
"exports": {
".": "./src/index.ts"
}
}
@@ -0,0 +1 @@
export const core = "core";
@@ -0,0 +1,11 @@
{
"name": "@ekklesia/contracts",
"version": "1.0.0",
"type": "module",
"exports": {
".": "./src/index.ts"
},
"ekklesia": {
"layer": "contracts"
}
}
@@ -0,0 +1 @@
export const contract = "contract";
@@ -0,0 +1,11 @@
{
"name": "@ekklesia/module-sdk",
"version": "1.0.0",
"type": "module",
"exports": {
".": "./src/index.ts"
},
"ekklesia": {
"layer": "module-sdk"
}
}
@@ -0,0 +1 @@
export const moduleSdk = "module-sdk";
@@ -0,0 +1,11 @@
{
"name": "@ekklesia/surface-sdk",
"version": "1.0.0",
"type": "module",
"exports": {
".": "./src/index.ts"
},
"ekklesia": {
"layer": "surface-sdk"
}
}
@@ -0,0 +1 @@
export const surfaceSdk = "surface-sdk";
@@ -0,0 +1,11 @@
{
"name": "@ekklesia/testing",
"version": "1.0.0",
"type": "module",
"exports": {
".": "./src/index.ts"
},
"ekklesia": {
"layer": "testing"
}
}
@@ -0,0 +1 @@
export const testing = "testing";
@@ -0,0 +1,11 @@
{
"name": "@ekklesia/module-calendar",
"version": "0.0.0",
"type": "module",
"dependencies": {
"@ekklesia/surface-dashboard": "workspace:*"
},
"exports": {
".": "./src/index.ts"
}
}
@@ -0,0 +1 @@
export const calendar = "calendar";
@@ -0,0 +1,8 @@
{
"name": "@ekklesia/surface-dashboard",
"version": "0.0.0",
"type": "module",
"exports": {
".": "./src/index.ts"
}
}
@@ -0,0 +1 @@
export const dashboard = "dashboard";
@@ -0,0 +1,14 @@
{
"name": "@ekklesia/one",
"version": "0.0.0",
"type": "module",
"dependencies": {
"@ekklesia/two": "workspace:*"
},
"exports": {
".": "./src/index.ts"
},
"ekklesia": {
"layer": "shared"
}
}
@@ -0,0 +1 @@
export const one = 1;
@@ -0,0 +1,14 @@
{
"name": "@ekklesia/two",
"version": "0.0.0",
"type": "module",
"dependencies": {
"@ekklesia/one": "workspace:*"
},
"exports": {
".": "./src/index.ts"
},
"ekklesia": {
"layer": "shared"
}
}
@@ -0,0 +1 @@
export const two = 2;
@@ -0,0 +1,11 @@
{
"name": "@ekklesia/core",
"version": "0.0.0",
"type": "module",
"dependencies": {
"@ekklesia/contracts": "workspace:*"
},
"exports": {
".": "./src/index.ts"
}
}
@@ -0,0 +1,3 @@
import { contract } from "@ekklesia/contracts";
export const invalidImport = contract;
@@ -0,0 +1,8 @@
{
"name": "@ekklesia/contracts",
"version": "0.0.0",
"type": "module",
"ekklesia": {
"layer": "contracts"
}
}
@@ -0,0 +1 @@
export const contract = "contract";
@@ -0,0 +1,11 @@
{
"name": "@ekklesia/core",
"version": "0.0.0",
"type": "module",
"dependencies": {
"@ekklesia/contracts": "workspace:*"
},
"exports": {
".": "./src/index.ts"
}
}
@@ -0,0 +1,3 @@
import { privateValue } from "@ekklesia/contracts/abcdzz";
export const invalidPrivateImport = privateValue;
@@ -0,0 +1,13 @@
{
"name": "@ekklesia/contracts",
"version": "0.0.0",
"type": "module",
"exports": {
".": "./src/index.ts",
"./a*cdzz": "./src/index.ts",
"./ab*zz": null
},
"ekklesia": {
"layer": "contracts"
}
}
@@ -0,0 +1 @@
export const privateValue = "private";
@@ -0,0 +1 @@
export const core = "core";
@@ -0,0 +1,3 @@
import { core } from "../../../core/src/index.js";
export const invalidSdk = core;
@@ -0,0 +1,3 @@
import { moduleSdk } from "../../module-sdk/src/index.js";
export const invalidContractDependency = moduleSdk;
@@ -0,0 +1,3 @@
import { surfaceSdk } from "../../surface-sdk/src/index.js";
export const moduleSdk = surfaceSdk;
@@ -0,0 +1 @@
export const surfaceSdk = "surface-sdk";
@@ -0,0 +1 @@
export const calendar = "calendar";
@@ -0,0 +1,3 @@
import { calendar } from "../../../modules/calendar/src/index.js";
export const invalidSurfaceFeature = calendar;
@@ -0,0 +1,11 @@
{
"name": "@ekklesia/consumer",
"version": "0.0.0",
"type": "module",
"exports": {
".": "./src/index.ts"
},
"ekklesia": {
"layer": "shared"
}
}
@@ -0,0 +1,3 @@
import { contract } from "@ekklesia/contracts";
export const invalidUndeclaredDependency = contract;
@@ -0,0 +1,11 @@
{
"name": "@ekklesia/contracts",
"version": "0.0.0",
"type": "module",
"exports": {
".": "./src/index.ts"
},
"ekklesia": {
"layer": "contracts"
}
}
@@ -0,0 +1 @@
export const contract = "contract";
@@ -0,0 +1,11 @@
{
"name": "@ekklesia/mystery",
"version": "0.0.0",
"type": "module",
"exports": {
".": "./src/index.ts"
},
"ekklesia": {
"layer": "mystery"
}
}
@@ -0,0 +1 @@
export const mystery = "mystery";
@@ -0,0 +1,12 @@
{
"name": "@ekklesia/module-calendar",
"version": "0.0.0",
"type": "module",
"dependencies": {
"@ekklesia/contracts": "workspace:@ekklesia/surface-dashboard@*",
"dashboard-data": "workspace:@ekklesia/surface-dashboard@*"
},
"exports": {
".": "./src/index.ts"
}
}
@@ -0,0 +1,4 @@
import { dashboard } from "dashboard-data";
import { hiddenDashboard } from "@ekklesia/contracts";
export const invalidAlias = [dashboard, hiddenDashboard];
@@ -0,0 +1,11 @@
{
"name": "@ekklesia/contracts",
"version": "0.0.0",
"type": "module",
"ekklesia": {
"layer": "contracts"
},
"exports": {
".": "./src/index.ts"
}
}
@@ -0,0 +1 @@
export const hiddenDashboard = "contracts";
@@ -0,0 +1,8 @@
{
"name": "@ekklesia/surface-dashboard",
"version": "0.0.0",
"type": "module",
"exports": {
".": "./src/index.ts"
}
}
@@ -0,0 +1 @@
export const dashboard = "dashboard";
@@ -0,0 +1,5 @@
{
"name": "@ekklesia/included",
"version": "0.0.0",
"type": "module"
}
@@ -0,0 +1 @@
export const excluded = true;
@@ -0,0 +1,11 @@
{
"name": "@ekklesia/included",
"version": "0.0.0",
"type": "module",
"exports": {
".": "./src/index.ts"
},
"ekklesia": {
"layer": "tooling"
}
}
@@ -0,0 +1 @@
export const included = true;
@@ -0,0 +1,3 @@
packages:
- "packages/*"
- "!packages/excluded"
@@ -0,0 +1,11 @@
{
"name": "@ekklesia/example",
"version": "0.0.0",
"type": "module",
"exports": {
".": "./src/index.ts"
},
"ekklesia": {
"layer": "tooling"
}
}
@@ -0,0 +1 @@
export const example = true;
@@ -0,0 +1,2 @@
packages:
- "packages/{example,other}"
+45 -2
View File
@@ -5,6 +5,7 @@ import { describe, expect, it } from "vitest";
import { parse } from "yaml"; import { parse } from "yaml";
interface ComposeService { interface ComposeService {
environment?: Record<string, string>;
healthcheck?: unknown; healthcheck?: unknown;
image?: string; image?: string;
ports?: string[]; ports?: string[];
@@ -16,6 +17,18 @@ interface ComposeDocument {
volumes?: Record<string, unknown>; volumes?: Record<string, unknown>;
} }
interface PackageDocument {
scripts?: Record<string, string>;
}
interface TurboDocument {
tasks?: Record<string, unknown>;
}
interface WorkspaceDocument {
packages?: string[];
}
const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
describe("development infrastructure", () => { describe("development infrastructure", () => {
@@ -32,6 +45,7 @@ describe("development infrastructure", () => {
expect(objectStorage?.image).toBe("chrislusf/seaweedfs:4.28"); expect(objectStorage?.image).toBe("chrislusf/seaweedfs:4.28");
expect(postgres?.healthcheck).toBeDefined(); expect(postgres?.healthcheck).toBeDefined();
expect(objectStorage?.healthcheck).toBeDefined(); expect(objectStorage?.healthcheck).toBeDefined();
expect(postgres?.environment?.POSTGRES_INITDB_ARGS).toContain("--auth-host=scram-sha-256");
for (const service of [postgres, objectStorage]) { for (const service of [postgres, objectStorage]) {
expect(service).toBeDefined(); expect(service).toBeDefined();
@@ -44,18 +58,47 @@ describe("development infrastructure", () => {
expect(compose.volumes).toHaveProperty("object-storage-data"); expect(compose.volumes).toHaveProperty("object-storage-data");
}); });
it("has a parseable CI workflow with a single repository gate", async () => { it("has a parseable CI workflow with static and live infrastructure gates", async () => {
const source = await readFile( const source = await readFile(
path.join(repositoryRoot, ".github", "workflows", "ci.yml"), path.join(repositoryRoot, ".github", "workflows", "ci.yml"),
"utf8", "utf8",
); );
const workflow = parse(source) as { const workflow = parse(source) as {
jobs?: { verify?: { steps?: Array<{ run?: string }> } }; jobs?: {
"infrastructure-smoke"?: { steps?: Array<{ run?: string }> };
verify?: { steps?: Array<{ run?: string }> };
};
}; };
const commands = workflow.jobs?.verify?.steps?.flatMap((step) => step.run ?? []) ?? []; const commands = workflow.jobs?.verify?.steps?.flatMap((step) => step.run ?? []) ?? [];
const infrastructureCommands =
workflow.jobs?.["infrastructure-smoke"]?.steps?.flatMap((step) => step.run ?? []) ?? [];
expect(workflow.jobs?.verify).toBeDefined(); expect(workflow.jobs?.verify).toBeDefined();
expect(commands).toContain("pnpm install --frozen-lockfile"); expect(commands).toContain("pnpm install --frozen-lockfile");
expect(commands).toContain("pnpm ci"); expect(commands).toContain("pnpm ci");
expect(infrastructureCommands).toContain("bash scripts/smoke-infrastructure.sh");
});
it("includes every official platform layer in the workspace", async () => {
const source = await readFile(path.join(repositoryRoot, "pnpm-workspace.yaml"), "utf8");
const workspace = parse(source) as WorkspaceDocument;
expect(workspace.packages).toEqual(
expect.arrayContaining(["apps/*", "builders/*", "core", "modules/*", "surfaces/*"]),
);
});
it("dispatches root and future package quality tasks through Turborepo", async () => {
const packageSource = await readFile(path.join(repositoryRoot, "package.json"), "utf8");
const turboSource = await readFile(path.join(repositoryRoot, "turbo.json"), "utf8");
const packageDocument = JSON.parse(packageSource) as PackageDocument;
const turboDocument = JSON.parse(turboSource) as TurboDocument;
expect(packageDocument.scripts?.lint).toBe("turbo run lint lint:root");
expect(packageDocument.scripts?.test).toBe("turbo run test test:root");
expect(packageDocument.scripts?.typecheck).toBe("turbo run typecheck typecheck:root");
expect(turboDocument.tasks).toHaveProperty("//#lint:root");
expect(turboDocument.tasks).toHaveProperty("//#test:root");
expect(turboDocument.tasks).toHaveProperty("//#typecheck:root");
}); });
}); });
+12 -3
View File
@@ -1,5 +1,6 @@
{ {
"$schema": "https://turbo.build/schema.json", "$schema": "https://turbo.build/schema.json",
"globalDependencies": ["biome.json", "vitest.config.ts"],
"ui": "stream", "ui": "stream",
"tasks": { "tasks": {
"build": { "build": {
@@ -7,16 +8,24 @@
"outputs": ["dist/**", ".next/**", "!.next/cache/**"] "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
}, },
"lint": { "lint": {
"dependsOn": ["^lint"],
"outputs": [] "outputs": []
}, },
"test": { "test": {
"dependsOn": ["build"], "dependsOn": ["^build"],
"outputs": ["coverage/**"] "outputs": []
}, },
"typecheck": { "typecheck": {
"dependsOn": ["^typecheck"], "dependsOn": ["^typecheck"],
"outputs": [] "outputs": []
},
"//#lint:root": {
"outputs": []
},
"//#test:root": {
"outputs": []
},
"//#typecheck:root": {
"outputs": []
} }
} }
} }