Skip to content

File naming and project structure

Invokta engines use filenames to make dependency direction visible. A reader should be able to tell whether a module defines a public capability, owns a business rule, declares an outbound boundary, wraps a provider, or starts a transport before opening the file.

This convention is guidance for custom engines and Invokta examples. Invokta does not inspect or enforce project paths or filenames at runtime. The engine still uses explicit imports, factory arguments, and engine.invoke; naming does not introduce discovery, registration, or a dependency container.

Use only the directories that the engine needs:

src/
application/
ports.ts
capabilities/
classify-ticket.ts
domain/
ticket.ts
infrastructure/
firecrawl-web-crawler.ts
engine.ts
direct.ts
cli.ts
mcp-stdio.ts
mcp-http.ts
test/
classify-ticket.test.ts
firecrawl-web-crawler.test.ts
entrypoints.test.ts

Small engines may begin with only src/engine.ts and an entrypoint. Extract a directory when a real domain rule, port, or implementation needs an independent owner. Empty architectural folders and pass-through modules add no boundary.

Write TypeScript source filenames in lowercase kebab-case. Name the owned concept or responsibility, then let the directory communicate the layer.

  • Use nouns for domain concepts: ticket.ts, incident-context.ts.
  • Use verb-object names for actions: classify-ticket.ts, collect-incident-context.ts.
  • Add a concrete qualifier before an implemented role: firecrawl-web-crawler.ts, postgres-ticket-repository.ts, in-memory-ticket-repository.ts.
  • Reserve a dot suffix for a tool-recognized role such as <subject>.test.ts. Do not encode architecture with names such as firecrawl.adapter.ts or ticket.service.ts.
  • Avoid ownerless names such as utils.ts, helpers.ts, common.ts, service.ts, provider.ts, and types.ts. Name the behavior or contract that the module owns.

The filename does not need to repeat its directory. Prefer capabilities/classify-ticket.ts over capabilities/classify-ticket-capability.ts.

An outbound provider adapter uses this pattern:

src/infrastructure/<provider>-<port-role>.ts

<provider> is the stable product or technology name in kebab-case. <port-role> is the engine-owned interface that the adapter implements, also converted to kebab-case. Name the module from the domain-facing port, not from a provider SDK class or endpoint.

Engine-owned port Implementation Filename
WebCrawler Firecrawl firecrawl-web-crawler.ts
IssueTracker Sentry sentry-issue-tracker.ts
TelemetryReader New Relic new-relic-telemetry-reader.ts
TicketRepository PostgreSQL postgres-ticket-repository.ts

Match the exported factory to the same pair of names:

src/infrastructure/firecrawl-web-crawler.ts
export function createFirecrawlWebCrawler(
options: FirecrawlWebCrawlerOptions,
): WebCrawler {
// Translate the Firecrawl protocol into the WebCrawler contract.
}

This makes replacements legible: FirecrawlWebCrawler and InternalWebCrawler can both satisfy WebCrawler without changing a capability.

Use <provider>-<bounded-context>-provider.ts only when one cohesive adapter intentionally implements several related ports and no single port role names it accurately. openai-image-provider.ts is an example: the adapter owns one image-provider boundary while implementing both ImageEditor and TextImageRenderer. Do not use the broader -provider suffix for a one-port adapter when a precise role is available.

A provider-neutral mechanism shared by several adapters may use its mechanism as the name, such as provider-http.ts. It remains inside infrastructure/ and must not become a provider registry or leak provider payloads into capability contracts.

“Service” is not a layer in an Invokta engine. Before creating a service.ts or services/ directory, classify what the code owns:

Responsibility Placement and pattern Example
Public use case src/capabilities/<verb>-<object>.ts capabilities/classify-ticket.ts
Pure business rule or calculation src/domain/<concept>.ts or a precise action name domain/ticket-priority.ts
Reusable application orchestration src/application/<verb>-<object>.ts application/assemble-incident-context.ts
Provider or technology integration src/infrastructure/<provider>-<port-role>.ts infrastructure/sentry-issue-tracker.ts
Inbound process or protocol host A named root entrypoint mcp-http.ts

Use -service.ts only when “service” is part of the established domain language and the preceding words still state a specific responsibility. It is not a default suffix for dependency-injected code. A name such as ticket-prioritization.ts communicates more than ticket-service.ts.

File kind Path pattern Rule
Capability src/capabilities/<verb>-<object>.ts Prefer the verb-object segment of its public capability ID.
Shared capability contract src/capabilities/<concept>-contract.ts Use for schemas or types shared by related capabilities.
Domain concept src/domain/<concept>.ts Own provider-neutral entities, values, policies, and invariants.
Outbound ports src/application/ports.ts Keep a small engine’s interfaces together; split to application/<port-role>.ts when they acquire separate owners.
Application operation src/application/<verb>-<object>.ts Use only for orchestration reused outside one capability.
Provider adapter src/infrastructure/<provider>-<port-role>.ts Pair the implementation qualifier with the engine-owned port role.
Local or technology adapter src/infrastructure/<technology-or-strategy>-<port-role>.ts Examples include file-agent-session-store.ts and in-memory-ticket-repository.ts.
Engine factory src/engine.ts Construct dependencies and call createEngine; start no transport here.
Capability composition src/capabilities.ts Use when imported and local capability maps need a separate, side-effect-free composition module.
Inbound entrypoint src/direct.ts, src/cli.ts, src/mcp-stdio.ts, or src/mcp-http.ts Keep each host thin and route execution through engine.invoke.
Focused configuration src/<concern>-config.ts Name the exact concern; keep provider credentials at the composition boundary.
Unit or integration test test/<subject>.test.ts Mirror the source subject; use entrypoints.test.ts for execution-channel parity.
Protocol test double test/<provider>-stub.ts Emulate the provider boundary without calling the public service.

Keep a schema beside its owner. A capability input or output schema belongs in the capability module until several related capabilities share it. A persisted domain record schema belongs with the application or domain concept that defines that record. A provider response validator stays inside the provider adapter because it is not a public engine contract.

Keep parsing, response validation, error translation, and request construction in one provider adapter while they change together. Split a module only when a part has a distinct owner or is reused by another adapter. When an integration does need several files, retain the same grammar inside a provider directory:

src/infrastructure/firecrawl/
firecrawl-web-crawler.ts
firecrawl-client.ts
firecrawl-contract.ts

Here firecrawl-client.ts is a low-level protocol client used by the adapter; capabilities still depend only on WebCrawler. Do not inject the provider client directly into a capability or treat the directory as a runtime plugin.

Before adding a file, ask:

  1. Does the directory identify the layer that owns this code?
  2. Does the filename identify a business concept, action, port role, or concrete implementation?
  3. For an outbound adapter, can a reader see both the provider or technology and the engine-owned role?
  4. Would replacing the provider leave the port and capability names unchanged?
  5. Is a generic word such as service, provider, client, or utils hiding a more precise responsibility?

See Wrap an external provider for a complete implementation and Inject a dependency for provider-neutral ports and composition-root wiring.