Package Exports
- codascon
Readme
codascon
A structural protocol for code organization with exhaustive compile-time type checking.
Codascon distills high-level design patterns and SOLID principles into a zero-overhead TypeScript protocol. You describe what your domain looks like — which entities exist, which operations apply to them, and which strategies handle each combination — and your architectural intent is guarded with mathematical certainty. If a single edge case is unhandled, the compiler won't just warn you — it will stop you.
The Runtime: 10 lines of code.
The Power: Pure type-level enforcement via Subject, Command, Template, and Strategy.
The Problem
When you have N entity types and M operations, the naive approach produces N×M branching logic scattered across your codebase. Add a new entity type and you must hunt down every switch and instanceof check. Miss one and you get a silent runtime bug.
Codascon makes that impossible. If you add a Subject and forget to handle it in any Command, your code doesn't compile.
How It Works
command.run(subject, object)
→ subject.getCommandStrategy(command, object) // double dispatch
→ command[subject.visitName](subject, object) // visit method selects strategy
→ returns a Template // the chosen strategy
→ template.execute(subject, object) // strategy executes
→ returns resultA Subject is an entity (Student, Professor, Visitor). A Command is an operation (AccessBuilding, CheckoutEquipment). Each Command declares one visit method per Subject — the visit method inspects the Subject and the context, then returns a Template to execute. A Strategy is a concrete Template subclass that narrows the subject union and provides the implementation. The Template may declare hooks — references to other Commands it invokes during execution.
Install
npm install codascon
# or
pnpm add codasconQuick Start
Define Subjects
import { Subject } from "codascon";
class Student extends Subject {
readonly visitName = "resolveStudent" as const;
constructor(
public readonly name: string,
public readonly department: string,
public readonly year: 1 | 2 | 3 | 4,
) {
super();
}
}
class Professor extends Subject {
readonly visitName = "resolveProfessor" as const;
constructor(
public readonly name: string,
public readonly tenured: boolean,
) {
super();
}
}Define a Command
import { Command, type Template, type CommandSubjectUnion } from "codascon";
interface Building {
name: string;
department: string;
}
interface AccessResult {
granted: boolean;
reason: string;
}
class AccessBuildingCommand extends Command<
{ name: string }, // base type — shared interface
Building, // object type — context
AccessResult, // return type
[Student, Professor] // subject union
> {
readonly commandName = "accessBuilding" as const;
resolveStudent(student: Student, building: Readonly<Building>) {
if (student.department === building.department) return new GrantAccess();
return new DenyAccess();
}
resolveProfessor(professor: Professor, building: Readonly<Building>) {
if (professor.tenured) return new GrantAccess();
return new DenyAccess();
}
}Define a Template and Strategies
// CommandSubjectUnion<C> extracts the subject union from a Command —
// no need to repeat Student | Professor manually
abstract class AccessTemplate implements Template<AccessBuildingCommand> {
abstract execute(
subject: CommandSubjectUnion<AccessBuildingCommand>,
building: Building,
): AccessResult;
}
class GrantAccess extends AccessTemplate {
execute(subject: CommandSubjectUnion<AccessBuildingCommand>): AccessResult {
return { granted: true, reason: `${subject.name} has access` };
}
}
class DenyAccess extends AccessTemplate {
execute(subject: CommandSubjectUnion<AccessBuildingCommand>): AccessResult {
return { granted: false, reason: `${subject.name} denied` };
}
}Run
const cmd = new AccessBuildingCommand();
const result = cmd.run(new Student("Alice", "CS", 3), { name: "Science Hall", department: "CS" });
// { granted: true, reason: "Alice has access" }What the Compiler Catches
Missing visit method — Remove resolveProfessor from the Command above. The call cmd.run(...) immediately shows a type error. Not at the class declaration, at the call site — you see the error exactly where it matters.
Wrong Subject type — Pass a Visitor to a Command that only handles [Student, Professor]. Compile error.
Missing hook property — Declare implements Template<Cmd, [AuditCommand]> without an audit property. Compile error.
Wrong return type — Return a string from execute when the Command expects AccessResult. Compile error.
Duplicate visitName — Two Subjects with the same visitName in one Command's union. The type system creates an impossible intersection, making the visit method unimplementable.
Missing abstract method in a Strategy — A Strategy that extends an abstract Template without implementing all abstract methods. Compile error at the class declaration.
Advanced Patterns
Parameterized Templates
A Template can leave its subject union as a type parameter, letting Strategy classes narrow which Subjects they handle:
abstract class CheckoutTemplate<SU extends CommandSubjectUnion<CheckoutCmd>> implements Template<
CheckoutCmd,
[AccessBuildingCommand],
SU
> {
readonly accessBuilding: AccessBuildingCommand;
constructor(cmd: AccessBuildingCommand) {
this.accessBuilding = cmd;
}
execute(subject: SU, equipment: Equipment): CheckoutResult {
const access = this.accessBuilding.run(subject, equipmentBuilding);
if (!access.granted) return deny(access.reason);
return this.computeTerms(subject, equipment);
}
protected abstract computeTerms(subject: SU, eq: Equipment): CheckoutResult;
}
// Strategy narrows to Student only
class StudentCheckout extends CheckoutTemplate<Student> {
protected computeTerms(student: Student, eq: Equipment): CheckoutResult {
return { approved: true, days: student.year >= 3 ? 14 : 7 };
}
}Command Hooks
Templates can declare dependencies on other Commands via the H parameter:
abstract class AuditedTemplate implements Template<MyCommand, [AuditCommand, LogCommand]> {
// Instantiated on the Template — shared across all Strategies
readonly log = new LogCommand();
// Abstract — each Strategy must provide its own instance
abstract readonly audit: AuditCommand;
}
class MyStrategy extends AuditedTemplate {
readonly audit = new AuditCommand(); // Strategy provides the abstract hook
}Async Commands
Set the return type to Promise<T>:
class AssignParkingCommand extends Command<
Person,
ParkingLot,
Promise<ParkingAssignment>,
[Student, Professor]
> {
/* ... */
}
// Usage
const result = await parkingCmd.run(student, lotA);Visit methods (strategy selection) remain synchronous. Only execute returns the Promise.
Real-World Example
Odetovibe — the YAML-to-TypeScript code generator that ships alongside this framework — is built entirely on the codascon protocol. The domain is described in YAML and its TypeScript scaffolding is generated by odetovibe itself.
When to Use
Good fit:
- Domain with multiple entity types and multiple operations that grow along both axes
- Permission / access control systems
- Document processing pipelines
- Game entity interactions
- Workflow engines where behavior varies by entity type and operation context
Not a good fit:
- Simple CRUD services
- Linear data pipelines
- Applications where a
switchor polymorphic method suffices - Domains with one or two entity types that rarely change
The abstraction tax is real. It pays off when extension happens along the axes the protocol anticipates.
For AI-Assisted Development
Codascon is particularly well-suited for LLM-assisted ("vibe") coding:
You are an expert TypeScript architect. Build a new domain using the **codascon** protocol — a strict, double-dispatch visitor framework.
### Step 1: Understand the Protocol
Read both resources in full before writing any code:
- README: https://raw.githubusercontent.com/scorpevans/codascon/main/packages/codascon/README.md
- SOURCE: https://raw.githubusercontent.com/scorpevans/codascon/main/packages/codascon/src/index.ts
### Step 2: Study the Reference Implementation
Mimic the file structure and patterns from these real-world files exactly:
- SUBJECTS: https://raw.githubusercontent.com/scorpevans/codascon/main/packages/odetovibe/src/extract/domain-types.ts
- COMMAND: https://raw.githubusercontent.com/scorpevans/codascon/main/packages/odetovibe/src/extract/commands/validate-entry.ts
- SCHEMA: https://raw.githubusercontent.com/scorpevans/codascon/main/packages/odetovibe/src/schema.ts
- YAML (extract): https://raw.githubusercontent.com/scorpevans/codascon/main/packages/odetovibe/src/extract.yaml
- YAML (transform): https://raw.githubusercontent.com/scorpevans/codascon/main/packages/odetovibe/src/transform.yaml
- YAML (load): https://raw.githubusercontent.com/scorpevans/codascon/main/packages/odetovibe/src/load.yaml
### Step 3: Apply These Structural Rules
All output must conform to this layout:
src/
└── [namespace] ← the namespace defined in the domain
├── TypesA.ts ← Subject classes and plain interfaces
├── TypesB.ts ← Subject classes and plain interfaces
└── commands/
├── FirstCommand.ts ← Command + its Templates and Strategies
└── SecondCommand.ts ← Command + its Templates and Strategies
### Step 4: Implement This Domain
[INSERT YOUR DOMAIN DESCRIPTION]
Output complete, compile-safe TypeScript with stub strategy implementations — or equivalently, a YAML config in the odetovibe schema format.- Structural rails — The protocol tells the LLM exactly where new code goes. "Add a
Contractorsubject toAccessBuildingCommand" has one unambiguous implementation path. - YAML as prompting surface — Hand the Odetovibe config to the LLM instead of describing changes in prose. Higher fidelity, lower ambiguity.
- Compiler as guardrail — Forgotten visit methods are compile errors, not silent bugs. The LLM gets immediate feedback.
- Predictable file structure — Each
Command+Templates +Strategyclasses lives in one file. No architectural decisions for the LLM to get wrong across iterations.
License
MIT