Skip to content

Inspection and testing

Forma deliberately makes the graph inspectable without making it mutable. Tests exercise Providers through a Composition and use describePlan() when architecture itself is the subject under test.

const plan = root.describePlan();

The description contains:

  • declared Entry Boundary ids;
  • every Surface alias, Intent identity, and root origin;
  • every registered Provider’s Intent identity;
  • unary or stream modality;
  • Boundary or Adapter origin;
  • declared and effective lifetime;
  • the narrowing path for an auto Provider made context-effective by its graph.
for (const provider of plan.providers) {
console.log({
intent: provider.intent,
mode: provider.mode,
origin: provider.origin,
lifetime: provider.lifetime,
});
}

The returned data is frozen diagnostic metadata. Reading it does not run factories or expose operations.

Useful architecture assertions include:

expect(plan.entryBoundaries).toEqual(["orders"]);
expect(plan.surface).toContainEqual({
alias: "getOrderDetails",
intent: ["orders", "getOrderDetailsUseCase"],
origin: { kind: "boundary", id: "orders" },
});
expect(plan.providers).toContainEqual(
expect.objectContaining({
intent: ["orders", "watchOrderEventsUseCase"],
mode: "stream",
})
);

Provider realization is internal. Do not call, replace, or monkey-patch a provider.factory: there is no public factory field.

Compose the smallest closed graph that represents the behavior under test:

const root = compose({
entryBoundaries: [Orders],
surface: { getOrderDetails: GetOrderDetailsProvider },
wiring: [FindOrderViaMemoryProvider],
});
try {
const outcome = await root.withExecutionContext((surface) => surface.getOrderDetails("42"), {
params: { role: "viewer" },
});
expect(outcome).toEqual({
kind: "success",
value: { id: "42", total: 100 },
});
} finally {
await root.dispose();
}

Counters, gates, failures, and cleanup observations belong inside test Provider callbacks and are observed through invocation and disposal. This tests the same lazy materialization and ownership path used by production code.

The Orders demo exposes the same Surface through two graphs:

const memoryRoot = compose({
entryBoundaries: [Orders],
surface: {
getOrderDetails: GetOrderDetailsProvider,
watchOrderEvents: WatchOrderEventsProvider,
},
wiring: [FindOrderViaMemoryProvider, WatchOrderEventsViaMemoryProvider],
});
const sqliteRoot = compose({
entryBoundaries: [Orders],
surface: {
getOrderDetails: GetOrderDetailsProvider,
watchOrderEvents: WatchOrderEventsProvider,
},
wiring: [
FindOrderViaSqliteProvider,
WatchOrderEventsViaSqliteProvider,
SelectOrderProvider,
SelectOrderEventsProvider,
DatabaseProvider,
],
});

Tests can run the same behavioral contract against both roots. The public Orders Contract does not know which storage Boundary closes its seams.

TypeScript provides early, local diagnostics where it can preserve exact tuple and literal types. Runtime validation remains authoritative for JavaScript callers, casts, prototype checks, and graph properties that TypeScript cannot reliably prove.

InvariantStatic checkRuntime check
Boundary-owned using stays in the same BoundaryYesNo dedicated same-Boundary check after a cast
Adapter adapts an Intent of its target BoundaryYesTarget authenticity only; target/Intent correspondence is static
Missing providers after alias rewritingYes with precise tuplesYes
Contract compatibility across resolved edgesYes for Input, Success/Item, Err, modalityReified modality
Entry Boundary and Surface origin correspondenceYes with precise originsYes
Competing realization or duplicate aliasBest effortYes
Cyclic graphNo general type-level proofYes, with full path
Transitive effective lifetimeLimited local checksYes, with causal path
Params type conflictsYesHost bag shape and properties
Protocol authenticityNominal public typesVersioned runtime brands

Runtime brands identify values created by a compatible Forma protocol implementation. They prevent ordinary structural lookalikes and spread clones from entering the graph; they are not a security boundary.

From @xndrjs/toolkit repository root you can run Forma demo with:

Terminal window
pnpm --filter @xndrjs/forma-demo start:orders:sqlite
pnpm --filter @xndrjs/forma-demo start:orders:memory
pnpm --filter @xndrjs/forma-demo start:editorial

Orders demonstrates interchangeable storage Adapters, Params, lazy materialization, unary and stream lifecycles, and real cleanup. Editorial demonstrates ownership extraction, a temporary Intent alias, semantic Adapters, and multiple Entry Boundaries.

The benchmark app separately exercises larger dependency DAGs, concurrent execution, streams, load, and soak behavior. Benchmarks establish operational evidence for the tested shapes; they do not replace semantic tests.

Forma currently supports closed executable Compositions. It does not yet define:

  • merge semantics for partial Compositions;
  • lazy selection among individual using dependencies after a Provider is requested;
  • multicast, replay, push, reconnect, or transport framing for streams;
  • mechanical enforcement of the TypeScript import graph between Boundary modules;
  • additional lifetime scopes beyond Composition and Execution context.

The core is intentionally not:

  • a general-purpose IoC container;
  • a service locator;
  • an application framework;
  • an orchestration DSL;
  • a required directory hierarchy;
  • a domain-modeling replacement.

Boundary isolation at the Forma API is enforced through same-Boundary using, Adapter targets, Entry Boundaries, and Composition validation. Preventing every illegal type-only or source import is a repository-tooling concern outside the current core.

The current package is alpha. Its implemented semantics—closed graphs, nominal protocol values, owned lifetimes, total Boundary exits, and managed streams—are tested. The alpha label means API and vocabulary may still receive intentional breaking changes before stabilization.

Use the package Glossary and ADRs when you need the rationale behind a decision. Use these guides as the operational path for building an application.