tx-agent-kit
Enforcement

Structural Checks

Cross-file invariant validation via scripts/lint/enforce-domain-invariants.mjs.

The structural invariant checker at scripts/lint/enforce-domain-invariants.mjs validates cross-file relationships that ESLint cannot check. It reads multiple files, compares structures, and reports violations.

Table Parity Checks

Effect Schema Parity

For every pgTable(...) declaration in packages/infra/db/src/, there must be a corresponding file in packages/infra/db/src/effect-schemas/:

users table    -> packages/infra/db/src/effect-schemas/users.ts
organizations  -> packages/infra/db/src/effect-schemas/organizations.ts
invitations    -> packages/infra/db/src/effect-schemas/invitations.ts

Each schema file must satisfy the following requirements:

RequirementDetail
Import sourceMust import from effect/Schema
Row schema exportMust export a *RowSchema constant
Row shape exportMust export a *RowShape type
Barrel re-exportMust be re-exported from effect-schemas/index.ts

Factory Parity

For every table, there must be a factory in packages/infra/db/src/factories/:

users table    -> packages/infra/db/src/factories/users.factory.ts
organizations  -> packages/infra/db/src/factories/organizations.factory.ts

Each factory must export a create*Factory function and be re-exported from factories/index.ts.

Domain Structure Validation

Required Folders

Every domain under packages/core/src/domains/ must contain the following:

Required FolderConstraint
domain/At least one .ts file
ports/At least one .ts file declaring Effect.Effect return types
application/At least one non-index .ts file
adapters/At least one .ts file importing from ports

Forbidden Folders

Domains must not contain repositories/ or services/ directories. Use ports/ for contracts and adapters/ for implementations.

Layer Direction

The checker validates every import path within domain files:

Source LayerAllowed Imports
domaindomain
portsdomain, ports
applicationdomain, ports, application
adaptersdomain, ports, adapters
runtimeAll layers
uiAll layers

Cross-domain imports are forbidden. Each domain is self-contained.

Kind Marker Validation

Repository Port Kinds

