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.
By enforcing exhaustive compile-time type checking, Codascon eliminates runtime dispatch errors at the source. For larger domains, Odetovibe pairs with it to surgically weave declarative YAML code blueprints directly into your TypeScript AST — keeping your business logic pure and your architecture predictable and unbreakable.
The Runtime: 10 lines of code.
The Power: Pure type-level enforcement via Subject, Command, Template, and Strategy.
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.
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 result
A 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.
| Package | Description |
|---|---|
codascon |
The framework — Subject, Command, Template, Strategy, and type utilities |
odetovibe |
CLI + library: YAML schema, validation, and TypeScript scaffolding codegen |
npm install codascon
# or
pnpm add codasconimport { 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();
}
}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();
}
}// 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` };
}
}const cmd = new AccessBuildingCommand();
const result = cmd.run(new Student("Alice", "CS", 3), { name: "Science Hall", department: "CS" });
// { granted: true, reason: "Alice has access" }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.
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 };
}
}This does not break LSP — a StudentCheckout is only returned for dispatches that route Students to it.
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
}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.
For larger domains, define the structure declaratively and let Odetovibe generate the TypeScript scaffolding:
namespace: campus
domainTypes:
Student:
visitName: resolveStudent
Professor:
visitName: resolveProfessor
Building: {}
CampusPerson: {}
AccessResult: {}
commands:
AccessBuildingCommand:
commandName: accessBuilding
baseType: CampusPerson
objectType: Building
returnType: AccessResult
subjectUnion: [Student, Professor]
dispatch:
Student: AccessTemplate.DepartmentMatch
Professor: GrantAccess
templates:
AccessTemplate:
isParameterized: true
subjectSubset: [Student, Professor]
strategies:
DepartmentMatch:
subjectSubset: [Student]
GrantAccess:
isParameterized: false
strategies: {}# CLI
npx odetovibe campus.yaml --out src/
npx odetovibe campus.yaml --out src/ --overwrite # unconditional overwrite
npx odetovibe campus.yaml --out src/ --no-overwrite # strict: write .ode.ts on conflict
npx odetovibe --help// Library — three phases: Extract → Transform → Load
import { Project } from "ts-morph";
import { parseYaml, validateYaml, emitAst, writeFiles } from "odetovibe";
// Extract: parse YAML and validate against schema rules
const configIndex = parseYaml("campus.yaml");
const { valid, validationResults } = validateYaml(configIndex);
if (!valid) {
for (const validationResult of validationResults) {
for (const error of validationResult.errors)
console.error(`[${error.entryKey}] ${error.rule}: ${error.message}`);
}
process.exit(1);
}
// Transform: emit TypeScript AST into an in-memory ts-morph Project
const project = new Project({ useInMemoryFileSystem: true });
emitAst(configIndex, { configIndex, project });
// Load: write SourceFiles to disk (merge preserves existing method bodies)
const results = await writeFiles(project, { targetDir: "./src", mode: "merge" });
for (const fileResult of results) {
if (fileResult.conflicted) console.warn("conflict →", fileResult.path);
else console.log(fileResult.created ? "created" : "updated", fileResult.path);
}Odetovibe reads the YAML blueprint, validates it against the schema rules, and emits TypeScript classes that conform to the Codascon protocol — with all the type constraints already in place. You fill in the business logic; the structure is guaranteed.
See packages/odetovibe/src/schema.ts for the full schema documentation and validation rules.
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.
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.
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/specs/extract.yaml
- YAML (transform): https://raw.githubusercontent.com/scorpevans/codascon/main/packages/odetovibe/specs/transform.yaml
- YAML (load): https://raw.githubusercontent.com/scorpevans/codascon/main/packages/odetovibe/specs/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.
codascon/ # monorepo root
├── packages/
│ ├── codascon/ # published as "codascon"
│ │ ├── src/
│ │ │ ├── index.test.ts
│ │ │ └── index.ts # Subject, Command, Template, Strategy + type machinery
│ │ └── README.md
│ └── odetovibe/ # published as "odetovibe"
│ ├── src/
│ │ ├── extract/ # parse YAML → validate → ConfigIndex
│ │ │ ├── commands/
│ │ │ │ └── validate-entry.ts
│ │ │ ├── domain-types.ts
│ │ │ ├── index.test.ts
│ │ │ └── index.ts
│ │ ├── load/ # ts-morph AST → write files to disk
│ │ │ ├── commands/
│ │ │ │ └── write-file.ts
│ │ │ ├── domain-types.ts
│ │ │ ├── index.test.ts
│ │ │ └── index.ts
│ │ ├── transform/ # ConfigIndex → ts-morph AST
│ │ │ ├── commands/
│ │ │ │ └── emit-ast.ts
│ │ │ ├── domain-types.ts
│ │ │ ├── index.test.ts
│ │ │ └── index.ts
│ │ ├── cli.ts # bin entry: odetovibe <schema.yaml> --out <dir>
│ │ ├── index.ts # library entry
│ │ └── schema.ts # YamlConfig type definitions
│ ├── specs/ # odetovibe's own codascon domain specs
│ │ ├── extract.yaml # extract phase domain config
│ │ ├── load.yaml # load phase domain config
│ │ └── transform.yaml # transform phase domain config
│ └── README.md
└── README.md
pnpm install # install all dependencies
pnpm build # compile both packages (respects project reference order)
pnpm test # run all tests
pnpm lint # ESLint across all packages
pnpm format # Prettier
pnpm clean # remove build artifactsMIT