The Kind System
Kind definitions, instances, location resolution, and discovery.
Overview
KindScript’s type system has two primitives:
- Kind definitions — type-level declarations of architectural patterns (
type X = Kind<...>) - Instance declarations — value-level declarations mapping patterns to a specific codebase (
satisfies Instance<T, Path>)
Kinds are normative (what the architecture MUST look like). Instances are descriptive (where the code ACTUALLY lives). The compiler compares them and reports violations.
Kind Definitions
A Kind is a TypeScript type alias using the Kind<N, Members, Constraints, Config> generic. Kind is a conditional type — when Config includes wraps, it produces a wrapped type for declaration-level enforcement via direct type annotation; otherwise it produces a structural type for satisfies Instance<T, Path>.
import type { Kind } from 'kindscript';
// Leaf kind (no members)
type DomainLayer = Kind<"DomainLayer">;
// Leaf kind with constraints
type PureDomainLayer = Kind<"PureDomainLayer", {}, { pure: true }>;
// Composite kind with members
type CleanContext = Kind<"CleanContext", {
domain: DomainLayer;
application: ApplicationLayer;
infrastructure: InfrastructureLayer;
}>;
// Composite kind with members AND constraints
type StrictCleanContext = Kind<"StrictCleanContext", {
domain: DomainLayer;
application: ApplicationLayer;
infrastructure: InfrastructureLayer;
}, {
noDependency: [
["domain", "infrastructure"],
["domain", "application"],
];
pure: true; // applies to this kind's own scope
}>;The Type Parameters
| Parameter | Required | Purpose |
|---|---|---|
N (name) | Yes | String literal discriminant — must match the type alias name |
Members | No | Object type mapping member names to their Kind types (plain or [KindRef, './path'] tuples) |
Constraints | No | Constraints<Members> — architectural rules to enforce |
_Config | No | KindConfig — { wraps?: T; scope?: 'folder' | 'file' }. wraps makes Kind produce a wrapped type for declaration-level enforcement via direct type annotation. scope declares the expected instance scope, validated by the scope plugin. |
Why type Instead of interface
KindScript uses type X = Kind<...> (type alias), not interface X extends Kind<...>. Reasons:
- No redundant name —
interfacerequired the name twice (interface Foo extends Kind<"Foo">) - Cleaner syntax — no empty
{}for leaf kinds - Constraints as type parameters — the 3rd type parameter on
Kindcarries constraints naturally - No false inheritance —
extendsimplied OOP inheritance, but kinds are pure type-level concepts
Instance Declarations
An instance maps a Kind to a specific location in a codebase using satisfies Instance<T, Path>:
import type { Kind, Instance } from 'kindscript';
type DomainLayer = Kind<"DomainLayer">;
type InfrastructureLayer = Kind<"InfrastructureLayer">;
type OrderingContext = Kind<"OrderingContext", {
domain: DomainLayer;
infrastructure: InfrastructureLayer;
}, {
noDependency: [["domain", "infrastructure"]];
}>;
// Instance declaration — context file is inside the directory it governs
export const ordering = {
domain: {},
infrastructure: {},
} satisfies Instance<OrderingContext, '.'>;What satisfies Gives Us
The satisfies keyword is valid TypeScript — no macros, no transforms, no runtime. It provides:
- Type safety — TypeScript validates the member object structure at compile time
- Zero runtime —
import typeis fully erased; nokindscriptdependency at runtime - IDE support — autocomplete, hover docs, go-to-definition all work natively
Explicit Location
The second type parameter on Instance<T, Path> specifies where the instance lives, relative to the declaration file (same resolution semantics as TypeScript imports):
// src/context.ts — '.' resolves to src/
export const app = { domain: {}, infra: {} } satisfies Instance<App, '.'>;
// src/architecture.ts — './ordering' resolves to src/ordering/
export const ordering = { domain: {}, infra: {} } satisfies Instance<App, './ordering'>;
// src/components/atoms/Button/Button.tsx — './Button.tsx' resolves to the file itself
export const _ = {} satisfies Instance<AtomSource, './Button.tsx'>;Path syntax determines granularity:
'./ordering'— folder (directory tree, all.tsfiles recursively)'./helpers.ts'— file (single source file)'./handlers.ts#validate'— sub-file (specific named export)
Location is required. Omitting it is a scanner error.
Explicit Member Locations
Members can specify their filesystem location explicitly using tuple syntax:
type App = Kind<"App", {
domain: [DomainLayer, './domain']; // explicit: member lives at ./domain
application: [ApplicationLayer, './application'];
infrastructure: [InfraLayer, './infrastructure'];
}>;The tuple [KindRef, string] pairs a Kind type with its relative path from the instance root. This decouples member names from filesystem paths — a member named core can live at ./domain, and vice versa.
Without a tuple, the member path is derived from the member name (e.g., member domain resolves to ./domain). Wrapped Kind members (matched by type annotation, not path) always use plain syntax:
type OrderModule = Kind<"OrderModule", {
deciders: Decider; // wrapped Kind — matched by type annotation, no path needed
effectors: Effector; // wrapped Kind — matched by type annotation, no path needed
helpers: [HelperKind, './lib']; // explicit: helpers are at ./lib, not ./helpers
}>;Location Resolution
KindScript resolves the instance root from the declared path, then derives member paths from explicit locations or member names.
Root Resolution
The declared path is resolved relative to the directory of the declaring file, using the same semantics as TypeScript import paths:
'.'— directory of the declaring file'./src'—src/subdirectory relative to the declaring file'../sibling'— a sibling directory one level up
Composite Instance (Directory Scope)
Given this file at src/context.ts:
export const ordering = {
domain: {},
infrastructure: {},
} satisfies Instance<OrderingContext, '.'>;KindScript resolves:
- Root:
src/(declared path'.'resolves to the directory ofcontext.ts) - domain:
src/domain/(root + member name) - infrastructure:
src/infrastructure/(root + member name)
Leaf Instance (File Scope)
Given this file at src/components/atoms/Button/v1.0.0/Button.tsx:
type AtomSource = Kind<"AtomSource", {}, { pure: true }>;
export const _ = {} satisfies Instance<AtomSource, './Button.tsx'>;KindScript resolves:
- Location:
Button.tsxitself (the path ends with.tsx, so it’s a single file)
Constraints on AtomSource (like pure) apply only to Button.tsx. Other files in the same directory (Button.stories.tsx, Button.test.tsx) are unaffected.
Path Granularity
| Path syntax | Example | Resolves to | Constraints apply to |
|---|---|---|---|
| No extension | '.', './ordering' | Directory | All .ts/.tsx files recursively |
.ts / .tsx extension | './Button.tsx' | Single file | That file only |
| Hash syntax | './handlers.ts#validate' | Named export in file | That specific export |
Scope Validation
Kinds can declare their expected instance scope using the scope property in Kind’s 4th type parameter (_Config):
type KindConfig = { wraps?: unknown; scope?: 'folder' | 'file' };When a Kind declares a scope, the compiler validates that instance locations match:
// This Kind expects instances to point at folders
type PureModule = Kind<"PureModule", {}, { pure: true }, { scope: "folder" }>;
// Valid — '.' resolves to a directory
export const m = {} satisfies Instance<PureModule, '.'>;
// Violation (KS70005) — './module.ts' is a file, not a folder
export const m = {} satisfies Instance<PureModule, './module.ts'>;The scope plugin generates a Scope contract during the bind stage and validates it during the check stage. If the instance’s resolved location doesn’t match the declared scope, a KS70005: Scope mismatch diagnostic is produced.
| Declared scope | Instance path must resolve to |
|---|---|
"folder" | A directory (no .ts/.tsx extension) |
"file" | A file (.ts or .tsx extension) |
| Not declared | No scope validation (any path is accepted) |
Nested Members
For kinds with nested members, paths compose naturally:
type DomainLayer = Kind<"DomainLayer", {
entities: EntitiesModule;
ports: PortsModule;
}>;
type CleanContext = Kind<"CleanContext", {
domain: DomainLayer;
}>;If the instance is at src/context.ts:
domain→src/domain/domain.entities→src/domain/entities/domain.ports→src/domain/ports/
Multi-Instance (Bounded Contexts)
Multiple instances of the same Kind can coexist in a project. Each instance has its own root, derived from its file location:
src/
ordering/
ordering.ts # OrderingContext instance → root is src/ordering/
domain/
infrastructure/
billing/
billing.ts # BillingContext instance → root is src/billing/
domain/
adapters/// src/ordering/ordering.ts
export const ordering = {
domain: {},
infrastructure: {},
} satisfies Instance<OrderingContext, '.'>;
// src/billing/billing.ts
export const billing = {
domain: {},
adapters: {},
} satisfies Instance<BillingContext, '.'>;Each instance is checked independently. Contracts on OrderingContext only apply to files under src/ordering/. Contracts on BillingContext only apply to files under src/billing/.
Carriers — Internal Representation
Carriers are the internal algebraic representation of “what code a symbol operates over.” Users don’t write carriers directly — they write Kind definitions and Instance declarations. The parser computes carriers from this user-facing syntax.
Carrier Algebra
A carrier is an algebraic expression built from atoms and operations:
Atoms:
path— code at a filesystem path (directory or file, determined at resolution time)annotation— all declarations annotated with a wrapped Kind type across the entire project
Operations:
union— files from any child carrierexclude— files from base minus files from excludedintersect— files common to all child carriers
Example Carriers
// Structural Kind instance at './src/domain/'
{ type: 'path', path: '/project/src/domain' }
// Wrapped Kind member (scopeless)
{ type: 'annotation', kindTypeName: 'Decider' }
// Scoped wrapped Kind member (annotated exports within parent scope)
{
type: 'intersect',
children: [
{ type: 'annotation', kindTypeName: 'Decider' },
{ type: 'path', path: '/project/src/ordering' }
]
}
// Union of multiple paths
{
type: 'union',
children: [
{ type: 'path', path: '/project/src/domain' },
{ type: 'path', path: '/project/src/application' }
]
}Key Functions
carrierKey(carrier: CarrierExpr): string— deterministic serialization usable as a Map key. For path carriers, returns the raw path string (ensures backward compatibility withsymbol.idlookup pattern).hasAnnotationAtom(carrier: CarrierExpr): boolean— checks if a carrier contains an annotation atom, replacing the oldisWrappedKind()pattern.
Resolution
Carriers are pure values — they have no behavior, no resolution logic, no filesystem access. The CarrierResolver service (application layer) translates carrier expressions into concrete file lists by:
- Path atoms — filesystem probing (directory listing or single file)
- Annotation atoms — filtering annotated exports from scan results
- Operations — set algebra on child results (union, intersection, difference)
Scoping is expressed through composition, not built into atoms. A “scoped annotation carrier” is intersect(annotation(K), path(scope)).
Discovery via TypeScript Piggybacking
KindScript does not use file extensions, config files, or naming conventions to find definitions. Instead, it piggybacks on TypeScript’s own type checker.
How Discovery Works
- The
ASTAdapterwalks each source file in the TypeScript program - For type aliases, it uses
checker.getSymbolAtLocation()to check if the type reference resolves toKindfrom the'kindscript'package - For
satisfiesexpressions, it checks if the type reference resolves toInstancefrom'kindscript' - This works through aliases, re-exports, and any valid TypeScript import path
What This Means
- No
.k.tsextension — definitions live in regular.tsfiles - No
kindscript.json— no config file needed for discovery - No naming convention — any file name works
- Alias-safe —
import type { Kind as K } from 'kindscript'works (because the type checker resolves the original symbol) - Invisible — KindScript adds zero artifacts to a project beyond the type imports
MemberMap
MemberMap<T> is the internal type projection used by Instance<T, Path> — it transforms a Kind into the object shape you write with satisfies. Users typically use Instance<T, Path> directly and never reference MemberMap<T> themselves.
The MemberMap<T> type transforms a Kind type into its instance shape — the object type used with satisfies Instance<T, Path>:
type MemberMap<T extends KindRef> = {
[K in keyof T as K extends 'kind' | 'location' | '__kindscript_ref' | '__kindscript_brand' ? never : K]:
T[K] extends KindRef
? MemberMap<T[K]> | Record<string, never>
: never;
};It strips the kind, location, and internal phantom properties (which are derived automatically), keeps member names, and recursively applies to child Kinds. Each member value is either a nested MemberMap (for Kinds with sub-members) or an empty object {} (for leaf Kinds). KindRef is a shared phantom marker type that both wrapped and structural Kinds satisfy. When members use tuple syntax ([KindRef, string]), MemberMap extracts the Kind from the tuple.
Member names must be valid TypeScript identifiers. By default, member names double as directory names for location resolution (e.g., member domain resolves to ./domain). With explicit tuple locations, the path is independent of the name — a member named core at [CoreKind, './domain'] resolves to ./domain.
Wrapped Kinds — Declaration-Level Enforcement
Where a plain Kind classifies places (directories and files), a wrapped Kind classifies types (individual exported declarations via their type annotations). A wrapped Kind is any Kind whose 4th type parameter (_Config) includes wraps.
Defining a Wrapped Kind
import type { Kind } from 'kindscript';
type DeciderFn = (command: Command) => readonly Event[];
type Decider = Kind<"Decider", {}, {}, { wraps: DeciderFn }>;
// With constraints (3rd parameter):
type PureDecider = Kind<"PureDecider", {}, { pure: true }, { wraps: DeciderFn }>;The 4th parameter { wraps: DeciderFn } tells KindScript this Kind classifies individual declarations, not directories. The implementation uses a phantom brand: T & { readonly __kindscript_brand?: N }. The brand is optional, so any value of the wrapped type satisfies the Kind — zero assignability impact.
Wrapped Kind Instances
Instances of wrapped Kinds are declared with direct Kind type annotation on exports — no satisfies, no register(), no companion const:
// These are wrapped Kind instances (type annotation uses the Kind directly):
export const validateOrder: Decider = (cmd) => { ... };
export const applyDiscount: Decider = (cmd) => { ... };
// This is NOT an instance (DeciderFn is not a wrapped Kind):
export const helper: DeciderFn = (cmd) => { ... };
// This is NOT an instance (no explicit type annotation):
export const inline = (cmd: Command): Event[] => { ... };Discovery is syntactic — the scanner checks whether the type annotation explicitly names a wrapped Kind, not whether the value structurally matches. The developer’s choice of type name is the opt-in signal.
Composability with Structural Kinds
Wrapped Kind members can be used inside filesystem Kinds, enabling cross-scope constraints:
type Decider = Kind<"Decider", {}, {}, { wraps: DeciderFn }>;
type Effector = Kind<"Effector", {}, {}, { wraps: EffectorFn }>;
type OrderModule = Kind<"OrderModule", {
deciders: Decider;
effectors: Effector;
}, {
noDependency: [["deciders", "effectors"]];
}>;
export const order = {
deciders: {},
effectors: {},
} satisfies Instance<OrderModule, '.'>;This means: “within the OrderModule directory, collect all Decider-annotated exports into one group and all Effector-annotated exports into another. No file containing a Decider may import from a file containing an Effector.”
The binder resolves wrapped Kind members by scanning typed exports within the parent Kind’s scope. Existing constraints (noDependency, noCycles, etc.) work unchanged — they operate on the resolvedFiles map, which the binder populates for both filesystem and wrapped Kind members.
Wrapped Kind vs Structural Kind
| Aspect | Structural Kind | Wrapped Kind |
|---|---|---|
| Classifies | Places (directories, files) | Types (exported declarations) |
| Scope | Directory or file | Individual declaration |
| Instance mechanism | satisfies Instance<T, Path> | Direct Kind type annotation |
| Resolution | Filesystem paths (binder) | Annotated-export collection within scope (binder) |
| Members | Subdirectories | As Kind members (composability) |
| Standalone constraints | All constraint types | All user-declared constraint types (via 3rd parameter) |
| Syntax | Kind<N, Members, C> | Kind<N, {}, C, { wraps: T }> |
Public API
KindScript exports seven types from src/types/index.ts:
| Type | Purpose |
|---|---|
Kind<N, Members?, Constraints?, Config?> | Define an architectural pattern (conditional type) |
Instance<T, Path> | Declare where a structural Kind is instantiated (used with satisfies) |
MemberMap<T> | Transforms a Kind type into its instance object shape (used internally by Instance) |
Constraints<Members> | Type for the constraints parameter (usually inlined) |
KindConfig | { wraps?: T; scope?: 'folder' | 'file' } — Kind behavior configuration |
KindRef | Phantom marker type — shared by both wrapped and structural Kinds |
All exports are export type — zero runtime footprint.