Every port file in packages/core/src/domains/*/ports/ must declare:

export const OrganizationRepositoryKind = 'crud' as const

If marked crud, the port must expose the full CRUD surface (list, getById, create, update, remove). If marked custom, it must not expose the full CRUD surface.

Route Kinds

Every route file in apps/api/src/routes/ must declare a kind marker. The checker validates that route kinds match their corresponding repository port kinds.

Source Hygiene

No TODO/FIXME/HACK

Placeholder comments are forbidden in source modules. Track work items in the issue tracker, not in code comments.

No Build Artifacts

Generated .js, .js.map, .d.ts, and .d.ts.map files are forbidden under src/ directories.

No Direct process.env

Source modules must read environment through dedicated env modules. Allowed env files are explicitly listed in the checker.

Integration Coverage Baselines

The checker verifies that critical integration suites exist and contain required markers:

SuiteRequirement
APIMust exercise all critical endpoints
WebMust cover all required components and pages
WorkerMust validate idempotent processing
ObservabilityMust cover all four services

Test Colocation

All tests must be colocated with their source modules. The following patterns are forbidden:

Forbidden PatternReason
__tests__/ directoriesTests must sit next to their source file
.spec.ts filesUse .test.ts instead
.integration.ts (without .test)Use .integration.test.ts instead

Every test file must have a colocated source module.

Domain Event Enforcement

The script scripts/lint/enforce-domain-event-contracts.mjs enforces ten rules that protect the integrity of the domain event system. Names are derived from event type strings via dot-split PascalCase conversion (e.g., organization.createdOrganizationCreatedEventPayload).

Rule 1 - Event Type Registry

All dot-separated event type strings used in eventType:, case, or === '...' patterns must be registered in domainEventTypes in packages/contracts/src/literals.ts. This prevents ad-hoc event types from slipping into domain or worker code without being centrally declared.

CheckDetail
Scanned directoriespackages/core/src, apps/worker/src
Patterns matchedeventType: '...', case '...', eventType === '...'
FilterOnly strings matching ^[a-z][a-z_]*\.[a-z][a-z_]*$ (ignores auth audit types)
FailureUnregistered event type string used in source

Rule 2 - Event Payload Interface Parity

Each registered event type must have a corresponding export interface <Type>EventPayload in the aggregate's domain/*-events.ts file.

CheckDetail
Source of truthdomainEventTypes in packages/contracts/src/literals.ts
Expected locationpackages/core/src/domains/<aggregate>/domain/*-events.ts
Expected exportexport interface OrganizationCreatedEventPayload (derived via PascalCase)
FailureMissing interface for a registered event type

Rule 3 - Event Payload Schema Parity

Each registered event type must have a corresponding export const <Type>EventPayloadSchema in packages/temporal-client/src/types/.

CheckDetail
Expected locationpackages/temporal-client/src/types/*.ts
Expected exportexport const OrganizationCreatedEventPayloadSchema (derived via PascalCase)
FailureMissing schema constant for a registered event type

Rule 5 - Transactional Event Write

Domain event inserts in repository files must appear inside a db.transaction() block or use a trx handle. This ensures atomicity between the business write and the outbox event.

CheckDetail
Scanned filesAll repository files in packages/infra/db/src/repositories/ (excluding domain-events.ts)
Pattern.insert(domainEvents) with no preceding trx or .transaction( in 3000-char window
FailureDomain event insert outside a transaction context

Rule 6 - Event Handler Completeness

Every registered event type must have a case '...' handler in the worker's workflow dispatcher files (apps/worker/src/workflows*.ts).

CheckDetail
Source of truthdomainEventTypes in contracts
Scanned filesapps/worker/src/workflows*.ts (non-test)
FailureMissing case branch for a registered event type

Rule 7 - Idempotent Workflow ID

Every startChild/executeChild call in apps/worker/src/workflows.ts must include a workflowId property containing event.id. This ensures deterministic, idempotent dispatch — if the same event is processed twice, Temporal returns WorkflowExecutionAlreadyStartedError instead of duplicating work.

CheckDetail
Scanned fileapps/worker/src/workflows.ts
ExtractionBrace-balanced block capture of startChild/executeChild call arguments
FailureMissing workflowId property, or workflowId value not containing event.id

Rule 8 - No Payload as Casts

.payload as <Type> assertions are forbidden in worker source files. Use effect/Schema decode for event payload deserialization instead.

CheckDetail
Scanned filesAll .ts files under apps/worker/src/ (non-test)
Pattern.payload as
FailureType assertion on event payload

Rule 9 - Event Type Naming Convention

All event type strings in domainEventTypes must match the pattern ^[a-z][a-z_]*\.[a-z][a-z_]*$ — lowercase, dot-separated, with underscores allowed within segments.

CheckDetail
Validated stringsEvery entry in domainEventTypes
Valid examplesorganization.created, billing.subscription_updated
Invalid examplesOrganization.Created, org-created, org.

Rule 11 - Retention Settings Completeness

Every table listed in retentionTableNames (from packages/contracts/src/literals.ts) must have a corresponding entry in the generated retention_settings reconcile schema. This prevents adding a table to the retention contract without providing its default retention configuration in the desired-state layer.

CheckDetail
Source of truthretentionTableNames array in packages/contracts/src/literals.ts
Validated againstGenerated packages/infra/db/schemas/system-settings/reconcile_retention_settings.sql
FailureMissing seed entry for a declared retention table

Rule 12 - Domain Event Insert Helper

Repositories outside domain-events.ts must not use inline .insert(domainEvents).values(...) patterns. All domain event inserts must go through the shared helper insertDomainEventInTransaction(trx, ...) exported from the domain-events repository.

CheckDetail
Scanned filesAll repository files in packages/infra/db/src/repositories/
Exemptiondomain-events.ts (defines the helper itself)
FailureDirect .insert(domainEvents).values(...) usage outside the exempted file

This rule ensures that domain events are always written through a single, auditable code path with consistent validation and transactional guarantees.

Rule 13 - Exhaustive Switch Default

Every switch (event.eventType) dispatcher in apps/worker/src/workflows*.ts must include a default: case. This prevents silent drops when a new event type is registered but the dispatcher is not updated.

CheckDetail
Scanned filesapps/worker/src/workflows*.ts (non-test)
Patternswitch blocks dispatching on event.eventType
FailureMissing default: case in event dispatcher

Migration Naming Convention

Migration files in packages/infra/db/drizzle/migrations/ must follow the naming pattern NNNN_descriptive_name.sql where NNNN is a zero-padded four-digit prefix and the remainder uses lowercase with underscores.

CheckDetail
Pattern^\d{4}_[a-z][a-z0-9_]*\.sql$
Duplicate prefix detectionNo two files may share the same numeric prefix
FailureFile name does not match convention, or duplicate prefix detected

Test File Vitest Import

Every .test.ts and .integration.test.ts file must reference vitest. This catches stale or empty test files that would silently pass without exercising any assertions.

CheckDetail
Scanned filesAll *.test.ts and *.integration.test.ts files
Required markersAt least one of: vitest, describe, it(, test(, expect(, vi.
FailureTest file with no vitest markers

On this page